Bug 1885602 - Part 5: Implement navigating to the SUMO help topic from the menu heade...
[gecko.git] / toolkit / modules / DateTimePickerPanel.sys.mjs
blobf20f2c1668d691f44ad42501cc65af6bd3bdff97
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 export var DateTimePickerPanel = class {
6   constructor(element) {
7     this.element = element;
9     this.TIME_PICKER_WIDTH = "13em";
10     this.TIME_PICKER_HEIGHT = "22em";
11     this.DATE_PICKER_WIDTH = "24em";
12     this.DATE_PICKER_HEIGHT = "27em";
13   }
15   get dateTimePopupFrame() {
16     let frame = this.element.querySelector("#dateTimePopupFrame");
17     if (!frame) {
18       frame = this.element.ownerDocument.createXULElement("iframe");
19       frame.id = "dateTimePopupFrame";
20       this.element.appendChild(frame);
21     }
22     return frame;
23   }
25   openPicker(type, rect, detail) {
26     if (type == "datetime-local") {
27       type = "date";
28     }
29     this.type = type;
30     this.pickerState = {};
31     // TODO: Resize picker according to content zoom level
32     this.element.style.fontSize = "10px";
33     switch (type) {
34       case "time": {
35         this.detail = detail;
36         this.dateTimePopupFrame.addEventListener("load", this, true);
37         this.dateTimePopupFrame.setAttribute(
38           "src",
39           "chrome://global/content/timepicker.xhtml"
40         );
41         this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
42         this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
43         break;
44       }
45       case "date": {
46         this.detail = detail;
47         this.dateTimePopupFrame.addEventListener("load", this, true);
48         this.dateTimePopupFrame.setAttribute(
49           "src",
50           "chrome://global/content/datepicker.xhtml"
51         );
52         this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH;
53         this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT;
54         break;
55       }
56     }
57     this.element.openPopupAtScreenRect(
58       "after_start",
59       rect.left,
60       rect.top,
61       rect.width,
62       rect.height,
63       false,
64       false
65     );
66   }
68   closePicker(clear) {
69     if (clear) {
70       this.element.dispatchEvent(new CustomEvent("DateTimePickerValueCleared"));
71     }
72     this.pickerState = {};
73     this.type = undefined;
74     this.dateTimePopupFrame.removeEventListener("load", this, true);
75     this.dateTimePopupFrame.contentDocument.removeEventListener(
76       "message",
77       this
78     );
79     this.dateTimePopupFrame.setAttribute("src", "");
80     this.element.hidePopup();
81   }
83   setPopupValue(data) {
84     switch (this.type) {
85       case "time": {
86         this.postMessageToPicker({
87           name: "PickerSetValue",
88           detail: data.value,
89         });
90         break;
91       }
92       case "date": {
93         const { year, month, day } = data.value;
94         this.postMessageToPicker({
95           name: "PickerSetValue",
96           detail: {
97             year,
98             // Month value from input box starts from 1 instead of 0
99             month: month == undefined ? undefined : month - 1,
100             day,
101           },
102         });
103         break;
104       }
105     }
106   }
108   initPicker(detail) {
109     let locale = new Services.intl.Locale(
110       Services.locale.webExposedLocales[0],
111       {
112         calendar: "gregory",
113       }
114     ).toString();
116     // Workaround for bug 1418061, while we wait for resolution of
117     // http://bugs.icu-project.org/trac/ticket/13592: drop the PT region code,
118     // because it results in "abbreviated" day names that are too long;
119     // the region-less "pt" locale has shorter forms that are better here.
120     locale = locale.replace(/^pt-PT/i, "pt");
122     const dir = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
124     switch (this.type) {
125       case "time": {
126         const { hour, minute } = detail.value;
127         const format = detail.format || "12";
129         this.postMessageToPicker({
130           name: "PickerInit",
131           detail: {
132             hour,
133             minute,
134             format,
135             locale,
136             min: detail.min,
137             max: detail.max,
138             step: detail.step,
139           },
140         });
141         break;
142       }
143       case "date": {
144         const { year, month, day } = detail.value;
145         const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale);
147         const monthDisplayNames = new Services.intl.DisplayNames(locale, {
148           type: "month",
149           style: "short",
150           calendar: "gregory",
151         });
152         const monthStrings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(
153           month => monthDisplayNames.of(month)
154         );
156         const weekdayDisplayNames = new Services.intl.DisplayNames(locale, {
157           type: "weekday",
158           style: "abbreviated",
159           calendar: "gregory",
160         });
161         const weekdayStrings = [
162           // Weekdays starting Sunday (7) to Saturday (6).
163           7, 1, 2, 3, 4, 5, 6,
164         ].map(weekday => weekdayDisplayNames.of(weekday));
166         this.postMessageToPicker({
167           name: "PickerInit",
168           detail: {
169             year,
170             // Month value from input box starts from 1 instead of 0
171             month: month == undefined ? undefined : month - 1,
172             day,
173             firstDayOfWeek,
174             weekends,
175             monthStrings,
176             weekdayStrings,
177             locale,
178             dir,
179             min: detail.min,
180             max: detail.max,
181             step: detail.step,
182             stepBase: detail.stepBase,
183           },
184         });
185         break;
186       }
187     }
188   }
190   /**
191    * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
192    */
193   setInputBoxValue(passAllValues) {
194     switch (this.type) {
195       case "time": {
196         const { hour, minute, isHourSet, isMinuteSet, isDayPeriodSet } =
197           this.pickerState;
198         const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet;
199         if (passAllValues && isAnyValueSet) {
200           this.sendPickerValueChanged({ hour, minute });
201         } else {
202           this.sendPickerValueChanged({
203             hour: isHourSet || isDayPeriodSet ? hour : undefined,
204             minute: isMinuteSet ? minute : undefined,
205           });
206         }
207         break;
208       }
209       case "date": {
210         this.sendPickerValueChanged(this.pickerState);
211         break;
212       }
213     }
214   }
216   sendPickerValueChanged(value) {
217     switch (this.type) {
218       case "time": {
219         this.element.dispatchEvent(
220           new CustomEvent("DateTimePickerValueChanged", {
221             detail: {
222               hour: value.hour,
223               minute: value.minute,
224             },
225           })
226         );
227         break;
228       }
229       case "date": {
230         this.element.dispatchEvent(
231           new CustomEvent("DateTimePickerValueChanged", {
232             detail: {
233               year: value.year,
234               // Month value from input box starts from 1 instead of 0
235               month: value.month == undefined ? undefined : value.month + 1,
236               day: value.day,
237             },
238           })
239         );
240         break;
241       }
242     }
243   }
245   getCalendarInfo(locale) {
246     const calendarInfo = Services.intl.getCalendarInfo(locale);
248     // Day of week from calendarInfo starts from 1 as Monday to 7 as Sunday,
249     // so they need to be mapped to JavaScript convention with 0 as Sunday
250     // and 6 as Saturday
251     function toDateWeekday(day) {
252       return day === 7 ? 0 : day;
253     }
255     let firstDayOfWeek = toDateWeekday(calendarInfo.firstDayOfWeek),
256       weekend = calendarInfo.weekend;
258     let weekends = weekend.map(toDateWeekday);
260     return {
261       firstDayOfWeek,
262       weekends,
263     };
264   }
266   handleEvent(aEvent) {
267     switch (aEvent.type) {
268       case "load": {
269         this.initPicker(this.detail);
270         this.dateTimePopupFrame.contentWindow.addEventListener("message", this);
271         break;
272       }
273       case "message": {
274         this.handleMessage(aEvent);
275         break;
276       }
277     }
278   }
280   handleMessage(aEvent) {
281     if (
282       !this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
283     ) {
284       return;
285     }
287     switch (aEvent.data.name) {
288       case "PickerPopupChanged": {
289         this.pickerState = aEvent.data.detail;
290         this.setInputBoxValue();
291         break;
292       }
293       case "ClosePopup": {
294         this.closePicker(aEvent.data.detail);
295         break;
296       }
297     }
298   }
300   postMessageToPicker(data) {
301     if (
302       this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
303     ) {
304       this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
305     }
306   }