Scott Wu b9f9b49999 Bug 1382175 - Fix time picker step and min/max regressions. r=mconley
Bug 1363672 uses .getStep() to obtain the step size for an input element in ms,
so we won't need to do conversion anymore. Bug 1364026 uses .getMinimum() and
.getMaximum() to obtain min and max in ms, rather than strings.

MozReview-Commit-ID: pFNcsqEaC0

--HG--
extra : rebase_source : dcba3a0c537580e0675db4cbe17a8d3209395014
2017-07-19 18:44:23 +08:00

417 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;
}
};
}