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
};