From e14c686ac0ad5e6cfdd933049c11b80a425283dc Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 23:50:34 +0200 Subject: Bug 1381421 - (Part 1) Handle dates earlier than 0001-01-01 and later than 275760-09-13 correctly --- toolkit/content/widgets/calendar.js | 35 +++++++------- toolkit/content/widgets/datekeeper.js | 86 ++++++++++++++++++++--------------- toolkit/content/widgets/datepicker.js | 16 +++---- 3 files changed, 75 insertions(+), 62 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js index 80c2976e0..44ba67501 100644 --- a/toolkit/content/widgets/calendar.js +++ b/toolkit/content/widgets/calendar.js @@ -11,6 +11,9 @@ * @param {Object} options * { * {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 * { @@ -24,9 +27,11 @@ function Calendar(options, context) { this.context = context; this.state = { days: [], - weekHeaders: [] + weekHeaders: [], + setSelection: options.setSelection, + getDayString: options.getDayString, + getWeekHeaderString: options.getWeekHeaderString }; - this.props = {}; this.elements = { weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), 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 * {Array} days: Data for days * { - * {Number} dateValue: Date in milliseconds - * {Number} textContent + * {Date} dateObj + * {Number} content * {Array} classNames + * {Boolean} enabled * } * {Array} weekHeaders: Data for weekHeaders * { - * {Number} textContent + * {Number} content * {Array} classNames - * {Boolean} enabled * } - * {Function} getDayString: Transform day number to string - * {Function} getWeekHeaderString: Transform day of week number to string - * {Function} setSelection: Set selection for dateKeeper * } */ setProps(props) { if (props.isVisible) { // 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 { - textContent: props.getDayString(dateObj.getUTCDate()), + dateObj, + textContent: this.state.getDayString(content), className: classNames.join(" "), enabled }; }); - const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => { + const weekHeaders = props.weekHeaders.map(({ content, classNames }) => { return { - textContent: props.getWeekHeaderString(textContent), + textContent: this.state.getWeekHeaderString(content), className: classNames.join(" ") }; }); @@ -92,8 +95,6 @@ function Calendar(options, context) { this.state.days = days; this.state.weekHeaders = weekHeaders; } - - this.props = Object.assign(this.props, props); }, /** @@ -150,9 +151,9 @@ function Calendar(options, context) { case "click": { if (event.target.parentNode == this.context.daysView) { let targetId = event.target.dataset.id; - let targetObj = this.props.days[targetId]; + let targetObj = this.state.days[targetId]; if (targetObj.enabled) { - this.props.setSelection(targetObj.dateObj); + this.state.setSelection(targetObj.dateObj); } } break; diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js index 4959b9609..5d70416a9 100644 --- a/toolkit/content/widgets/datekeeper.js +++ b/toolkit/content/widgets/datekeeper.js @@ -19,9 +19,11 @@ function DateKeeper(props) { // The min value is 0001-01-01 based on HTML spec: // https://html.spec.whatwg.org/#valid-date-string 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 - MAX_DATE = 8640000000000000; + MAX_DATE = 8640000000000000, + MAX_YEAR = 275760, + MAX_MONTH = 9; DateKeeper.prototype = { get year() { @@ -32,10 +34,6 @@ function DateKeeper(props) { return this.state.dateObj.getUTCMonth(); }, - get day() { - return this.state.dateObj.getUTCDate(); - }, - get 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 }) { const today = new Date(); - const isDateSet = year != undefined && month != undefined && day != undefined; this.state = { step, firstDayOfWeek, weekends, calViewSize, @@ -66,28 +63,34 @@ function DateKeeper(props) { today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), years: [], - months: [], - days: [], + dateObj: new Date(0), selection: { year, month, day }, }; - this.state.dateObj = isDateSet ? - this._newUTCDate(year, month, day) : - new Date(this.state.today); + this.setCalendarMonth({ + year: year === undefined ? today.getFullYear() : year, + month: month === undefined ? today.getMonth() : month + }); }, /** - * Set new date. The year is always treated as full year, so the short-form - * is not supported. + * Set new calendar month. The year is always treated as full year, so the + * short-form is not supported. * @param {Object} date parts * { * {Number} year [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 - 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 */ setMonth(month) { - const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate(); - this.set({ year: this.year, - month, - day: Math.min(this.day, lastDayOfMonth) }); + this.setCalendarMonth({ year: this.year, month }); }, /** @@ -118,10 +118,7 @@ function DateKeeper(props) { * @param {Number} year */ setYear(year) { - const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate(); - this.set({ year, - month: this.month, - day: Math.min(this.day, lastDayOfMonth) }); + this.setCalendarMonth({ year, month: this.month }); }, /** @@ -129,10 +126,7 @@ function DateKeeper(props) { * @param {Number} offset */ setMonthByOffset(offset) { - const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate(); - this.set({ year: this.year, - month: this.month + offset, - day: Math.min(this.day, lastDayOfMonth) }); + this.setCalendarMonth({ year: this.year, month: this.month + offset }); }, /** @@ -178,10 +172,13 @@ function DateKeeper(props) { currentYear >= lastItem.value - YEAR_BUFFER_SIZE) { // 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++) { - years.push({ - value: currentYear + i, - enabled: true - }); + const year = currentYear + i; + if (year >= 1 && year <= MAX_YEAR) { + years.push({ + value: year, + enabled: true + }); + } } this.state.years = years; } @@ -193,6 +190,7 @@ function DateKeeper(props) { * @return {Array} * { * {Date} dateObj + * {Number} content * {Array} classNames * {Boolean} enabled * } @@ -206,9 +204,22 @@ function DateKeeper(props) { const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); + let classNames = []; 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 isCurrentMonth = month == dateObj.getUTCMonth(); const isSelection = this.state.selection.year == dateObj.getUTCFullYear() && @@ -244,6 +255,7 @@ function DateKeeper(props) { } days.push({ dateObj, + content: dateObj.getUTCDate(), classNames, enabled, }); @@ -275,7 +287,7 @@ function DateKeeper(props) { * @param {Array} weekends * @return {Array} * { - * {Number} textContent + * {Number} content * {Array} classNames * } */ @@ -285,7 +297,7 @@ function DateKeeper(props) { for (let i = 0; i < DAYS_IN_A_WEEK; i++) { headers.push({ - textContent: dayOfWeek % DAYS_IN_A_WEEK, + content: dayOfWeek % DAYS_IN_A_WEEK, classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] }); dayOfWeek++; @@ -318,7 +330,7 @@ function DateKeeper(props) { * @return {Date} */ _newUTCDate(...parts) { - return new Date(Date.UTC(...parts)); - } + return new Date(new Date(0).setUTCFullYear(...parts)); + }, }; } diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 0c288d917..f5659b736 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -54,7 +54,7 @@ function DatePicker(context) { dateKeeper, locale, isMonthPickerVisible: false, - getDayString: new Intl.NumberFormat(locale).format, + getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "", getWeekHeaderString: weekday => weekdayStrings[weekday], getMonthString: month => monthStrings[month], setSelection: date => { @@ -101,7 +101,10 @@ function DatePicker(context) { this.components = { calendar: new Calendar({ 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, daysView: this.context.daysView @@ -141,10 +144,7 @@ function DatePicker(context) { this.components.calendar.setProps({ isVisible: !isMonthPickerVisible, days: this.state.days, - weekHeaders: dateKeeper.state.weekHeaders, - setSelection: this.state.setSelection, - getDayString: this.state.getDayString, - getWeekHeaderString: this.state.getWeekHeaderString + weekHeaders: dateKeeper.state.weekHeaders }); isMonthPickerVisible ? @@ -254,8 +254,8 @@ function DatePicker(context) { set({ year, month, day }) { const { dateKeeper } = this.state; - dateKeeper.set({ - year, month, day + dateKeeper.setCalendarMonth({ + year, month }); dateKeeper.setSelection({ year, month, day -- cgit v1.2.3