Backed out changeset 7ea2efe7df94 (bug 1832837) for causing /fullscreen/api wpt unexp...
[gecko.git] / toolkit / modules / DateTimePickerPanel.sys.mjs
blobdeff1df031baa5b9e69718c5053a91cf5a411d39
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     } else {
72       this.setInputBoxValue(true);
73     }
74     this.pickerState = {};
75     this.type = undefined;
76     this.dateTimePopupFrame.removeEventListener("load", this, true);
77     this.dateTimePopupFrame.contentDocument.removeEventListener(
78       "message",
79       this
80     );
81     this.dateTimePopupFrame.setAttribute("src", "");
82     this.element.hidePopup();
83   }
85   setPopupValue(data) {
86     switch (this.type) {
87       case "time": {
88         this.postMessageToPicker({
89           name: "PickerSetValue",
90           detail: data.value,
91         });
92         break;
93       }
94       case "date": {
95         const { year, month, day } = data.value;
96         this.postMessageToPicker({
97           name: "PickerSetValue",
98           detail: {
99             year,
100             // Month value from input box starts from 1 instead of 0
101             month: month == undefined ? undefined : month - 1,
102             day,
103           },
104         });
105         break;
106       }
107     }
108   }
110   initPicker(detail) {
111     let locale = new Services.intl.Locale(
112       Services.locale.webExposedLocales[0],
113       {
114         calendar: "gregory",
115       }
116     ).toString();
118     // Workaround for bug 1418061, while we wait for resolution of
119     // http://bugs.icu-project.org/trac/ticket/13592: drop the PT region code,
120     // because it results in "abbreviated" day names that are too long;
121     // the region-less "pt" locale has shorter forms that are better here.
122     locale = locale.replace(/^pt-PT/i, "pt");
124     const dir = Services.locale.isAppLocaleRTL ? "rtl" : "ltr";
126     switch (this.type) {
127       case "time": {
128         const { hour, minute } = detail.value;
129         const format = detail.format || "12";
131         this.postMessageToPicker({
132           name: "PickerInit",
133           detail: {
134             hour,
135             minute,
136             format,
137             locale,
138             min: detail.min,
139             max: detail.max,
140             step: detail.step,
141           },
142         });
143         break;
144       }
145       case "date": {
146         const { year, month, day } = detail.value;
147         const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale);
149         const monthDisplayNames = new Services.intl.DisplayNames(locale, {
150           type: "month",
151           style: "short",
152           calendar: "gregory",
153         });
154         const monthStrings = [
155           1,
156           2,
157           3,
158           4,
159           5,
160           6,
161           7,
162           8,
163           9,
164           10,
165           11,
166           12,
167         ].map(month => monthDisplayNames.of(month));
169         const weekdayDisplayNames = new Services.intl.DisplayNames(locale, {
170           type: "weekday",
171           style: "abbreviated",
172           calendar: "gregory",
173         });
174         const weekdayStrings = [
175           // Weekdays starting Sunday (7) to Saturday (6).
176           7,
177           1,
178           2,
179           3,
180           4,
181           5,
182           6,
183         ].map(weekday => weekdayDisplayNames.of(weekday));
185         this.postMessageToPicker({
186           name: "PickerInit",
187           detail: {
188             year,
189             // Month value from input box starts from 1 instead of 0
190             month: month == undefined ? undefined : month - 1,
191             day,
192             firstDayOfWeek,
193             weekends,
194             monthStrings,
195             weekdayStrings,
196             locale,
197             dir,
198             min: detail.min,
199             max: detail.max,
200             step: detail.step,
201             stepBase: detail.stepBase,
202           },
203         });
204         break;
205       }
206     }
207   }
209   /**
210    * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
211    */
212   setInputBoxValue(passAllValues) {
213     switch (this.type) {
214       case "time": {
215         const {
216           hour,
217           minute,
218           isHourSet,
219           isMinuteSet,
220           isDayPeriodSet,
221         } = this.pickerState;
222         const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet;
223         if (passAllValues && isAnyValueSet) {
224           this.sendPickerValueChanged({ hour, minute });
225         } else {
226           this.sendPickerValueChanged({
227             hour: isHourSet || isDayPeriodSet ? hour : undefined,
228             minute: isMinuteSet ? minute : undefined,
229           });
230         }
231         break;
232       }
233       case "date": {
234         this.sendPickerValueChanged(this.pickerState);
235         break;
236       }
237     }
238   }
240   sendPickerValueChanged(value) {
241     switch (this.type) {
242       case "time": {
243         this.element.dispatchEvent(
244           new CustomEvent("DateTimePickerValueChanged", {
245             detail: {
246               hour: value.hour,
247               minute: value.minute,
248             },
249           })
250         );
251         break;
252       }
253       case "date": {
254         this.element.dispatchEvent(
255           new CustomEvent("DateTimePickerValueChanged", {
256             detail: {
257               year: value.year,
258               // Month value from input box starts from 1 instead of 0
259               month: value.month == undefined ? undefined : value.month + 1,
260               day: value.day,
261             },
262           })
263         );
264         break;
265       }
266     }
267   }
269   getCalendarInfo(locale) {
270     const calendarInfo = Services.intl.getCalendarInfo(locale);
272     // Day of week from calendarInfo starts from 1 as Monday to 7 as Sunday,
273     // so they need to be mapped to JavaScript convention with 0 as Sunday
274     // and 6 as Saturday
275     function toDateWeekday(day) {
276       return day === 7 ? 0 : day;
277     }
279     let firstDayOfWeek = toDateWeekday(calendarInfo.firstDayOfWeek),
280       weekend = calendarInfo.weekend;
282     let weekends = weekend.map(toDateWeekday);
284     return {
285       firstDayOfWeek,
286       weekends,
287     };
288   }
290   handleEvent(aEvent) {
291     switch (aEvent.type) {
292       case "load": {
293         this.initPicker(this.detail);
294         this.dateTimePopupFrame.contentWindow.addEventListener("message", this);
295         break;
296       }
297       case "message": {
298         this.handleMessage(aEvent);
299         break;
300       }
301     }
302   }
304   handleMessage(aEvent) {
305     if (
306       !this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
307     ) {
308       return;
309     }
311     switch (aEvent.data.name) {
312       case "PickerPopupChanged": {
313         this.pickerState = aEvent.data.detail;
314         this.setInputBoxValue();
315         break;
316       }
317       case "ClosePopup": {
318         this.closePicker(aEvent.data.detail);
319         break;
320       }
321     }
322   }
324   postMessageToPicker(data) {
325     if (
326       this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
327     ) {
328       this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
329     }
330   }