From 54ad40c9420f37c54a80a3148cf0d0c5feaa0f33 Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Wed, 9 Nov 2016 23:38:11 +0800 Subject: [PATCH] Bug 1283385 - (1/2) Implement date picker UI; r=mconley MozReview-Commit-ID: 8uscU75qrkR --HG-- extra : rebase_source : 3040a648c0e744e2a8f7b1b4d07ff02e0cee318b --- toolkit/content/datepicker.xhtml | 61 +++++ toolkit/content/jar.mn | 4 + toolkit/content/widgets/calendar.js | 174 +++++++++++++ toolkit/content/widgets/datekeeper.js | 246 ++++++++++++++++++ toolkit/content/widgets/datepicker.js | 355 ++++++++++++++++++++++++++ toolkit/content/widgets/spinner.js | 18 +- toolkit/themes/shared/timepicker.css | 150 ++++++++++- 7 files changed, 994 insertions(+), 14 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 diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml new file mode 100644 index 000000000000..82305c7df1c1 --- /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 7c49a4dd6dec..ff42d0d325c4 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 content/global/devicestorage.properties #ifndef MOZ_FENNEC content/global/editMenuOverlay.js @@ -69,8 +70,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/widgets/calendar.js b/toolkit/content/widgets/calendar.js new file mode 100644 index 000000000000..53a06b62b409 --- /dev/null +++ b/toolkit/content/widgets/calendar.js @@ -0,0 +1,174 @@ +/* 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(); +} + +{ + const debug = 0 ? console.log.bind(console, "[calendar]") : function() {}; + + 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 000000000000..90f745dee5ac --- /dev/null +++ b/toolkit/content/widgets/datekeeper.js @@ -0,0 +1,246 @@ +/* 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; + + const debug = 0 ? console.log.bind(console, "[datekeeper]") : function() {}; + + 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 000000000000..21aae78b2a73 --- /dev/null +++ b/toolkit/content/widgets/datepicker.js @@ -0,0 +1,355 @@ +/* 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; + const debug = 0 ? console.log.bind(console, "[datepicker]") : function() {}; + + 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, isYearSet, isMonthSet, 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 eb1357b58a18..1dc4ba8f449b 100644 --- a/toolkit/content/widgets/spinner.js +++ b/toolkit/content/widgets/spinner.js @@ -96,7 +96,7 @@ function Spinner(props, context) { */ setState(newState) { 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); @@ -104,15 +104,17 @@ function Spinner(props, context) { this._scrollTo(newValue, true); } else if (newValue != value) { this.state = Object.assign(this.state, newState); - this._smoothScrollTo(newValue); + if (smoothScroll) { + this._smoothScrollTo(newValue, true); + } else { + this._scrollTo(newValue, true); + } } - if (isValueSet) { - if (isInvalid) { - this._removeSelection(); - } else { - this._updateSelection(); - } + if (isValueSet && !isInvalid) { + this._updateSelection(); + } else { + this._removeSelection(); } }, diff --git a/toolkit/themes/shared/timepicker.css b/toolkit/themes/shared/timepicker.css index e053abf5fdb7..41b06ee6efaf 100644 --- a/toolkit/themes/shared/timepicker.css +++ b/toolkit/themes/shared/timepicker.css @@ -11,6 +11,8 @@ --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; @@ -25,6 +27,11 @@ --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; } @@ -35,17 +42,145 @@ html { body { margin: 0; color: var(--font-color); + font: message-box; font-size: var(--font-size-default); } -#time-picker { +.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: space-around; + justify-content: center; } .spinner-container { - font-family: sans-serif; display: flex; flex-direction: column; width: var(--spinner-width); @@ -101,7 +236,8 @@ body { scroll-snap-coordinate: 0 0; } -.spinner-container > .spinner > div:hover::before { +.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); @@ -114,11 +250,13 @@ body { z-index: -10; } -.spinner-container > .spinner:not(.scrolling) > div.selection { +.spinner-container > .spinner:not(.scrolling) > div.selection, +.calendar-container .days-view > div.selection { color: var(--selected-font-color); } -.spinner-container > .spinner > div.selection::before { +.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);