diff options
Diffstat (limited to 'components/bindings/content/timekeeper.js')
-rw-r--r-- | components/bindings/content/timekeeper.js | 418 |
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; + } + }; +} |