summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/widgets/datetimepickers.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/widgets/datetimepickers.js')
-rw-r--r--comm/calendar/base/content/widgets/datetimepickers.js1529
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);
+ }
+}