summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--toolkit/content/datepicker.xhtml61
-rw-r--r--toolkit/content/jar.mn4
-rw-r--r--toolkit/content/timepicker.xhtml2
-rw-r--r--toolkit/content/widgets/calendar.js172
-rw-r--r--toolkit/content/widgets/datekeeper.js244
-rw-r--r--toolkit/content/widgets/datepicker.js354
-rw-r--r--toolkit/content/widgets/spinner.js18
-rw-r--r--toolkit/themes/shared/datetimeinputpickers.css (renamed from toolkit/themes/shared/timepicker.css)150
-rw-r--r--toolkit/themes/shared/jar.inc.mn2
9 files changed, 991 insertions, 16 deletions
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+ <title>Date Picker</title>
+ <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/>
+ <script type="application/javascript" src="chrome://global/content/bindings/datekeeper.js"></script>
+ <script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script>
+ <script type="application/javascript" src="chrome://global/content/bindings/calendar.js"></script>
+ <script type="application/javascript" src="chrome://global/content/bindings/datepicker.js"></script>
+</head>
+<body>
+ <div id="date-picker">
+ <div class="calendar-container">
+ <div class="nav">
+ <button class="left">&lt;</button>
+ <button class="right">&gt;</button>
+ </div>
+ <div class="week-header"></div>
+ <div class="days-viewport">
+ <div class="days-view"></div>
+ </div>
+ </div>
+ <div class="month-year">
+ <div class="month-year-label"></div>
+ <div class="month-year-arrow"></div>
+ </div>
+ <div class="month-year-view"></div>
+ </div>
+ <template id="spinner-template">
+ <div class="spinner-container">
+ <button class="up"/>
+ <div class="spinner"></div>
+ <button class="down"/>
+ </div>
+ </template>
+ <script type="application/javascript">
+ // We need to hide the scroll bar but maintain its scrolling
+ // capability, so using |overflow: hidden| is not an option.
+ // Instead, we are inserting a user agent stylesheet that is
+ // capable of selecting scrollbars, and do |display: none|.
+ var domWinUtls = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+ getInterface(Components.interfaces.nsIDOMWindowUtils);
+ domWinUtls.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbar { display: none; }', domWinUtls.AGENT_SHEET);
+ // Create a DatePicker instance and prepare to be
+ // initialized by the "DatePickerInit" event from datetimepopup.xml
+ const root = document.getElementById("date-picker");
+ new DatePicker({
+ monthYear: root.querySelector(".month-year"),
+ monthYearView: root.querySelector(".month-year-view"),
+ buttonLeft: root.querySelector(".left"),
+ buttonRight: root.querySelector(".right"),
+ weekHeader: root.querySelector(".week-header"),
+ daysView: root.querySelector(".days-view")
+ });
+ </script>
+</body>
+</html> \ 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 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<head>
<title>Time Picker</title>
- <link rel="stylesheet" href="chrome://global/skin/timepicker.css"/>
+ <link rel="stylesheet" href="chrome://global/skin/datetimeinputpickers.css"/>
<script type="application/javascript" src="chrome://global/content/bindings/timekeeper.js"></script>
<script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script>
<script type="application/javascript" src="chrome://global/content/bindings/timepicker.js"></script>
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<Object>} days: Data for days
+ * {
+ * {Number} dateValue: Date in milliseconds
+ * {Number} textContent
+ * {Array<String>} classNames
+ * }
+ * {Array<Object>} weekHeaders: Data for weekHeaders
+ * {
+ * {Number} textContent
+ * {Array<String>} 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<DOMElement>} elements
+ * {Array<Object>} items
+ * {Array<Object>} 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<DOMElement>}
+ */
+ _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<Number>} 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<Object>}
+ * {
+ * {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<Object>}
+ * {
+ * {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<Object>}
+ * {
+ * {Number} dateValue
+ * {Number} textContent
+ * {Array<String>} 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<Object>}
+ * {
+ * {Number} textContent
+ * {Array<String>} 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<Object>} months
+ * {Array<Object>} 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/timepicker.css b/toolkit/themes/shared/datetimeinputpickers.css
index ca4955748..df93bc3a1 100644
--- a/toolkit/themes/shared/timepicker.css
+++ b/toolkit/themes/shared/datetimeinputpickers.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);
@@ -100,7 +235,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);
@@ -113,11 +249,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);
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)