web-time-picker
Version:
496 lines (459 loc) • 14.2 kB
JavaScript
'use strict';
import TimePickerHourPlate from './time-picker-hour-plate.js';
import TimePickerMinutesPlate from './time-picker-minutes-plate.js';
import PubSub from './internals/pub-sub.js';
import webClockLite from './../node_modules/web-clock-lite/src/web-clock-lite.js';
let pubsub = new PubSub();
// TODO: Cleanup & add settings menu
/**
* @extends HTMLElement
*/
class TimePicker extends HTMLElement {
/**
* Attributes to observer
* @return {Array} []
*/
static get observedAttributes() {
return ['no-clock', 'hour', 'minutes'];
}
/**
* Calls super
*/
constructor() {
super();
this.root = this.attachShadow({mode: 'open'});
this._onUpdateHour = this._onUpdateHour.bind(this);
this._onUpdateMinutes = this._onUpdateMinutes.bind(this);
this._onWebClockClick = this._onWebClockClick.bind(this);
this._onHourClick = this._onHourClick.bind(this);
this._onMinutesClick = this._onMinutesClick.bind(this);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
}
/**
* Stamps innerHTML
*/
connectedCallback() {
this.root.innerHTML = `
<style>
:host {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: 80px;
box-shadow: 0 14px 45px rgba(0, 0, 0, 0.25),
0 10px 18px rgba(0, 0, 0, 0.22);
background: #FFF;
--time-picker-plate-size: 200px;
--time-picker-plate-padding: 22px 0 20px 0;
transition: transform ease-out 160ms, opacity ease-out 160ms, scale ease-out 160ms;
transform-origin: top left;
will-change: transform, height, width, opacity;
--primary-color: #00bcd4;
--primary-text-color: #555;
--clock-container-background: var(--primary-color);
}
.backdrop {
position: absolute;
top: 0;
left: 0;
}
.am-pm, .actions, time-picker-hour-plate, time-picker-minutes-plate {
width: 0;
height: 0;
opacity: 0;
margin: 0;
padding: 0;
pointer-events: none;
}
time-picker-hour-plate, time-picker-minutes-plate {
display: none;
}
:host([show-on-demand]), :host([show-on-demand]) .clock-container {
opacity: 0;
height: 0;
width: 0;
}
:host(.no-clock) {
opacity: 0;
pointer-events: none;
width: 0;
height: 0;
}
:host([show-on-demand] .picker-opened) :host(.picker-opened) {
opacity: 1;
}
:host(.picker-opened) .clock-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 64px;
width: 100%;
background: var(--clock-container-background);
color: var(--clock-container-color);
transition: background ease-in 300ms;
pointer-events: auto;
}
:host(.picker-opened) {
z-index: 100;
}
.clock-container {
box-sizing: border-box;
}
.am-pm, .actions {
display: flex;
flex-direction: row;
}
.am-pm {
align-items: flex-end;
box-sizing: border-box;
}
.actions {
align-items: center;
box-sizing: border-box;
}
.am, .pm {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--clock-background);
text-transform: uppercase;
}
button {
border: none;
border-radius: 3px;
text-transform: uppercase;
padding: 8px;
height: 40px;
min-width: 100px;
background: transparent;
cursor: pointer;
outline: none;
}
.flex {
flex: 1;
}
.flex-2 {
flex: 2;
}
:host(.picker-opened) {
opacity: 1;
display: flex;
flex-direction: column;
width: 100%;
height: auto;
max-width: 320px;
box-shadow: 0 14px 45px rgba(0, 0, 0, 0.25),
0 10px 18px rgba(0, 0, 0, 0.22);
background: #FFF;
--clock-background: rgba(0, 0, 0, 0.05);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: transform ease-in 300ms, opacity ease-in 300ms, scale ease-in 300ms;
}
:host(.picker-opened) .am-pm, :host(.picker-opened) .actions {
height: 64px;
width: 100%;
padding: 8px 24px;
pointer-events: auto;
}
:host(.picker-opened) .am-pm, :host(.picker-opened) .actions {
opacity: 1;
}
:host(.picker-opened[hour-picker]) time-picker-hour-plate,
:host(.picker-opened[minutes-picker]) time-picker-minutes-plate {
opacity: 1;
display: flex;
margin: auto;
width: var(--time-picker-plate-size);
height: var(--time-picker-plate-size);
padding: var(--time-picker-plate-padding);
pointer-events: auto;
}
</style>
<span class="clock-container">
<web-clock-lite style="cursor: pointer;"></web-clock-lite>
</span>
<div class="am-pm">
<span class="flex"></span>
<div class="am">am</div>
<span class="flex-2"></span>
<div class="pm">pm</div>
<span class="flex"></span>
</div>
<time-picker-hour-plate></time-picker-hour-plate>
<time-picker-minutes-plate></time-picker-minutes-plate>
<div class="actions">
<button class="cancel">cancel</button>
<span class="flex"></span>
<button class="ok">ok</button>
</div>
`;
if (this.noClock === false) {
this.webClock.addEventListener('click', this._onWebClockClick);
}
this.timeFormat = this.timeFormat;
this.hourPicker = true;
}
get webClock() {
return this.root.querySelector('web-clock-lite');
}
get plate() {
return this.root.querySelector('time-picker-hour-plate');
}
get minutesPlate() {
return this.root.querySelector('time-picker-minutes-plate');
}
get animations() {
return {
entry: {
opacity: 1,
transform: 'translateY(-50%) translateX(-50%) scale(1)'
},
out: {
opacity: 1,
transform: 'translateY(0) translateX(0) scale(1)'
},
shared: {
translate: (x, y) => {
return {opacity: '0.1', transform: `translateY(${y}px) translateX(${x}px) scale(0.1)`};
}
}
}
}
get time() {
return this._time || {hour: '8', minutes: '00'};
}
get cancelButton() {
return this.root.querySelector('.cancel');
}
get okButton() {
return this.root.querySelector('.ok');
}
get timeFormat() {
return this._timeFormat || 24;
}
get noClock() {
return this._noClock || false;
}
set opened(value) {
this._opened = value;
}
get opened() {
return this._opened;
}
set noClock(value) {
this._noClock = value;
if (value) {
this.classList.add('no-clock');
} else {
this.classList.remove('no-clock');
}
}
set hourPicker(value) {
this._timeFormat = value;
let plate = this.root.querySelector('time-picker-hour-plate');
let minutesPlate = this.root.querySelector('time-picker-minutes-plate');
if (value) {
plate.addEventListener('update-hour', this._onUpdateHour);
minutesPlate.removeEventListener('update-minutes', this._onUpdateMinutes);
this.removeAttribute('minutes-picker');
this.setAttribute('hour-picker', '');
} else {
plate.removeEventListener('update-hour', this._onUpdateHour);
minutesPlate.addEventListener('update-minutes', this._onUpdateMinutes);
this.removeAttribute('hour-picker');
this.setAttribute('minutes-picker', '');
}
}
set hour(value) {
this._onUpdateHour(value);
}
set minutes(value) {
this._onUpdateMinutes(value);
}
set time(value) {
this._time = value;
if (!this.webClockLiteReady) {
customElements.whenDefined('web-clock-lite').then(() => {
this._updateTime(value.hour, value.minutes);
this.webClockLiteReady = true;
});
return;
}
this._updateTime(value.hour, value.minutes);
}
_updateTime(hour, minutes) {
this.webClock.hour = hour;
this.webClock.minutes = minutes;
this.dispatchEvent(new CustomEvent('time-change', {detail: this.time}));
}
set timeFormat(value) {
let amPm = this.root.querySelector('.am-pm');
if (value !== 'am' && value !== 'pm') {
amPm.style.opacity = 0;
amPm.style.height = 0;
amPm.style.pointerEvents = 'none';
} else {
amPm.style.opacity = 1;
amPm.style.height = 'initial';
amPm.style.pointerEvents = 'auto';
}
this.plate.timeFormat = value;
}
/**
* Runs whenever attribute changes are detected
* @param {string} name The name of the attribute that changed.
* @param {string|object|array} oldValue
* @param {string|object|array} newValue
*/
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[this._toJsProp(name)] = newValue;
}
}
_onHourClick() {
this.hourPicker = true;
}
_onMinutesClick() {
this.hourPicker = false;
}
_onUpdateHour(event) {
let hour = event.detail || event;
let time = this.time;
// place a 0 before the digit when length is shorter than 2
hour = this._transformToTime(hour);
time.hour = hour;
this._notify('time', time);
}
_onUpdateMinutes(event) {
let minutes = event.detail || event;
let time = this.time;
minutes = this._transformToTime(minutes);
time.minutes = minutes;
this._notify('time', time);
}
_transformToTime(number) {
// place a 0 before the digit when needed
if (String(number).length === 1) {
return number = `0${number}`;
}
return number;
}
_notify(prop, value) {
this[prop] = value;
}
_onWebClockClick(event) {
event.preventDefault();
if (this.opened) {
return;
}
this.open();
}
open() {
this.opened = true;
this.flip(true);
}
flip(opened) {
let animations;
// Get the first position.
var first = this.getBoundingClientRect();
let hourEl = this.webClock.root.querySelector('.hour');
let minutesEl = this.webClock.root.querySelector('.minutes');
// Get the last position.
if (opened) {
hourEl.addEventListener('click', this._onHourClick);
minutesEl.addEventListener('click', this._onMinutesClick);
this.removeEventListener('click', this._onWebClockClick);
this.okButton.addEventListener('click', this._onOk);
this.cancelButton.addEventListener('click', this._onCancel);
this.classList.add('picker-opened');
} else {
hourEl.removeEventListener('click', this._onHourClick);
minutesEl.removeEventListener('click', this._onMinutesClick);
this.addEventListener('click', this._onWebClockClick);
this.okButton.removeEventListener('click', this._onOk);
this.cancelButton.removeEventListener('click', this._onCancel);
this.classList.remove('picker-opened');
}
var last = this.getBoundingClientRect();
// Invert.
let top = first.top - last.top;
let left = first.left - last.left;
if (opened) {
let color = this.style.getPropertyValue('--primary-color');
animations = [
this.animations.shared.translate(left, top), this.animations.entry
]
requestAnimationFrame(() => {
this.style.setProperty('--clock-container-background', '#FFF');
});
requestAnimationFrame(() => {
this.style.setProperty('--clock-container-background', color);
});
this.style.setProperty('--web-clock-color', '#FFF');
} else {
let textColor = this.style.getPropertyValue('--primary-text-color');
animations = [
this.animations.shared.translate(left, top), this.animations.out
]
this.style.setProperty('--web-clock-color', textColor);
}
// Go from the inverted position to last.
var player = this.animate(animations, {
duration: 300,
easing: 'cubic-bezier(0,0,0.32,1)',
});
// Do any tidy up at the end
// of the animation.
player.addEventListener('finish', () => {
// Workaround for blurry hours bug.
if (opened) requestAnimationFrame(() => {
this.style.display = 'block';
this.plate.style.display = 'block';
this.minutesPlate.style.display = 'block';
});
else requestAnimationFrame(() => {
this.style.display = 'flex';
this.plate.style.display = 'block';
this.minutesPlate.style.display = 'block';
});
});
}
_onOk(event) {
event.stopPropagation();
event.preventDefault();
this.close('ok');
}
_onCancel(event) {
event.stopPropagation();
event.preventDefault();
this.close('cancel');
}
close(action) {
this.opened = false;
this.flip(false);
this.dispatchEvent(new CustomEvent('time-picker-action', {
detail: {
action: action,
time: this.time
}
}));
}
_toJsProp(string) {
var parts = string.split('-');
if (parts.length > 1) {
var upper = parts[1].charAt(0).toUpperCase();
string = parts[0] + upper + parts[1].slice(1).toLowerCase();
}
return string;
}
}
customElements.define('time-picker', TimePicker);