diff options
Diffstat (limited to 'comm/calendar/base/content/widgets/datetimepickers.js')
-rw-r--r-- | comm/calendar/base/content/widgets/datetimepickers.js | 1529 |
1 files changed, 1529 insertions, 0 deletions
diff --git a/comm/calendar/base/content/widgets/datetimepickers.js b/comm/calendar/base/content/widgets/datetimepickers.js new file mode 100644 index 0000000000..ae2c87caf8 --- /dev/null +++ b/comm/calendar/base/content/widgets/datetimepickers.js @@ -0,0 +1,1529 @@ +/* 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/. */ + +/* global MozElements, MozXULElement */ + +// Wrap in a block to prevent leaking to window scope. +{ + const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + + // Leave these first arguments as `undefined`, to use the OS style if + // intl.regional_prefs.use_os_locales is true or the app language matches the OS language. + // Otherwise, the app language is used. + let dateFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "short" }); + let timeFormatter = new Services.intl.DateTimeFormat(undefined, { timeStyle: "short" }); + + let probeSucceeded; + let alphaMonths; + let yearIndex, monthIndex, dayIndex; + let ampmIndex, amRegExp, pmRegExp; + let parseTimeRegExp, parseShortDateRegex; + + class MozTimepickerMinute extends MozXULElement { + static get observedAttributes() { + return ["label", "selected"]; + } + + constructor() { + super(); + + this.addEventListener("wheel", event => { + const pixelThreshold = 50; + let deltaView = 0; + + if (event.deltaMode == event.DOM_DELTA_PAGE || event.deltaMode == event.DOM_DELTA_LINE) { + // Line/Page scrolling is usually vertical + if (event.deltaY) { + deltaView = event.deltaY < 0 ? -1 : 1; + } + } else if (event.deltaMode == event.DOM_DELTA_PIXEL) { + // The natural direction for pixel scrolling is left/right + this.pixelScrollDelta += event.deltaX; + if (this.pixelScrollDelta > pixelThreshold) { + deltaView = 1; + this.pixelScrollDelta = 0; + } else if (this.pixelScrollDelta < -pixelThreshold) { + deltaView = -1; + this.pixelScrollDelta = 0; + } + } + + if (deltaView != 0) { + this.moveMinutes(deltaView); + } + + event.stopPropagation(); + event.preventDefault(); + }); + + this.clickMinute = (minuteItem, minuteNumber) => { + this.closest("timepicker-grids").clickMinute(minuteItem, minuteNumber); + }; + this.moveMinutes = number => { + this.closest("timepicker-grids").moveMinutes(number); + }; + } + + connectedCallback() { + if (this.hasChildNodes()) { + return; + } + + const spacer = document.createXULElement("spacer"); + spacer.setAttribute("flex", "1"); + + const minutebox = document.createXULElement("vbox"); + minutebox.addEventListener("click", () => { + this.clickMinute(this, this.getAttribute("value")); + }); + + const box = document.createXULElement("box"); + + this.label = document.createXULElement("label"); + this.label.classList.add("time-picker-minute-label"); + + box.appendChild(this.label); + minutebox.appendChild(box); + + this.appendChild(spacer.cloneNode()); + this.appendChild(minutebox); + this.appendChild(spacer); + + this.pixelScrollDelta = 0; + + this._updateAttributes(); + } + + attributeChangedCallback() { + this._updateAttributes(); + } + + _updateAttributes() { + if (!this.label) { + return; + } + + if (this.hasAttribute("label")) { + this.label.setAttribute("value", this.getAttribute("label")); + } else { + this.label.removeAttribute("value"); + } + + if (this.hasAttribute("selected")) { + this.label.setAttribute("selected", this.getAttribute("selected")); + } else { + this.label.removeAttribute("selected"); + } + } + } + + class MozTimepickerHour extends MozXULElement { + static get observedAttributes() { + return ["label", "selected"]; + } + + constructor() { + super(); + + this.addEventListener("wheel", event => { + const pixelThreshold = 50; + let deltaView = 0; + + if (event.deltaMode == event.DOM_DELTA_PAGE || event.deltaMode == event.DOM_DELTA_LINE) { + // Line/Page scrolling is usually vertical + if (event.deltaY) { + deltaView = event.deltaY < 0 ? -1 : 1; + } + } else if (event.deltaMode == event.DOM_DELTA_PIXEL) { + // The natural direction for pixel scrolling is left/right + this.pixelScrollDelta += event.deltaX; + if (this.pixelScrollDelta > pixelThreshold) { + deltaView = 1; + this.pixelScrollDelta = 0; + } else if (this.pixelScrollDelta < -pixelThreshold) { + deltaView = -1; + this.pixelScrollDelta = 0; + } + } + + if (deltaView != 0) { + this.moveHours(deltaView); + } + + event.stopPropagation(); + event.preventDefault(); + }); + + this.clickHour = (hourItem, hourNumber) => { + this.closest("timepicker-grids").clickHour(hourItem, hourNumber); + }; + this.moveHours = number => { + this.closest("timepicker-grids").moveHours(number); + }; + this.doubleClickHour = (hourItem, hourNumber) => { + this.closest("timepicker-grids").doubleClickHour(hourItem, hourNumber); + }; + } + + connectedCallback() { + if (this.hasChildNodes()) { + return; + } + + const spacer = document.createXULElement("spacer"); + spacer.setAttribute("flex", "1"); + + const hourbox = document.createXULElement("vbox"); + hourbox.addEventListener("click", () => { + this.clickHour(this, this.getAttribute("value")); + }); + hourbox.addEventListener("dblclick", () => { + this.doubleClickHour(this, this.getAttribute("value")); + }); + + const box = document.createXULElement("box"); + + this.label = document.createXULElement("label"); + this.label.classList.add("time-picker-hour-label"); + + box.appendChild(this.label); + hourbox.appendChild(box); + hourbox.appendChild(spacer.cloneNode()); + + this.appendChild(spacer.cloneNode()); + this.appendChild(hourbox); + this.appendChild(spacer); + + this._updateAttributes(); + } + + attributeChangedCallback() { + this._updateAttributes(); + } + + _updateAttributes() { + if (!this.label) { + return; + } + + if (this.hasAttribute("label")) { + this.label.setAttribute("value", this.getAttribute("label")); + } else { + this.label.removeAttribute("value"); + } + + if (this.hasAttribute("selected")) { + this.label.setAttribute("selected", this.getAttribute("selected")); + } else { + this.label.removeAttribute("selected"); + } + } + } + + /** + * The MozTimepickerGrids widget displays the grid of times to select, e.g. for an event. + * Typically it represents the popup content that let's the user select a time, in a + * <timepicker> widget. + * + * @augments MozXULElement + */ + class MozTimepickerGrids extends MozXULElement { + constructor() { + super(); + + this.content = MozXULElement.parseXULToFragment(` + <vbox class="time-picker-grids"> + <vbox class="time-picker-hour-grid" format12hours="false"> + <hbox flex="1" class="timepicker-topRow-hour-class"> + <timepicker-hour class="time-picker-hour-box-class" value="0" label="0"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="1" label="1"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="2" label="2"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="3" label="3"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="4" label="4"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="5" label="5"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="6" label="6"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="7" label="7"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="8" label="8"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="9" label="9"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="10" label="10"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="11" label="11"></timepicker-hour> + <hbox class="timepicker-amLabelBox-class amLabelBox" hidden="true"> + <label></label> + </hbox> + </hbox> + <hbox flex="1" class="timepicker-bottomRow-hour-class"> + <timepicker-hour class="time-picker-hour-box-class" value="12" label="12"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="13" label="13"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="14" label="14"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="15" label="15"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="16" label="16"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="17" label="17"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="18" label="18"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="19" label="19"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="20" label="20"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="21" label="21"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="22" label="22"></timepicker-hour> + <timepicker-hour class="time-picker-hour-box-class" value="23" label="23"></timepicker-hour> + <hbox class="pmLabelBox timepicker-pmLabelBox-class" hidden="true"> + <label></label> + </hbox> + </hbox> + </vbox> + <vbox class="time-picker-five-minute-grid-box"> + <vbox class="time-picker-five-minute-grid"> + <hbox flex="1"> + <timepicker-minute class="time-picker-five-minute-class" value="0" label=":00" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="5" label=":05" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="10" label=":10" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="15" label=":15" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="20" label=":20" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="25" label=":25" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-five-minute-class" value="30" label=":30" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="35" label=":35" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="40" label=":40" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="45" label=":45" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="50" label=":50" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-five-minute-class" value="55" label=":55" flex="1"></timepicker-minute> + </hbox> + </vbox> + <hbox class="time-picker-minutes-bottom"> + <spacer flex="1"></spacer> + <label class="time-picker-more-control-label" value="»" onclick="clickMore()"></label> + </hbox> + </vbox> + <vbox class="time-picker-one-minute-grid-box" flex="1" hidden="true"> + <vbox class="time-picker-one-minute-grid" flex="1"> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="0" label=":00" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="1" label=":01" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="2" label=":02" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="3" label=":03" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="4" label=":04" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="5" label=":05" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="6" label=":06" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="7" label=":07" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="8" label=":08" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="9" label=":09" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="10" label=":10" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="11" label=":11" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="12" label=":12" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="13" label=":13" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="14" label=":14" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="15" label=":15" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="16" label=":16" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="17" label=":17" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="18" label=":18" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="19" label=":19" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="20" label=":20" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="21" label=":21" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="22" label=":22" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="23" label=":23" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="24" label=":24" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="25" label=":25" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="26" label=":26" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="27" label=":27" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="28" label=":28" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="29" label=":29" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="30" label=":30" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="31" label=":31" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="32" label=":32" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="33" label=":33" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="34" label=":34" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="35" label=":35" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="36" label=":36" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="37" label=":37" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="38" label=":38" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="39" label=":39" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="40" label=":40" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="41" label=":41" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="42" label=":42" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="43" label=":43" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="44" label=":44" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="45" label=":45" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="46" label=":46" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="47" label=":47" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="48" label=":48" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="49" label=":49" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="50" label=":50" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="51" label=":51" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="52" label=":52" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="53" label=":53" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="54" label=":54" flex="1"></timepicker-minute> + </hbox> + <hbox flex="1"> + <timepicker-minute class="time-picker-one-minute-class" value="55" label=":55" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="56" label=":56" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="57" label=":57" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="58" label=":58" flex="1"></timepicker-minute> + <timepicker-minute class="time-picker-one-minute-class" value="59" label=":59" flex="1"></timepicker-minute> + </hbox> + </vbox> + <hbox class="time-picker-minutes-bottom"> + <spacer flex="1"></spacer> + <label class="time-picker-more-control-label" value="«" onclick="clickLess()"></label> + </hbox> + </vbox> + </vbox> + `); + } + + connectedCallback() { + if (!this.hasChildNodes()) { + this.appendChild(document.importNode(this.content, true)); + } + + // set by onPopupShowing + this.mPicker = null; + + // The currently selected time + this.mSelectedTime = new Date(); + // The selected hour and selected minute items + this.mSelectedHourItem = null; + this.mSelectedMinuteItem = null; + // constants use to specify one and five minute view + this.kMINUTE_VIEW_FIVE = 5; + this.kMINUTE_VIEW_ONE = 1; + } + + /** + * Sets new mSelectedTime. + * + * @param {string | Array} val new mSelectedTime value + */ + set value(val) { + if (typeof val == "string") { + val = parseTime(val); + } else if (Array.isArray(val)) { + let [hours, minutes] = val; + val = new Date(); + val.setHours(hours); + val.setMinutes(minutes); + } + this.mSelectedTime = val; + } + + /** + * @returns {Array} An array containing mSelectedTime hours and mSelectedTime minutes + */ + get value() { + return [this.mSelectedTime.getHours(), this.mSelectedTime.getMinutes()]; + } + + /** + * Set up the picker, called when the popup pops. + */ + onPopupShowing() { + // select the hour item + let hours24 = this.mSelectedTime.getHours(); + let hourItem = this.querySelector(`.time-picker-hour-box-class[value="${hours24}"]`); + this.selectHourItem(hourItem); + + // Show the five minute view if we are an even five minutes, + // otherwise one minute view + let minutesByFive = this.calcNearestFiveMinutes(this.mSelectedTime); + + if (minutesByFive == this.mSelectedTime.getMinutes()) { + this.clickLess(); + } else { + this.clickMore(); + } + } + + /** + * Switches popup to minute view and selects the selected minute item. + */ + clickMore() { + // switch to one minute view + this.switchMinuteView(this.kMINUTE_VIEW_ONE); + + // select minute box corresponding to the time + let minutes = this.mSelectedTime.getMinutes(); + let oneMinuteItem = this.querySelector(`.time-picker-one-minute-class[value="${minutes}"]`); + this.selectMinuteItem(oneMinuteItem); + } + + /** + * Switches popup to five-minute view and selects the five-minute item nearest to selected + * minute item. + */ + clickLess() { + // switch to five minute view + this.switchMinuteView(this.kMINUTE_VIEW_FIVE); + + // select closest five minute box, + // BUT leave the selected time at what may NOT be an even five minutes + // So that If they click more again the proper non-even-five minute + // box will be selected + let minutesByFive = this.calcNearestFiveMinutes(this.mSelectedTime); + let fiveMinuteItem = this.querySelector( + `.time-picker-five-minute-class[value="${minutesByFive}"]` + ); + this.selectMinuteItem(fiveMinuteItem); + } + + /** + * Selects the hour item which was clicked. + * + * @param {Node} hourItem - Hour item which was clicked + * @param {number} hourNumber - Hour value of the clicked hour item + */ + clickHour(hourItem, hourNumber) { + // select the item + this.selectHourItem(hourItem); + + // Change the hour in the selected time. + this.mSelectedTime.setHours(hourNumber); + + this.hasChanged = true; + } + + /** + * Called when one of the hour boxes is double clicked. + * Sets the time to the selected hour, on the hour, and closes the popup. + * + * @param {Node} hourItem - Hour item which was clicked + * @param {number} hourNumber - Hour value of the clicked hour item + */ + doubleClickHour(hourItem, hourNumber) { + // set the minutes to :00 + this.mSelectedTime.setMinutes(0); + + this.dispatchEvent(new CustomEvent("select")); + } + + /** + * Changes selectedTime's minute, calls the client's onchange and closes + * the popup. + * + * @param {Node} minuteItem - Minute item which was clicked + * @param {number} minuteNumber - Minute value of the clicked minute item + */ + clickMinute(minuteItem, minuteNumber) { + // set the minutes in the selected time + this.mSelectedTime.setMinutes(minuteNumber); + this.selectMinuteItem(minuteItem); + this.hasChanged = true; + + this.dispatchEvent(new CustomEvent("select")); + } + + /** + * Helper function to switch between "one" and "five" minute views. + * + * @param {number} view - Number representing minute view + */ + switchMinuteView(view) { + let fiveMinuteBox = this.querySelector(".time-picker-five-minute-grid-box"); + let oneMinuteBox = this.querySelector(".time-picker-one-minute-grid-box"); + + if (view == this.kMINUTE_VIEW_ONE) { + fiveMinuteBox.setAttribute("hidden", true); + oneMinuteBox.setAttribute("hidden", false); + } else { + fiveMinuteBox.setAttribute("hidden", false); + oneMinuteBox.setAttribute("hidden", true); + } + } + + /** + * Selects an hour item. + * + * @param {Node} hourItem - Hour item node to be selected + */ + selectHourItem(hourItem) { + // clear old selection, if there is one + if (this.mSelectedHourItem != null) { + this.mSelectedHourItem.removeAttribute("selected"); + } + // set selected attribute, to cause the selected style to apply + hourItem.setAttribute("selected", "true"); + // remember the selected item so we can deselect it + this.mSelectedHourItem = hourItem; + } + + /** + * Selects a minute item. + * + * @param {Node} minuteItem - Minute item node to be selected + */ + selectMinuteItem(minuteItem) { + // clear old selection, if there is one + if (this.mSelectedMinuteItem != null) { + this.mSelectedMinuteItem.removeAttribute("selected"); + } + // set selected attribute, to cause the selected style to apply + minuteItem.setAttribute("selected", "true"); + // remember the selected item so we can deselect it + this.mSelectedMinuteItem = minuteItem; + } + + /** + * Moves minute by the number passed and handle rollover cases where the minutes gets + * greater than 59 or less than 60. + * + * @param {number} number - Moves minute by the number 'number' + */ + moveMinutes(number) { + if (!this.mSelectedTime) { + return; + } + + let idPrefix = ".time-picker-one-minute-class"; + + // Everything above assumes that we are showing the one-minute-grid, + // If not, we need to do these corrections; + let fiveMinuteBox = this.querySelector(".time-picker-five-minute-grid-box"); + + if (!fiveMinuteBox.hidden) { + number *= 5; + idPrefix = ".time-picker-five-minute-class"; + + // If the detailed view was shown before, then mSelectedTime.getMinutes + // might not be a multiple of 5. + this.mSelectedTime.setMinutes(this.calcNearestFiveMinutes(this.mSelectedTime)); + } + + let newMinutes = this.mSelectedTime.getMinutes() + number; + + // Handle rollover cases + if (newMinutes < 0) { + newMinutes += 60; + } + if (newMinutes > 59) { + newMinutes -= 60; + } + + this.mSelectedTime.setMinutes(newMinutes); + + let minuteItemId = `${idPrefix}[value="${this.mSelectedTime.getMinutes()}"]`; + let minuteItem = this.querySelector(minuteItemId); + + this.selectMinuteItem(minuteItem); + this.mPicker.kTextBox.value = this.mPicker.formatTime(this.mSelectedTime); + this.hasChanged = true; + } + + /** + * Moves hours by the number passed and handle rollover cases where the hours gets greater + * than 23 or less than 0. + * + * @param {number} number - Moves hours by the number 'number' + */ + moveHours(number) { + if (!this.mSelectedTime) { + return; + } + + let newHours = this.mSelectedTime.getHours() + number; + + // Handle rollover cases + if (newHours < 0) { + newHours += 24; + } + if (newHours > 23) { + newHours -= 24; + } + + this.mSelectedTime.setHours(newHours); + + let hourItemId = `.time-picker-hour-box-class[value="${this.mSelectedTime.getHours()}"]`; + let hourItem = this.querySelector(hourItemId); + + this.selectHourItem(hourItem); + this.mPicker.kTextBox.value = this.mPicker.formatTime(this.mSelectedTime); + this.hasChanged = true; + } + + /** + * Calculates the nearest even five minutes. + * + * @param {calDateTime} time - Time near to which nearest five minutes have to be found + */ + calcNearestFiveMinutes(time) { + let minutes = time.getMinutes(); + let minutesByFive = Math.round(minutes / 5) * 5; + + if (minutesByFive > 59) { + minutesByFive = 55; + } + return minutesByFive; + } + + /** + * Changes to 12 hours format by showing am/pm label. + * + * @param {string} amLabel - amLabelBox value + * @param {string} pmLabel - pmLabelBox value + */ + changeTo12HoursFormat(amLabel, pmLabel) { + if (!this.firstElementChild) { + this.appendChild(document.importNode(this.content, true)); + } + + let amLabelBox = this.querySelector(".amLabelBox"); + amLabelBox.removeAttribute("hidden"); + amLabelBox.firstElementChild.setAttribute("value", amLabel); + let pmLabelBox = this.querySelector(".pmLabelBox"); + pmLabelBox.removeAttribute("hidden"); + pmLabelBox.firstElementChild.setAttribute("value", pmLabel); + this.querySelector(".time-picker-hour-box-class[value='0']").setAttribute("label", "12"); + for (let i = 13; i < 24; i++) { + this.querySelector(`.time-picker-hour-box-class[value="${i}"]`).setAttribute( + "label", + i - 12 + ); + } + this.querySelector(".time-picker-hour-grid").setAttribute("format12hours", "true"); + } + } + + class CalendarDatePicker extends MozXULElement { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.prepend(CalendarDatePicker.fragment.cloneNode(true)); + this._menulist = this.querySelector(".datepicker-menulist"); + this._inputField = this._menulist._inputField; + this._popup = this._menulist.menupopup; + this._minimonth = this.querySelector("calendar-minimonth"); + + if (this.getAttribute("type") == "forever") { + this._valueIsForever = false; + this._foreverString = cal.l10n.getString( + "calendar-event-dialog", + "eventRecurrenceForeverLabel" + ); + + this._foreverItem = document.createXULElement("button"); + this._foreverItem.setAttribute("label", this._foreverString); + this._popup.appendChild(document.createXULElement("menuseparator")); + this._popup.appendChild(this._foreverItem); + + this._foreverItem.addEventListener("command", () => { + this.value = "forever"; + this._popup.hidePopup(); + }); + } + + this.value = this.getAttribute("value") || new Date(); + + // Other attributes handled in inheritedAttributes. + this._handleMutation = mutations => { + this.value = this.getAttribute("value"); + }; + this._attributeObserver = new MutationObserver(this._handleMutation); + this._attributeObserver.observe(this, { + attributes: true, + attributeFilter: ["value"], + }); + + this.initializeAttributeInheritance(); + + this.addEventListener("keydown", event => { + if (event.key == "Escape") { + this._popup.hidePopup(); + } + }); + this._menulist.addEventListener("change", event => { + event.stopPropagation(); + + let value = parseDateTime(this._inputBoxValue); + if (!value) { + this._inputBoxValue = this._minimonthValue; + return; + } + this._inputBoxValue = this._minimonthValue = value; + this._valueIsForever = false; + + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + }); + this._popup.addEventListener("popupshown", () => { + this._minimonth.focusDate(this._minimonthValue); + const calendar = this._minimonth.querySelector(".minimonth-calendar"); + calendar.querySelector("td[selected]").focus(); + }); + this._minimonth.addEventListener("change", event => { + event.stopPropagation(); + }); + this._minimonth.addEventListener("select", () => { + this._inputBoxValue = this._minimonthValue; + this._valueIsForever = false; + this._popup.hidePopup(); + + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + this._attributeObserver.disconnect(); + + if (this._menulist) { + this._menulist.remove(); + this._menulist = null; + this._inputField = null; + this._popup = null; + this._minimonth = null; + this._foreverItem = null; + } + } + + static get fragment() { + // Accessibility information of these nodes will be + // presented on XULComboboxAccessible generated from <menulist>; + // hide these nodes from the accessibility tree. + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <menulist is="menulist-editable" class="datepicker-menulist" editable="true" sizetopopup="false"> + <menupopup ignorekeys="true" popupanchor="bottomright" popupalign="topright"> + <calendar-minimonth tabindex="0"/> + </menupopup> + </menulist> + `), + true + ); + + Object.defineProperty(this, "fragment", { value: frag }); + return frag; + } + + static get inheritedAttributes() { + return { ".datepicker-menulist": "disabled" }; + } + + set value(val) { + let wasForever = this._valueIsForever; + if (this.getAttribute("type") == "forever" && val == "forever") { + this._valueIsForever = true; + this._inputBoxValue = val; + if (!wasForever) { + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + } + return; + } else if (typeof val == "string") { + val = parseDateTime(val); + } + + let existingValue = this._minimonthValue; + this._valueIsForever = false; + this._inputBoxValue = this._minimonthValue = val; + + if ( + wasForever || + existingValue.getFullYear() != val.getFullYear() || + existingValue.getMonth() != val.getMonth() || + existingValue.getDate() != val.getDate() + ) { + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + } + } + + get value() { + if (this._valueIsForever) { + return "forever"; + } + return this._minimonth.value; + } + + focus() { + this._menulist.focus(); + } + + set _inputBoxValue(val) { + if (val == "forever") { + this._inputField.value = this._foreverString; + return; + } + this._inputField.value = formatDate(val); + } + + get _inputBoxValue() { + return this._inputField.value; + } + + set _minimonthValue(val) { + if (val == "forever") { + return; + } + this._minimonth.value = val; + } + + get _minimonthValue() { + return this._minimonth.value; + } + } + + const MenuBaseControl = MozElements.BaseControlMixin(MozElements.MozElementMixin(XULMenuElement)); + MenuBaseControl.implementCustomInterface(CalendarDatePicker, [ + Ci.nsIDOMXULMenuListElement, + Ci.nsIDOMXULSelectControlElement, + ]); + + class CalendarTimePicker extends MozXULElement { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.prepend(CalendarTimePicker.fragment.cloneNode(true)); + this._menulist = this.firstElementChild; + this._inputField = this._menulist._inputField; + this._popup = this._menulist.menupopup; + this._grid = this._popup.firstElementChild; + + this.value = this.getAttribute("value") || new Date(); + + // Change the grids in the timepicker-grids for 12-hours time format. + if (ampmIndex) { + // Find the locale strings for the AM/PM prefix/suffix. + let amTime = new Date(2000, 0, 1, 6, 12, 34); + let pmTime = new Date(2000, 0, 1, 18, 12, 34); + amTime = timeFormatter.format(amTime); + pmTime = timeFormatter.format(pmTime); + let amLabel = parseTimeRegExp.exec(amTime)[ampmIndex] || "AM"; + let pmLabel = parseTimeRegExp.exec(pmTime)[ampmIndex] || "PM"; + + this._grid.changeTo12HoursFormat(amLabel, pmLabel); + } + + // Other attributes handled in inheritedAttributes. + this._handleMutation = mutations => { + this.value = this.getAttribute("value"); + }; + this._attributeObserver = new MutationObserver(this._handleMutation); + this._attributeObserver.observe(this, { + attributes: true, + attributeFilter: ["value"], + }); + + this.initializeAttributeInheritance(); + + this._inputField.addEventListener("change", event => { + event.stopPropagation(); + + let value = parseTime(this._inputBoxValue); + if (!value) { + this._inputBoxValue = this._gridValue; + return; + } + this.value = value; + }); + this._menulist.menupopup.addEventListener("popupshowing", () => { + this._grid.onPopupShowing(); + }); + this._menulist.menupopup.addEventListener("popuphiding", () => { + this.value = this._gridValue; + }); + this._grid.addEventListener("select", event => { + event.stopPropagation(); + + this.value = this._gridValue; + this._popup.hidePopup(); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + this._attributeObserver.disconnect(); + + if (this._menulist) { + this._menulist.remove(); + this._menulist = null; + this._inputField = null; + this._popup = null; + this._grid = null; + } + } + + static get fragment() { + // Accessibility information of these nodes will be + // presented on XULComboboxAccessible generated from <menulist>; + // hide these nodes from the accessibility tree. + let frag = document.importNode( + MozXULElement.parseXULToFragment(` + <menulist is="menulist-editable" class="timepicker-menulist" editable="true" sizetopopup="false"> + <menupopup popupanchor="bottomright" popupalign="topright"> + <timepicker-grids/> + </menupopup> + </menulist> + `), + true + ); + + Object.defineProperty(this, "fragment", { value: frag }); + return frag; + } + + static get inheritedAttributes() { + return { ".timepicker-menulist": "disabled" }; + } + + set value(val) { + if (typeof val == "string") { + val = parseTime(val); + } else if (Array.isArray(val)) { + let [hours, minutes] = val; + val = new Date(); + val.setHours(hours); + val.setMinutes(minutes); + } + if (val.getHours() != this._hours || val.getMinutes() != this._minutes) { + let settingInitalValue = this._hours === undefined; + + this._inputBoxValue = this._gridValue = val; + [this._hours, this._minutes] = this._gridValue; + + if (!settingInitalValue) { + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + } + } + } + + get value() { + return [this._hours, this._minutes]; + } + + focus() { + this._menulist.focus(); + } + + set _inputBoxValue(val) { + if (typeof val == "string") { + val = parseTime(val); + } else if (Array.isArray(val)) { + let [hours, minutes] = val; + val = new Date(); + val.setHours(hours); + val.setMinutes(minutes); + } + this._inputField.value = formatTime(val); + } + + get _inputBoxValue() { + return this._inputField.value; + } + + set _gridValue(val) { + this._grid.value = val; + } + + get _gridValue() { + return this._grid.value; + } + } + + MenuBaseControl.implementCustomInterface(CalendarTimePicker, [ + Ci.nsIDOMXULMenuListElement, + Ci.nsIDOMXULSelectControlElement, + ]); + + class CalendarDateTimePicker extends MozXULElement { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this._datepicker = document.createXULElement("datepicker"); + this._datepicker.classList.add("datetimepicker-datepicker"); + this._datepicker.setAttribute("anonid", "datepicker"); + this._timepicker = document.createXULElement("timepicker"); + this._timepicker.classList.add("datetimepicker-timepicker"); + this._timepicker.setAttribute("anonid", "timepicker"); + this.appendChild(this._datepicker); + this.appendChild(this._timepicker); + + if (this.getAttribute("value")) { + this._datepicker.value = this.getAttribute("value"); + this._timepicker.value = this.getAttribute("value"); + } + + this.initializeAttributeInheritance(); + + this._datepicker.addEventListener("change", event => { + event.stopPropagation(); + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + }); + this._timepicker.addEventListener("change", event => { + event.stopPropagation(); + this.dispatchEvent(new CustomEvent("change", { bubbles: true })); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + if (this._datepicker) { + this._datepicker.remove(); + } + if (this._timepicker) { + this._timepicker.remove(); + } + } + + static get inheritedAttributes() { + return { + ".datetimepicker-datepicker": "value,disabled,disabled=datepickerdisabled", + ".datetimepicker-timepicker": "value,disabled,disabled=timepickerdisabled", + }; + } + + set value(val) { + this._datepicker.value = this._timepicker.value = val; + } + + get value() { + let dateValue = this._datepicker.value; + let [hours, minutes] = this._timepicker.value; + dateValue.setHours(hours); + dateValue.setMinutes(minutes); + dateValue.setSeconds(0); + dateValue.setMilliseconds(0); + return dateValue; + } + + focus() { + this._datepicker.focus(); + } + } + + initDateFormat(); + initTimeFormat(); + customElements.define("timepicker-minute", MozTimepickerMinute); + customElements.define("timepicker-hour", MozTimepickerHour); + customElements.define("timepicker-grids", MozTimepickerGrids); + customElements.whenDefined("menulist-editable").then(() => { + customElements.define("datepicker", CalendarDatePicker); + customElements.define("timepicker", CalendarTimePicker); + customElements.define("datetimepicker", CalendarDateTimePicker); + }); + + /** + * Parameter aValue may be a date or a date time. Dates are + * read according to locale/OS setting (d-m-y or m-d-y or ...). + * (see initDateFormat). Uses parseTime() for times. + */ + function parseDateTime(aValue) { + let tempDate = null; + if (!probeSucceeded) { + return null; // avoid errors accessing uninitialized data. + } + + let year = Number.MIN_VALUE; + let month = -1; + let day = -1; + let timeString = null; + + if (alphaMonths == null) { + // SHORT NUMERIC DATE, such as 2002-03-04, 4/3/2002, or CE2002Y03M04D. + // Made of digits & nonDigits. (Nondigits may be unicode letters + // which do not match \w, esp. in CJK locales.) + // (.*)? binds to null if no suffix. + let parseNumShortDateRegex = /^\D*(\d+)\D+(\d+)\D+(\d+)(.*)?$/; + let dateNumbersArray = parseNumShortDateRegex.exec(aValue); + if (dateNumbersArray != null) { + year = Number(dateNumbersArray[yearIndex]); + month = Number(dateNumbersArray[monthIndex]) - 1; // 0-based + day = Number(dateNumbersArray[dayIndex]); + timeString = dateNumbersArray[4]; + } + } else { + // SHORT DATE WITH ALPHABETIC MONTH, such as "dd MMM yy" or "MMMM dd, yyyy" + // (\d+|[^\d\W]) is digits or letters, not both together. + // Allows 31dec1999 (no delimiters between parts) if OS does (w2k does not). + // Allows Dec 31, 1999 (comma and space between parts) + // (Only accepts ASCII month names; JavaScript RegExp does not have an + // easy way to describe unicode letters short of a HUGE character range + // regexp derived from the Alphabetic ranges in + // http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt) + // (.*)? binds to null if no suffix. + let parseAlphShortDateRegex = + /^\s*(\d+|[^\d\W]+)\W{0,2}(\d+|[^\d\W]+)\W{0,2}(\d+|[^\d\W]+)(.*)?$/; + let datePartsArray = parseAlphShortDateRegex.exec(aValue); + if (datePartsArray != null) { + year = Number(datePartsArray[yearIndex]); + let monthString = datePartsArray[monthIndex].toUpperCase(); + for (let monthIdx = 0; monthIdx < alphaMonths.length; monthIdx++) { + if (monthString == alphaMonths[monthIdx]) { + month = monthIdx; + break; + } + } + day = Number(datePartsArray[dayIndex]); + timeString = datePartsArray[4]; + } + } + if (year != Number.MIN_VALUE && month != -1 && day != -1) { + // year, month, day successfully parsed + if (year >= 0 && year < 100) { + // If 0 <= year < 100, treat as 2-digit year (like formatDate): + // parse year as up to 30 years in future or 69 years in past. + // (Covers 30-year mortgage and most working people's birthdate.) + // otherwise will be treated as four digit year. + let currentYear = new Date().getFullYear(); + let currentCentury = currentYear - (currentYear % 100); + year = currentCentury + year; + if (year < currentYear - 69) { + year += 100; + } + if (year > currentYear + 30) { + year -= 100; + } + } + // if time is also present, parse it + let hours = 0; + let minutes = 0; + let seconds = 0; + if (timeString != null) { + let time = parseTime(timeString); + if (time != null) { + hours = time.getHours(); + minutes = time.getMinutes(); + seconds = time.getSeconds(); + } + } + tempDate = new Date(year, month, day, hours, minutes, seconds, 0); + } // else did not match regex, not a valid date + return tempDate; + } + + /** + * Parse a variety of time formats so that cut and paste is likely to work. + * separator: ':' '.' ' ' symbol none + * "12:34:56" "12.34.56" "12 34 56" "12h34m56s" "123456" + * seconds optional: "02:34" "02.34" "02 34" "02h34m" "0234" + * minutes optional: "12" "12" "12" "12h" "12" + * 1st hr digit optional:"9:34" " 9.34" "9 34" "9H34M" "934am" + * skip nondigit prefix " 12:34" "t12.34" " 12 34" "T12H34M" "T0234" + * am/pm optional "02:34 a.m.""02.34pm" "02 34 A M" "02H34M P.M." "0234pm" + * am/pm prefix "a.m. 02:34""pm02.34" "A M 02 34" "P.M. 02H34M" "pm0234" + * am/pm cyrillic "02:34\u0430.\u043c." "02 34 \u0420 \u041c" + * am/pm arabic "\u063502:34" (RTL 02:34a) "\u0645 02.34" (RTL 02.34 p) + * above/below noon "\u4e0a\u534802:34" "\u4e0b\u5348 02 34" + * noon before/after "\u5348\u524d02:34" "\u5348\u5f8c 02 34" + */ + function parseTime(aValue) { + let now = new Date(); + + let noon = cal.l10n.getDateFmtString("noon"); + if (aValue.toLowerCase() == noon.toLowerCase()) { + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 12, 0, 0, 0); + } + + let midnight = cal.l10n.getDateFmtString("midnight"); + if (aValue.toLowerCase() == midnight.toLowerCase()) { + return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0); + } + + let time = null; + let timePartsArray = parseTimeRegExp.exec(aValue); + const PRE_INDEX = 1, + HR_INDEX = 2, + MIN_INDEX = 4, + SEC_INDEX = 6, + POST_INDEX = 8; + + if (timePartsArray != null) { + let hoursString = timePartsArray[HR_INDEX]; + let hours = Number(hoursString); + if (!(hours >= 0 && hours < 24)) { + return null; + } + + let minutesString = timePartsArray[MIN_INDEX]; + let minutes = minutesString == null ? 0 : Number(minutesString); + if (!(minutes >= 0 && minutes < 60)) { + return null; + } + + let secondsString = timePartsArray[SEC_INDEX]; + let seconds = secondsString == null ? 0 : Number(secondsString); + if (!(seconds >= 0 && seconds < 60)) { + return null; + } + + let ampmCode = null; + if (timePartsArray[PRE_INDEX] || timePartsArray[POST_INDEX]) { + if (ampmIndex && timePartsArray[ampmIndex]) { + // try current format order first + let ampmString = timePartsArray[ampmIndex]; + if (amRegExp.test(ampmString)) { + ampmCode = "AM"; + } else if (pmRegExp.test(ampmString)) { + ampmCode = "PM"; + } + } + if (ampmCode == null) { + // not yet found + // try any format order + let preString = timePartsArray[PRE_INDEX]; + let postString = timePartsArray[POST_INDEX]; + if ( + (preString && amRegExp.test(preString)) || + (postString && amRegExp.test(postString)) + ) { + ampmCode = "AM"; + } else if ( + (preString && pmRegExp.test(preString)) || + (postString && pmRegExp.test(postString)) + ) { + ampmCode = "PM"; + } // else no match, ignore and treat as 24hour time. + } + } + if (ampmCode == "AM") { + if (hours == 12) { + hours = 0; + } + } else if (ampmCode == "PM") { + if (hours < 12) { + hours += 12; + } + } + time = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, seconds, 0); + } // else did not match regex, not valid time + return time; + } + + function initDateFormat() { + // probe the dateformat + yearIndex = -1; + monthIndex = -1; + dayIndex = -1; + alphaMonths = null; + probeSucceeded = false; + + // SHORT NUMERIC DATE, such as 2002-03-04, 4/3/2002, or CE2002Y03M04D. + // Made of digits & nonDigits. (Nondigits may be unicode letters + // which do not match \w, esp. in CJK locales.) + parseShortDateRegex = /^\D*(\d+)\D+(\d+)\D+(\d+)\D?$/; + // Make sure to use UTC date and timezone here to avoid the pattern + // detection to fail if the probe date output would have an timezone + // offset due to our lack of support of historic timezone definitions. + let probeDate = new Date(Date.UTC(2002, 3, 6)); // month is 0-based + let probeString = formatDate(probeDate, cal.dtz.UTC); + let probeArray = parseShortDateRegex.exec(probeString); + if (probeArray) { + // Numeric month format + for (let i = 1; i <= 3; i++) { + switch (Number(probeArray[i])) { + case 2: // falls through + case 2002: + yearIndex = i; + break; + case 4: + monthIndex = i; + break; + case 5: // falls through for OS timezones western to GMT + case 6: + dayIndex = i; + break; + } + } + // All three indexes are set (not -1) at this point. + probeSucceeded = true; + } else { + // SHORT DATE WITH ALPHABETIC MONTH, such as "dd MMM yy" or "MMMM dd, yyyy" + // (\d+|[^\d\W]) is digits or letters, not both together. + // Allows 31dec1999 (no delimiters between parts) if OS does (w2k does not). + // Allows Dec 31, 1999 (comma and space between parts) + // (Only accepts ASCII month names; JavaScript RegExp does not have an + // easy way to describe unicode letters short of a HUGE character range + // regexp derived from the Alphabetic ranges in + // http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt) + parseShortDateRegex = /^\s*(\d+|[^\d\W]+)\W{0,2}(\d+|[^\d\W]+)\W{0,2}(\d+|[^\d\W]+)\s*$/; + probeArray = parseShortDateRegex.exec(probeString); + if (probeArray != null) { + for (let j = 1; j <= 3; j++) { + switch (Number(probeArray[j])) { + case 2: // falls through + case 2002: + yearIndex = j; + break; + case 5: // falls through for OS timezones western to GMT + case 6: + dayIndex = j; + break; + default: + monthIndex = j; + break; + } + } + if (yearIndex != -1 && dayIndex != -1 && monthIndex != -1) { + probeSucceeded = true; + // Fill alphaMonths with month names. + alphaMonths = new Array(12); + for (let monthIdx = 0; monthIdx < 12; monthIdx++) { + probeDate.setMonth(monthIdx); + probeString = formatDate(probeDate); + probeArray = parseShortDateRegex.exec(probeString); + if (probeArray) { + alphaMonths[monthIdx] = probeArray[monthIndex].toUpperCase(); + } else { + probeSucceeded = false; + } + } + } + } + } + if (!probeSucceeded) { + dump("\nOperating system short date format is not recognized: " + probeString + "\n"); + } + } + + /** + * Time format in 24-hour format or 12-hour format with am/pm string. + * Should match formats + * HH:mm, H:mm, HH:mm:ss, H:mm:ss + * hh:mm tt, h:mm tt, hh:mm:ss tt, h:mm:ss tt + * tt hh:mm, tt h:mm, tt hh:mm:ss, tt h:mm:ss + * where + * HH is 24 hour digits, with leading 0. H is 24 hour digits, no leading 0. + * hh is 12 hour digits, with leading 0. h is 12 hour digits, no leading 0. + * mm and ss are is minutes and seconds digits, with leading 0. + * tt is localized AM or PM string. + * ':' may be ':' or a units marker such as 'h', 'm', or 's' in 15h12m00s + * or may be omitted as in 151200. + */ + function initTimeFormat() { + // probe the Time format + ampmIndex = null; + // Digits HR sep MIN sep SEC sep + // Index: 2 3 4 5 6 7 + // prettier-ignore + let digitsExpr = "(\\d?\\d)\\s?(\\D)?\\s?(?:(\\d\\d)\\s?(\\D)?\\s?(?:(\\d\\d)\\s?(\\D)?\\s?)?)?"; + // digitsExpr has 6 captures, so index of first ampmExpr is 1, of last is 8. + let probeTimeRegExp = new RegExp("^\\s*(\\D*)\\s?" + digitsExpr + "\\s?(\\D*)\\s*$"); + const PRE_INDEX = 1, + HR_INDEX = 2, + // eslint-disable-next-line no-unused-vars + MIN_INDEX = 4, + SEC_INDEX = 6, + POST_INDEX = 8; + let amProbeTime = new Date(2000, 0, 1, 6, 12, 34); + let pmProbeTime = new Date(2000, 0, 1, 18, 12, 34); + let amProbeString = timeFormatter.format(amProbeTime); + let pmProbeString = timeFormatter.format(pmProbeTime); + let amFormatExpr = null, + pmFormatExpr = null; + if (amProbeString != pmProbeString) { + let amProbeArray = probeTimeRegExp.exec(amProbeString); + let pmProbeArray = probeTimeRegExp.exec(pmProbeString); + if (amProbeArray != null && pmProbeArray != null) { + if ( + amProbeArray[PRE_INDEX] && + pmProbeArray[PRE_INDEX] && + amProbeArray[PRE_INDEX] != pmProbeArray[PRE_INDEX] + ) { + ampmIndex = PRE_INDEX; + } else if (amProbeArray[POST_INDEX] && pmProbeArray[POST_INDEX]) { + if (amProbeArray[POST_INDEX] == pmProbeArray[POST_INDEX]) { + // check if need to append previous character, + // captured by the optional separator pattern after seconds digits, + // or after minutes if no seconds, or after hours if no minutes. + for (let k = SEC_INDEX; k >= HR_INDEX; k -= 2) { + let nextSepI = k + 1; + let nextDigitsI = k + 2; + if ( + (k == SEC_INDEX || (!amProbeArray[nextDigitsI] && !pmProbeArray[nextDigitsI])) && + amProbeArray[nextSepI] && + pmProbeArray[nextSepI] && + amProbeArray[nextSepI] != pmProbeArray[nextSepI] + ) { + amProbeArray[POST_INDEX] = amProbeArray[nextSepI] + amProbeArray[POST_INDEX]; + pmProbeArray[POST_INDEX] = pmProbeArray[nextSepI] + pmProbeArray[POST_INDEX]; + ampmIndex = POST_INDEX; + break; + } + } + } else { + ampmIndex = POST_INDEX; + } + } + if (ampmIndex) { + let makeFormatRegExp = function (string) { + // make expr to accept either as provided, lowercased, or uppercased + let regExp = string.replace(/(\W)/g, "[$1]"); // escape punctuation + let lowercased = string.toLowerCase(); + if (string != lowercased) { + regExp += "|" + lowercased; + } + let uppercased = string.toUpperCase(); + if (string != uppercased) { + regExp += "|" + uppercased; + } + return regExp; + }; + amFormatExpr = makeFormatRegExp(amProbeArray[ampmIndex]); + pmFormatExpr = makeFormatRegExp(pmProbeArray[ampmIndex]); + } + } + } + // International formats ([roman, cyrillic]|arabic|chinese/kanji characters) + // covering languages of U.N. (en,fr,sp,ru,ar,zh) and G8 (en,fr,de,it,ru,ja). + // See examples at parseTimeOfDay. + let amExpr = "[Aa\u0410\u0430][. ]?[Mm\u041c\u043c][. ]?|\u0635|\u4e0a\u5348|\u5348\u524d"; + let pmExpr = "[Pp\u0420\u0440][. ]?[Mm\u041c\u043c][. ]?|\u0645|\u4e0b\u5348|\u5348\u5f8c"; + if (ampmIndex) { + amExpr = amFormatExpr + "|" + amExpr; + pmExpr = pmFormatExpr + "|" + pmExpr; + } + let ampmExpr = amExpr + "|" + pmExpr; + // Must build am/pm formats into parse time regexp so that it can + // match them without mistaking the initial char for an optional divider. + // (For example, want to be able to parse both "12:34pm" and + // "12H34M56Spm" for any characters H,M,S and any language's "pm". + // The character between the last digit and the "pm" is optional. + // Must recognize "pm" directly, otherwise in "12:34pm" the "S" pattern + // matches the "p" character so only "m" is matched as ampm suffix.) + // + // digitsExpr has 6 captures, so index of first ampmExpr is 1, of last is 8. + parseTimeRegExp = new RegExp( + "(" + ampmExpr + ")?\\s?" + digitsExpr + "(" + ampmExpr + ")?\\s*$" + ); + amRegExp = new RegExp("^(?:" + amExpr + ")$"); + pmRegExp = new RegExp("^(?:" + pmExpr + ")$"); + } + + function formatDate(aDate, aTimezone) { + // Usually, floating is ok here, so no need to pass aTimezone - we just need to pass + // it in if we need to make sure formatting happens without a timezone conversion. + let formatter = aTimezone + ? new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeZone: aTimezone.tzid, + }) + : dateFormatter; + return formatter.format(aDate); + } + + function formatTime(aValue) { + return timeFormatter.format(aValue); + } +} |