Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* import-globals-from timekeeper.js */
/* import-globals-from spinner.js */
"use strict";
function TimePicker(context) {
this.context = context;
this._attachEventListeners();
}
{
const DAY_PERIOD_IN_HOURS = 12,
DAY_IN_MS = 86400000;
TimePicker.prototype = {
/**
* Initializes the time picker. Set the default states and properties.
* @param {Object} props
* {
* {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour
* {Number} minute [optional]: Minute (0~59), default is current minute
* {Number} min: Minimum time, in ms
* {Number} max: Maximum time, in ms
* {Number} step: Step size in ms
* {String} format [optional]: "12" for 12 hours, "24" for 24 hours format
* {String} locale [optional]: User preferred locale
* }
*/
init(props) {
this.props = props || {};
this._setDefaultState();
this._createComponents();
this._setComponentStates();
// TODO(bug 1828721): This is a bit sad.
window.PICKER_READY = true;
document.dispatchEvent(new CustomEvent("PickerReady"));
},
/*
* Set initial time states. If there's no hour & minute, it will
* use the current time. The Time module keeps track of the time states,
* and calculates the valid options given the time, min, max, step,
* and format (12 or 24).
*/
_setDefaultState() {
const { hour, minute, min, max, step, format } = this.props;
const now = new Date();
let timerHour = hour == undefined ? now.getHours() : hour;
let timerMinute = minute == undefined ? now.getMinutes() : minute;
let timeKeeper = new TimeKeeper({
min: new Date(Number.isNaN(min) ? 0 : min),
max: new Date(Number.isNaN(max) ? DAY_IN_MS - 1 : max),
step,
format: format || "12",
});
timeKeeper.setState({ hour: timerHour, minute: timerMinute });
this.state = { timeKeeper };
},
/**
* Initalize the spinner components.
*/
_createComponents() {
const { locale, format } = this.props;
const { timeKeeper } = this.state;
const wrapSetValueFn = setTimeFunction => {
return value => {
setTimeFunction(value);
this._setComponentStates();
this._dispatchState();
};
};
const numberFormat = new Intl.NumberFormat(locale).format;
this.components = {
hour: new Spinner(
{
setValue: wrapSetValueFn(value => {
timeKeeper.setHour(value);
this.state.isHourSet = true;
}),
getDisplayString: hour => {
if (format == "24") {
return numberFormat(hour);
}
// Hour 0 in 12 hour format is displayed as 12.
const hourIn12 = hour % DAY_PERIOD_IN_HOURS;
return hourIn12 == 0 ? numberFormat(12) : numberFormat(hourIn12);
},
},
this.context
),
minute: new Spinner(
{
setValue: wrapSetValueFn(value => {
timeKeeper.setMinute(value);
this.state.isMinuteSet = true;
}),
getDisplayString: minute => numberFormat(minute),
},
this.context
),
};
this._insertLayoutElement({
tag: "div",
textContent: ":",
className: "colon",
insertBefore: this.components.minute.elements.container,
});
// The AM/PM spinner is only available in 12hr mode
// TODO: Replace AM & PM string with localized string
if (format == "12") {
this.components.dayPeriod = new Spinner(
{
setValue: wrapSetValueFn(value => {
timeKeeper.setDayPeriod(value);
this.state.isDayPeriodSet = true;
}),
getDisplayString: dayPeriod => (dayPeriod == 0 ? "AM" : "PM"),
hideButtons: true,
},
this.context
);
this._insertLayoutElement({
tag: "div",
className: "spacer",
insertBefore: this.components.dayPeriod.elements.container,
});
}
},
/**
* Insert element for layout purposes.
*
* @param {Object}
* {
* {String} tag: The tag to create
* {DOMElement} insertBefore: The DOM node to insert before
* {String} className [optional]: Class name
* {String} textContent [optional]: Text content
* }
*/
_insertLayoutElement({ tag, insertBefore, className, textContent }) {
let el = document.createElement(tag);
el.textContent = textContent;
el.className = className;
this.context.insertBefore(el, insertBefore);
},
/**
* Set component states.
*/
_setComponentStates() {
const { timeKeeper, isHourSet, isMinuteSet, isDayPeriodSet } = this.state;
const isInvalid = timeKeeper.state.isInvalid;
// Value is set to min if it's first opened and time state is invalid
const setToMinValue =
!isHourSet && !isMinuteSet && !isDayPeriodSet && isInvalid;
this.components.hour.setState({
value: setToMinValue
? timeKeeper.ranges.hours[0].value
: timeKeeper.hour,
items: timeKeeper.ranges.hours,
isInfiniteScroll: true,
isValueSet: isHourSet,
isInvalid,
});
this.components.minute.setState({
value: setToMinValue
? timeKeeper.ranges.minutes[0].value
: timeKeeper.minute,
items: timeKeeper.ranges.minutes,
isInfiniteScroll: true,
isValueSet: isMinuteSet,
isInvalid,
});
// The AM/PM spinner is only available in 12hr mode
if (this.props.format == "12") {
this.components.dayPeriod.setState({
value: setToMinValue
? timeKeeper.ranges.dayPeriod[0].value
: timeKeeper.dayPeriod,
items: timeKeeper.ranges.dayPeriod,
isInfiniteScroll: false,
isValueSet: isDayPeriodSet,
isInvalid,
});
}
},
/**
* Dispatch CustomEvent to pass the state of picker to the panel.
*/
_dispatchState() {
const { hour, minute } = this.state.timeKeeper;
const { isHourSet, isMinuteSet, isDayPeriodSet } = this.state;
// The panel is listening to window for postMessage event, so we
// do postMessage to itself to send data to input boxes.
window.postMessage(
{
name: "PickerPopupChanged",
detail: {
hour,
minute,
isHourSet,
isMinuteSet,
isDayPeriodSet,
},
},
"*"
);
},
_attachEventListeners() {
window.addEventListener("message", this);
document.addEventListener("mousedown", this);
},
/**
* Handle events.
*
* @param {Event} event
*/
handleEvent(event) {
switch (event.type) {
case "message": {
this.handleMessage(event);
break;
}
case "mousedown": {
// Use preventDefault to keep focus on input boxes
event.preventDefault();
event.target.setCapture();
break;
}
}
},
/**
* Handle postMessage events.
*
* @param {Event} event
*/
handleMessage(event) {
switch (event.data.name) {
case "PickerSetValue": {
this.set(event.data.detail);
break;
}
case "PickerInit": {
this.init(event.data.detail);
break;
}
}
},
/**
* Set the time state and update the components with the new state.
*
* @param {Object} timeState
* {
* {Number} hour [optional]
* {Number} minute [optional]
* {Number} second [optional]
* {Number} millisecond [optional]
* }
*/
set(timeState) {
if (timeState.hour != undefined) {
this.state.isHourSet = true;
}
if (timeState.minute != undefined) {
this.state.isMinuteSet = true;
}
this.state.timeKeeper.setState(timeState);
this._setComponentStates();
},
};
}