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 +- toolkit/themes/shared/datetimeinputpickers.css | 291 ++++++++++++++++++++ toolkit/themes/shared/jar.inc.mn | 2 +- toolkit/themes/shared/timepicker.css | 153 ----------- 10 files changed, 1138 insertions(+), 163 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 create mode 100644 toolkit/themes/shared/datetimeinputpickers.css delete mode 100644 toolkit/themes/shared/timepicker.css 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(); + } }, /** diff --git a/toolkit/themes/shared/datetimeinputpickers.css b/toolkit/themes/shared/datetimeinputpickers.css new file mode 100644 index 000000000..df93bc3a1 --- /dev/null +++ b/toolkit/themes/shared/datetimeinputpickers.css @@ -0,0 +1,291 @@ +/* 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/. */ + +:root { + --font-size-default: 1.1rem; + --spinner-width: 3rem; + --spinner-margin-top-bottom: 0.4rem; + --spinner-item-height: 2.4rem; + --spinner-item-margin-bottom: 0.1rem; + --spinner-button-height: 1.2rem; + --colon-width: 2rem; + --day-period-spacing-width: 1rem; + --calendar-width: 23.1rem; + --date-picker-item-height: 2.4rem; + + --border: 0.1rem solid #D6D6D6; + --border-radius: 0.3rem; + + --font-color: #191919; + --fill-color: #EBEBEB; + + --selected-font-color: #FFFFFF; + --selected-fill-color: #0996F8; + + --button-font-color: #858585; + --button-font-color-hover: #4D4D4D; + --button-font-color-active: #191919; + + --weekday-font-color: #6C6C6C; + --weekday-outside-font-color: #6C6C6C; + --weekend-font-color: #DA4E44; + --weekend-outside-font-color: #FF988F; + + --disabled-opacity: 0.2; +} + +html { + font-size: 10px; +} + +body { + margin: 0; + color: var(--font-color); + font: message-box; + font-size: var(--font-size-default); +} + +.nav { + display: flex; + width: var(--calendar-width); + height: 2.4rem; + margin-bottom: 0.8rem; + justify-content: space-between; +} + +.nav button { + -moz-appearance: none; + background: none; + border: none; + width: 3rem; + height: var(--date-picker-item-height); +} + +.month-year { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 3rem; + width: 17.1rem; + height: var(--date-picker-item-height); + z-index: 10; +} + +.month-year-view { + position: absolute; + z-index: 5; + padding-top: 3.2rem; + top: 0; + left: 0; + bottom: 0; + width: var(--calendar-width); + background: window; + opacity: 1; + transition: opacity 0.15s; +} + +.month-year-view.hidden { + visibility: hidden; + opacity: 0; +} + +.month-year-view > .spinner-container { + width: 5.5rem; + margin: 0 0.5rem; +} + +.month-year-view .spinner { + transform: scaleY(1); + transform-origin: top; + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner { + transform: scaleY(0); + transition: none; +} + +.month-year-view .spinner > div { + transform: scaleY(1); + transition: transform 0.15s; +} + +.month-year-view.hidden .spinner > div { + transform: scaleY(2.5); + transition: none; +} + +.calendar-container { + cursor: default; + display: flex; + flex-direction: column; + width: var(--calendar-width); +} + +.week-header { + display: flex; +} + +.week-header > div { + color: var(--weekday-font-color); +} + +.week-header > div.weekend { + color: var(--weekend-font-color); +} + +.days-viewport { + height: 15rem; + overflow: hidden; + position: relative; +} + +.days-view { + position: absolute; + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.week-header > div, +.days-view > div { + align-items: center; + display: flex; + height: var(--date-picker-item-height); + margin: 0.05rem 0.15rem; + position: relative; + justify-content: center; + width: 3rem; +} + +.days-view > div.outside { + color: var(--weekday-outside-font-color); +} + +.days-view > div.weekend { + color: var(--weekend-font-color); +} + +.days-view > div.weekend.outside { + color: var(--weekend-outside-font-color); +} + +#time-picker, +.month-year-view { + display: flex; + flex-direction: row; + justify-content: center; +} + +.spinner-container { + display: flex; + flex-direction: column; + width: var(--spinner-width); +} + +.spinner-container > button { + -moz-appearance: none; + border: none; + background: none; + background-color: var(--button-font-color); + height: var(--spinner-button-height); +} + +.spinner-container > button:hover { + background-color: var(--button-font-color-hover); +} + +.spinner-container > button.active { + background-color: var(--button-font-color-active); +} + +.spinner-container > button.up { + background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; +} + +.spinner-container > button.down { + background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; +} + +.spinner-container.hide-buttons > button { + visibility: hidden; +} + +.spinner-container > .spinner { + position: relative; + width: 100%; + margin: var(--spinner-margin-top-bottom) 0; + cursor: default; + overflow-y: scroll; + scroll-snap-type: mandatory; + scroll-snap-points-y: repeat(100%); +} + +.spinner-container > .spinner > div { + box-sizing: border-box; + position: relative; + text-align: center; + padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; + margin-bottom: var(--spinner-item-margin-bottom); + height: var(--spinner-item-height); + -moz-user-select: none; + scroll-snap-coordinate: 0 0; +} + +.spinner-container > .spinner > div:hover::before, +.calendar-container .days-view > div:hover::before { + background: var(--fill-color); + border: var(--border); + border-radius: var(--border-radius); + content: ""; + position: absolute; + top: 0%; + bottom: 0%; + left: 0%; + right: 0%; + z-index: -10; +} + +.spinner-container > .spinner:not(.scrolling) > div.selection, +.calendar-container .days-view > div.selection { + color: var(--selected-font-color); +} + +.spinner-container > .spinner > div.selection::before, +.calendar-container .days-view > div.selection::before { + background: var(--selected-fill-color); + border: none; + border-radius: var(--border-radius); + content: ""; + position: absolute; + top: 0%; + bottom: 0%; + left: 0%; + right: 0%; + z-index: -10; +} + +.spinner-container > .spinner > div.disabled::before, +.spinner-container > .spinner.scrolling > div.selection::before, +.spinner-container > .spinner.scrolling > div:hover::before { + display: none; +} + +.spinner-container > .spinner > div.disabled { + opacity: var(--disabled-opacity); +} + +.colon { + display: flex; + justify-content: center; + align-items: center; + width: var(--colon-width); + margin-bottom: 0.3rem; +} + +.spacer { + width: var(--day-period-spacing-width); +} \ No newline at end of file diff --git a/toolkit/themes/shared/jar.inc.mn b/toolkit/themes/shared/jar.inc.mn index cfb4e99af..e7f1f7c67 100644 --- a/toolkit/themes/shared/jar.inc.mn +++ b/toolkit/themes/shared/jar.inc.mn @@ -21,7 +21,7 @@ toolkit.jar: skin/classic/global/aboutSupport.css (../../shared/aboutSupport.css) skin/classic/global/appPicker.css (../../shared/appPicker.css) skin/classic/global/config.css (../../shared/config.css) - skin/classic/global/timepicker.css (../../shared/timepicker.css) + skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css) skin/classic/global/icons/find-arrows.svg (../../shared/icons/find-arrows.svg) skin/classic/global/icons/info.svg (../../shared/incontent-icons/info.svg) skin/classic/global/icons/input-clear.svg (../../shared/icons/input-clear.svg) diff --git a/toolkit/themes/shared/timepicker.css b/toolkit/themes/shared/timepicker.css deleted file mode 100644 index ca4955748..000000000 --- a/toolkit/themes/shared/timepicker.css +++ /dev/null @@ -1,153 +0,0 @@ -/* 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/. */ - -:root { - --font-size-default: 1.1rem; - --spinner-width: 3rem; - --spinner-margin-top-bottom: 0.4rem; - --spinner-item-height: 2.4rem; - --spinner-item-margin-bottom: 0.1rem; - --spinner-button-height: 1.2rem; - --colon-width: 2rem; - --day-period-spacing-width: 1rem; - - --border: 0.1rem solid #D6D6D6; - --border-radius: 0.3rem; - - --font-color: #191919; - --fill-color: #EBEBEB; - - --selected-font-color: #FFFFFF; - --selected-fill-color: #0996F8; - - --button-font-color: #858585; - --button-font-color-hover: #4D4D4D; - --button-font-color-active: #191919; - - --disabled-opacity: 0.2; -} - -html { - font-size: 10px; -} - -body { - margin: 0; - color: var(--font-color); - font-size: var(--font-size-default); -} - -#time-picker { - display: flex; - flex-direction: row; - justify-content: space-around; -} - -.spinner-container { - font-family: sans-serif; - display: flex; - flex-direction: column; - width: var(--spinner-width); -} - -.spinner-container > button { - -moz-appearance: none; - border: none; - background: none; - background-color: var(--button-font-color); - height: var(--spinner-button-height); -} - -.spinner-container > button:hover { - background-color: var(--button-font-color-hover); -} - -.spinner-container > button.active { - background-color: var(--button-font-color-active); -} - -.spinner-container > button.up { - background: url("chrome://global/skin/icons/spinner-arrows.svg#up") no-repeat 50% 50%; -} - -.spinner-container > button.down { - background: url("chrome://global/skin/icons/spinner-arrows.svg#down") no-repeat 50% 50%; -} - -.spinner-container.hide-buttons > button { - visibility: hidden; -} - -.spinner-container > .spinner { - position: relative; - width: 100%; - margin: var(--spinner-margin-top-bottom) 0; - cursor: default; - overflow-y: scroll; - scroll-snap-type: mandatory; - scroll-snap-points-y: repeat(100%); -} - -.spinner-container > .spinner > div { - box-sizing: border-box; - position: relative; - text-align: center; - padding: calc((var(--spinner-item-height) - var(--font-size-default)) / 2) 0; - margin-bottom: var(--spinner-item-margin-bottom); - height: var(--spinner-item-height); - -moz-user-select: none; - scroll-snap-coordinate: 0 0; -} - -.spinner-container > .spinner > div:hover::before { - background: var(--fill-color); - border: var(--border); - border-radius: var(--border-radius); - content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; -} - -.spinner-container > .spinner:not(.scrolling) > div.selection { - color: var(--selected-font-color); -} - -.spinner-container > .spinner > div.selection::before { - background: var(--selected-fill-color); - border: none; - border-radius: var(--border-radius); - content: ""; - position: absolute; - top: 0%; - bottom: 0%; - left: 0%; - right: 0%; - z-index: -10; -} - -.spinner-container > .spinner > div.disabled::before, -.spinner-container > .spinner.scrolling > div.selection::before, -.spinner-container > .spinner.scrolling > div:hover::before { - display: none; -} - -.spinner-container > .spinner > div.disabled { - opacity: var(--disabled-opacity); -} - -.colon { - display: flex; - justify-content: center; - align-items: center; - width: var(--colon-width); - margin-bottom: 0.3rem; -} - -.spacer { - width: var(--day-period-spacing-width); -} \ No newline at end of file -- cgit v1.2.3