i18n-element
Version:
I18N Base Element for lit-html and Polymer
387 lines (337 loc) • 11.6 kB
JavaScript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {render, svg} from 'lit-html/lit-html.js';
import {repeat} from 'lit-html/directives/repeat.js';
import {html, i18n, bind} from '../../i18n.js';
import {getMessage, binding as messageBinding} from './message.js';
import './shadow-repeat.js';
const i18nAttrRepoContainer = document.createElement('template');
i18nAttrRepoContainer.innerHTML = `<i18n-attr-repo>
<template id="custom">
<div i18n-target-attr="$"></div>
<div i18n-target-attr2="$"></div>
</template>
</i18n-attr-repo>`;
document.head.appendChild(i18nAttrRepoContainer.content);
/**
* Adapted from the Ractive.js clock example: http://www.ractivejs.org/examples/clock/
*/
export class LitClock extends i18n(HTMLElement) {
static get importMeta() {
return import.meta;
}
static get observedAttributes() {
let attributes = new Set(super.observedAttributes);
[/* list of additional observedAttributes */].forEach(attr => attributes.add(attr));
return [...attributes];
}
get date() { return this._date; }
set date(v) { this._date = v; this.invalidate(); }
constructor() {
super();
//console.log(`${this.is}.constructor()`);
this.attachShadow({mode: 'open'});
this.setupListeners();
}
connectedCallback() {
//console.log(`${this.is}.connectedCallback()`);
if (super.connectedCallback) {
super.connectedCallback();
}
this.setupListeners();
this.start();
}
disconnectedCallback() {
//console.log(`${this.is}.disconnectedCallback()`);
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
this.stop();
this.teardownListeners();
}
start() {
this.date = new Date();
this.intervalId = setInterval(() => {
//console.log(`${this.is}: ticking`);
this.date = new Date();
}, 1000);
}
stop() {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
setupListeners() {
if (!this._langUpdatedBindThis) {
this._langUpdatedBindThis = this._langUpdated.bind(this);
this.addEventListener('lang-updated', this._langUpdatedBindThis); // invalidate on this 'lang-updated'
messageBinding.element.addEventListener('lang-updated', this._langUpdatedBindThis); // invalidate on getMessage()'s 'lang-updated'
}
}
teardownListeners() {
if (this._langUpdatedBindThis) {
this.removeEventListener('lang-updated', this._langUpdatedBindThis);
messageBinding.element.removeEventListener('lang-updated', this._langUpdatedBindThis);
this._langUpdatedBindThis = null;
}
}
_langUpdated(event) {
// TODO: It should be more efficient to skip successive 'lang-updated' events from different bindings in a short period and invalidate on the last one.
this.invalidate();
}
render() {
return html`${bind(this, 'lit-clock')}
<style>
:host {
display: block;
}
.square {
position: relative;
width: 100%;
height: 0;
padding-bottom: 100%;
}
svg {
position: absolute;
width: 100%;
height: 100%;
}
.clock-face {
stroke: #333;
fill: white;
}
.minor {
stroke: #999;
stroke-width: 0.5;
}
.major {
stroke: #333;
stroke-width: 1;
}
.hour {
stroke: #333;
}
.minute {
stroke: #666;
}
.second, .second-counterweight {
stroke: rgb(180,0,0);
}
.second-counterweight {
stroke-width: 3;
}
</style>
<div id='target'
@click=${(event) => {
let div = event.composedPath().filter(el => el.id === 'target')[0];
alert('div.outerHTML = ' + div.outerHTML +
' div.property = ' + div.property +
' div.getAttribute("attr") = ' + div.getAttribute('attr') +
' div.getAttribute("i18n-target-attr") = ' + div.getAttribute('i18n-target-attr'))
}}
.property=${'property value'}
attr=${'attr value'}
?enabled-boolean-attr=${true}
?disabled-boolean-attr=${false}
i18n-target-attr="I18N target attribute value"
i18n-target-attr2="I18N target with ${'param1'} and ${'param2'}">Time: ${this.date.getHours()}:${this.date.getMinutes()}</div>
<div>${getMessage()}</div>
<div class='square'> <!-- so the SVG keeps its aspect ratio -->
<svg viewBox='0 0 100 100'>
<!-- first create a group and move it to 50,50 so
all co-ords are relative to the center -->
<g transform='translate(50,50)'>
<circle class='clock-face' r='48'/>
<g>${minuteTicks}</g><!-- g tag to avoid i18n-format conversion -->
<g>${hourTicks}</g><!-- g tag to avoid i18n-format conversion -->
<!-- hour hand -->
<line class='hour' y1='2' y2='-20'
transform='rotate(${ 30 * this.date.getHours() + this.date.getMinutes() / 2 })'/>
<!-- minute hand -->
<line class='minute' y1='4' y2='-30'
transform='rotate(${ 6 * this.date.getMinutes() + this.date.getSeconds() / 10 })'/>
<!-- second hand -->
<g transform='rotate(${ 6 * this.date.getSeconds() })'>
<line class='second' y1='10' y2='-38'/>
<line class='second-counterweight' y1='10' y2='2'/>
</g>
</g>
</svg>
</div>
`;
}
invalidate() {
if (!this.needsRender) {
this.needsRender = true;
Promise.resolve().then(() => {
this.needsRender = false;
render(this.render(), this.shadowRoot);
});
}
}
attributeChangedCallback(name, oldValue, newValue) {
//console.log(`${this.is}.attributeChangedCallback: name=${name} oldValue=${oldValue} newValue=${newValue}`);
const handleOnlyBySelf = [];
if (handleOnlyBySelf.indexOf(name) < 0) {
if (super.attributeChangedCallback) {
super.attributeChangedCallback(name, oldValue, newValue);
}
}
switch (name) {
//case 'target-attribute': break;
default:
break;
}
}
}
customElements.define('lit-clock', LitClock);
const minuteTicks = (() => {
const lines = [];
for (let i = 0; i < 60; i++) {
lines.push(svg`
<line
class='minor'
y1='42'
y2='45'
transform='rotate(${360 * i / 60})'/>
`);
}
return lines;
})();
const hourTicks = (() => {
const lines = [];
for (let i = 0; i < 12; i++) {
lines.push(svg`
<line
class='major'
y1='32'
y2='45'
transform='rotate(${360 * i / 12})'/>
`);
}
return lines;
})();
class WorldClock extends LitClock {
static get importMeta() {
return import.meta;
}
get date() { this.__date.setTime(this._date.getTime() + this._date.getTimezoneOffset() * 60 * 1000 + this.timezone * 60 * 1000); return this.__date; }
set date(v) { this._date = v; this.invalidate(); }
constructor() {
super();
this.__date = this._date = new Date();
}
connectedCallback() {
super.connectedCallback();
if (this.timezone === undefined) {
this.timezone = -this._date.getTimezoneOffset();
}
}
invalidate() {
if (this.timezone !== undefined) {
super.invalidate();
}
}
render() {
return html`${bind(this, 'world-clock')}
<style>
:host {
display: block;
width: 100%;
max-width: 350px;
padding: 2px;
}
</style>
<div>
Timezone: GMT${(this.timezone < 0 ? '' : '+') + (this.timezone / 60)}
<button @click=${() => this.timezone -= 60}>-1h</button>
<button @click=${() => this.timezone += 60}>+1h</button>
</div>
${super.render()}
`;
}
}
customElements.define('world-clock', WorldClock);
class WorldClockContainer extends i18n(HTMLElement) {
static get importMeta() {
return import.meta;
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this._timezones = this.timezones = [ 0, -new Date().getTimezoneOffset() /*, +new Date().getTimezoneOffset() */];
let _langUpdatedBindThis = this._langUpdated.bind(this);
this.addEventListener('lang-updated', _langUpdatedBindThis); // invalidate on this 'lang-updated'
}
_langUpdated(event) {
if (this.lang) {
this.invalidate();
}
}
connectedCallback() {
super.connectedCallback();
this.connected = true;
this.invalidate();
}
render() {
return html`${bind(this)}
<style>
:host {
display: block;
width: 100%;
}
world-clock {
display: flow;
max-width: 300px;
}
</style>
<div>World Clocks</div>
<button @click=${() => { this.hide = !this.hide; this.disconnect = false; setTimeout(() => this.invalidate(), 100); } }
>${this.text.hide_labels ? this.text.hide_labels[this.hide ? 1 : 0] : ''}</button>
<button @click=${() => { this.hide = false; this.disconnect = !this.disconnect; setTimeout(() => this.invalidate(), 100); } }
>${this.text.disconnect_labels ? this.text.disconnect_labels[this.disconnect ? 1 : 0] : ''}</button>
<shadow-repeat .repeater=${() => repeat(this.hide ? [] : this.timezones, (item, index) => html`<slot name=${index}></slot>`)}>
<!-- stock views in Light DOM and show selected views in shadow DOM via slot names -->
${repeat(this.disconnect ? [] : this.timezones,
(item, index) => index, // index as the key
(item, index) =>
/* no I18N for this template itself */html`<world-clock slot=${index} .timezone=${this.timezones[item]}></world-clock>`)}
</shadow-repeat>
<i18n-format id="compound-format-text" class="text">
<!-- <json-data> is to be preprocessed as .data property -->
<json-data>{
"0": "No timezones",
"1": "Only 1 timezone for {2} is shown.",
"one": "{1} timezone other than {2} is shown.",
"other": "{1} timezones other than {2} are shown."
}</json-data>
<i18n-number offset="1">${this.hide || this.disconnect ? 0 : this.timezones.length}</i18n-number>
<span>${'GMT' + (this.timezones[0] < 0 ? '' : '+') + (this.timezones[0] / 60)}</span>
</i18n-format>
<template>
<json-data id="hide_labels">[ "Hide", "Show" ]</json-data>
<json-data id="disconnect_labels">[ "Disconnect", "Redraw" ]</json-data>
</template>
`;
}
invalidate() {
if (!this.needsRender && this.connected) {
this.needsRender = true;
Promise.resolve().then(() => {
this.needsRender = false;
render(this.render(), this.shadowRoot);
});
}
}
}
customElements.define('world-clock-container', WorldClockContainer);