/* 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] * {Number} min * {Number} max * {Number} step * {Number} stepBase * {Number} firstDayOfWeek * {Array} weekends * {Array} monthStrings * {Array} weekdayStrings * {String} locale [optional]: User preferred locale * } */ init(props = {}) { this.props = props; this._setDefaultState(); this._createComponents(); this._update(); document.dispatchEvent(new CustomEvent("PickerReady")); }, /* * Set initial date picker states. */ _setDefaultState() { const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, monthStrings, weekdayStrings, locale } = this.props; const dateKeeper = new DateKeeper({ year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends, calViewSize: CAL_VIEW_SIZE }); this.state = { dateKeeper, locale, isMonthPickerVisible: false, getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "", getWeekHeaderString: weekday => weekdayStrings[weekday], getMonthString: month => monthStrings[month], setSelection: date => { dateKeeper.setSelection({ year: date.getUTCFullYear(), month: date.getUTCMonth(), day: date.getUTCDate(), }); this._update(); this._dispatchState(); this._closePopup(); }, setYear: year => { dateKeeper.setYear(year); dateKeeper.setSelection({ year, month: dateKeeper.selection.month, day: dateKeeper.selection.day, }); this._update(); this._dispatchState(); }, setMonth: month => { dateKeeper.setMonth(month); dateKeeper.setSelection({ year: dateKeeper.selection.year, month, day: dateKeeper.selection.day, }); 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, setSelection: this.state.setSelection, getDayString: this.state.getDayString, getWeekHeaderString: this.state.getWeekHeaderString }, { weekHeader: this.context.weekHeader, daysView: this.context.daysView }), monthYear: new MonthYear({ setYear: this.state.setYear, setMonth: this.state.setMonth, getMonthString: this.state.getMonthString, locale: this.state.locale }, { monthYear: this.context.monthYear, monthYearView: this.context.monthYearView }) }; }, /** * Update date picker and its components. */ _update(options = {}) { const { dateKeeper, 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, months: this.state.months, years: this.state.years, toggleMonthPicker: this.state.toggleMonthPicker, noSmoothScroll: options.noSmoothScroll }); this.components.calendar.setProps({ isVisible: !isMonthPickerVisible, days: this.state.days, weekHeaders: dateKeeper.state.weekHeaders }); isMonthPickerVisible ? this.context.monthYearView.classList.remove("hidden") : this.context.monthYearView.classList.add("hidden"); }, /** * Use postMessage to close the picker. */ _closePopup() { window.postMessage({ name: "ClosePopup" }, "*"); }, /** * Use postMessage to pass the state of picker to the panel. */ _dispatchState() { const { year, month, day } = this.state.dateKeeper.selection; // The panel is listening to window for postMessage event, so we // do postMessage to itself to send data to input boxes. window.postMessage({ name: "PickerPopupChanged", detail: { year, month, day, } }, "*"); }, /** * Attach event listeners */ _attachEventListeners() { window.addEventListener("message", this); document.addEventListener("mouseup", this, { passive: true }); document.addEventListener("mousedown", this); }, /** * Handle events. * * @param {Event} event */ handleEvent(event) { switch (event.type) { case "message": { this.handleMessage(event); break; } case "mousedown": { // Use preventDefault to keep focus on input boxes event.preventDefault(); event.target.setCapture(); if (event.target == this.context.buttonLeft) { event.target.classList.add("active"); this.state.dateKeeper.setMonthByOffset(-1); this._update(); } else if (event.target == this.context.buttonRight) { event.target.classList.add("active"); this.state.dateKeeper.setMonthByOffset(1); this._update(); } break; } case "mouseup": { if (event.target == this.context.buttonLeft || event.target == this.context.buttonRight) { event.target.classList.remove("active"); } } } }, /** * Handle postMessage events. * * @param {Event} event */ handleMessage(event) { switch (event.data.name) { case "PickerSetValue": { this.set(event.data.detail); break; } case "PickerInit": { 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({ year, month, day }) { const { dateKeeper } = this.state; dateKeeper.setCalendarMonth({ year, month }); dateKeeper.setSelection({ year, month, day }); this._update({ noSmoothScroll: true }); } }; /** * MonthYear is a component that handles the month & year spinners * * @param {Object} options * { * {String} locale * {Function} setYear * {Function} setMonth * {Function} getMonthString * } * @param {DOMElement} context */ function MonthYear(options, context) { const spinnerSize = 5; const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", timeZone: "UTC" }).format; const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long", timeZone: "UTC" }).format; this.context = context; this.state = { dateFormat }; this.props = {}; this.components = { month: new Spinner({ setValue: month => { this.state.isMonthSet = true; options.setMonth(month); }, getDisplayString: options.getMonthString, viewportSize: spinnerSize }, context.monthYearView), year: new Spinner({ setValue: year => { this.state.isYearSet = true; options.setYear(year); }, getDisplayString: year => yearFormat(new Date(new Date(0).setUTCFullYear(year))), viewportSize: spinnerSize }, context.monthYearView) }; this._attachEventListeners(); } MonthYear.prototype = { /** * Set new properties and pass them to components * * @param {Object} props * { * {Boolean} isVisible * {Date} dateObj * {Array} months * {Array} years * {Function} toggleMonthPicker * } */ setProps(props) { this.context.monthYear.textContent = this.state.dateFormat(props.dateObj); if (props.isVisible) { this.context.monthYear.classList.add("active"); this.components.month.setState({ value: props.dateObj.getUTCMonth(), items: props.months, isInfiniteScroll: true, isValueSet: this.state.isMonthSet, smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) }); this.components.year.setState({ value: props.dateObj.getUTCFullYear(), items: props.years, isInfiniteScroll: false, isValueSet: this.state.isYearSet, smoothScroll: !(this.state.firstOpened || props.noSmoothScroll) }); this.state.firstOpened = false; } else { this.context.monthYear.classList.remove("active"); 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); } }; }