const template = document.createElement('template'); template.innerHTML = /* html */ `
`; function clone() { return document.importNode(template.content, true); } class Clock { constructor() { /* DOM variables */ let frag = this.frag = clone(); this.clockNode = frag.querySelector('.clock'); this.hourNode = frag.querySelector('#hour'); this.minuteNode = frag.querySelector('#minute'); this.secondNode = frag.querySelector('#second'); /* State variables */ this.hour; this.minute; this.second; this.mounted; this.threshold = 200; this.offset = null; this.time = Date.now() - this.threshold; this.rafId; this.onNewFrame = this.onNewFrame.bind(this); this.setClockNodeMounted = this.setClockNodeMounted.bind(this); } /* DOM update functions */ setHourNode(value) { if(value === 0) { this.clockNode.classList.remove('mounted'); this.hourNode.style.setProperty('--deg', value + 'deg'); this.setMountedOnNextFrame(); } else { this.hourNode.style.setProperty('--deg', value + 'deg'); } } setMinuteNode(value) { if(value === 0) { this.clockNode.classList.remove('mounted'); this.minuteNode.style.setProperty('--deg', value + 'deg'); this.setMountedOnNextFrame(); } else { this.minuteNode.style.setProperty('--deg', value + 'deg'); } } setSecondNode(value) { if(value === 0) { this.clockNode.classList.remove('mounted'); this.secondNode.style.setProperty('--deg', value + 'deg'); this.setMountedOnNextFrame(); } else { this.secondNode.style.setProperty('--deg', value + 'deg'); } } setClockNode(value) { this.clockNode.classList.add(value); } setClockNodeMounted() { if(!this.clockNode.classList.contains('mounted')) { this.setClockNode('mounted'); } } setClockNodeMode(dark) { this.clockNode.classList[dark ? 'add' : 'remove']('dark'); } /* State update functions */ setHour(value) { this.hour = value; this.setHourNode(this.getDegree(value, 12)); } setMinute(value) { this.minute = value; this.setMinuteNode(this.getDegree(value, 60)); } setSecond(value) { this.second = value; this.setSecondNode(this.getDegree(value, 60)); } setMounted(value) { this.mounted = value; this.setClockNodeMounted(); } setOffset(value) { this.offset = Number(value); this.updateTime(Date.now()); } setDark(value) { this.setClockNodeMode(value); } /* State logic */ updateTime(newTime) { let time = this.time = newTime; let date = new Date(time); if(this.offset != null) { let utc = time + (date.getTimezoneOffset() * 60000); date = new Date(utc + (3600000 * this.offset)); } this.setHour(date.getHours()); this.setMinute(date.getMinutes()); this.setSecond(date.getSeconds()); } setTime() { let last = this.time; let now = Date.now(); let diff = now - last; if(diff >= this.threshold) { this.updateTime(now); } } setExplicitTime(time) { this.stop(); this.updateTime(Number(time)); } getDegree(value, max) { let fraction = value / max; return Math.floor(360 * fraction); } setMountedOnNextFrame() { requestAnimationFrame(this.setClockNodeMounted); } start() { this.rafId = requestAnimationFrame(this.onNewFrame); } stop() { cancelAnimationFrame(this.rafId); } /* Event listeners */ onNewFrame() { this.setTime(); this.start(); } /* Init functionality */ connect() { this.setTime(); this.setMounted(true); this.start(); } disconnect() { cancelAnimationFrame(this.rafId); } update(data = {}) { if(data.time) this.setExplicitTime(data.time); if(data.offset != null) this.setOffset(data.offset); if(data.dark != null) this.setDark(data.dark); if(data.stop) this.stop(); if(data.start) this.start(); return this.frag; } } const view = Symbol('clock.view'); class ClockElement extends HTMLElement { static get observedAttributes() { return ['dark', 'offset', 'time']; } constructor() { super(); this.attachShadow({ mode: 'open' }); this[view] = new Clock(); } connectedCallback() { this[view].connect(); let frag = this[view].update({ offset: this.offset, time: this.time, dark: this.dark }); this.shadowRoot.appendChild(frag); } disconnectedCallback() { this[view].disconnect(); } attributeChangedCallback(attr, oldVal, newVal) { this[attr] = newVal; } get time() { return this._time; } set time(time) { this._time = time; this[view].update({ time }); } get offset() { return this._offset; } set offset(offset) { this._offset = offset; this[view].update({ offset }); } get dark() { return this._dark || false; } set dark(val) { let dark = typeof val === 'boolean' ? val : val === ''; this._dark = dark; this[view].update({ dark }); } stop() { this[view].update({ stop: true }); } start() { this[view].update({ start: true }); } } customElements.define('analog-clock', ClockElement); export { ClockElement as default, ClockElement as AnalogClock };