forked from mirrors/gecko-dev
		
	 0773795931
			
		
	
	
		0773795931
		
	
	
	
	
		
			
			# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D36053 --HG-- extra : source : 651d8f947a29f5d80b7e833f7e6b99e2afe8bf9d
		
			
				
	
	
		
			445 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			445 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 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;
 | |
|     },
 | |
|   };
 | |
| }
 |