Bug 1381421 - (Part 1) Handle dates earlier than 0001-01-01 and later than 275760-09-13 correctly

This commit is contained in:
janekptacijarabaci 2018-03-30 23:50:34 +02:00 • committed by Roy Tam
commit 63855a98c0
3 changed files with 75 additions and 62 deletions

View file

@ -11,6 +11,9 @@
* @param {Object} options * @param {Object} options
* { * {
* {Number} calViewSize: Number of days to appear on a calendar view * {Number} calViewSize: Number of days to appear on a calendar view
* {Function} getDayString: Transform day number to string
* {Function} getWeekHeaderString: Transform day of week number to string
* {Function} setSelection: Set selection for dateKeeper
* } * }
* @param {Object} context * @param {Object} context
* { * {
@ -24,9 +27,11 @@ function Calendar(options, context) {
this.context = context; this.context = context;
this.state = { this.state = {
days: [], days: [],
weekHeaders: [] weekHeaders: [],
setSelection: options.setSelection,
getDayString: options.getDayString,
getWeekHeaderString: options.getWeekHeaderString
}; };
this.props = {};
this.elements = { this.elements = {
weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader),
daysView: this._generateNodes(options.calViewSize, context.daysView) daysView: this._generateNodes(options.calViewSize, context.daysView)
@ -46,34 +51,32 @@ function Calendar(options, context) {
* {Boolean} isVisible: Whether or not the calendar is in view * {Boolean} isVisible: Whether or not the calendar is in view
* {Array<Object>} days: Data for days * {Array<Object>} days: Data for days
* { * {
* {Number} dateValue: Date in milliseconds * {Date} dateObj
* {Number} textContent * {Number} content
* {Array<String>} classNames
* }
* {Array<Object>} weekHeaders: Data for weekHeaders
* {
* {Number} textContent
* {Array<String>} classNames * {Array<String>} classNames
* {Boolean} enabled * {Boolean} enabled
* } * }
* {Function} getDayString: Transform day number to string * {Array<Object>} weekHeaders: Data for weekHeaders
* {Function} getWeekHeaderString: Transform day of week number to string * {
* {Function} setSelection: Set selection for dateKeeper * {Number} content
* {Array<String>} classNames
* }
* } * }
*/ */
setProps(props) { setProps(props) {
if (props.isVisible) { if (props.isVisible) {
// Transform the days and weekHeaders array for rendering // Transform the days and weekHeaders array for rendering
const days = props.days.map(({ dateObj, classNames, enabled }) => { const days = props.days.map(({ dateObj, content, classNames, enabled }) => {
return { return {
textContent: props.getDayString(dateObj.getUTCDate()), dateObj,
textContent: this.state.getDayString(content),
className: classNames.join(" "), className: classNames.join(" "),
enabled enabled
}; };
}); });
const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => { const weekHeaders = props.weekHeaders.map(({ content, classNames }) => {
return { return {
textContent: props.getWeekHeaderString(textContent), textContent: this.state.getWeekHeaderString(content),
className: classNames.join(" ") className: classNames.join(" ")
}; };
}); });
@ -92,8 +95,6 @@ function Calendar(options, context) {
this.state.days = days; this.state.days = days;
this.state.weekHeaders = weekHeaders; this.state.weekHeaders = weekHeaders;
} }
this.props = Object.assign(this.props, props);
}, },
/** /**
@ -150,9 +151,9 @@ function Calendar(options, context) {
case "click": { case "click": {
if (event.target.parentNode == this.context.daysView) { if (event.target.parentNode == this.context.daysView) {
let targetId = event.target.dataset.id; let targetId = event.target.dataset.id;
let targetObj = this.props.days[targetId]; let targetObj = this.state.days[targetId];
if (targetObj.enabled) { if (targetObj.enabled) {
this.props.setSelection(targetObj.dateObj); this.state.setSelection(targetObj.dateObj);
} }
} }
break; break;

View file

@ -19,9 +19,11 @@ function DateKeeper(props) {
// The min value is 0001-01-01 based on HTML spec: // The min value is 0001-01-01 based on HTML spec:
// https://html.spec.whatwg.org/#valid-date-string // https://html.spec.whatwg.org/#valid-date-string
MIN_DATE = -62135596800000, MIN_DATE = -62135596800000,
// The max value is derived from the ECMAScript spec: // The max value is derived from the ECMAScript spec (275760-09-13):
// http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
MAX_DATE = 8640000000000000; MAX_DATE = 8640000000000000,
MAX_YEAR = 275760,
MAX_MONTH = 9;
DateKeeper.prototype = { DateKeeper.prototype = {
get year() { get year() {
@ -32,10 +34,6 @@ function DateKeeper(props) {
return this.state.dateObj.getUTCMonth(); return this.state.dateObj.getUTCMonth();
}, },
get day() {
return this.state.dateObj.getUTCDate();
},
get selection() { get selection() {
return this.state.selection; return this.state.selection;
}, },
@ -55,7 +53,6 @@ function DateKeeper(props) {
*/ */
init({ year, month, day, min, max, step, stepBase, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { init({ year, month, day, min, max, step, stepBase, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
const today = new Date(); const today = new Date();
const isDateSet = year != undefined && month != undefined && day != undefined;
this.state = { this.state = {
step, firstDayOfWeek, weekends, calViewSize, step, firstDayOfWeek, weekends, calViewSize,
@ -66,28 +63,34 @@ function DateKeeper(props) {
today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
years: [], years: [],
months: [], dateObj: new Date(0),
days: [],
selection: { year, month, day }, selection: { year, month, day },
}; };
this.state.dateObj = isDateSet ? this.setCalendarMonth({
this._newUTCDate(year, month, day) : year: year === undefined ? today.getFullYear() : year,
new Date(this.state.today); month: month === undefined ? today.getMonth() : month
});
}, },
/** /**
* Set new date. The year is always treated as full year, so the short-form * Set new calendar month. The year is always treated as full year, so the
* is not supported. * short-form is not supported.
* @param {Object} date parts * @param {Object} date parts
* { * {
* {Number} year [optional] * {Number} year [optional]
* {Number} month [optional] * {Number} month [optional]
* {Number} date [optional]
* } * }
*/ */
set({ year = this.year, month = this.month, day = this.day }) { setCalendarMonth({ year = this.year, month = this.month }) {
// Make sure the date is valid before setting.
// Use setUTCFullYear so that year 99 doesn't get parsed as 1999 // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
this.state.dateObj.setUTCFullYear(year, month, day); if (year > MAX_YEAR || year === MAX_YEAR && month >= MAX_MONTH) {
this.state.dateObj.setUTCFullYear(MAX_YEAR, MAX_MONTH - 1, 1);
} else if (year < 1 || year === 1 && month < 0) {
this.state.dateObj.setUTCFullYear(1, 0, 1);
} else {
this.state.dateObj.setUTCFullYear(year, month, 1);
}
}, },
/** /**
@ -107,10 +110,7 @@ function DateKeeper(props) {
* @param {Number} month * @param {Number} month
*/ */
setMonth(month) { setMonth(month) {
const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate(); this.setCalendarMonth({ year: this.year, month });
this.set({ year: this.year,
month,
day: Math.min(this.day, lastDayOfMonth) });
}, },
/** /**
@ -118,10 +118,7 @@ function DateKeeper(props) {
* @param {Number} year * @param {Number} year
*/ */
setYear(year) { setYear(year) {
const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate(); this.setCalendarMonth({ year, month: this.month });
this.set({ year,
month: this.month,
day: Math.min(this.day, lastDayOfMonth) });
}, },
/** /**
@ -129,10 +126,7 @@ function DateKeeper(props) {
* @param {Number} offset * @param {Number} offset
*/ */
setMonthByOffset(offset) { setMonthByOffset(offset) {
const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate(); this.setCalendarMonth({ year: this.year, month: this.month + offset });
this.set({ year: this.year,
month: this.month + offset,
day: Math.min(this.day, lastDayOfMonth) });
}, },
/** /**
@ -178,10 +172,13 @@ function DateKeeper(props) {
currentYear >= lastItem.value - YEAR_BUFFER_SIZE) { currentYear >= lastItem.value - YEAR_BUFFER_SIZE) {
// The year is set in the middle with items on both directions // The year is set in the middle with items on both directions
for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) { for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) {
years.push({ const year = currentYear + i;
value: currentYear + i, if (year >= 1 && year <= MAX_YEAR) {
enabled: true years.push({
}); value: year,
enabled: true
});
}
} }
this.state.years = years; this.state.years = years;
} }
@ -193,6 +190,7 @@ function DateKeeper(props) {
* @return {Array<Object>} * @return {Array<Object>}
* { * {
* {Date} dateObj * {Date} dateObj
* {Number} content
* {Array<String>} classNames * {Array<String>} classNames
* {Boolean} enabled * {Boolean} enabled
* } * }
@ -206,9 +204,22 @@ function DateKeeper(props) {
const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(),
firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCMonth(),
firstDayOfMonth.getUTCDate() + i); firstDayOfMonth.getUTCDate() + i);
let classNames = []; let classNames = [];
let enabled = true; let enabled = true;
const isValid = dateObj.getTime() >= MIN_DATE && dateObj.getTime() <= MAX_DATE;
if (!isValid) {
classNames.push("out-of-range");
enabled = false;
days.push({
classNames,
enabled,
});
continue;
}
const isWeekend = this.state.weekends.includes(dateObj.getUTCDay()); const isWeekend = this.state.weekends.includes(dateObj.getUTCDay());
const isCurrentMonth = month == dateObj.getUTCMonth(); const isCurrentMonth = month == dateObj.getUTCMonth();
const isSelection = this.state.selection.year == dateObj.getUTCFullYear() && const isSelection = this.state.selection.year == dateObj.getUTCFullYear() &&
@ -244,6 +255,7 @@ function DateKeeper(props) {
} }
days.push({ days.push({
dateObj, dateObj,
content: dateObj.getUTCDate(),
classNames, classNames,
enabled, enabled,
}); });
@ -275,7 +287,7 @@ function DateKeeper(props) {
* @param {Array<Number>} weekends * @param {Array<Number>} weekends
* @return {Array<Object>} * @return {Array<Object>}
* { * {
* {Number} textContent * {Number} content
* {Array<String>} classNames * {Array<String>} classNames
* } * }
*/ */
@ -285,7 +297,7 @@ function DateKeeper(props) {
for (let i = 0; i < DAYS_IN_A_WEEK; i++) { for (let i = 0; i < DAYS_IN_A_WEEK; i++) {
headers.push({ headers.push({
textContent: dayOfWeek % DAYS_IN_A_WEEK, content: dayOfWeek % DAYS_IN_A_WEEK,
classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
}); });
dayOfWeek++; dayOfWeek++;
@ -318,7 +330,7 @@ function DateKeeper(props) {
* @return {Date} * @return {Date}
*/ */
_newUTCDate(...parts) { _newUTCDate(...parts) {
return new Date(Date.UTC(...parts)); return new Date(new Date(0).setUTCFullYear(...parts));
} },
}; };
} }

View file

@ -54,7 +54,7 @@ function DatePicker(context) {
dateKeeper, dateKeeper,
locale, locale,
isMonthPickerVisible: false, isMonthPickerVisible: false,
getDayString: new Intl.NumberFormat(locale).format, getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "",
getWeekHeaderString: weekday => weekdayStrings[weekday], getWeekHeaderString: weekday => weekdayStrings[weekday],
getMonthString: month => monthStrings[month], getMonthString: month => monthStrings[month],
setSelection: date => { setSelection: date => {
@ -101,7 +101,10 @@ function DatePicker(context) {
this.components = { this.components = {
calendar: new Calendar({ calendar: new Calendar({
calViewSize: CAL_VIEW_SIZE, calViewSize: CAL_VIEW_SIZE,
locale: this.state.locale locale: this.state.locale,
setSelection: this.state.setSelection,
getDayString: this.state.getDayString,
getWeekHeaderString: this.state.getWeekHeaderString
}, { }, {
weekHeader: this.context.weekHeader, weekHeader: this.context.weekHeader,
daysView: this.context.daysView daysView: this.context.daysView
@ -141,10 +144,7 @@ function DatePicker(context) {
this.components.calendar.setProps({ this.components.calendar.setProps({
isVisible: !isMonthPickerVisible, isVisible: !isMonthPickerVisible,
days: this.state.days, days: this.state.days,
weekHeaders: dateKeeper.state.weekHeaders, weekHeaders: dateKeeper.state.weekHeaders
setSelection: this.state.setSelection,
getDayString: this.state.getDayString,
getWeekHeaderString: this.state.getWeekHeaderString
}); });
isMonthPickerVisible ? isMonthPickerVisible ?
@ -254,8 +254,8 @@ function DatePicker(context) {
set({ year, month, day }) { set({ year, month, day }) {
const { dateKeeper } = this.state; const { dateKeeper } = this.state;
dateKeeper.set({ dateKeeper.setCalendarMonth({
year, month, day year, month
}); });
dateKeeper.setSelection({ dateKeeper.setSelection({
year, month, day year, month, day