summaryrefslogtreecommitdiff
path: root/toolkit/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content')
-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
7 files changed, 846 insertions, 9 deletions
diff --git a/toolkit/content/datepicker.xhtml b/toolkit/content/datepicker.xhtml
new file mode 100644
index 0000000000..abc7db3039
--- /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 590356b646..79065e78ac 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 1396223f1b..77b9fba410 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 0000000000..72e0d9d610
--- /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 0000000000..62fcfadbc3
--- /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 0000000000..d3f0fd1a43
--- /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 208ab1931b..b3d996fb50 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();
+ }
},
/**