summaryrefslogtreecommitdiff
path: root/components/bindings/content/timekeeper.js
diff options
context:
space:
mode:
Diffstat (limited to 'components/bindings/content/timekeeper.js')
-rw-r--r--components/bindings/content/timekeeper.js418
1 files changed, 418 insertions, 0 deletions
diff --git a/components/bindings/content/timekeeper.js b/components/bindings/content/timekeeper.js
new file mode 100644
index 000000000..3b4e7eb0a
--- /dev/null
+++ b/components/bindings/content/timekeeper.js
@@ -0,0 +1,418 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * TimeKeeper keeps track of the time states. Given min, max, step, and
+ * format (12/24hr), TimeKeeper will determine the ranges of possible
+ * selections, and whether or not the current time state is out of range
+ * or off step.
+ *
+ * @param {Object} props
+ * {
+ * {Date} min
+ * {Date} max
+ * {Number} step
+ * {String} format: Either "12" or "24"
+ * }
+ */
+function TimeKeeper(props) {
+ this.props = props;
+ this.state = { time: new Date(0), ranges: {} };
+}
+
+{
+ const debug = 0 ? console.log.bind(console, '[timekeeper]') : function() {};
+
+ const DAY_PERIOD_IN_HOURS = 12,
+ SECOND_IN_MS = 1000,
+ MINUTE_IN_MS = 60000,
+ HOUR_IN_MS = 3600000,
+ DAY_PERIOD_IN_MS = 43200000,
+ DAY_IN_MS = 86400000,
+ TIME_FORMAT_24 = "24";
+
+ TimeKeeper.prototype = {
+ /**
+ * Getters for different time units.
+ * @return {Number}
+ */
+ get hour() {
+ return this.state.time.getUTCHours();
+ },
+ get minute() {
+ return this.state.time.getUTCMinutes();
+ },
+ get second() {
+ return this.state.time.getUTCSeconds();
+ },
+ get millisecond() {
+ return this.state.time.getUTCMilliseconds();
+ },
+ get dayPeriod() {
+ // 0 stands for AM and 12 for PM
+ return this.state.time.getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS;
+ },
+
+ /**
+ * Get the ranges of different time units.
+ * @return {Object}
+ * {
+ * {Array<Number>} dayPeriod
+ * {Array<Number>} hours
+ * {Array<Number>} minutes
+ * {Array<Number>} seconds
+ * {Array<Number>} milliseconds
+ * }
+ */
+ get ranges() {
+ return this.state.ranges;
+ },
+
+ /**
+ * Set new time, check if the current state is valid, and set ranges.
+ *
+ * @param {Object} timeState: The new time
+ * {
+ * {Number} hour [optional]
+ * {Number} minute [optional]
+ * {Number} second [optional]
+ * {Number} millisecond [optional]
+ * }
+ */
+ setState(timeState) {
+ const { min, max } = this.props;
+ const { hour, minute, second, millisecond } = timeState;
+
+ if (hour != undefined) {
+ this.state.time.setUTCHours(hour);
+ }
+ if (minute != undefined) {
+ this.state.time.setUTCMinutes(minute);
+ }
+ if (second != undefined) {
+ this.state.time.setUTCSeconds(second);
+ }
+ if (millisecond != undefined) {
+ this.state.time.setUTCMilliseconds(millisecond);
+ }
+
+ this.state.isOffStep = this._isOffStep(this.state.time);
+ this.state.isOutOfRange = (this.state.time < min || this.state.time > max);
+ this.state.isInvalid = this.state.isOutOfRange || this.state.isOffStep;
+
+ this._setRanges(this.dayPeriod, this.hour, this.minute, this.second);
+ },
+
+ /**
+ * Set day-period (AM/PM)
+ * @param {Number} dayPeriod: 0 as AM, 12 as PM
+ */
+ setDayPeriod(dayPeriod) {
+ if (dayPeriod == this.dayPeriod) {
+ return;
+ }
+
+ if (dayPeriod == 0) {
+ this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS });
+ } else {
+ this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS });
+ }
+ },
+
+ /**
+ * Set hour in 24hr format (0 ~ 23)
+ * @param {Number} hour
+ */
+ setHour(hour) {
+ this.setState({ hour });
+ },
+
+ /**
+ * Set minute (0 ~ 59)
+ * @param {Number} minute
+ */
+ setMinute(minute) {
+ this.setState({ minute });
+ },
+
+ /**
+ * Set second (0 ~ 59)
+ * @param {Number} second
+ */
+ setSecond(second) {
+ this.setState({ second });
+ },
+
+ /**
+ * Set millisecond (0 ~ 999)
+ * @param {Number} millisecond
+ */
+ setMillisecond(millisecond) {
+ this.setState({ millisecond });
+ },
+
+ /**
+ * Calculate the range of possible choices for each time unit.
+ * Reuse the old result if the input has not changed.
+ *
+ * @param {Number} dayPeriod
+ * @param {Number} hour
+ * @param {Number} minute
+ * @param {Number} second
+ */
+ _setRanges(dayPeriod, hour, minute, second) {
+ this.state.ranges.dayPeriod =
+ this.state.ranges.dayPeriod || this._getDayPeriodRange();
+
+ if (this.state.dayPeriod != dayPeriod) {
+ this.state.ranges.hours = this._getHoursRange(dayPeriod);
+ }
+
+ if (this.state.hour != hour) {
+ this.state.ranges.minutes = this._getMinutesRange(hour);
+ }
+
+ if (this.state.hour != hour || this.state.minute != minute) {
+ this.state.ranges.seconds = this._getSecondsRange(hour, minute);
+ }
+
+ if (this.state.hour != hour || this.state.minute != minute || this.state.second != second) {
+ this.state.ranges.milliseconds = this._getMillisecondsRange(hour, minute, second);
+ }
+
+ // Save the time states for comparison.
+ this.state.dayPeriod = dayPeriod;
+ this.state.hour = hour;
+ this.state.minute = minute;
+ this.state.second = second;
+ },
+
+ /**
+ * Get the AM/PM range. Return an empty array if in 24hr mode.
+ *
+ * @return {Array<Number>}
+ */
+ _getDayPeriodRange() {
+ if (this.props.format == TIME_FORMAT_24) {
+ return [];
+ }
+
+ const start = 0;
+ const end = DAY_IN_MS - 1;
+ const minStep = DAY_PERIOD_IN_MS;
+ const formatter = (time) =>
+ new Date(time).getUTCHours() < DAY_PERIOD_IN_HOURS ? 0 : DAY_PERIOD_IN_HOURS;
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the hours range.
+ *
+ * @param {Number} dayPeriod
+ * @return {Array<Number>}
+ */
+ _getHoursRange(dayPeriod) {
+ const { format } = this.props;
+ const start = format == "24" ? 0 : dayPeriod * HOUR_IN_MS;
+ const end = format == "24" ? DAY_IN_MS - 1 : start + DAY_PERIOD_IN_MS - 1;
+ const minStep = HOUR_IN_MS;
+ const formatter = (time) => new Date(time).getUTCHours();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the minutes range
+ *
+ * @param {Number} hour
+ * @return {Array<Number>}
+ */
+ _getMinutesRange(hour) {
+ const start = hour * HOUR_IN_MS;
+ const end = start + HOUR_IN_MS - 1;
+ const minStep = MINUTE_IN_MS;
+ const formatter = (time) => new Date(time).getUTCMinutes();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the seconds range
+ *
+ * @param {Number} hour
+ * @param {Number} minute
+ * @return {Array<Number>}
+ */
+ _getSecondsRange(hour, minute) {
+ const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS;
+ const end = start + MINUTE_IN_MS - 1;
+ const minStep = SECOND_IN_MS;
+ const formatter = (time) => new Date(time).getUTCSeconds();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Get the milliseconds range
+ * @param {Number} hour
+ * @param {Number} minute
+ * @param {Number} second
+ * @return {Array<Number>}
+ */
+ _getMillisecondsRange(hour, minute, second) {
+ const start = hour * HOUR_IN_MS + minute * MINUTE_IN_MS + second * SECOND_IN_MS;
+ const end = start + SECOND_IN_MS - 1;
+ const minStep = 1;
+ const formatter = (time) => new Date(time).getUTCMilliseconds();
+
+ return this._getSteps(start, end, minStep, formatter);
+ },
+
+ /**
+ * Calculate the range of possible steps.
+ *
+ * @param {Number} startValue: Start time in ms
+ * @param {Number} endValue: End time in ms
+ * @param {Number} minStep: Smallest step in ms for the time unit
+ * @param {Function} formatter: Outputs time in a particular format
+ * @return {Array<Object>}
+ * {
+ * {Number} value
+ * {Boolean} enabled
+ * }
+ */
+ _getSteps(startValue, endValue, minStep, formatter) {
+ const { min, max, step } = this.props;
+ // The timeStep should be big enough so that there won't be
+ // duplications. Ex: minimum step for minute should be 60000ms,
+ // if smaller than that, next step might return the same minute.
+ const timeStep = Math.max(minStep, step);
+
+ // Make sure the starting point and end point is not off step
+ let time = min.valueOf() + Math.ceil((startValue - min.valueOf()) / timeStep) * timeStep;
+ let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / step) * step;
+ let steps = [];
+
+ // Increment by timeStep until reaching the end of the range.
+ while (time <= endValue) {
+ steps.push({
+ value: formatter(time),
+ // Check if the value is within the min and max. If it's out of range,
+ // also check for the case when minStep is too large, and has stepped out
+ // of range when it should be enabled.
+ enabled: (time >= min.valueOf() && time <= max.valueOf()) ||
+ (time > maxValue && startValue <= maxValue &&
+ endValue >= maxValue && formatter(time) == formatter(maxValue))
+ });
+ time += timeStep;
+ }
+
+ return steps;
+ },
+
+ /**
+ * A generic function for stepping up or down from a value of a range.
+ * It stops at the upper and lower limits.
+ *
+ * @param {Number} current: The current value
+ * @param {Number} offset: The offset relative to current value
+ * @param {Array<Object>} range: List of possible steps
+ * @return {Number} The new value
+ */
+ _step(current, offset, range) {
+ const index = range.findIndex(step => step.value == current);
+ const newIndex = offset > 0 ?
+ Math.min(index + offset, range.length - 1) :
+ Math.max(index + offset, 0);
+ return range[newIndex].value;
+ },
+
+ /**
+ * Step up or down AM/PM
+ *
+ * @param {Number} offset
+ */
+ stepDayPeriodBy(offset) {
+ const current = this.dayPeriod;
+ const dayPeriod = this._step(current, offset, this.state.ranges.dayPeriod);
+
+ if (current != dayPeriod) {
+ this.hour < DAY_PERIOD_IN_HOURS ?
+ this.setState({ hour: this.hour + DAY_PERIOD_IN_HOURS }) :
+ this.setState({ hour: this.hour - DAY_PERIOD_IN_HOURS });
+ }
+ },
+
+ /**
+ * Step up or down hours
+ *
+ * @param {Number} offset
+ */
+ stepHourBy(offset) {
+ const current = this.hour;
+ const hour = this._step(current, offset, this.state.ranges.hours);
+
+ if (current != hour) {
+ this.setState({ hour });
+ }
+ },
+
+ /**
+ * Step up or down minutes
+ *
+ * @param {Number} offset
+ */
+ stepMinuteBy(offset) {
+ const current = this.minute;
+ const minute = this._step(current, offset, this.state.ranges.minutes);
+
+ if (current != minute) {
+ this.setState({ minute });
+ }
+ },
+
+ /**
+ * Step up or down seconds
+ *
+ * @param {Number} offset
+ */
+ stepSecondBy(offset) {
+ const current = this.second;
+ const second = this._step(current, offset, this.state.ranges.seconds);
+
+ if (current != second) {
+ this.setState({ second });
+ }
+ },
+
+ /**
+ * Step up or down milliseconds
+ *
+ * @param {Number} offset
+ */
+ stepMillisecondBy(offset) {
+ const current = this.milliseconds;
+ const millisecond = this._step(current, offset, this.state.ranges.millisecond);
+
+ if (current != millisecond) {
+ this.setState({ millisecond });
+ }
+ },
+
+ /**
+ * Checks if the time state is off step.
+ *
+ * @param {Date} time
+ * @return {Boolean}
+ */
+ _isOffStep(time) {
+ const { min, step } = this.props;
+
+ return (time.valueOf() - min.valueOf()) % step != 0;
+ }
+ };
+}