From 8fd370c9f39b4bc1d78cc0f763bf4e99dfd0c382 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 12:32:46 +0100 Subject: Bug 1283385: Implement UI for --- toolkit/content/datepicker.xhtml | 61 ++++++ toolkit/content/jar.mn | 4 + toolkit/content/timepicker.xhtml | 2 +- toolkit/content/widgets/calendar.js | 172 +++++++++++++++++ toolkit/content/widgets/datekeeper.js | 244 +++++++++++++++++++++++ toolkit/content/widgets/datepicker.js | 354 ++++++++++++++++++++++++++++++++++ toolkit/content/widgets/spinner.js | 18 +- 7 files changed, 846 insertions(+), 9 deletions(-) create mode 100644 toolkit/content/datepicker.xhtml create mode 100644 toolkit/content/widgets/calendar.js create mode 100644 toolkit/content/widgets/datekeeper.js create mode 100644 toolkit/content/widgets/datepicker.js (limited to 'toolkit/content') diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml new file mode 100644 index 000000000..abc7db303 --- /dev/null +++ b/toolkit/content/datepicker.xhtml @@ -0,0 +1,61 @@ + + + %htmlDTD; +]> + + + Date Picker + + + + + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index 590356b64..79065e78a 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -45,6 +45,7 @@ toolkit.jar: content/global/customizeToolbar.js content/global/customizeToolbar.xul #endif + content/global/datepicker.xhtml #ifndef MOZ_FENNEC content/global/editMenuOverlay.js * content/global/editMenuOverlay.xul @@ -68,8 +69,11 @@ toolkit.jar: content/global/bindings/autocomplete.xml (widgets/autocomplete.xml) content/global/bindings/browser.xml (widgets/browser.xml) content/global/bindings/button.xml (widgets/button.xml) + content/global/bindings/calendar.js (widgets/calendar.js) content/global/bindings/checkbox.xml (widgets/checkbox.xml) content/global/bindings/colorpicker.xml (widgets/colorpicker.xml) + content/global/bindings/datekeeper.js (widgets/datekeeper.js) + content/global/bindings/datepicker.js (widgets/datepicker.js) content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml) content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml) content/global/bindings/datetimebox.xml (widgets/datetimebox.xml) diff --git a/toolkit/content/timepicker.xhtml b/toolkit/content/timepicker.xhtml index 1396223f1..77b9fba41 100644 --- a/toolkit/content/timepicker.xhtml +++ b/toolkit/content/timepicker.xhtml @@ -6,7 +6,7 @@ Time Picker - + diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js new file mode 100644 index 000000000..72e0d9d61 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,172 @@ +/* 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"; + +/** + * Initialize the Calendar and generate nodes for week headers and days, and + * attach event listeners. + * + * @param {Object} options + * { + * {Number} calViewSize: Number of days to appear on a calendar view + * } + * @param {Object} context + * { + * {DOMElement} weekHeader + * {DOMElement} daysView + * } + */ +function Calendar(options, context) { + const DAYS_IN_A_WEEK = 7; + + this.context = context; + this.state = { + days: [], + weekHeaders: [] + }; + this.props = {}; + this.elements = { + weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader), + daysView: this._generateNodes(options.calViewSize, context.daysView) + }; + + this._attachEventListeners(); +} + +{ + Calendar.prototype = { + + /** + * Set new properties and render them. + * + * @param {Object} props + * { + * {Boolean} isVisible: Whether or not the calendar is in view + * {Array} days: Data for days + * { + * {Number} dateValue: Date in milliseconds + * {Number} textContent + * {Array} classNames + * } + * {Array} weekHeaders: Data for weekHeaders + * { + * {Number} textContent + * {Array} classNames + * } + * {Function} getDayString: Transform day number to string + * {Function} getWeekHeaderString: Transform day of week number to string + * {Function} setValue: Set value for dateKeeper + * {Number} selectionValue: The selection date value + * } + */ + setProps(props) { + if (props.isVisible) { + // Transform the days and weekHeaders array for rendering + const days = props.days.map(({ dateValue, textContent, classNames }) => { + return { + dateValue, + textContent: props.getDayString(textContent), + className: dateValue == props.selectionValue ? + classNames.concat("selection").join(" ") : + classNames.join(" ") + }; + }); + const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => { + return { + textContent: props.getWeekHeaderString(textContent), + className: classNames.join(" ") + }; + }); + // Update the DOM nodes states + this._render({ + elements: this.elements.daysView, + items: days, + prevState: this.state.days + }); + this._render({ + elements: this.elements.weekHeaders, + items: weekHeaders, + prevState: this.state.weekHeaders, + }); + // Update the state to current + this.state.days = days; + this.state.weekHeaders = weekHeaders; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Render the items onto the DOM nodes + * @param {Object} + * { + * {Array} elements + * {Array} items + * {Array} prevState: state of items from last render + * } + */ + _render({ elements, items, prevState }) { + for (let i = 0, l = items.length; i < l; i++) { + let el = elements[i]; + + // Check if state from last render has changed, if so, update the elements + if (!prevState[i] || prevState[i].textContent != items[i].textContent) { + el.textContent = items[i].textContent; + } + if (!prevState[i] || prevState[i].className != items[i].className) { + el.className = items[i].className; + } + } + }, + + /** + * Generate DOM nodes + * + * @param {Number} size: Number of nodes to generate + * @param {DOMElement} context: Element to append the nodes to + * @return {Array} + */ + _generateNodes(size, context) { + let frag = document.createDocumentFragment(); + let refs = []; + + for (let i = 0; i < size; i++) { + let el = document.createElement("div"); + el.dataset.id = i; + refs.push(el); + frag.appendChild(el); + } + context.appendChild(frag); + + return refs; + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + if (event.target.parentNode == this.context.daysView) { + let targetId = event.target.dataset.id; + this.props.setValue({ + selectionValue: this.props.days[targetId].dateValue, + dateValue: this.props.days[targetId].dateValue + }); + } + break; + } + } + }, + + /** + * Attach event listener to daysView + */ + _attachEventListeners() { + this.context.daysView.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js new file mode 100644 index 000000000..62fcfadbc --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,244 @@ +/* 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"; + +/** + * DateKeeper keeps track of the date states. + * + * @param {Object} date parts + * { + * {Number} year + * {Number} month + * {Number} date + * } + * {Object} options + * { + * {Number} firstDayOfWeek [optional] + * {Array} weekends [optional] + * {Number} calViewSize [optional] + * } + */ +function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { + this.state = { + firstDayOfWeek, weekends, calViewSize, + dateObj: new Date(0), + years: [], + months: [], + days: [] + }; + this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek); + this._update(year, month, date); +} + +{ + const DAYS_IN_A_WEEK = 7, + MONTHS_IN_A_YEAR = 12, + YEAR_VIEW_SIZE = 200, + YEAR_BUFFER_SIZE = 10; + + DateKeeper.prototype = { + /** + * Set new date + * @param {Object} date parts + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + set({ year = this.state.year, month = this.state.month, date = this.state.date }) { + this._update(year, month, date); + }, + + /** + * Set date with value + * @param {Number} value: Date value + */ + setValue(value) { + const dateObj = new Date(value); + this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate()); + }, + + /** + * Set month. Makes sure the date is <= the last day of the month + * @param {Number} month + */ + setMonth(month) { + const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate(); + this._update(this.state.year, month, Math.min(this.state.date, lastDayOfMonth)); + }, + + /** + * Set year. Makes sure the date is <= the last day of the month + * @param {Number} year + */ + setYear(year) { + const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate(); + this._update(year, this.state.month, Math.min(this.state.date, lastDayOfMonth)); + }, + + /** + * Set month by offset. Makes sure the date is <= the last day of the month + * @param {Number} offset + */ + setMonthByOffset(offset) { + const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate(); + this._update(this.state.year, this.state.month + offset, Math.min(this.state.date, lastDayOfMonth)); + }, + + /** + * Update the states. + * @param {Number} year [description] + * @param {Number} month [description] + * @param {Number} date [description] + */ + _update(year, month, date) { + // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 + this.state.dateObj.setUTCFullYear(year, month, date); + this.state.year = this.state.dateObj.getUTCFullYear(); + this.state.month = this.state.dateObj.getUTCMonth(); + this.state.date = this.state.dateObj.getUTCDate(); + }, + + /** + * Generate the array of months + * @return {Array} + * { + * {Number} value: Month in int + * {Boolean} enabled + * } + */ + getMonths() { + // TODO: add min/max and step support + let months = []; + + for (let i = 0; i < MONTHS_IN_A_YEAR; i++) { + months.push({ + value: i, + enabled: true + }); + } + + return months; + }, + + /** + * Generate the array of years + * @return {Array} + * { + * {Number} value: Year in int + * {Boolean} enabled + * } + */ + getYears() { + // TODO: add min/max and step support + let years = []; + + const firstItem = this.state.years[0]; + const lastItem = this.state.years[this.state.years.length - 1]; + const currentYear = this.state.dateObj.getUTCFullYear(); + + // Generate new years array when the year is outside of the first & + // last item range. If not, return the cached result. + if (!firstItem || !lastItem || + currentYear <= firstItem.value + YEAR_BUFFER_SIZE || + 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 + }); + } + this.state.years = years; + } + return this.state.years; + }, + + /** + * Get days for calendar + * @return {Array} + * { + * {Number} dateValue + * {Number} textContent + * {Array} classNames + * } + */ + getDays() { + // TODO: add min/max and step support + let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + let days = []; + let month = this.state.dateObj.getUTCMonth(); + + for (let i = 0; i < this.state.calViewSize; i++) { + let dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); + let classNames = []; + if (this.state.weekends.includes(dateObj.getUTCDay())) { + classNames.push("weekend"); + } + if (month != dateObj.getUTCMonth()) { + classNames.push("outside"); + } + days.push({ + dateValue: dateObj.getTime(), + textContent: dateObj.getUTCDate(), + classNames + }); + } + return days; + }, + + /** + * Get week headers for calendar + * @param {Number} firstDayOfWeek + * @return {Array} + * { + * {Number} textContent + * {Array} classNames + * } + */ + _getWeekHeaders(firstDayOfWeek) { + let headers = []; + let day = firstDayOfWeek; + + for (let i = 0; i < DAYS_IN_A_WEEK; i++) { + headers.push({ + textContent: day % DAYS_IN_A_WEEK, + classNames: this.state.weekends.includes(day % DAYS_IN_A_WEEK) ? ["weekend"] : [] + }); + day++; + } + return headers; + }, + + /** + * Get the first day on a calendar month + * @param {Date} dateObj + * @param {Number} firstDayOfWeek + * @return {Date} + */ + _getFirstCalendarDate(dateObj, firstDayOfWeek) { + const daysOffset = 1 - DAYS_IN_A_WEEK; + let firstDayOfMonth = this._newUTCDate(dateObj.getUTCFullYear(), dateObj.getUTCMonth()); + let dayOfWeek = firstDayOfMonth.getUTCDay(); + + return this._newUTCDate( + firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + // When first calendar date is the same as first day of the week, add + // another row on top of it. + firstDayOfWeek == dayOfWeek ? daysOffset : (firstDayOfWeek - dayOfWeek + daysOffset) % DAYS_IN_A_WEEK); + }, + + /** + * Helper function for creating UTC dates + * @param {...[Number]} parts + * @return {Date} + */ + _newUTCDate(...parts) { + return new Date(Date.UTC(...parts)); + } + }; +} diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js new file mode 100644 index 000000000..d3f0fd1a4 --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,354 @@ +/* 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"; + +function DatePicker(context) { + this.context = context; + this._attachEventListeners(); +} + +{ + const CAL_VIEW_SIZE = 42; + + DatePicker.prototype = { + /** + * Initializes the date picker. Set the default states and properties. + * @param {Object} props + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * {String} locale [optional]: User preferred locale + * } + */ + init(props = {}) { + this.props = props; + this._setDefaultState(); + this._createComponents(); + this._update(); + }, + + /* + * Set initial date picker states. + */ + _setDefaultState() { + const now = new Date(); + const { year = now.getFullYear(), + month = now.getMonth(), + date = now.getDate(), + locale } = this.props; + + // TODO: Use calendar info API to get first day of week & weekends + // (Bug 1287503) + const dateKeeper = new DateKeeper({ + year, month, date + }, { + calViewSize: CAL_VIEW_SIZE, + firstDayOfWeek: 0, + weekends: [0] + }); + + this.state = { + dateKeeper, + locale, + isMonthPickerVisible: false, + isYearSet: false, + isMonthSet: false, + isDateSet: false, + getDayString: new Intl.NumberFormat(locale).format, + // TODO: use calendar terms when available (Bug 1287677) + getWeekHeaderString: weekday => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday], + setValue: ({ dateValue, selectionValue }) => { + dateKeeper.setValue(dateValue); + this.state.selectionValue = selectionValue; + this.state.isYearSet = true; + this.state.isMonthSet = true; + this.state.isDateSet = true; + this._update(); + this._dispatchState(); + }, + setYear: year => { + dateKeeper.setYear(year); + this.state.isYearSet = true; + this._update(); + this._dispatchState(); + }, + setMonth: month => { + dateKeeper.setMonth(month); + this.state.isMonthSet = true; + this._update(); + this._dispatchState(); + }, + toggleMonthPicker: () => { + this.state.isMonthPickerVisible = !this.state.isMonthPickerVisible; + this._update(); + } + }; + }, + + /** + * Initalize the date picker components. + */ + _createComponents() { + this.components = { + calendar: new Calendar({ + calViewSize: CAL_VIEW_SIZE, + locale: this.state.locale + }, { + weekHeader: this.context.weekHeader, + daysView: this.context.daysView + }), + monthYear: new MonthYear({ + setYear: this.state.setYear, + setMonth: this.state.setMonth, + locale: this.state.locale + }, { + monthYear: this.context.monthYear, + monthYearView: this.context.monthYearView + }) + }; + }, + + /** + * Update date picker and its components. + */ + _update() { + const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state; + + if (isMonthPickerVisible) { + this.state.months = dateKeeper.getMonths(); + this.state.years = dateKeeper.getYears(); + } else { + this.state.days = dateKeeper.getDays(); + } + + this.components.monthYear.setProps({ + isVisible: isMonthPickerVisible, + dateObj: dateKeeper.state.dateObj, + month: dateKeeper.state.month, + months: this.state.months, + year: dateKeeper.state.year, + years: this.state.years, + toggleMonthPicker: this.state.toggleMonthPicker + }); + this.components.calendar.setProps({ + isVisible: !isMonthPickerVisible, + days: this.state.days, + weekHeaders: dateKeeper.state.weekHeaders, + setValue: this.state.setValue, + getDayString: this.state.getDayString, + getWeekHeaderString: this.state.getWeekHeaderString, + selectionValue + }); + + isMonthPickerVisible ? + this.context.monthYearView.classList.remove("hidden") : + this.context.monthYearView.classList.add("hidden"); + }, + + /** + * Use postMessage to pass the state of picker to the panel. + */ + _dispatchState() { + const { year, month, date } = this.state.dateKeeper.state; + const { isYearSet, isMonthSet, isDateSet } = this.state; + // The panel is listening to window for postMessage event, so we + // do postMessage to itself to send data to input boxes. + window.postMessage({ + name: "DatePickerPopupChanged", + detail: { + year, + month, + date, + isYearSet, + isMonthSet, + isDateSet + } + }, "*"); + }, + + /** + * Attach event listeners + */ + _attachEventListeners() { + window.addEventListener("message", this); + document.addEventListener("click", this); + }, + + /** + * Handle events. + * + * @param {Event} event + */ + handleEvent(event) { + switch (event.type) { + case "message": { + this.handleMessage(event); + break; + } + case "click": { + if (event.target == this.context.buttonLeft) { + this.state.dateKeeper.setMonthByOffset(-1); + this._update(); + } else if (event.target == this.context.buttonRight) { + this.state.dateKeeper.setMonthByOffset(1); + this._update(); + } + break; + } + } + }, + + /** + * Handle postMessage events. + * + * @param {Event} event + */ + handleMessage(event) { + switch (event.data.name) { + case "DatePickerSetValue": { + this.set(event.data.detail); + break; + } + case "DatePickerInit": { + this.init(event.data.detail); + break; + } + } + }, + + /** + * Set the date state and update the components with the new state. + * + * @param {Object} dateState + * { + * {Number} year [optional] + * {Number} month [optional] + * {Number} date [optional] + * } + */ + set(dateState) { + if (dateState.year != undefined) { + this.state.isYearSet = true; + } + if (dateState.month != undefined) { + this.state.isMonthSet = true; + } + if (dateState.date != undefined) { + this.state.isDateSet = true; + } + + this.state.dateKeeper.set(dateState); + this._update(); + } + }; + + /** + * MonthYear is a component that handles the month & year spinners + * + * @param {Object} options + * { + * {String} locale + * {Function} setYear + * {Function} setMonth + * } + * @param {DOMElement} context + */ + function MonthYear(options, context) { + const spinnerSize = 5; + const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "short" }).format; + const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format; + const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format; + + this.context = context; + this.state = { dateFormat }; + this.props = {}; + this.components = { + month: new Spinner({ + setValue: month => { + this.state.isMonthSet = true; + options.setMonth(month); + }, + getDisplayString: month => monthFormat(new Date(0, month)), + viewportSize: spinnerSize + }, context.monthYearView), + year: new Spinner({ + setValue: year => { + this.state.isYearSet = true; + options.setYear(year); + }, + getDisplayString: year => yearFormat(new Date(new Date(0).setFullYear(year))), + viewportSize: spinnerSize + }, context.monthYearView) + }; + + this._attachEventListeners(); + } + + MonthYear.prototype = { + + /** + * Set new properties and pass them to components + * + * @param {Object} props + * { + * {Boolean} isVisible + * {Date} dateObj + * {Number} month + * {Number} year + * {Array} months + * {Array} years + * {Function} toggleMonthPicker + * } + */ + setProps(props) { + this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); + + if (props.isVisible) { + this.components.month.setState({ + value: props.month, + items: props.months, + isInfiniteScroll: true, + isValueSet: this.state.isMonthSet, + smoothScroll: !this.state.firstOpened + }); + this.components.year.setState({ + value: props.year, + items: props.years, + isInfiniteScroll: false, + isValueSet: this.state.isYearSet, + smoothScroll: !this.state.firstOpened + }); + this.state.firstOpened = false; + } else { + this.state.isMonthSet = false; + this.state.isYearSet = false; + this.state.firstOpened = true; + } + + this.props = Object.assign(this.props, props); + }, + + /** + * Handle events + * @param {DOMEvent} event + */ + handleEvent(event) { + switch (event.type) { + case "click": { + this.props.toggleMonthPicker(); + break; + } + } + }, + + /** + * Attach event listener to monthYear button + */ + _attachEventListeners() { + this.context.monthYear.addEventListener("click", this); + } + }; +} diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 208ab1931..b3d996fb5 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -98,7 +98,7 @@ function Spinner(props, context) { setState(newState) { const { spinner } = this.elements; const { value, items } = this.state; - const { value: newValue, items: newItems, isValueSet, isInvalid } = newState; + const { value: newValue, items: newItems, isValueSet, isInvalid, smoothScroll = true } = newState; if (this._isArrayDiff(newItems, items)) { this.state = Object.assign(this.state, newState); @@ -106,16 +106,18 @@ function Spinner(props, context) { this._scrollTo(newValue, true); } else if (newValue != value) { this.state = Object.assign(this.state, newState); - this._smoothScrollTo(newValue); - } - - if (isValueSet) { - if (isInvalid) { - this._removeSelection(); + if (smoothScroll) { + this._smoothScrollTo(newValue, true); } else { - this._updateSelection(); + this._scrollTo(newValue, true); } } + + if (isValueSet && !isInvalid) { + this._updateSelection(); + } else { + this._removeSelection(); + } }, /** -- cgit v1.2.3 From 260e399a1ff7756cda9b6320a801f1198b27604a Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 13:24:40 +0100 Subject: Bug 1323109: [DateTimePicker] Date time picker popup does not open near input box --- toolkit/content/widgets/datetimepopup.xml | 3 +++ 1 file changed, 3 insertions(+) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 327f45368..69edffe25 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -11,6 +11,9 @@ xmlns:xbl="http://www.mozilla.org/xbl"> + + + this.querySelector("#dateTimePopupFrame"); -- cgit v1.2.3 From c87dbe6922ec79f988744f5aab0cde1a166292e6 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 13:38:47 +0100 Subject: Bug 1325922: [DateTimePicker] Add arrows svg file and style month-year button for date picker --- toolkit/content/datepicker.xhtml | 8 ++++---- toolkit/content/widgets/datepicker.js | 19 +++++++++++++++++-- toolkit/content/widgets/spinner.js | 9 +++------ toolkit/content/widgets/timepicker.js | 7 +++++++ 4 files changed, 31 insertions(+), 12 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml index abc7db303..d79b53102 100644 --- a/toolkit/content/datepicker.xhtml +++ b/toolkit/content/datepicker.xhtml @@ -16,8 +16,8 @@
@@ -25,8 +25,8 @@
-
-
+
+
diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index d3f0fd1a4..7453b67eb 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -174,7 +174,8 @@ function DatePicker(context) { */ _attachEventListeners() { window.addEventListener("message", this); - document.addEventListener("click", this); + document.addEventListener("mouseup", this, { passive: true }); + document.addEventListener("mousedown", this); }, /** @@ -188,16 +189,28 @@ function DatePicker(context) { this.handleMessage(event); break; } - case "click": { + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + if (event.target == this.context.buttonLeft) { + event.target.classList.add("active"); this.state.dateKeeper.setMonthByOffset(-1); this._update(); } else if (event.target == this.context.buttonRight) { + event.target.classList.add("active"); this.state.dateKeeper.setMonthByOffset(1); this._update(); } break; } + case "mouseup": { + if (event.target == this.context.buttonLeft || event.target == this.context.buttonRight) { + event.target.classList.remove("active"); + } + + } } }, @@ -307,6 +320,7 @@ function DatePicker(context) { this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); if (props.isVisible) { + this.context.monthYear.classList.add("active"); this.components.month.setState({ value: props.month, items: props.months, @@ -323,6 +337,7 @@ function DatePicker(context) { }); this.state.firstOpened = false; } else { + this.context.monthYear.classList.remove("active"); this.state.isMonthSet = false; this.state.isYearSet = false; this.state.firstOpened = true; diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index b3d996fb5..059e151fc 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -268,11 +268,11 @@ function Spinner(props, context) { * Attach event listeners to the spinner and buttons. */ _attachEventListeners() { - const { spinner } = this.elements; + const { spinner, container } = this.elements; spinner.addEventListener("scroll", this, { passive: true }); - document.addEventListener("mouseup", this, { passive: true }); - document.addEventListener("mousedown", this); + container.addEventListener("mouseup", this, { passive: true }); + container.addEventListener("mousedown", this, { passive: true }); }, /** @@ -290,9 +290,6 @@ function Spinner(props, context) { break; } case "mousedown": { - // Use preventDefault to keep focus on input boxes - event.preventDefault(); - event.target.setCapture(); this.state.mouseState = { down: true, layerX: event.layerX, diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index f438e9ec6..544032e9f 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -218,6 +218,7 @@ function TimePicker(context) { }, _attachEventListeners() { window.addEventListener("message", this); + document.addEventListener("mousedown", this); }, /** @@ -231,6 +232,12 @@ function TimePicker(context) { this.handleMessage(event); break; } + case "mousedown": { + // Use preventDefault to keep focus on input boxes + event.preventDefault(); + event.target.setCapture(); + break; + } } }, -- cgit v1.2.3 From 631b690ac3fecb1406246237d282390283c77e2c Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 14:03:55 +0100 Subject: Bug 1320225: [DateTimeInput] Integration of input type=date input box with picker (part 1) --- toolkit/content/widgets/datekeeper.js | 38 +++++++++--------- toolkit/content/widgets/datepicker.js | 44 +++++++++++++------- toolkit/content/widgets/datetimepopup.xml | 67 ++++++++++++++++++++++++++++--- toolkit/content/widgets/timepicker.js | 6 +-- 4 files changed, 113 insertions(+), 42 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js index 62fcfadbc..de01fdade 100644 --- a/toolkit/content/widgets/datekeeper.js +++ b/toolkit/content/widgets/datekeeper.js @@ -11,7 +11,7 @@ * { * {Number} year * {Number} month - * {Number} date + * {Number} day * } * {Object} options * { @@ -20,7 +20,7 @@ * {Number} calViewSize [optional] * } */ -function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { +function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { this.state = { firstDayOfWeek, weekends, calViewSize, dateObj: new Date(0), @@ -29,7 +29,7 @@ function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], days: [] }; this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek); - this._update(year, month, date); + this._update(year, month, day); } { @@ -48,8 +48,8 @@ function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], * {Number} date [optional] * } */ - set({ year = this.state.year, month = this.state.month, date = this.state.date }) { - this._update(year, month, date); + set({ year = this.state.year, month = this.state.month, day = this.state.day }) { + this._update(year, month, day); }, /** @@ -62,44 +62,44 @@ function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], }, /** - * Set month. Makes sure the date is <= the last day of the month + * Set month. Makes sure the day is <= the last day of the month * @param {Number} month */ setMonth(month) { const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate(); - this._update(this.state.year, month, Math.min(this.state.date, lastDayOfMonth)); + this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth)); }, /** - * Set year. Makes sure the date is <= the last day of the month + * Set year. Makes sure the day is <= the last day of the month * @param {Number} year */ setYear(year) { const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate(); - this._update(year, this.state.month, Math.min(this.state.date, lastDayOfMonth)); + this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth)); }, /** - * Set month by offset. Makes sure the date is <= the last day of the month + * Set month by offset. Makes sure the day is <= the last day of the month * @param {Number} offset */ setMonthByOffset(offset) { const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate(); - this._update(this.state.year, this.state.month + offset, Math.min(this.state.date, lastDayOfMonth)); + this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth)); }, /** * Update the states. * @param {Number} year [description] * @param {Number} month [description] - * @param {Number} date [description] + * @param {Number} day [description] */ - _update(year, month, date) { + _update(year, month, day) { // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 - this.state.dateObj.setUTCFullYear(year, month, date); + this.state.dateObj.setUTCFullYear(year, month, day); this.state.year = this.state.dateObj.getUTCFullYear(); this.state.month = this.state.dateObj.getUTCMonth(); - this.state.date = this.state.dateObj.getUTCDate(); + this.state.day = this.state.dateObj.getUTCDate(); }, /** @@ -201,14 +201,14 @@ function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], */ _getWeekHeaders(firstDayOfWeek) { let headers = []; - let day = firstDayOfWeek; + let dayOfWeek = firstDayOfWeek; for (let i = 0; i < DAYS_IN_A_WEEK; i++) { headers.push({ - textContent: day % DAYS_IN_A_WEEK, - classNames: this.state.weekends.includes(day % DAYS_IN_A_WEEK) ? ["weekend"] : [] + textContent: dayOfWeek % DAYS_IN_A_WEEK, + classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] }); - day++; + dayOfWeek++; } return headers; }, diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 7453b67eb..210ca856c 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -37,13 +37,13 @@ function DatePicker(context) { const now = new Date(); const { year = now.getFullYear(), month = now.getMonth(), - date = now.getDate(), + day = now.getDate(), locale } = this.props; // TODO: Use calendar info API to get first day of week & weekends // (Bug 1287503) const dateKeeper = new DateKeeper({ - year, month, date + year, month, day }, { calViewSize: CAL_VIEW_SIZE, firstDayOfWeek: 0, @@ -68,6 +68,7 @@ function DatePicker(context) { this.state.isDateSet = true; this._update(); this._dispatchState(); + this._closePopup(); }, setYear: year => { dateKeeper.setYear(year); @@ -148,23 +149,32 @@ function DatePicker(context) { this.context.monthYearView.classList.add("hidden"); }, + /** + * Use postMessage to close the picker. + */ + _closePopup() { + window.postMessage({ + name: "ClosePopup" + }, "*"); + }, + /** * Use postMessage to pass the state of picker to the panel. */ _dispatchState() { - const { year, month, date } = this.state.dateKeeper.state; - const { isYearSet, isMonthSet, isDateSet } = this.state; + const { year, month, day } = this.state.dateKeeper.state; + const { isYearSet, isMonthSet, isDaySet } = this.state; // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ - name: "DatePickerPopupChanged", + name: "PickerPopupChanged", detail: { year, month, - date, + day, isYearSet, isMonthSet, - isDateSet + isDaySet } }, "*"); }, @@ -221,11 +231,11 @@ function DatePicker(context) { */ handleMessage(event) { switch (event.data.name) { - case "DatePickerSetValue": { + case "PickerSetValue": { this.set(event.data.detail); break; } - case "DatePickerInit": { + case "PickerInit": { this.init(event.data.detail); break; } @@ -242,18 +252,22 @@ function DatePicker(context) { * {Number} date [optional] * } */ - set(dateState) { - if (dateState.year != undefined) { + set({ year, month, day }) { + const { dateKeeper } = this.state; + + if (year != undefined) { this.state.isYearSet = true; } - if (dateState.month != undefined) { + if (month != undefined) { this.state.isMonthSet = true; } - if (dateState.date != undefined) { - this.state.isDateSet = true; + if (day != undefined) { + this.state.isDaySet = true; } - this.state.dateKeeper.set(dateState); + dateKeeper.set({ + year, month, day + }); this._update(); } }; diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 69edffe25..86e8780c1 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -20,6 +20,8 @@ "12em" "21em" + "23.1em" + "20.7em" @@ -38,6 +40,14 @@ this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; break; } + case "date": { + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/datepicker.xhtml"); + this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; + break; + } } ]]> @@ -48,7 +58,7 @@ this.pickerState = {}; this.type = undefined; this.dateTimePopupFrame.removeEventListener("load", this, true); - this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false); + this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); ]]> @@ -58,25 +68,39 @@ switch (this.type) { case "time": { this.postMessageToPicker({ - name: "TimePickerSetValue", + name: "PickerSetValue", detail: data.value }); break; } + case "date": { + const { year, month, day } = data.value; + this.postMessageToPicker({ + name: "PickerSetValue", + detail: { + year, + // Month value from input box starts from 1 instead of 0 + month: month == undefined ? undefined : month - 1, + day + } + }); + break; + } } ]]> @@ -112,6 +150,10 @@ } break; } + case "date": { + this.sendPickerValueChanged(this.pickerState); + break; + } } ]]> @@ -128,6 +170,17 @@ })); break; } + case "date": { + this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", { + detail: { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day + } + })); + break; + } } ]]> @@ -155,11 +208,15 @@ } switch (aEvent.data.name) { - case "TimePickerPopupChanged": { + case "PickerPopupChanged": { this.pickerState = aEvent.data.detail; this.setInputBoxValue(); break; } + case "ClosePopup": { + this.closePicker(); + break; + } } ]]> diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index 544032e9f..8c2fb89dd 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -206,7 +206,7 @@ function TimePicker(context) { // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ - name: "TimePickerPopupChanged", + name: "PickerPopupChanged", detail: { hour, minute, @@ -248,11 +248,11 @@ function TimePicker(context) { */ handleMessage(event) { switch (event.data.name) { - case "TimePickerSetValue": { + case "PickerSetValue": { this.set(event.data.detail); break; } - case "TimePickerInit": { + case "PickerInit": { this.init(event.data.detail); break; } -- cgit v1.2.3 From 34125a031ed9c7814d2145070294ead44b7504b3 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 14:10:13 +0100 Subject: Bug 1314544: [DateTimeInput] browser keyboard shortcut does not work when focus is on input box --- toolkit/content/widgets/datetimebox.css | 2 + toolkit/content/widgets/datetimebox.xml | 208 +++++++++++++++++++++++--------- 2 files changed, 151 insertions(+), 59 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css index 4a9593a69..18ff024c7 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -20,6 +20,8 @@ border: 0; margin: 0; ime-mode: disabled; + cursor: default; + -moz-user-select: none; } .datetime-separator { diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 05591e65a..21cc6c1bd 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -282,21 +282,25 @@ if (this.mHourField && !this.mHourField.disabled && !this.mHourField.readOnly) { this.mHourField.value = ""; + this.mHourField.setAttribute("typeBuffer", ""); } if (this.mMinuteField && !this.mMinuteField.disabled && !this.mMinuteField.readOnly) { this.mMinuteField.value = ""; + this.mMinuteField.setAttribute("typeBuffer", ""); } if (this.mSecondField && !this.mSecondField.disabled && !this.mSecondField.readOnly) { this.mSecondField.value = ""; + this.mSecondField.setAttribute("typeBuffer", ""); } if (this.mMillisecField && !this.mMillisecField.disabled && !this.mMillisecField.readOnly) { this.mMillisecField.value = ""; + this.mMillisecField.setAttribute("typeBuffer", ""); } if (this.mDayPeriodField && !this.mDayPeriodField.disabled && @@ -579,9 +583,40 @@ this.mMax = this.mInputElement.max; this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + + this.EVENTS.forEach((eventName) => { + this.addEventListener(eventName, this, { mozSystemGroup: true }); + }); + // Handle keypress separately since we need to catch it on capturing. + this.addEventListener("keypress", this, { + capture: true, + mozSystemGroup: true + }); ]]> + + { + this.removeEventListener(eventName, this, { mozSystemGroup: true }); + }); + this.removeEventListener("keypress", onKeyPress, { + capture: true, + mozSystemGroup: true + }); + ]]> + + + + + + + + @@ -736,72 +771,127 @@ - - - - - - - - - - + + + + - + + - if (!(event.originalTarget instanceof HTMLButtonElement)) { - this.mInputElement.openDateTimePicker(this.getCurrentValue()); - } - ]]> - + + + + - + + - if (key == "Backspace" || key == "Tab") { - return; - } + + + + + + - if (key == "Enter" || key == " ") { - // Close picker on Enter and Space. - this.mInputElement.closeDateTimePicker(); - } + + + + - - + if (!(aEvent.originalTarget instanceof HTMLButtonElement)) { + this.mInputElement.openDateTimePicker(this.getCurrentValue()); + } + ]]> + + + -- cgit v1.2.3 From 8a15fd8d24e4373f462046b46fbe8558f57f3403 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 14:41:19 +0100 Subject: Bug 1286182: Implement the layout for --- toolkit/content/widgets/datetimebox.xml | 420 +++++++++++++++++++++++++++++++- 1 file changed, 419 insertions(+), 1 deletion(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 21cc6c1bd..677d3fc21 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -10,6 +10,405 @@ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xbl="http://www.mozilla.org/xbl"> + + + + + + + + + + input box + this.mMonthPlaceHolder = "mm"; + this.mDayPlaceHolder = "dd"; + this.mYearPlaceHolder = "yyyy"; + this.mSeparatorText = "/"; + this.mMinMonth = 1; + this.mMaxMonth = 12; + this.mMinDay = 1; + this.mMaxDay = 31; + this.mMinYear = 1; + // Maximum year limited by ECMAScript date object range, year <= 275760. + this.mMaxYear = 275760; + this.mMonthDayLength = 2; + this.mYearLength = 4; + this.mMonthPageUpDownInterval = 3; + this.mDayPageUpDownInterval = 7; + this.mYearPageUpDownInterval = 10; + + // Default to en-US, month-day-year order. + this.mMonthField = + document.getAnonymousElementByAttribute(this, "anonid", "input-one"); + this.mDayField = + document.getAnonymousElementByAttribute(this, "anonid", "input-two"); + this.mYearField = + document.getAnonymousElementByAttribute(this, "anonid", "input-three"); + this.mYearField.size = this.mYearLength; + this.mYearField.maxLength = this.mMaxYear.toString().length; + + this.mMonthField.placeholder = this.mMonthPlaceHolder; + this.mDayField.placeholder = this.mDayPlaceHolder; + this.mYearField.placeholder = this.mYearPlaceHolder; + + this.mMonthField.setAttribute("min", this.mMinMonth); + this.mMonthField.setAttribute("max", this.mMaxMonth); + this.mMonthField.setAttribute("pginterval", + this.mMonthPageUpDownInterval); + this.mDayField.setAttribute("min", this.mMinDay); + this.mDayField.setAttribute("max", this.mMaxDay); + this.mDayField.setAttribute("pginterval", this.mDayPageUpDownInterval); + this.mYearField.setAttribute("min", this.mMinYear); + this.mYearField.setAttribute("max", this.mMaxYear); + this.mYearField.setAttribute("pginterval", + this.mYearPageUpDownInterval); + + this.mDaySeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-first"); + this.mDaySeparator.textContent = this.mSeparatorText; + this.mYearSeparator = + document.getAnonymousElementByAttribute(this, "anonid", "sep-second"); + this.mYearSeparator.textContent = this.mSeparatorText; + + if (this.mInputElement.value) { + this.setFieldsFromInputValue(); + } + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + max) { + return true; + } + + return false; + ]]> + + + + + + this.getDaysInMonth(month, year)) { + // Don't set invalid date, otherwise input element's value will be + // set to empty. + return; + } + + let date = [year, month, day].join("-"); + + this.log("setInputValueFromFields: " + date); + this.mInputElement.setUserInput(date); + ]]> + + + + + + + + + + + + + = targetField.maxLength || n * 10 > max) { + buffer = ""; + this.advanceToNextField(); + } + targetField.setAttribute("typeBuffer", buffer); + } + ]]> + + + + + + + + max) { + value -= (max - min + 1); + } else if (value < min) { + value += (max - min + 1); + } + this.setFieldValue(aTargetField, value); + aTargetField.select(); + ]]> + + + + + + + + + + + + + + + + + + + + + max) { + value = max; + } + } + + if (aField == this.mMonthField || + aField == this.mDayField) { + // prepend zero + if (value < 10) { + value = "0" + value; + } + } else { + // prepend zeroes + if (value < 10) { + value = "000" + value; + } else if (value < 100) { + value = "00" + value; + } else if (value < 1000) { + value = "0" + value; + } + + if (value.toString().length > this.mYearLength && + value.toString().length <= this.mMaxYear.toString().length) { + this.mYearField.size = value.toString().length; + } + } + + aField.value = value; + ]]> + + + + + + + + + + + + @@ -745,6 +1144,12 @@ + + + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + + + + + + + + + -- cgit v1.2.3 From 73c6f6b3deb95fff270898dfb3e48c116f714d5c Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 14:45:58 +0100 Subject: Bug 1320225: [DateTimeInput] Integration of input type=date input box with picker (part 2) --- toolkit/content/browser-content.js | 1 + toolkit/content/widgets/datetimebox.xml | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index 4ae798fbd..731b55185 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1737,6 +1737,7 @@ let DateTimePickerListener = { } case "MozUpdateDateTimePicker": { let value = this._inputElement.getDateTimeInputBoxValue(); + value.type = this._inputElement.type; sendAsyncMessage("FormDateTime:UpdatePicker", { value }); break; } diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 677d3fc21..5859f80dd 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -199,10 +199,24 @@ + -- cgit v1.2.3 From eddd0de2ae80e176011f41a5400e81522d53f4f3 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Wed, 14 Feb 2018 16:10:55 +0100 Subject: [follow up] Bug 1325922 - Add arrows svg file and style month-year button for date picker --- toolkit/content/datepicker.xhtml | 1 - 1 file changed, 1 deletion(-) (limited to 'toolkit/content') diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml index d79b53102..4da6e398f 100644 --- a/toolkit/content/datepicker.xhtml +++ b/toolkit/content/datepicker.xhtml @@ -24,7 +24,6 @@
-
-- cgit v1.2.3 From e25430117a67f5c898e5e9388ebd44b185d469ab Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 12:17:17 +0200 Subject: moebius#92: HTML - input - datetime + native in moebius: Bug 1317600: https://bugzilla.mozilla.org/show_bug.cgi?id=1317600 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1283385: https://bugzilla.mozilla.org/show_bug.cgi?id=1283385 Bug 1323109: https://bugzilla.mozilla.org/show_bug.cgi?id=1323109 Bug 1314544: https://bugzilla.mozilla.org/show_bug.cgi?id=1314544 Bug 1286182: https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 Bug 1325922: https://bugzilla.mozilla.org/show_bug.cgi?id=1325922 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1320225: https://bugzilla.mozilla.org/show_bug.cgi?id=1320225 Bug 1341190: https://bugzilla.mozilla.org/show_bug.cgi?id=1341190 --- toolkit/content/tests/browser/browser.ini | 1 + .../tests/browser/browser_datetime_datepicker.js | 222 +++++++++++++++++++++ toolkit/content/tests/browser/head.js | 85 ++++++++ toolkit/content/widgets/calendar.js | 22 +- toolkit/content/widgets/datekeeper.js | 171 ++++++++++------ toolkit/content/widgets/datepicker.js | 94 ++++----- toolkit/content/widgets/datetimebox.css | 10 +- toolkit/content/widgets/datetimebox.xml | 156 ++++++++++++--- toolkit/content/widgets/datetimepicker.xml | 6 +- toolkit/content/widgets/datetimepopup.xml | 101 +++++++++- toolkit/content/widgets/spinner.js | 4 +- 11 files changed, 700 insertions(+), 172 deletions(-) create mode 100644 toolkit/content/tests/browser/browser_datetime_datepicker.js (limited to 'toolkit/content') diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index 278b2ffe0..67ba2f850 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -26,6 +26,7 @@ skip-if = !e10s [browser_contentTitle.js] [browser_crash_previous_frameloader.js] run-if = e10s && crashreporter +[browser_datetime_datepicker.js] [browser_default_image_filename.js] [browser_f7_caret_browsing.js] [browser_findbar.js] diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js new file mode 100644 index 000000000..a0db761b7 --- /dev/null +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -0,0 +1,222 @@ +/* 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"; + +const MONTH_YEAR = ".month-year", + DAYS_VIEW = ".days-view", + BTN_PREV_MONTH = ".prev", + BTN_NEXT_MONTH = ".next"; +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format; + +// Create a list of abbreviations for calendar class names +const W = "weekend", + O = "outside", + S = "selection", + R = "out-of-range", + T = "today", + P = "off-step"; + +// Calendar classlist for 2016-12. Used to verify the classNames are correct. +const calendarClasslist_201612 = [ + [W, O], [O], [O], [O], [], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [S], [], [W], + [W], [], [], [], [], [], [W], + [W], [], [], [], [], [], [W], + [W, O], [O], [O], [O], [O], [O], [W, O], +]; + +function getCalendarText() { + return helper.getChildren(DAYS_VIEW).map(child => child.textContent); +} + +function getCalendarClassList() { + return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList)); +} + +function mergeArrays(a, b) { + return a.map((classlist, index) => classlist.concat(b[index])); +} + +let helper = new DateTimeTestHelper(); + +registerCleanupFunction(() => { + helper.cleanup(); +}); + +/** + * Test that date picker opens to today's date when input field is blank + */ +add_task(async function test_datepicker_today() { + const date = new Date(); + + await helper.openPicker("data:text/html, "); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date)); + + await helper.tearDown(); +}); + +/** + * Test that date picker opens to the correct month, with calendar days + * displayed correctly, given a date value is set. + */ +add_task(async function test_datepicker_open() { + const inputValue = "2016-12-15"; + + await helper.openPicker(`data:text/html, `); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + Assert.deepEqual( + getCalendarText(), + [ + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + "11", "12", "13", "14", "15", "16", "17", + "18", "19", "20", "21", "22", "23", "24", + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + ], + "2016-12", + ); + Assert.deepEqual( + getCalendarClassList(), + calendarClasslist_201612, + "2016-12 classNames" + ); + + await helper.tearDown(); +}); + +/** + * When the prev month button is clicked, calendar should display the dates for + * the previous month. + */ +add_task(async function test_datepicker_prev_month_btn() { + const inputValue = "2016-12-15"; + const prevMonth = "2016-11-01"; + + await helper.openPicker(`data:text/html, `); + helper.click(helper.getElement(BTN_PREV_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "30", "31", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "10", "11", "12", + "13", "14", "15", "16", "17", "18", "19", + "20", "21", "22", "23", "24", "25", "26", + "27", "28", "29", "30", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "2016-11", + ); + + await helper.tearDown(); +}); + +/** + * When the next month button is clicked, calendar should display the dates for + * the next month. + */ +add_task(async function test_datepicker_next_month_btn() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, `); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + Assert.deepEqual( + getCalendarText(), + [ + "25", "26", "27", "28", "29", "30", "31", + "1", "2", "3", "4", "5", "6", "7", + "8", "9", "10", "11", "12", "13", "14", + "15", "16", "17", "18", "19", "20", "21", + "22", "23", "24", "25", "26", "27", "28", + "29", "30", "31", "1", "2", "3", "4", + ], + "2017-01", + ); + + await helper.tearDown(); +}); + +/** + * When a date on the calendar is clicked, date picker should close and set + * value to the input box. + */ +add_task(async function test_datepicker_clicked() { + const inputValue = "2016-12-15"; + const firstDayOnCalendar = "2016-11-27"; + + await helper.openPicker(`data:text/html, `); + // Click the first item (top-left corner) of the calendar + helper.click(helper.getElement(DAYS_VIEW).children[0]); + await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() { + let inputEl = content.document.querySelector("input"); + await ContentTaskUtils.waitForEvent(inputEl, "input"); + }); + + Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar); + + await helper.tearDown(); +}); + +/** + * When min and max attributes are set, calendar should show some dates as + * out-of-range. + */ +add_task(async function test_datepicker_min_max() { + const inputValue = "2016-12-15"; + const inputMin = "2016-12-05"; + const inputMax = "2016-12-25"; + + await helper.openPicker(`data:text/html, `); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // R denotes out-of-range + [R], [R], [R], [R], [R], [R], [R], + [R], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [], [], [], [], [], [], + [], [R], [R], [R], [R], [R], [R], + [R], [R], [R], [R], [R], [R], [R], + ]), + "2016-12 with min & max", + ); + + await helper.tearDown(); +}); + +/** + * When step attribute is set, calendar should show some dates as off-step. + */ +add_task(async function test_datepicker_step() { + const inputValue = "2016-12-15"; + const inputStep = "5"; + + await helper.openPicker(`data:text/html, `); + + Assert.deepEqual( + getCalendarClassList(), + mergeArrays(calendarClasslist_201612, [ + // P denotes off-step + [P], [P], [P], [], [P], [P], [P], + [P], [], [P], [P], [P], [P], [], + [P], [P], [P], [P], [], [P], [P], + [P], [P], [], [P], [P], [P], [P], + [], [P], [P], [P], [P], [], [P], + [P], [P], [P], [], [P], [P], [P], + ]), + "2016-12 with step", + ); + + await helper.tearDown(); +}); diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index 1c6c2b54f..e3ef19538 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -31,3 +31,88 @@ function pushPrefs(...aPrefs) { SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve); return deferred.promise; } + +/** + * Helper class for testing datetime input picker widget + */ +class DateTimeTestHelper { + constructor() { + this.panel = document.getElementById("DateTimePickerPanel"); + this.panel.setAttribute("animate", false); + this.tab = null; + this.frame = null; + } + + /** + * Opens a new tab with the URL of the test page, and make sure the picker is + * ready for testing. + * + * @param {String} pageUrl + */ + async openPicker(pageUrl) { + this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl); + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + // If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached + if (!this.panel.dateTimePopupFrame) { + await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady") + } + this.frame = this.panel.dateTimePopupFrame; + await BrowserTestUtils.waitForEvent(this.frame, "load", true); + // Wait for picker elements to be ready and open panel transition to end + await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady"); + } + + /** + * Find an element on the picker. + * + * @param {String} selector + * @return {DOMElement} + */ + getElement(selector) { + return this.frame.contentDocument.querySelector(selector); + } + + /** + * Find the children of an element on the picker. + * + * @param {String} selector + * @return {Array} + */ + getChildren(selector) { + return Array.from(this.getElement(selector).children); + } + + /** + * Click on an element + * + * @param {DOMElement} element + */ + click(element) { + EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow); + } + + /** + * Close the panel and the tab + */ + async tearDown() { + if (!this.panel.hidden) { + let pickerClosePromise = new Promise(resolve => { + this.panel.addEventListener("popuphidden", resolve, {once: true}); + }); + this.panel.closePicker(); + await pickerClosePromise; + } + await BrowserTestUtils.removeTab(this.tab); + this.tab = null; + } + + /** + * Clean up after tests. Remove the frame to prevent leak. + */ + cleanup() { + this.frame.remove(); + this.frame = null; + this.panel.removeAttribute("animate"); + this.panel = null; + } +} diff --git a/toolkit/content/widgets/calendar.js b/toolkit/content/widgets/calendar.js index 72e0d9d61..80c2976e0 100644 --- a/toolkit/content/widgets/calendar.js +++ b/toolkit/content/widgets/calendar.js @@ -54,23 +54,21 @@ function Calendar(options, context) { * { * {Number} textContent * {Array} classNames + * {Boolean} enabled * } * {Function} getDayString: Transform day number to string * {Function} getWeekHeaderString: Transform day of week number to string - * {Function} setValue: Set value for dateKeeper - * {Number} selectionValue: The selection date value + * {Function} setSelection: Set selection for dateKeeper * } */ setProps(props) { if (props.isVisible) { // Transform the days and weekHeaders array for rendering - const days = props.days.map(({ dateValue, textContent, classNames }) => { + const days = props.days.map(({ dateObj, classNames, enabled }) => { return { - dateValue, - textContent: props.getDayString(textContent), - className: dateValue == props.selectionValue ? - classNames.concat("selection").join(" ") : - classNames.join(" ") + textContent: props.getDayString(dateObj.getUTCDate()), + className: classNames.join(" "), + enabled }; }); const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => { @@ -152,10 +150,10 @@ function Calendar(options, context) { case "click": { if (event.target.parentNode == this.context.daysView) { let targetId = event.target.dataset.id; - this.props.setValue({ - selectionValue: this.props.days[targetId].dateValue, - dateValue: this.props.days[targetId].dateValue - }); + let targetObj = this.props.days[targetId]; + if (targetObj.enabled) { + this.props.setSelection(targetObj.dateObj); + } } break; } diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js index de01fdade..9777ee647 100644 --- a/toolkit/content/widgets/datekeeper.js +++ b/toolkit/content/widgets/datekeeper.js @@ -6,41 +6,72 @@ /** * DateKeeper keeps track of the date states. - * - * @param {Object} date parts - * { - * {Number} year - * {Number} month - * {Number} day - * } - * {Object} options - * { - * {Number} firstDayOfWeek [optional] - * {Array} weekends [optional] - * {Number} calViewSize [optional] - * } */ -function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { - this.state = { - firstDayOfWeek, weekends, calViewSize, - dateObj: new Date(0), - years: [], - months: [], - days: [] - }; - this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek); - this._update(year, month, day); +function DateKeeper(props) { + this.init(props); } { const DAYS_IN_A_WEEK = 7, MONTHS_IN_A_YEAR = 12, YEAR_VIEW_SIZE = 200, - YEAR_BUFFER_SIZE = 10; + YEAR_BUFFER_SIZE = 10, + // The min and max values are derived from the ECMAScript spec: + // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 + MIN_DATE = -8640000000000000, + MAX_DATE = 8640000000000000; DateKeeper.prototype = { + get year() { + return this.state.dateObj.getUTCFullYear(); + }, + + get month() { + return this.state.dateObj.getUTCMonth(); + }, + + get day() { + return this.state.dateObj.getUTCDate(); + }, + + get selection() { + return this.state.selection; + }, + + /** + * Initialize DateKeeper + * @param {Number} year + * @param {Number} month + * @param {Number} day + * @param {String} min + * @param {String} max + * @param {Number} firstDayOfWeek + * @param {Array} weekends + * @param {Number} calViewSize + */ + init({ year, month, day, min, max, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) { + const today = new Date(); + const isDateSet = year != undefined && month != undefined && day != undefined; + + this.state = { + firstDayOfWeek, weekends, calViewSize, + min: new Date(min != undefined ? min : MIN_DATE), + max: new Date(max != undefined ? max : MAX_DATE), + today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), + weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), + years: [], + months: [], + days: [], + selection: { year, month, day }, + }; + + this.state.dateObj = isDateSet ? + this._newUTCDate(year, month, day) : + new Date(this.state.today); + }, /** - * Set new date + * Set new date. The year is always treated as full year, so the short-form + * is not supported. * @param {Object} date parts * { * {Number} year [optional] @@ -48,17 +79,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * {Number} date [optional] * } */ - set({ year = this.state.year, month = this.state.month, day = this.state.day }) { - this._update(year, month, day); + set({ year = this.year, month = this.month, day = this.day }) { + // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 + this.state.dateObj.setUTCFullYear(year, month, day); }, /** - * Set date with value - * @param {Number} value: Date value + * Set selection date + * @param {Number} year + * @param {Number} month + * @param {Number} day */ - setValue(value) { - const dateObj = new Date(value); - this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate()); + setSelection({ year, month, day }) { + this.state.selection.year = year; + this.state.selection.month = month; + this.state.selection.day = day; }, /** @@ -66,8 +101,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * @param {Number} month */ setMonth(month) { - const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate(); - this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth)); + const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate(); + this.set({ year: this.year, + month, + day: Math.min(this.day, lastDayOfMonth) }); }, /** @@ -75,8 +112,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * @param {Number} year */ setYear(year) { - const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate(); - this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth)); + const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate(); + this.set({ year, + month: this.month, + day: Math.min(this.day, lastDayOfMonth) }); }, /** @@ -84,22 +123,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * @param {Number} offset */ setMonthByOffset(offset) { - const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate(); - this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth)); - }, - - /** - * Update the states. - * @param {Number} year [description] - * @param {Number} month [description] - * @param {Number} day [description] - */ - _update(year, month, day) { - // Use setUTCFullYear so that year 99 doesn't get parsed as 1999 - this.state.dateObj.setUTCFullYear(year, month, day); - this.state.year = this.state.dateObj.getUTCFullYear(); - this.state.month = this.state.dateObj.getUTCMonth(); - this.state.day = this.state.dateObj.getUTCDate(); + 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) }); }, /** @@ -111,7 +138,6 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * } */ getMonths() { - // TODO: add min/max and step support let months = []; for (let i = 0; i < MONTHS_IN_A_YEAR; i++) { @@ -133,12 +159,11 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * } */ getYears() { - // TODO: add min/max and step support let years = []; const firstItem = this.state.years[0]; const lastItem = this.state.years[this.state.years.length - 1]; - const currentYear = this.state.dateObj.getUTCFullYear(); + const currentYear = this.year; // Generate new years array when the year is outside of the first & // last item range. If not, return the cached result. @@ -161,30 +186,43 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], * Get days for calendar * @return {Array} * { - * {Number} dateValue - * {Number} textContent + * {Date} dateObj * {Array} classNames + * {Boolean} enabled * } */ getDays() { - // TODO: add min/max and step support - let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + // TODO: add step support + const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); + const month = this.month; let days = []; - let month = this.state.dateObj.getUTCMonth(); for (let i = 0; i < this.state.calViewSize; i++) { - let dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); + const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); let classNames = []; + let enabled = true; if (this.state.weekends.includes(dateObj.getUTCDay())) { classNames.push("weekend"); } if (month != dateObj.getUTCMonth()) { classNames.push("outside"); } + if (this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate()) { + classNames.push("selection"); + } + if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) { + classNames.push("out-of-range"); + enabled = false; + } + if (this.state.today.getTime() == dateObj.getTime()) { + classNames.push("today"); + } days.push({ - dateValue: dateObj.getTime(), - textContent: dateObj.getUTCDate(), - classNames + dateObj, + classNames, + enabled, }); } return days; @@ -193,20 +231,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], /** * Get week headers for calendar * @param {Number} firstDayOfWeek + * @param {Array} weekends * @return {Array} * { * {Number} textContent * {Array} classNames * } */ - _getWeekHeaders(firstDayOfWeek) { + _getWeekHeaders(firstDayOfWeek, weekends) { let headers = []; let dayOfWeek = firstDayOfWeek; for (let i = 0; i < DAYS_IN_A_WEEK; i++) { headers.push({ textContent: dayOfWeek % DAYS_IN_A_WEEK, - classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] + classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : [] }); dayOfWeek++; } diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 210ca856c..317f0ae94 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -20,6 +20,12 @@ function DatePicker(context) { * {Number} year [optional] * {Number} month [optional] * {Number} date [optional] + * {String} min + * {String} max + * {Number} firstDayOfWeek + * {Array} weekends + * {Array} monthStrings + * {Array} weekdayStrings * {String} locale [optional]: User preferred locale * } */ @@ -28,57 +34,54 @@ function DatePicker(context) { this._setDefaultState(); this._createComponents(); this._update(); + document.dispatchEvent(new CustomEvent("PickerReady")); }, /* * Set initial date picker states. */ _setDefaultState() { - const now = new Date(); - const { year = now.getFullYear(), - month = now.getMonth(), - day = now.getDate(), - locale } = this.props; - - // TODO: Use calendar info API to get first day of week & weekends - // (Bug 1287503) + const { year, month, day, min, max, firstDayOfWeek, weekends, + monthStrings, weekdayStrings, locale } = this.props; const dateKeeper = new DateKeeper({ - year, month, day - }, { - calViewSize: CAL_VIEW_SIZE, - firstDayOfWeek: 0, - weekends: [0] + year, month, day, min, max, firstDayOfWeek, weekends, + calViewSize: CAL_VIEW_SIZE }); this.state = { dateKeeper, locale, isMonthPickerVisible: false, - isYearSet: false, - isMonthSet: false, - isDateSet: false, getDayString: new Intl.NumberFormat(locale).format, - // TODO: use calendar terms when available (Bug 1287677) - getWeekHeaderString: weekday => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday], - setValue: ({ dateValue, selectionValue }) => { - dateKeeper.setValue(dateValue); - this.state.selectionValue = selectionValue; - this.state.isYearSet = true; - this.state.isMonthSet = true; - this.state.isDateSet = true; + getWeekHeaderString: weekday => weekdayStrings[weekday], + getMonthString: month => monthStrings[month], + setSelection: date => { + dateKeeper.setSelection({ + year: date.getUTCFullYear(), + month: date.getUTCMonth(), + day: date.getUTCDate(), + }); this._update(); this._dispatchState(); this._closePopup(); }, setYear: year => { dateKeeper.setYear(year); - this.state.isYearSet = true; + dateKeeper.setSelection({ + year, + month: dateKeeper.selection.month, + day: dateKeeper.selection.day, + }); this._update(); this._dispatchState(); }, setMonth: month => { dateKeeper.setMonth(month); - this.state.isMonthSet = true; + dateKeeper.setSelection({ + year: dateKeeper.selection.year, + month, + day: dateKeeper.selection.day, + }); this._update(); this._dispatchState(); }, @@ -104,6 +107,7 @@ function DatePicker(context) { monthYear: new MonthYear({ setYear: this.state.setYear, setMonth: this.state.setMonth, + getMonthString: this.state.getMonthString, locale: this.state.locale }, { monthYear: this.context.monthYear, @@ -116,7 +120,7 @@ function DatePicker(context) { * Update date picker and its components. */ _update() { - const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state; + const { dateKeeper, isMonthPickerVisible } = this.state; if (isMonthPickerVisible) { this.state.months = dateKeeper.getMonths(); @@ -128,9 +132,7 @@ function DatePicker(context) { this.components.monthYear.setProps({ isVisible: isMonthPickerVisible, dateObj: dateKeeper.state.dateObj, - month: dateKeeper.state.month, months: this.state.months, - year: dateKeeper.state.year, years: this.state.years, toggleMonthPicker: this.state.toggleMonthPicker }); @@ -138,10 +140,9 @@ function DatePicker(context) { isVisible: !isMonthPickerVisible, days: this.state.days, weekHeaders: dateKeeper.state.weekHeaders, - setValue: this.state.setValue, + setSelection: this.state.setSelection, getDayString: this.state.getDayString, - getWeekHeaderString: this.state.getWeekHeaderString, - selectionValue + getWeekHeaderString: this.state.getWeekHeaderString }); isMonthPickerVisible ? @@ -162,8 +163,7 @@ function DatePicker(context) { * Use postMessage to pass the state of picker to the panel. */ _dispatchState() { - const { year, month, day } = this.state.dateKeeper.state; - const { isYearSet, isMonthSet, isDaySet } = this.state; + const { year, month, day } = this.state.dateKeeper.selection; // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ @@ -172,9 +172,6 @@ function DatePicker(context) { year, month, day, - isYearSet, - isMonthSet, - isDaySet } }, "*"); }, @@ -255,19 +252,12 @@ function DatePicker(context) { set({ year, month, day }) { const { dateKeeper } = this.state; - if (year != undefined) { - this.state.isYearSet = true; - } - if (month != undefined) { - this.state.isMonthSet = true; - } - if (day != undefined) { - this.state.isDaySet = true; - } - dateKeeper.set({ year, month, day }); + dateKeeper.setSelection({ + year, month, day + }); this._update(); } }; @@ -280,12 +270,12 @@ function DatePicker(context) { * {String} locale * {Function} setYear * {Function} setMonth + * {Function} getMonthString * } * @param {DOMElement} context */ function MonthYear(options, context) { const spinnerSize = 5; - const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "short" }).format; const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format; const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format; @@ -298,7 +288,7 @@ function DatePicker(context) { this.state.isMonthSet = true; options.setMonth(month); }, - getDisplayString: month => monthFormat(new Date(0, month)), + getDisplayString: options.getMonthString, viewportSize: spinnerSize }, context.monthYearView), year: new Spinner({ @@ -323,8 +313,6 @@ function DatePicker(context) { * { * {Boolean} isVisible * {Date} dateObj - * {Number} month - * {Number} year * {Array} months * {Array} years * {Function} toggleMonthPicker @@ -336,14 +324,14 @@ function DatePicker(context) { if (props.isVisible) { this.context.monthYear.classList.add("active"); this.components.month.setState({ - value: props.month, + value: props.dateObj.getUTCMonth(), items: props.months, isInfiniteScroll: true, isValueSet: this.state.isMonthSet, smoothScroll: !this.state.firstOpened }); this.components.year.setState({ - value: props.year, + value: props.dateObj.getUTCFullYear(), items: props.years, isInfiniteScroll: false, isValueSet: this.state.isYearSet, diff --git a/toolkit/content/widgets/datetimebox.css b/toolkit/content/widgets/datetimebox.css index 18ff024c7..ce638078f 100644 --- a/toolkit/content/widgets/datetimebox.css +++ b/toolkit/content/widgets/datetimebox.css @@ -8,9 +8,17 @@ .datetime-input-box-wrapper { -moz-appearance: none; display: inline-flex; + flex: 1; cursor: default; background-color: inherit; color: inherit; + min-width: 0; + justify-content: space-between; +} + +.datetime-input-edit-wrapper { + overflow: hidden; + white-space: nowrap; } .datetime-input { @@ -43,5 +51,5 @@ height: 12px; width: 12px; align-self: center; - justify-content: flex-end; + flex: none; } diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 5859f80dd..c276265a3 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -4,6 +4,11 @@ - 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/. --> + +%datetimeboxDTD; +]> + input box - this.mMonthPlaceHolder = "mm"; - this.mDayPlaceHolder = "dd"; - this.mYearPlaceHolder = "yyyy"; + /* eslint-disable no-multi-spaces */ + this.mYearPlaceHolder = ]]>"&date.year.placeholder;""&date.month.placeholder;""&date.day.placeholder;" @@ -107,9 +114,11 @@ this.mYearField.setAttribute("typeBuffer", ""); } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> @@ -170,6 +179,13 @@ @@ -217,6 +237,9 @@ if (!this.isEmpty(day)) { this.setFieldValue(this.mDayField, day); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> @@ -302,6 +325,12 @@ let targetField = aEvent.originalTarget; let key = aEvent.key; + // Home/End key does nothing on year field. + if (targetField == this.mYearField && (key == "Home" || + key == "End")) { + return; + } + switch (key) { case "ArrowUp": this.incrementFieldValue(targetField, 1); @@ -406,11 +435,13 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> - + + @@ -551,7 +583,7 @@ } this.log("setFieldsFromInputValue: " + value); - let [hour, minute, second] = value.split(':'); + let [hour, minute, second] = value.split(":"); this.setFieldValue(this.mHourField, hour); this.setFieldValue(this.mMinuteField, minute); @@ -617,6 +649,13 @@ @@ -678,6 +721,9 @@ if (!this.isEmpty(minute)) { this.setFieldValue(this.mMinuteField, minute); } + + // Update input element's .value if needed. + this.setInputValueFromFields(); ]]> @@ -721,9 +767,11 @@ this.mDayPeriodField.value = ""; } - if (!aFromInputElement) { + if (!aFromInputElement && this.mInputElement.value) { this.mInputElement.setUserInput(""); } + + this.updateResetButtonVisibility(); ]]> @@ -793,6 +841,7 @@ this.mDayPeriodField.value == this.mAMIndicator ? this.mPMIndicator : this.mAMIndicator; this.mDayPeriodField.select(); + this.updateResetButtonVisibility(); this.setInputValueFromFields(); return; } @@ -850,6 +899,7 @@ this.mDayPeriodField.value = this.mPMIndicator; this.mDayPeriodField.select(); } + this.updateResetButtonVisibility(); return; } @@ -905,16 +955,30 @@ } aField.value = value; + this.updateResetButtonVisibility(); ]]> - + + @@ -963,7 +1027,8 @@ - + - + @@ -997,6 +1061,9 @@ this.mStep = this.mInputElement.step; this.mIsPickerOpen = false; + this.mResetButton = + document.getAnonymousElementByAttribute(this, "anonid", "reset-button"); + this.EVENTS.forEach((eventName) => { this.addEventListener(eventName, this, { mozSystemGroup: true }); }); @@ -1005,6 +1072,9 @@ capture: true, mozSystemGroup: true }); + // This is to close the picker when input element blurs. + this.mInputElement.addEventListener("blur", this, + { mozSystemGroup: true }); ]]> @@ -1025,7 +1095,7 @@ @@ -1041,6 +1111,18 @@ + + + + + + + + + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + + + @@ -1213,6 +1301,12 @@ this.onBlur(aEvent); break; } + case "mousedown": { + if (aEvent.originalTarget == this.mResetButton) { + aEvent.preventDefault(); + } + break; + } case "copy": case "cut": case "paste": { @@ -1245,7 +1339,12 @@ diff --git a/toolkit/content/widgets/datetimepicker.xml b/toolkit/content/widgets/datetimepicker.xml index 5f16f1ff0..1d6a5e772 100644 --- a/toolkit/content/widgets/datetimepicker.xml +++ b/toolkit/content/widgets/datetimepicker.xml @@ -999,13 +999,13 @@ "21em" "23.1em" "20.7em" - + + + @@ -115,6 +127,34 @@ } case "date": { const { year, month, day } = detail.value; + const { firstDayOfWeek, weekends } = + this.getCalendarInfo(locale); + const monthStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/months/january", + "dates/gregorian/months/february", + "dates/gregorian/months/march", + "dates/gregorian/months/april", + "dates/gregorian/months/may", + "dates/gregorian/months/june", + "dates/gregorian/months/july", + "dates/gregorian/months/august", + "dates/gregorian/months/september", + "dates/gregorian/months/october", + "dates/gregorian/months/november", + "dates/gregorian/months/december", + ], "short"); + const weekdayStrings = this.getDisplayNames( + locale, [ + "dates/gregorian/weekdays/sunday", + "dates/gregorian/weekdays/monday", + "dates/gregorian/weekdays/tuesday", + "dates/gregorian/weekdays/wednesday", + "dates/gregorian/weekdays/thursday", + "dates/gregorian/weekdays/friday", + "dates/gregorian/weekdays/saturday", + ], "short"); + this.postMessageToPicker({ name: "PickerInit", detail: { @@ -122,7 +162,13 @@ // Month value from input box starts from 1 instead of 0 month: month == undefined ? undefined : month - 1, day, - locale + firstDayOfWeek, + weekends, + monthStrings, + weekdayStrings, + locale, + min: detail.min, + max: detail.max, } }); break; @@ -184,6 +230,46 @@ } ]]> + + + + + + + + + displayNames.values[key]); + ]]> + - - - - - diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 059e151fc..6ef929f8a 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -299,11 +299,11 @@ function Spinner(props, context) { // An "active" class is needed to simulate :active pseudo-class // because element is not focused. event.target.classList.add("active"); - this._smoothScrollToIndex(index + 1); + this._smoothScrollToIndex(index - 1); } if (event.target == down) { event.target.classList.add("active"); - this._smoothScrollToIndex(index - 1); + this._smoothScrollToIndex(index + 1); } if (event.target.parentNode == spinner) { // Listen to dragging events -- cgit v1.2.3 From 6f1fcab2d81caedd96d9404386bc92f9884c30ce Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 20:56:33 +0200 Subject: Bug 1363672 - Add step support to date picker --- toolkit/content/browser-content.js | 3 +- toolkit/content/widgets/datekeeper.js | 60 +++++++++++++++++++++++++------ toolkit/content/widgets/datepicker.js | 6 ++-- toolkit/content/widgets/datetimepopup.xml | 2 ++ 4 files changed, 57 insertions(+), 14 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index 731b55185..b955bd29d 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1728,9 +1728,10 @@ let DateTimePickerListener = { // element's value. value: Object.keys(value).length > 0 ? value : this._inputElement.value, - step: this._inputElement.step, min: this._inputElement.min, max: this._inputElement.max, + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), }, }); break; diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js index 9777ee647..9517e2154 100644 --- a/toolkit/content/widgets/datekeeper.js +++ b/toolkit/content/widgets/datekeeper.js @@ -45,18 +45,21 @@ function DateKeeper(props) { * @param {Number} day * @param {String} min * @param {String} max + * @param {Number} step + * @param {Number} stepBase * @param {Number} firstDayOfWeek * @param {Array} weekends * @param {Number} calViewSize */ - init({ year, month, day, min, max, 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 isDateSet = year != undefined && month != undefined && day != undefined; this.state = { - firstDayOfWeek, weekends, calViewSize, + step, firstDayOfWeek, weekends, calViewSize, min: new Date(min != undefined ? min : MIN_DATE), max: new Date(max != undefined ? max : MAX_DATE), + stepBase: new Date(stepBase), today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), years: [], @@ -192,33 +195,50 @@ function DateKeeper(props) { * } */ getDays() { - // TODO: add step support const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek); const month = this.month; let days = []; for (let i = 0; i < this.state.calViewSize; i++) { - const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i); + const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), + firstDayOfMonth.getUTCMonth(), + firstDayOfMonth.getUTCDate() + i); let classNames = []; let enabled = true; - if (this.state.weekends.includes(dateObj.getUTCDay())) { + + const isWeekend = this.state.weekends.includes(dateObj.getUTCDay()); + const isCurrentMonth = month == dateObj.getUTCMonth(); + const isSelection = this.state.selection.year == dateObj.getUTCFullYear() && + this.state.selection.month == dateObj.getUTCMonth() && + this.state.selection.day == dateObj.getUTCDate(); + const isOutOfRange = dateObj.getTime() < this.state.min.getTime() || + dateObj.getTime() > this.state.max.getTime(); + const isToday = this.state.today.getTime() == dateObj.getTime(); + const isOffStep = this._checkIsOffStep(dateObj, + this._newUTCDate(dateObj.getUTCFullYear(), + dateObj.getUTCMonth(), + dateObj.getUTCDate() + 1)); + + if (isWeekend) { classNames.push("weekend"); } - if (month != dateObj.getUTCMonth()) { + if (!isCurrentMonth) { classNames.push("outside"); } - if (this.state.selection.year == dateObj.getUTCFullYear() && - this.state.selection.month == dateObj.getUTCMonth() && - this.state.selection.day == dateObj.getUTCDate()) { + if (isSelection && !isOutOfRange && !isOffStep) { classNames.push("selection"); } - if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) { + if (isOutOfRange) { classNames.push("out-of-range"); enabled = false; } - if (this.state.today.getTime() == dateObj.getTime()) { + if (isToday) { classNames.push("today"); } + if (isOffStep) { + classNames.push("off-step"); + enabled = false; + } days.push({ dateObj, classNames, @@ -228,6 +248,24 @@ function DateKeeper(props) { return days; }, + /** + * Check if a date is off step given a starting point and the next increment + * @param {Date} start + * @param {Date} next + * @return {Boolean} + */ + _checkIsOffStep(start, next) { + // If the increment is larger or equal to the step, it must not be off-step. + if (next - start >= this.state.step) { + return false; + } + // Calculate the last valid date + const lastValidStep = Math.floor((next - 1 - this.state.stepBase) / this.state.step); + const lastValidTimeInMs = lastValidStep * this.state.step + this.state.stepBase.getTime(); + // The date is off-step if the last valid date is smaller than the start date + return lastValidTimeInMs < start.getTime(); + }, + /** * Get week headers for calendar * @param {Number} firstDayOfWeek diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 317f0ae94..25b15dae6 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -22,6 +22,8 @@ function DatePicker(context) { * {Number} date [optional] * {String} min * {String} max + * {Number} step + * {Number} stepBase * {Number} firstDayOfWeek * {Array} weekends * {Array} monthStrings @@ -41,10 +43,10 @@ function DatePicker(context) { * Set initial date picker states. */ _setDefaultState() { - const { year, month, day, min, max, firstDayOfWeek, weekends, + const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, monthStrings, weekdayStrings, locale } = this.props; const dateKeeper = new DateKeeper({ - year, month, day, min, max, firstDayOfWeek, weekends, + year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, calViewSize: CAL_VIEW_SIZE }); diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 52df7de75..1cb9617ea 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -169,6 +169,8 @@ locale, min: detail.min, max: detail.max, + step: detail.step, + stepBase: detail.stepBase, } }); break; -- cgit v1.2.3 From 6a44ab26592fbe95b69e1bf4d3a3b0de03a99b26 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 21:14:18 +0200 Subject: Bug 1364026 - (Part 2) Check if min and max attributes on input type date are valid date strings --- toolkit/content/browser-content.js | 4 ++-- toolkit/content/widgets/datekeeper.js | 15 +++++++++------ toolkit/content/widgets/datepicker.js | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index b955bd29d..145de9608 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1728,8 +1728,8 @@ let DateTimePickerListener = { // element's value. value: Object.keys(value).length > 0 ? value : this._inputElement.value, - min: this._inputElement.min, - max: this._inputElement.max, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), step: this._inputElement.getStep(), stepBase: this._inputElement.getStepBase(), }, diff --git a/toolkit/content/widgets/datekeeper.js b/toolkit/content/widgets/datekeeper.js index 9517e2154..4959b9609 100644 --- a/toolkit/content/widgets/datekeeper.js +++ b/toolkit/content/widgets/datekeeper.js @@ -16,9 +16,11 @@ function DateKeeper(props) { MONTHS_IN_A_YEAR = 12, YEAR_VIEW_SIZE = 200, YEAR_BUFFER_SIZE = 10, - // The min and max values are derived from the ECMAScript spec: + // 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: // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 - MIN_DATE = -8640000000000000, MAX_DATE = 8640000000000000; DateKeeper.prototype = { @@ -43,8 +45,8 @@ function DateKeeper(props) { * @param {Number} year * @param {Number} month * @param {Number} day - * @param {String} min - * @param {String} max + * @param {Number} min + * @param {Number} max * @param {Number} step * @param {Number} stepBase * @param {Number} firstDayOfWeek @@ -57,8 +59,9 @@ function DateKeeper(props) { this.state = { step, firstDayOfWeek, weekends, calViewSize, - min: new Date(min != undefined ? min : MIN_DATE), - max: new Date(max != undefined ? max : MAX_DATE), + // min & max are NaN if empty or invalid + min: new Date(Number.isNaN(min) ? MIN_DATE : min), + max: new Date(Number.isNaN(max) ? MAX_DATE : max), stepBase: new Date(stepBase), today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()), weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends), diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 25b15dae6..0c288d917 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -20,8 +20,8 @@ function DatePicker(context) { * {Number} year [optional] * {Number} month [optional] * {Number} date [optional] - * {String} min - * {String} max + * {Number} min + * {Number} max * {Number} step * {Number} stepBase * {Number} firstDayOfWeek -- cgit v1.2.3 From 5941771281f0385328444f2a7f9643fb87e0a2e1 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 22:46:03 +0200 Subject: Bug 1382175 - Fix time picker step and min/max regressions --- toolkit/content/widgets/timekeeper.js | 12 ++++++------ toolkit/content/widgets/timepicker.js | 26 ++++++-------------------- 2 files changed, 12 insertions(+), 26 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/timekeeper.js b/toolkit/content/widgets/timekeeper.js index 2234c9e50..3b4e7eb0a 100644 --- a/toolkit/content/widgets/timekeeper.js +++ b/toolkit/content/widgets/timekeeper.js @@ -14,7 +14,7 @@ * { * {Date} min * {Date} max - * {Number} stepInMs + * {Number} step * {String} format: Either "12" or "24" * } */ @@ -286,15 +286,15 @@ function TimeKeeper(props) { * } */ _getSteps(startValue, endValue, minStep, formatter) { - const { min, max, stepInMs } = this.props; + 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, stepInMs); + 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()) / stepInMs) * stepInMs; + let maxValue = min.valueOf() + Math.floor((max.valueOf() - min.valueOf()) / step) * step; let steps = []; // Increment by timeStep until reaching the end of the range. @@ -410,9 +410,9 @@ function TimeKeeper(props) { * @return {Boolean} */ _isOffStep(time) { - const { min, stepInMs } = this.props; + const { min, step } = this.props; - return (time.valueOf() - min.valueOf()) % stepInMs != 0; + return (time.valueOf() - min.valueOf()) % step != 0; } }; } diff --git a/toolkit/content/widgets/timepicker.js b/toolkit/content/widgets/timepicker.js index 8c2fb89dd..1f0463fe4 100644 --- a/toolkit/content/widgets/timepicker.js +++ b/toolkit/content/widgets/timepicker.js @@ -13,8 +13,6 @@ function TimePicker(context) { const debug = 0 ? console.log.bind(console, "[timepicker]") : function() {}; const DAY_PERIOD_IN_HOURS = 12, - SECOND_IN_MS = 1000, - MINUTE_IN_MS = 60000, DAY_IN_MS = 86400000; TimePicker.prototype = { @@ -24,9 +22,9 @@ function TimePicker(context) { * { * {Number} hour [optional]: Hour in 24 hours format (0~23), default is current hour * {Number} minute [optional]: Minute (0~59), default is current minute - * {String} min [optional]: Minimum time, in 24 hours format. ex: "05:45" - * {String} max [optional]: Maximum time, in 24 hours format. ex: "23:00" - * {Number} step [optional]: Step size in minutes. Default is 60. + * {Number} min: Minimum time, in ms + * {Number} max: Maximum time, in ms + * {Number} step: Step size in ms * {String} format [optional]: "12" for 12 hours, "24" for 24 hours format * {String} locale [optional]: User preferred locale * } @@ -51,11 +49,10 @@ function TimePicker(context) { let timerHour = hour == undefined ? now.getHours() : hour; let timerMinute = minute == undefined ? now.getMinutes() : minute; - // The spec defines 1 step == 1 second, need to convert to ms for timekeeper let timeKeeper = new TimeKeeper({ - min: this._parseTimeString(min) || new Date(0), - max: this._parseTimeString(max) || new Date(DAY_IN_MS - 1), - stepInMs: step ? step * SECOND_IN_MS : MINUTE_IN_MS, + min: new Date(Number.isNaN(min) ? 0 : min), + max: new Date(Number.isNaN(max) ? DAY_IN_MS - 1 : max), + step, format: format || "12" }); timeKeeper.setState({ hour: timerHour, minute: timerMinute }); @@ -63,17 +60,6 @@ function TimePicker(context) { this.state = { timeKeeper }; }, - /** - * Convert a time string from DOM attribute to a date object. - * - * @param {String} timeString: (ex. "10:30", "23:55", "12:34:56.789") - * @return {Date/Boolean} Date object or false if date is invalid. - */ - _parseTimeString(timeString) { - let time = new Date("1970-01-01T" + timeString + "Z"); - return time.toString() == "Invalid Date" ? false : time; - }, - /** * Initalize the spinner components. */ -- cgit v1.2.3 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 From 7dfc8b77b94ef102962b474dcbcee272fb8934f7 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 23:50:46 +0200 Subject: Bug 1381421 - (Part 2) Add browser chrome tests for the minimum and maximum dates --- .../tests/browser/browser_datetime_datepicker.js | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'toolkit/content') diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js index a0db761b7..808ef4fef 100644 --- a/toolkit/content/tests/browser/browser_datetime_datepicker.js +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -220,3 +220,43 @@ add_task(async function test_datepicker_step() { await helper.tearDown(); }); + +add_task(async function test_datepicker_abs_min() { + const inputValue = "0001-01-01"; + await helper.openPicker(`data:text/html, `); + + Assert.deepEqual( + getCalendarText(), + [ + "", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "14", "15", "16", "17", "18", "19", "20", + "21", "22", "23", "24", "25", "26", "27", + "28", "29", "30", "31", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "10", + ], + "0001-01", + ); + + await helper.tearDown(); +}); + +add_task(async function test_datepicker_abs_max() { + const inputValue = "275760-09-13"; + await helper.openPicker(`data:text/html, `); + + Assert.deepEqual( + getCalendarText(), + [ + "31", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "10", "11", "12", "13", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + "", "", "", "", "", "", "", + ], + "275760-09", + ); + + await helper.tearDown(); +}); -- cgit v1.2.3 From 94980111da8fa3cbe24cc20186b239afd41450fa Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Fri, 30 Mar 2018 23:54:41 +0200 Subject: Bug 1371111 - Open picker when input element's padding area is clicked + Bug 1341029: https://bugzilla.mozilla.org/show_bug.cgi?id=1341029 (partially - partially - fix typo in removeEventListener): https://hg.mozilla.org/mozilla-central/diff/6055065a2ed5/toolkit/content/widgets/datetimebox.xml + Bug 1410292: https://bugzilla.mozilla.org/show_bug.cgi?id=1410292 --- toolkit/content/widgets/datetimebox.xml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index c276265a3..c45124ef7 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -1072,6 +1072,10 @@ capture: true, mozSystemGroup: true }); + // This is to open the picker when input element is clicked (this + // includes padding area). + this.mInputElement.addEventListener("click", this, + { mozSystemGroup: true }); // This is to close the picker when input element blurs. this.mInputElement.addEventListener("blur", this, { mozSystemGroup: true }); @@ -1080,22 +1084,26 @@ { this.removeEventListener(eventName, this, { mozSystemGroup: true }); }); - this.removeEventListener("keypress", onKeyPress, { + this.removeEventListener("keypress", this, { capture: true, mozSystemGroup: true }); + this.mInputElement.removeEventListener("click", this, + { mozSystemGroup: true }); + this.mInputElement.removeEventListener("blur", this, + { mozSystemGroup: true }); + + this.mInputElement = null; ]]> @@ -1413,12 +1421,9 @@ Date: Sat, 31 Mar 2018 06:58:05 +0200 Subject: Bug 1390794 - Use 'norolluponanchor' to avoid closing the picker when the anchored input box is clicked --- toolkit/content/widgets/datetimebox.xml | 9 --------- 1 file changed, 9 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index c45124ef7..3a011cac1 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -1076,9 +1076,6 @@ // includes padding area). this.mInputElement.addEventListener("click", this, { mozSystemGroup: true }); - // This is to close the picker when input element blurs. - this.mInputElement.addEventListener("blur", this, - { mozSystemGroup: true }); ]]> @@ -1093,8 +1090,6 @@ }); this.mInputElement.removeEventListener("click", this, { mozSystemGroup: true }); - this.mInputElement.removeEventListener("blur", this, - { mozSystemGroup: true }); this.mInputElement = null; ]]> @@ -1350,10 +1345,6 @@ this.log("onBlur originalTarget: " + aEvent.originalTarget + " target: " + aEvent.target); - if (aEvent.target == this.mInputElement && this.mIsPickerOpen) { - this.mInputElement.closeDateTimePicker(); - } - let target = aEvent.originalTarget; target.setAttribute("typeBuffer", ""); this.setInputValueFromFields(); -- cgit v1.2.3 From de16d196679e3deaf8797abcc05db5cf0ead6ae8 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 06:58:52 +0200 Subject: Bug 1401876 - (Part 1) Close datetime picker on popuphidden --- toolkit/content/tests/browser/head.js | 1 + toolkit/content/widgets/datetimepopup.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'toolkit/content') diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index e3ef19538..399b63e34 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -99,6 +99,7 @@ class DateTimeTestHelper { let pickerClosePromise = new Promise(resolve => { this.panel.addEventListener("popuphidden", resolve, {once: true}); }); + this.panel.hidePopup(); this.panel.closePicker(); await pickerClosePromise; } diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index 1cb9617ea..b4335e1ce 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -70,7 +70,6 @@ this.dateTimePopupFrame.removeEventListener("load", this, true); this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false); this.dateTimePopupFrame.setAttribute("src", ""); - this.hidePopup(); this.hidden = true; ]]> @@ -302,6 +301,7 @@ break; } case "ClosePopup": { + this.hidePopup(); this.closePicker(); break; } -- cgit v1.2.3 From 2f54cd30fc6f6c16db7e95819740e1f229b20984 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 07:02:03 +0200 Subject: Bug 1401876 - (Part 2) Add browser-chrome test for reopening picker --- .../tests/browser/browser_datetime_datepicker.js | 22 ++++++++++++++++++++++ toolkit/content/tests/browser/head.js | 6 +++++- 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'toolkit/content') diff --git a/toolkit/content/tests/browser/browser_datetime_datepicker.js b/toolkit/content/tests/browser/browser_datetime_datepicker.js index 808ef4fef..966a74e7a 100644 --- a/toolkit/content/tests/browser/browser_datetime_datepicker.js +++ b/toolkit/content/tests/browser/browser_datetime_datepicker.js @@ -167,6 +167,28 @@ add_task(async function test_datepicker_clicked() { await helper.tearDown(); }); +/** + * Make sure picker is in correct state when it is reopened. + */ +add_task(async function test_datepicker_reopen_state() { + const inputValue = "2016-12-15"; + const nextMonth = "2017-01-01"; + + await helper.openPicker(`data:text/html, `); + // Navigate to the next month but does not commit the change + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + helper.click(helper.getElement(BTN_NEXT_MONTH)); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth))); + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + + // Ensures the picker opens to the month of the input value + await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser); + await helper.waitForPickerReady(); + Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue))); + + await helper.tearDown(); +}); + /** * When min and max attributes are set, calendar should show some dates as * out-of-range. diff --git a/toolkit/content/tests/browser/head.js b/toolkit/content/tests/browser/head.js index 399b63e34..d7ed7a9ff 100644 --- a/toolkit/content/tests/browser/head.js +++ b/toolkit/content/tests/browser/head.js @@ -57,8 +57,12 @@ class DateTimeTestHelper { await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady") } this.frame = this.panel.dateTimePopupFrame; + await this.waitForPickerReady(); + } + + async waitForPickerReady() { await BrowserTestUtils.waitForEvent(this.frame, "load", true); - // Wait for picker elements to be ready and open panel transition to end + // Wait for picker elements to be ready await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady"); } -- cgit v1.2.3 From 0a2b11d18138283e5fd1520e1230451f2e41a0c9 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 07:05:58 +0200 Subject: Bug 1397114 - Disable smooth scrolling when value changes come from input box --- toolkit/content/widgets/datepicker.js | 11 ++++++----- toolkit/content/widgets/spinner.js | 16 ++-------------- 2 files changed, 8 insertions(+), 19 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index f5659b736..4387ae632 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -124,7 +124,7 @@ function DatePicker(context) { /** * Update date picker and its components. */ - _update() { + _update(options = {}) { const { dateKeeper, isMonthPickerVisible } = this.state; if (isMonthPickerVisible) { @@ -139,7 +139,8 @@ function DatePicker(context) { dateObj: dateKeeper.state.dateObj, months: this.state.months, years: this.state.years, - toggleMonthPicker: this.state.toggleMonthPicker + toggleMonthPicker: this.state.toggleMonthPicker, + noSmoothScroll: options.noSmoothScroll }); this.components.calendar.setProps({ isVisible: !isMonthPickerVisible, @@ -260,7 +261,7 @@ function DatePicker(context) { dateKeeper.setSelection({ year, month, day }); - this._update(); + this._update({ noSmoothScroll: true }); } }; @@ -330,14 +331,14 @@ function DatePicker(context) { items: props.months, isInfiniteScroll: true, isValueSet: this.state.isMonthSet, - smoothScroll: !this.state.firstOpened + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) }); this.components.year.setState({ value: props.dateObj.getUTCFullYear(), items: props.years, isInfiniteScroll: false, isValueSet: this.state.isYearSet, - smoothScroll: !this.state.firstOpened + smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) }); this.state.firstOpened = false; } else { diff --git a/toolkit/content/widgets/spinner.js b/toolkit/content/widgets/spinner.js index 6ef929f8a..4901320b5 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -123,8 +123,6 @@ function Spinner(props, context) { /** * Whenever scroll event is detected: * - Update the index state - * - If a smooth scroll has reached its destination, set [isScrolling] state - * to false * - If the value has changed, update the [value] state and call [setValue] * - If infinite scrolling is on, reset the scrolling position if necessary */ @@ -137,14 +135,8 @@ function Spinner(props, context) { const value = itemsView[this.state.index + viewportTopOffset].value; - // Check if smooth scrolling has reached its destination. - // This prevents input box jump when input box changes values. - if (this.state.value == value && this.state.isScrolling) { - this.state.isScrolling = false; - } - - // Call setValue if value has changed, and is not smooth scrolling - if (this.state.value != value && !this.state.isScrolling) { + // Call setValue if value has changed + if (this.state.value != value) { this.state.value = value; this.props.setValue(value); } @@ -443,10 +435,6 @@ function Spinner(props, context) { _smoothScrollToIndex(index) { const element = this.elements.spinner.children[index]; if (element) { - // Set the isScrolling flag before smooth scrolling begins - // and remove it when it has reached the destination. - // This prevents input box jump when input box changes values - this.state.isScrolling = true; element.scrollIntoView({ behavior: "smooth", block: "start" }); -- cgit v1.2.3 From 09cc05a97f387d2342ad49c64d66da0b0b9ae7e7 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 07:07:45 +0200 Subject: Bug 1406859 - [DateTimePicker] Let the first picker close gracefully before opening a second picker --- toolkit/content/browser-content.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'toolkit/content') diff --git a/toolkit/content/browser-content.js b/toolkit/content/browser-content.js index 145de9608..1376f70a3 100644 --- a/toolkit/content/browser-content.js +++ b/toolkit/content/browser-content.js @@ -1714,6 +1714,14 @@ let DateTimePickerListener = { (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())) { return; } + + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } + this._inputElement = aEvent.originalTarget; this._inputElement.setDateTimePickerState(true); this.addListeners(); -- cgit v1.2.3 From 3430e8690ada79729c9347e46c6b7818bee50b28 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 11:05:38 +0200 Subject: Bug 1446342 - Input type="date" not working if the other form elements has name="document" (any alternative solution) --- toolkit/content/widgets/datetimebox.xml | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datetimebox.xml b/toolkit/content/widgets/datetimebox.xml index 3a011cac1..94574038a 100644 --- a/toolkit/content/widgets/datetimebox.xml +++ b/toolkit/content/widgets/datetimebox.xml @@ -9,6 +9,16 @@ %datetimeboxDTD; ]> + + { this.addEventListener(eventName, this, { mozSystemGroup: true }); @@ -1130,7 +1140,7 @@ -- cgit v1.2.3 From f4d1b4804a0dad0a616b01edc31187dace152165 Mon Sep 17 00:00:00 2001 From: janekptacijarabaci Date: Sat, 31 Mar 2018 11:33:23 +0200 Subject: moebius#110: HTML - input - datetime - Datepicker shows incorrect month for the first day of the month --- toolkit/content/widgets/datepicker.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'toolkit/content') diff --git a/toolkit/content/widgets/datepicker.js b/toolkit/content/widgets/datepicker.js index 4387ae632..0e9c9a6e6 100644 --- a/toolkit/content/widgets/datepicker.js +++ b/toolkit/content/widgets/datepicker.js @@ -279,9 +279,11 @@ function DatePicker(context) { */ function MonthYear(options, context) { const spinnerSize = 5; - const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format; - const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format; - + const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + timeZone: "UTC" }).format; + const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", + month: "long", + timeZone: "UTC" }).format; this.context = context; this.state = { dateFormat }; this.props = {}; @@ -299,7 +301,7 @@ function DatePicker(context) { this.state.isYearSet = true; options.setYear(year); }, - getDisplayString: year => yearFormat(new Date(new Date(0).setFullYear(year))), + getDisplayString: year => yearFormat(new Date(new Date(0).setUTCFullYear(year))), viewportSize: spinnerSize }, context.monthYearView) }; -- cgit v1.2.3