summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/timepicker.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/timepicker.js')
-rw-r--r--toolkit/content/widgets/timepicker.js291
1 files changed, 291 insertions, 0 deletions
diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js
new file mode 100644
index 0000000000..83c4840a70
--- /dev/null
+++ b/toolkit/content/widgets/timepicker.js
@@ -0,0 +1,291 @@
+/* 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();
+ },
+ };
+}