Bug 1586801 - Use the contextual WalkerFront in _duplicateNode(). r=pbro
[gecko.git] / toolkit / modules / DateTimePickerPanel.jsm
blob6a065715164ab5c0601dc1e51be736c080fa71a6
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 "use strict";
7 var EXPORTED_SYMBOLS = ["DateTimePickerPanel"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 var DateTimePickerPanel = class {
12   constructor(element) {
13     this.element = element;
15     this.TIME_PICKER_WIDTH = "12em";
16     this.TIME_PICKER_HEIGHT = "21em";
17     this.DATE_PICKER_WIDTH = "23.1em";
18     this.DATE_PICKER_HEIGHT = "20.7em";
19   }
21   get dateTimePopupFrame() {
22     let frame = this.element.querySelector("#dateTimePopupFrame");
23     if (!frame) {
24       frame = this.element.ownerDocument.createXULElement("iframe");
25       frame.id = "dateTimePopupFrame";
26       this.element.appendChild(frame);
27     }
28     return frame;
29   }
31   openPicker(type, anchor, detail) {
32     this.type = type;
33     this.pickerState = {};
34     // TODO: Resize picker according to content zoom level
35     this.element.style.fontSize = "10px";
36     switch (type) {
37       case "time": {
38         this.detail = detail;
39         this.dateTimePopupFrame.addEventListener("load", this, true);
40         this.dateTimePopupFrame.setAttribute(
41           "src",
42           "chrome://global/content/timepicker.xhtml"
43         );
44         this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
45         this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
46         break;
47       }
48       case "date": {
49         this.detail = detail;
50         this.dateTimePopupFrame.addEventListener("load", this, true);
51         this.dateTimePopupFrame.setAttribute(
52           "src",
53           "chrome://global/content/datepicker.xhtml"
54         );
55         this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH;
56         this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT;
57         break;
58       }
59     }
60     this.element.hidden = false;
61     this.element.openPopup(anchor, "after_start", 0, 0);
62   }
64   closePicker() {
65     this.setInputBoxValue(true);
66     this.pickerState = {};
67     this.type = undefined;
68     this.dateTimePopupFrame.removeEventListener("load", this, true);
69     this.dateTimePopupFrame.contentDocument.removeEventListener(
70       "message",
71       this
72     );
73     this.dateTimePopupFrame.setAttribute("src", "");
74     this.element.hidden = true;
75   }
77   setPopupValue(data) {
78     switch (this.type) {
79       case "time": {
80         this.postMessageToPicker({
81           name: "PickerSetValue",
82           detail: data.value,
83         });
84         break;
85       }
86       case "date": {
87         const { year, month, day } = data.value;
88         this.postMessageToPicker({
89           name: "PickerSetValue",
90           detail: {
91             year,
92             // Month value from input box starts from 1 instead of 0
93             month: month == undefined ? undefined : month - 1,
94             day,
95           },
96         });
97         break;
98       }
99     }
100   }
102   initPicker(detail) {
103     // TODO: When bug 1376616 lands, replace this.setGregorian with
104     //       mozIntl.Locale for setting calendar to Gregorian
105     let locale = this.setGregorian(Services.locale.webExposedLocales[0]);
107     // Workaround for bug 1418061, while we wait for resolution of
108     // http://bugs.icu-project.org/trac/ticket/13592: drop the PT region code,
109     // because it results in "abbreviated" day names that are too long;
110     // the region-less "pt" locale has shorter forms that are better here.
111     locale = locale.replace(/^pt-PT/i, "pt");
113     const dir = Services.intl.getLocaleInfo(locale).direction;
115     switch (this.type) {
116       case "time": {
117         const { hour, minute } = detail.value;
118         const format = detail.format || "12";
120         this.postMessageToPicker({
121           name: "PickerInit",
122           detail: {
123             hour,
124             minute,
125             format,
126             locale,
127             min: detail.min,
128             max: detail.max,
129             step: detail.step,
130           },
131         });
132         break;
133       }
134       case "date": {
135         const { year, month, day } = detail.value;
136         const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale);
137         const monthStrings = this.getDisplayNames(
138           locale,
139           [
140             "dates/gregorian/months/january",
141             "dates/gregorian/months/february",
142             "dates/gregorian/months/march",
143             "dates/gregorian/months/april",
144             "dates/gregorian/months/may",
145             "dates/gregorian/months/june",
146             "dates/gregorian/months/july",
147             "dates/gregorian/months/august",
148             "dates/gregorian/months/september",
149             "dates/gregorian/months/october",
150             "dates/gregorian/months/november",
151             "dates/gregorian/months/december",
152           ],
153           "short"
154         );
155         const weekdayStrings = this.getDisplayNames(
156           locale,
157           [
158             "dates/gregorian/weekdays/sunday",
159             "dates/gregorian/weekdays/monday",
160             "dates/gregorian/weekdays/tuesday",
161             "dates/gregorian/weekdays/wednesday",
162             "dates/gregorian/weekdays/thursday",
163             "dates/gregorian/weekdays/friday",
164             "dates/gregorian/weekdays/saturday",
165           ],
166           "short"
167         );
169         this.postMessageToPicker({
170           name: "PickerInit",
171           detail: {
172             year,
173             // Month value from input box starts from 1 instead of 0
174             month: month == undefined ? undefined : month - 1,
175             day,
176             firstDayOfWeek,
177             weekends,
178             monthStrings,
179             weekdayStrings,
180             locale,
181             dir,
182             min: detail.min,
183             max: detail.max,
184             step: detail.step,
185             stepBase: detail.stepBase,
186           },
187         });
188         break;
189       }
190     }
191   }
193   /**
194    * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
195    */
196   setInputBoxValue(passAllValues) {
197     switch (this.type) {
198       case "time": {
199         const {
200           hour,
201           minute,
202           isHourSet,
203           isMinuteSet,
204           isDayPeriodSet,
205         } = this.pickerState;
206         const isAnyValueSet = isHourSet || isMinuteSet || isDayPeriodSet;
207         if (passAllValues && isAnyValueSet) {
208           this.sendPickerValueChanged({ hour, minute });
209         } else {
210           this.sendPickerValueChanged({
211             hour: isHourSet || isDayPeriodSet ? hour : undefined,
212             minute: isMinuteSet ? minute : undefined,
213           });
214         }
215         break;
216       }
217       case "date": {
218         this.sendPickerValueChanged(this.pickerState);
219         break;
220       }
221     }
222   }
224   sendPickerValueChanged(value) {
225     switch (this.type) {
226       case "time": {
227         this.element.dispatchEvent(
228           new CustomEvent("DateTimePickerValueChanged", {
229             detail: {
230               hour: value.hour,
231               minute: value.minute,
232             },
233           })
234         );
235         break;
236       }
237       case "date": {
238         this.element.dispatchEvent(
239           new CustomEvent("DateTimePickerValueChanged", {
240             detail: {
241               year: value.year,
242               // Month value from input box starts from 1 instead of 0
243               month: value.month == undefined ? undefined : value.month + 1,
244               day: value.day,
245             },
246           })
247         );
248         break;
249       }
250     }
251   }
253   getCalendarInfo(locale) {
254     const calendarInfo = Services.intl.getCalendarInfo(locale);
256     // Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday,
257     // so they need to be mapped to JavaScript convention with 0 as Sunday
258     // and 6 as Saturday
259     let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1,
260       weekendStart = calendarInfo.weekendStart - 1,
261       weekendEnd = calendarInfo.weekendEnd - 1;
263     let weekends = [];
265     // Make sure weekendEnd is greater than weekendStart
266     if (weekendEnd < weekendStart) {
267       weekendEnd += 7;
268     }
270     // We get the weekends by incrementing weekendStart up to weekendEnd.
271     // If the start and end is the same day, then weekends only has one day.
272     for (let day = weekendStart; day <= weekendEnd; day++) {
273       weekends.push(day % 7);
274     }
276     return {
277       firstDayOfWeek,
278       weekends,
279     };
280   }
282   getDisplayNames(locale, keys, style) {
283     const displayNames = Services.intl.getDisplayNames(locale, { keys, style });
284     return keys.map(key => displayNames.values[key]);
285   }
287   setGregorian(locale) {
288     if (locale.match(/u-ca-/)) {
289       return locale.replace(/u-ca-[^-]+/, "u-ca-gregory");
290     }
291     return locale + "-u-ca-gregory";
292   }
294   handleEvent(aEvent) {
295     switch (aEvent.type) {
296       case "load": {
297         this.initPicker(this.detail);
298         this.dateTimePopupFrame.contentWindow.addEventListener("message", this);
299         break;
300       }
301       case "message": {
302         this.handleMessage(aEvent);
303         break;
304       }
305     }
306   }
308   handleMessage(aEvent) {
309     if (
310       !this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
311     ) {
312       return;
313     }
315     switch (aEvent.data.name) {
316       case "PickerPopupChanged": {
317         this.pickerState = aEvent.data.detail;
318         this.setInputBoxValue();
319         break;
320       }
321       case "ClosePopup": {
322         this.element.hidePopup();
323         this.closePicker();
324         break;
325       }
326     }
327   }
329   postMessageToPicker(data) {
330     if (
331       this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal
332     ) {
333       this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
334     }
335   }