98cb7de3963249b286453048e0ff7db6dac3d03b
[WebKit-https.git] / Source / WebCore / Resources / pagepopups / calendarPicker.js
1 "use strict";
2 /*
3  * Copyright (C) 2012 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 // FIXME:
33 //  - Touch event
34
35 /**
36  * CSS class names.
37  *
38  * @enum {string}
39  */
40 var ClassNames = {
41     Available: "available",
42     CancelButton: "cancel-button",
43     ClearButton: "clear-button",
44     Day: "day",
45     DayLabel: "day-label",
46     DayLabelContainer: "day-label-container",
47     DaysArea: "days-area",
48     DaysAreaContainer: "days-area-container",
49     MonthMode: "month-mode",
50     MonthSelector: "month-selector",
51     MonthSelectorBox: "month-selector-box",
52     MonthSelectorPopup: "month-selector-popup",
53     MonthSelectorPopupContents: "month-selector-popup-contents",
54     MonthSelectorPopupEntry: "month-selector-popup-entry",
55     MonthSelectorWall: "month-selector-wall",
56     NoFocusRing: "no-focus-ring",
57     NotThisMonth: "not-this-month",
58     Selected: "day-selected",
59     SelectedMonthYear: "selected-month-year",
60     TodayButton: "today-button",
61     TodayClearArea: "today-clear-area",
62     Unavailable: "unavailable",
63     WeekContainer: "week-container",
64     YearMonthArea: "year-month-area",
65     YearMonthButton: "year-month-button",
66     YearMonthButtonLeft: "year-month-button-left",
67     YearMonthButtonRight: "year-month-button-right",
68     YearMonthUpper: "year-month-upper"
69 };
70
71 /**
72  * @type {Object}
73  */
74 var global = {
75     argumentsReceived: false,
76     params: null,
77     picker: null
78 };
79
80 // ----------------------------------------------------------------
81 // Utility functions
82
83 /**
84  * @return {!string} lowercase locale name. e.g. "en-us"
85  */
86 function getLocale() {
87     return (global.params.locale || "en-us").toLowerCase();
88 }
89
90 /**
91  * @return {!string} lowercase language code. e.g. "en"
92  */
93 function getLanguage() {
94     var locale = getLocale();
95     var result = locale.match(/^([a-z]+)/);
96     if (!result)
97         return "en";
98     return result[1];
99 }
100
101 /**
102  * @param {!number} number
103  * @return {!string}
104  */
105 function localizeNumber(number) {
106     return window.pagePopupController.localizeNumberString(number);
107 }
108
109 /*
110  * @const
111  * @type {number}
112  */
113 var ImperialEraLimit = 2087;
114
115 /**
116  * @param {!number} year
117  * @param {!number} month
118  * @return {!string}
119  */
120 function formatJapaneseImperialEra(year, month) {
121     // We don't show an imperial era if it is greater than 99 becase of space
122     // limitation.
123     if (year > ImperialEraLimit)
124         return "";
125     if (year > 1989)
126         return "(平成" + localizeNumber(year - 1988) + "年)";
127     if (year == 1989)
128         return "(平成元年)";
129     if (year >= 1927)
130         return "(昭和" + localizeNumber(year - 1925) + "年)";
131     if (year > 1912)
132         return "(大正" + localizeNumber(year - 1911) + "年)";
133     if (year == 1912 && month >= 7)
134         return "(大正元年)";
135     if (year > 1868)
136         return "(明治" + localizeNumber(year - 1867) + "年)";
137     if (year == 1868)
138         return "(明治元年)";
139     return "";
140 }
141
142 /**
143  * @return {!string}
144  */
145 Month.prototype.toLocaleString = function() {
146     if (isNaN(this.year) || isNaN(this.year))
147         return "Invalid Month";
148     var yearString = localizeNumber(this.year);
149     var monthString = global.params.monthLabels[this.month];
150     switch (getLanguage()) {
151     case "eu":
152     case "fil":
153     case "lt":
154     case "ml":
155     case "mt":
156     case "tl":
157     case "ur":
158         return yearString + " " + monthString;
159     case "hu":
160         return yearString + ". " + monthString;
161     case "ja":
162         return yearString + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + monthString;
163     case "zh":
164         return yearString + "年" + monthString;
165     case "ko":
166         return yearString + "년 " + monthString;
167     case "lv":
168         return yearString + ". g. " + monthString;
169     case "pt":
170         return monthString + " de " + yearString;
171     case "sr":
172         return monthString + ". " + yearString;
173     default:
174         return monthString + " " + yearString;
175     }
176 };
177
178 function createUTCDate(year, month, date) {
179     var newDate = new Date(0);
180     newDate.setUTCFullYear(year);
181     newDate.setUTCMonth(month);
182     newDate.setUTCDate(date);
183     return newDate;
184 };
185
186 /**
187  * @param {string} dateString
188  * @return {?Day|Month}
189  */
190 function parseDateString(dateString) {
191     var month = Month.parse(dateString);
192     if (month)
193         return month;
194     return Day.parse(dateString);
195 }
196
197 /**
198  * @param {!number|Day} valueOrDayOrYear
199  * @param {!number=} month
200  * @param {!number=} date
201  */
202 function Day(valueOrDayOrYear, month, date) {
203     var dateObject;
204     if (arguments.length == 3)
205         dateObject = createUTCDate(valueOrDayOrYear, month, date);
206     else if (valueOrDayOrYear instanceof Day)
207         dateObject = createUTCDate(valueOrDayOrYear.year, valueOrDayOrYear.month, valueOrDayOrYear.date);
208     else
209         dateObject = new Date(valueOrDayOrYear);
210     this.year = dateObject.getUTCFullYear();    
211     this.month = dateObject.getUTCMonth();
212     this.date = dateObject.getUTCDate();
213 };
214
215 Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)$/;
216
217 // See WebCore/platform/DateComponents.h.
218 Day.Minimum = new Day(-62135596800000.0);
219 Day.Maximum = new Day(8640000000000000.0);
220
221 /**
222  * @param {!string} str
223  * @return {?Month}
224  */
225 Day.parse = function(str) {
226     var match = Day.ISOStringRegExp.exec(str);
227     if (!match)
228         return null;
229     var year = parseInt(match[1], 10);
230     var month = parseInt(match[2], 10) - 1;
231     var date = parseInt(match[3], 10);
232     return new Day(year, month, date);
233 };
234
235 /**
236  * @param {!Date} date
237  * @return {!Month}
238  */
239 Day.createFromDate = function(date) {
240     return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
241 };
242
243 /**
244  * @return {!Month}
245  */
246 Day.createFromToday = function() {
247     var now = new Date();
248     return new Day(now.getFullYear(), now.getMonth(), now.getDate());
249 };
250
251 /**
252  * @param {!Day} other
253  * @return {!bool}
254  */
255 Day.prototype.equals = function(other) {
256     return this.year === other.year && this.month === other.month && this.date === other.date;
257 };
258
259 /**
260  * @return {!Day}
261  */
262 Day.prototype.previous = function() {
263     return new Day(this.year, this.month, this.date - 1);
264 };
265
266 /**
267  * @return {!Day}
268  */
269 Day.prototype.next = function() {
270     return new Day(this.year, this.month, this.date + 1);
271 };
272
273 /**
274  * @return {!Date}
275  */
276 Day.prototype.startDate = function() {
277     return createUTCDate(this.year, this.month, this.date);
278 };
279
280 /**
281  * @return {!Date}
282  */
283 Day.prototype.endDate = function() {
284     return createUTCDate(this.year, this.month, this.date + 1);
285 };
286
287 /**
288  * @return {!number}
289  */
290 Day.prototype.valueOf = function() {
291     return this.startDate().getTime();
292 };
293
294 /**
295  * @return {!string}
296  */
297 Day.prototype.toString = function() {
298     var yearString = String(this.year);
299     if (yearString.length < 4)
300         yearString = ("000" + yearString).substr(-4, 4);
301     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2);
302 };
303
304 /**
305  * @param {!number|Month} valueOrMonthOrYear
306  * @param {!number=} month
307  */
308 function Month(valueOrMonthOrYear, month) {
309     if (arguments.length == 2) {
310         this.year = valueOrMonthOrYear;
311         this.month = month;
312     } else if (valueOrMonthOrYear instanceof Month) {
313         this.year = valueOrMonthOrYear.year;
314         this.month = valueOrMonthOrYear.month;
315     } else {
316         this.year = 1970;
317         this.month = valueOrMonthOrYear;
318     }
319     this.year = this.year + Math.floor(this.month / 12);
320     this.month = this.month < 0 ? this.month % 12 + 12 : this.month % 12;
321     if (this.year <= 0 || Month.Maximum < this) {
322         this.year = NaN;
323         this.month = NaN;
324     }
325 };
326
327 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
328
329 // See WebCore/platform/DateComponents.h.
330 Month.Minimum = new Month(1, 0);
331 Month.Maximum = new Month(275760, 8);
332
333 /**
334  * @param {!string} str
335  * @return {?Month}
336  */
337 Month.parse = function(str) {
338     var match = Month.ISOStringRegExp.exec(str);
339     if (!match)
340         return null;
341     var year = parseInt(match[1], 10);
342     var month = parseInt(match[2], 10) - 1;
343     return new Month(year, month);
344 };
345
346 /**
347  * @param {!Date} date
348  * @return {!Month}
349  */
350 Month.createFromDate = function(date) {
351     return new Month(date.getUTCFullYear(), date.getUTCMonth());
352 };
353
354 /**
355  * @return {!Month}
356  */
357 Month.createFromToday = function() {
358     var now = new Date();
359     return new Month(now.getFullYear(), now.getMonth());
360 };
361
362 /**
363  * @param {!Month} other
364  * @return {!bool}
365  */
366 Month.prototype.equals = function(other) {
367     return this.year === other.year && this.month === other.month;
368 };
369
370 /**
371  * @return {!Month}
372  */
373 Month.prototype.previous = function() {
374     return new Month(this.year, this.month - 1);
375 };
376
377 /**
378  * @return {!Month}
379  */
380 Month.prototype.next = function() {
381     return new Month(this.year, this.month + 1);
382 };
383
384 /**
385  * @return {!Date}
386  */
387 Month.prototype.startDate = function() {
388     return createUTCDate(this.year, this.month, 1);
389 };
390
391 /**
392  * @return {!Date}
393  */
394 Month.prototype.endDate = function() {
395     if (this.equals(Month.Maximum))
396         return Day.Maximum.startDate();
397     return this.next().startDate();
398 };
399
400 /**
401  * @return {!number}
402  */
403 Month.prototype.valueOf = function() {
404     return (this.year - 1970) * 12 + this.month;
405 };
406
407 /**
408  * @return {!string}
409  */
410 Month.prototype.toString = function() {
411     var yearString = String(this.year);
412     if (yearString.length < 4)
413         yearString = ("000" + yearString).substr(-4, 4);
414     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
415 };
416
417 // ----------------------------------------------------------------
418 // Initialization
419
420 /**
421  * @param {Event} event
422  */
423 function handleMessage(event) {
424     if (global.argumentsReceived)
425         return;
426     global.argumentsReceived = true;
427     initialize(JSON.parse(event.data));
428 }
429
430 function handleArgumentsTimeout() {
431     if (global.argumentsReceived)
432         return;
433     var args = {
434         monthLabels : ["m1", "m2", "m3", "m4", "m5", "m6",
435                        "m7", "m8", "m9", "m10", "m11", "m12"],
436         dayLabels : ["d1", "d2", "d3", "d4", "d5", "d6", "d7"],
437         todayLabel : "Today",
438         clearLabel : "Clear",
439         cancelLabel : "Cancel",
440         currentValue : "",
441         weekStartDay : 0,
442         step : CalendarPicker.DefaultStepScaleFactor,
443         stepBase: CalendarPicker.DefaultStepBase
444     };
445     initialize(args);
446 }
447
448 /**
449  * @param {!Object} config
450  * @return {?string} An error message, or null if the argument has no errors.
451  */
452 CalendarPicker.validateConfig = function(config) {
453     if (!config.monthLabels)
454         return "No monthLabels.";
455     if (config.monthLabels.length != 12)
456         return "monthLabels is not an array with 12 elements.";
457     if (!config.dayLabels)
458         return "No dayLabels.";
459     if (config.dayLabels.length != 7)
460         return "dayLabels is not an array with 7 elements.";
461     if (!config.clearLabel)
462         return "No clearLabel.";
463     if (!config.todayLabel)
464         return "No todayLabel.";
465     if (config.weekStartDay) {
466         if (config.weekStartDay < 0 || config.weekStartDay > 6)
467             return "Invalid weekStartDay: " + config.weekStartDay;
468     }
469     return null;
470 }
471
472 /**
473  * @param {!Object} args
474  */
475 function initialize(args) { 
476     global.params = args;
477     var errorString = CalendarPicker.validateConfig(args);
478     if (args.suggestionValues)
479         errorString = errorString || SuggestionPicker.validateConfig(args)
480     if (errorString) {
481         var main = $("main");
482         main.textContent = "Internal error: " + errorString;
483         resizeWindow(main.offsetWidth, main.offsetHeight);
484     } else {
485         if (global.params.suggestionValues && global.params.suggestionValues.length)
486             openSuggestionPicker();
487         else
488             openCalendarPicker();
489     }
490 }
491
492 function closePicker() {
493     if (global.picker)
494         global.picker.cleanup();
495     var main = $("main");
496     main.innerHTML = "";
497     main.className = "";
498 };
499
500 function openSuggestionPicker() {
501     closePicker();
502     global.picker = new SuggestionPicker($("main"), global.params);
503 };
504
505 function openCalendarPicker() {
506     closePicker();
507     global.picker = new CalendarPicker($("main"), global.params);
508 };
509
510 /**
511  * @constructor
512  * @param {!Element} element
513  * @param {!Object} config
514  */
515 function CalendarPicker(element, config) {
516     Picker.call(this, element, config);
517     if (this._config.mode === "month") {
518         this.selectionConstructor = Month;
519         this._daysTable = new MonthPickerDaysTable(this);
520         this._element.classList.add(ClassNames.MonthMode);
521     } else {
522         this.selectionConstructor = Day;
523         this._daysTable = new DaysTable(this);
524     }
525     this._element.classList.add("calendar-picker");
526     this._element.classList.add("preparing");
527     this._handleWindowResizeBound = this._handleWindowResize.bind(this);
528     window.addEventListener("resize", this._handleWindowResizeBound, false);
529     // We assume this._config.min/max are valid dates or months.
530     var minimum = (typeof this._config.min !== "undefined") ? parseDateString(this._config.min) : this.selectionConstructor.Minimum;
531     var maximum = (typeof this._config.max !== "undefined") ? parseDateString(this._config.max) : this.selectionConstructor.Maximum;
532     this._minimumValue = minimum.valueOf();
533     this._maximumValue = maximum.valueOf();
534     this.step = (typeof this._config.step !== undefined) ? Number(this._config.step) : CalendarPicker.DefaultStepScaleFactor;
535     this.stepBase = (typeof this._config.stepBase !== "undefined") ? Number(this._config.stepBase) : CalendarPicker.DefaultStepBase;
536     this._minimumMonth = Month.createFromDate(minimum.startDate());
537     this.maximumMonth = Month.createFromDate(maximum.startDate());
538     this._currentMonth = new Month(NaN, NaN);
539     this._yearMonthController = new YearMonthController(this);
540     this._hadKeyEvent = false;
541     this._layout();
542     var initialSelection = parseDateString(this._config.currentValue);
543     if (!initialSelection)
544         initialSelection = this.selectionConstructor.createFromToday();
545     if (initialSelection.valueOf() < this._minimumValue)
546         initialSelection = new this.selectionConstructor(this._minimumValue);
547     else if (initialSelection.valueOf() > this._maximumValue)
548         initialSelection = new this.selectionConstructor(this._maximumValue);
549     this.showMonth(Month.createFromDate(initialSelection.startDate()), false);
550     this._daysTable.selectRangeAndShowEntireRange(initialSelection);
551     this.fixWindowSize();
552     this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
553     document.body.addEventListener("keydown", this._handleBodyKeyDownBound, false);
554 }
555 CalendarPicker.prototype = Object.create(Picker.prototype);
556
557 // See WebCore/html/DateInputType.cpp.
558 CalendarPicker.DefaultStepScaleFactor = 86400000;
559 CalendarPicker.DefaultStepBase = 0.0;
560
561 CalendarPicker.prototype._handleWindowResize = function() {
562     this._element.classList.remove("preparing");
563 };
564
565 CalendarPicker.prototype.cleanup = function() {
566     document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
567 };
568
569 CalendarPicker.prototype._layout = function() {
570     if (this._config.isCalendarRTL)
571         this._element.classList.add("rtl");
572     this._yearMonthController.attachTo(this._element);
573     this._daysTable.attachTo(this._element);
574     this._layoutButtons();
575     // DaysTable will have focus but we don't want to show its focus ring until the first key event.
576     this._element.classList.add(ClassNames.NoFocusRing);
577 };
578
579 CalendarPicker.prototype.handleToday = function() {
580     var today = this.selectionConstructor.createFromToday();
581     this._daysTable.selectRangeAndShowEntireRange(today);
582     this.submitValue(today.toString());
583 };
584
585 CalendarPicker.prototype.handleClear = function() {
586     this.submitValue("");
587 };
588
589 CalendarPicker.prototype.fixWindowSize = function() {
590     var yearMonthRightElement = this._element.getElementsByClassName(ClassNames.YearMonthButtonRight)[0];
591     var daysAreaElement = this._element.getElementsByClassName(ClassNames.DaysArea)[0];
592     var headers = daysAreaElement.getElementsByClassName(ClassNames.DayLabel);
593     var maxCellWidth = 0;
594     for (var i = 0; i < headers.length; ++i) {
595         if (maxCellWidth < headers[i].offsetWidth)
596             maxCellWidth = headers[i].offsetWidth;
597     }
598     var DaysAreaContainerBorder = 1;
599     var yearMonthEnd;
600     var daysAreaEnd;
601     if (global.params.isCalendarRTL) {
602         var startOffset = this._element.offsetLeft + this._element.offsetWidth;
603         yearMonthEnd = startOffset - yearMonthRightElement.offsetLeft;
604         daysAreaEnd = startOffset - (daysAreaElement.offsetLeft + daysAreaElement.offsetWidth) + maxCellWidth * 7 + DaysAreaContainerBorder;
605     } else {
606         yearMonthEnd = yearMonthRightElement.offsetLeft + yearMonthRightElement.offsetWidth;
607         daysAreaEnd = daysAreaElement.offsetLeft + maxCellWidth * 7 + DaysAreaContainerBorder;
608     }
609     var maxEnd = Math.max(yearMonthEnd, daysAreaEnd);
610     var MainPadding = 6; // FIXME: Fix name.
611     var MainBorder = 1;
612     var desiredBodyWidth = maxEnd + MainPadding + MainBorder;
613
614     var elementHeight = this._element.offsetHeight;
615     this._element.style.width = "auto";
616     daysAreaElement.style.width = "100%";
617     daysAreaElement.style.tableLayout = "fixed";
618     this._element.getElementsByClassName(ClassNames.YearMonthUpper)[0].style.display = "-webkit-box";
619     this._element.getElementsByClassName(ClassNames.MonthSelectorBox)[0].style.display = "block";
620     resizeWindow(desiredBodyWidth, elementHeight);
621 };
622
623 CalendarPicker.prototype._layoutButtons = function() {
624     var container = createElement("div", ClassNames.TodayClearArea);
625     this.today = createElement("input", ClassNames.TodayButton);
626     this.today.disabled = !this.isValidDate(this.selectionConstructor.createFromToday());
627     this.today.type = "button";
628     this.today.value = this._config.todayLabel;
629     this.today.addEventListener("click", this.handleToday.bind(this), false);
630     container.appendChild(this.today);
631     this.clear = null;
632     if (!this._config.required) {
633         this.clear = createElement("input", ClassNames.ClearButton);
634         this.clear.type = "button";
635         this.clear.value = this._config.clearLabel;
636         this.clear.addEventListener("click", this.handleClear.bind(this), false);
637         container.appendChild(this.clear);
638     }
639     this._element.appendChild(container);
640
641     this.lastFocusableControl = this.clear || this.today;
642 };
643
644 /**
645  * @param {!Month} month
646  * @return {!bool}
647  */
648 CalendarPicker.prototype.shouldShowMonth = function(month) {
649     return this._minimumMonth.valueOf() <= month.valueOf() && this.maximumMonth.valueOf() >= month.valueOf();
650 };
651
652 /**
653  * @param {!Month} month
654  * @param {!bool=} animate
655  * @param {!bool=} keepSelectionPosition
656  */
657 CalendarPicker.prototype.showMonth = function(month, animate, keepSelectionPosition) {
658     if (this._currentMonth.equals(month))
659         return;
660     else if (month.valueOf() < this._minimumMonth.valueOf())
661         month = this._minimumMonth;
662     else if (month.valueOf() > this.maximumMonth.valueOf())
663         month = this.maximumMonth;
664     this._yearMonthController.setMonth(month);
665     this._daysTable.navigateToMonth(month, animate, keepSelectionPosition);
666     this._currentMonth = month;
667 };
668
669 /**
670  * @return {!Month}
671  */
672 CalendarPicker.prototype.currentMonth = function() {
673     return this._currentMonth;
674 };
675
676 // ----------------------------------------------------------------
677
678 /**
679  * @constructor
680  * @param {!CalendarPicker} picker
681  */
682 function YearMonthController(picker) {
683     this.picker = picker;
684 }
685
686 /**
687  * @param {!Element} element
688  */
689 YearMonthController.prototype.attachTo = function(element) {
690     var outerContainer = createElement("div", ClassNames.YearMonthArea);
691
692     var innerContainer = createElement("div", ClassNames.YearMonthUpper);
693     outerContainer.appendChild(innerContainer);
694
695     this._attachLeftButtonsTo(innerContainer);
696
697     var box = createElement("div", ClassNames.MonthSelectorBox);
698     innerContainer.appendChild(box);
699     // We can't use <select> popup in PagePopup.
700     this._monthPopup = createElement("div", ClassNames.MonthSelectorPopup);
701     this._monthPopup.addEventListener("click", this._handleYearMonthChange.bind(this), false);
702     this._monthPopup.addEventListener("keydown", this._handleMonthPopupKey.bind(this), false);
703     this._monthPopup.addEventListener("mousemove", this._handleMouseMove.bind(this), false);
704     this._updateSelectionOnMouseMove = true;
705     this._monthPopup.tabIndex = 0;
706     this._monthPopupContents = createElement("div", ClassNames.MonthSelectorPopupContents);
707     this._monthPopup.appendChild(this._monthPopupContents);
708     box.appendChild(this._monthPopup);
709     this._month = createElement("div", ClassNames.MonthSelector);
710     this._month.addEventListener("click", this._showPopup.bind(this), false);
711     box.appendChild(this._month);
712
713     this._attachRightButtonsTo(innerContainer);
714     element.appendChild(outerContainer);
715
716     this._wall = createElement("div", ClassNames.MonthSelectorWall);
717     this._wall.addEventListener("click", this._closePopup.bind(this), false);
718     element.appendChild(this._wall);
719
720     var month = this.picker.maximumMonth;
721     var maxWidth = 0;
722     for (var m = 0; m < 12; ++m) {
723         this._month.textContent = month.toLocaleString();
724         maxWidth = Math.max(maxWidth, this._month.offsetWidth);
725         month = month.previous();
726     }
727     if (getLanguage() == "ja" && ImperialEraLimit < maximumYear) {
728         for (var m = 0; m < 12; ++m) {
729             this._month.textContent = new Month(ImperialEraLimit, m).toLocaleString();
730             maxWidth = Math.max(maxWidth, this._month.offsetWidth);
731         }
732     }
733     this._month.style.minWidth = maxWidth + 'px';
734
735     this.picker.firstFocusableControl = this._left2; // FIXME: Should it be this.month?
736 };
737
738 YearMonthController.addTenYearsButtons = false;
739
740 /**
741  * @param {!Element} parent
742  */
743 YearMonthController.prototype._attachLeftButtonsTo = function(parent) {
744     var container = createElement("div", ClassNames.YearMonthButtonLeft);
745     parent.appendChild(container);
746
747     if (YearMonthController.addTenYearsButtons) {
748         this._left3 = createElement("input", ClassNames.YearMonthButton);
749         this._left3.type = "button";
750         this._left3.value = "<<<";
751         this._left3.addEventListener("click", this._handleButtonClick.bind(this), false);
752         container.appendChild(this._left3);
753     }
754
755     this._left2 = createElement("input", ClassNames.YearMonthButton);
756     this._left2.type = "button";
757     this._left2.value = "<<";
758     this._left2.addEventListener("click", this._handleButtonClick.bind(this), false);
759     container.appendChild(this._left2);
760
761     this._left1 = createElement("input", ClassNames.YearMonthButton);
762     this._left1.type = "button";
763     this._left1.value = "<";
764     this._left1.addEventListener("click", this._handleButtonClick.bind(this), false);
765     container.appendChild(this._left1);
766 };
767
768 /**
769  * @param {!Element} parent
770  */
771 YearMonthController.prototype._attachRightButtonsTo = function(parent) {
772     var container = createElement("div", ClassNames.YearMonthButtonRight);
773     parent.appendChild(container);
774     this._right1 = createElement("input", ClassNames.YearMonthButton);
775     this._right1.type = "button";
776     this._right1.value = ">";
777     this._right1.addEventListener("click", this._handleButtonClick.bind(this), false);
778     container.appendChild(this._right1);
779
780     this._right2 = createElement("input", ClassNames.YearMonthButton);
781     this._right2.type = "button";
782     this._right2.value = ">>";
783     this._right2.addEventListener("click", this._handleButtonClick.bind(this), false);
784     container.appendChild(this._right2);
785
786     if (YearMonthController.addTenYearsButtons) {
787         this._right3 = createElement("input", ClassNames.YearMonthButton);
788         this._right3.type = "button";
789         this._right3.value = ">>>";
790         this._right3.addEventListener("click", this._handleButtonClick.bind(this), false);
791         container.appendChild(this._right3);
792     }
793 };
794
795 /**
796  * @param {!Month} month
797  */
798 YearMonthController.prototype.setMonth = function(month) {
799     var monthValue = month.valueOf();
800     if (this._left3)
801         this._left3.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 13));
802     this._left2.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 2));
803     this._left1.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 1));
804     this._right1.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 1));
805     this._right2.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 2));
806     if (this._right3)
807         this._left3.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 13));
808     this._month.innerText = month.toLocaleString();
809     while (this._monthPopupContents.hasChildNodes())
810         this._monthPopupContents.removeChild(this._monthPopupContents.firstChild);
811
812     for (var m = monthValue - 6; m <= monthValue + 6; m++) {
813         var month = new Month(m);
814         if (!this.picker.shouldShowMonth(month))
815             continue;
816         var option = createElement("div", ClassNames.MonthSelectorPopupEntry, month.toLocaleString());
817         option.dataset.value = month.toString();
818         this._monthPopupContents.appendChild(option);
819         if (m == monthValue)
820             option.classList.add(ClassNames.SelectedMonthYear);
821     }
822 };
823
824 YearMonthController.prototype._showPopup = function() {
825     this._monthPopup.style.display = "block";
826     this._monthPopup.style.zIndex = "1000"; // Larger than the days area.
827     this._monthPopup.style.left = this._month.offsetLeft + (this._month.offsetWidth - this._monthPopup.offsetWidth) / 2 + "px";
828     this._monthPopup.style.top = this._month.offsetTop + this._month.offsetHeight + "px";
829
830     this._wall.style.display = "block";
831     this._wall.style.zIndex = "999"; // This should be smaller than the z-index of monthPopup.
832
833     var popupHeight = this._monthPopup.clientHeight;
834     var fullHeight = this._monthPopupContents.clientHeight;
835     if (fullHeight > popupHeight) {
836         var selected = this._getSelection();
837         if (selected) {
838            var bottom = selected.offsetTop + selected.clientHeight;
839            if (bottom > popupHeight)
840                this._monthPopup.scrollTop = bottom - popupHeight;
841         }
842         this._monthPopup.style.webkitPaddingEnd = getScrollbarWidth() + 'px';
843     }
844     this._monthPopup.focus();
845 };
846
847 YearMonthController.prototype._closePopup = function() {
848     this._monthPopup.style.display = "none";
849     this._wall.style.display = "none";
850     var container = document.querySelector("." + ClassNames.DaysAreaContainer);
851     container.focus();
852 };
853
854 /**
855  * @return {Element} Selected element in the month-year popup.
856  */
857 YearMonthController.prototype._getSelection = function()
858 {
859     return document.querySelector("." + ClassNames.SelectedMonthYear);
860 }
861
862 /**
863  * @param {Event} event
864  */
865 YearMonthController.prototype._handleMouseMove = function(event)
866 {
867     if (!this._updateSelectionOnMouseMove) {
868         // Selection update turned off while navigating with keyboard to prevent a mouse
869         // move trigged during a scroll from resetting the selection. Automatically
870         // rearm control to enable mouse-based selection.
871         this._updateSelectionOnMouseMove = true;
872     } else {
873         var target = event.target;
874         var selection = this._getSelection();
875         if (target && target != selection && target.classList.contains(ClassNames.MonthSelectorPopupEntry)) {
876             if (selection)
877                 selection.classList.remove(ClassNames.SelectedMonthYear);
878             target.classList.add(ClassNames.SelectedMonthYear);
879         }
880     }
881     event.stopPropagation();
882     event.preventDefault();
883 }
884
885 /**
886  * @param {Event} event
887  */
888 YearMonthController.prototype._handleMonthPopupKey = function(event)
889 {
890     var key = event.keyIdentifier;
891     if (key == "Down") {
892         var selected = this._getSelection();
893         if (selected) {
894             var next = selected.nextSibling;
895             if (next) {
896                 selected.classList.remove(ClassNames.SelectedMonthYear);
897                 next.classList.add(ClassNames.SelectedMonthYear);
898                 var bottom = next.offsetTop + next.clientHeight;
899                 if (bottom > this._monthPopup.scrollTop + this._monthPopup.clientHeight) {
900                     this._updateSelectionOnMouseMove = false;
901                     this._monthPopup.scrollTop = bottom - this._monthPopup.clientHeight;
902                 }
903             }
904         }
905         event.stopPropagation();
906         event.preventDefault();
907     } else if (key == "Up") {
908         var selected = this._getSelection();
909         if (selected) {
910             var previous = selected.previousSibling;
911             if (previous) {
912                 selected.classList.remove(ClassNames.SelectedMonthYear);
913                 previous.classList.add(ClassNames.SelectedMonthYear);
914                 if (previous.offsetTop < this._monthPopup.scrollTop) {
915                     this._updateSelectionOnMouseMove = false;
916                     this._monthPopup.scrollTop = previous.offsetTop;
917                 }
918             }
919         }
920         event.stopPropagation();
921         event.preventDefault();
922     } else if (key == "U+001B") {
923         this._closePopup();
924         event.stopPropagation();
925         event.preventDefault();
926     } else if (key == "Enter") {
927         this._handleYearMonthChange();
928         event.stopPropagation();
929         event.preventDefault();
930     }
931 }
932
933 YearMonthController.prototype._handleYearMonthChange = function() {
934     this._closePopup();
935     var selection = this._getSelection();
936     if (!selection)
937         return;
938     this.picker.showMonth(Month.parse(selection.dataset.value));
939 };
940
941 /*
942  * @const
943  * @type {number}
944  */
945 YearMonthController.PreviousTenYears = -120;
946 /*
947  * @const
948  * @type {number}
949  */
950 YearMonthController.PreviousYear = -12;
951 /*
952  * @const
953  * @type {number}
954  */
955 YearMonthController.PreviousMonth = -1;
956 /*
957  * @const
958  * @type {number}
959  */
960 YearMonthController.NextMonth = 1;
961 /*
962  * @const
963  * @type {number}
964  */
965 YearMonthController.NextYear = 12;
966 /*
967  * @const
968  * @type {number}
969  */
970 YearMonthController.NextTenYears = 120;
971
972 /**
973  * @param {Event} event
974  */
975 YearMonthController.prototype._handleButtonClick = function(event) {
976     if (event.target == this._left3)
977         this.moveRelatively(YearMonthController.PreviousTenYears);
978     else if (event.target == this._left2)
979         this.moveRelatively(YearMonthController.PreviousYear);
980     else if (event.target == this._left1)
981         this.moveRelatively(YearMonthController.PreviousMonth);
982     else if (event.target == this._right1)
983         this.moveRelatively(YearMonthController.NextMonth)
984     else if (event.target == this._right2)
985         this.moveRelatively(YearMonthController.NextYear);
986     else if (event.target == this._right3)
987         this.moveRelatively(YearMonthController.NextTenYears);
988     else
989         return;
990 };
991
992 /**
993  * @param {!number} amount
994  */
995 YearMonthController.prototype.moveRelatively = function(amount) {
996     var current = this.picker.currentMonth().valueOf();
997     var updated = new Month(current + amount);
998     this.picker.showMonth(updated, true, true);
999 };
1000
1001 // ----------------------------------------------------------------
1002
1003 /**
1004  * @constructor
1005  * @param {!CalendarPicker} picker
1006  */
1007 function DaysTable(picker) {
1008     this.picker = picker;
1009 }
1010
1011 /**
1012  * @return {!boolean}
1013  */
1014 DaysTable.prototype._hasSelection = function() {
1015     return !!this._firstNodeInSelectedRange();
1016 }
1017
1018 /**
1019  * The number of week lines in the screen.
1020  * @const
1021  * @type {number}
1022  */
1023 DaysTable._Weeks = 6;
1024
1025 /**
1026  * @param {!Element} element
1027  */
1028 DaysTable.prototype.attachTo = function(element) {
1029     this._daysContainer = createElement("table", ClassNames.DaysArea);
1030     this._daysContainer.addEventListener("click", this._handleDayClick.bind(this), false);
1031     this._daysContainer.addEventListener("mouseover", this._handleMouseOver.bind(this), false);
1032     this._daysContainer.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
1033     this._daysContainer.addEventListener("webkitTransitionEnd", this._moveInDays.bind(this), false);
1034     var container = createElement("tr", ClassNames.DayLabelContainer);
1035     var weekStartDay = global.params.weekStartDay || 0;
1036     for (var i = 0; i < 7; i++)
1037         container.appendChild(createElement("th", ClassNames.DayLabel, global.params.dayLabels[(weekStartDay + i) % 7]));
1038     this._daysContainer.appendChild(container);
1039     this._days = [];
1040     for (var w = 0; w < DaysTable._Weeks; w++) {
1041         container = createElement("tr", ClassNames.WeekContainer);
1042         var week = [];
1043         for (var d = 0; d < 7; d++) {
1044             var day = createElement("td", ClassNames.Day, " ");
1045             day.setAttribute("data-position-x", String(d));
1046             day.setAttribute("data-position-y", String(w));
1047             week.push(day);
1048             container.appendChild(day);
1049         }
1050         this._days.push(week);
1051         this._daysContainer.appendChild(container);
1052     }
1053     container = createElement("div", ClassNames.DaysAreaContainer);
1054     container.appendChild(this._daysContainer);
1055     container.tabIndex = 0;
1056     container.addEventListener("keydown", this._handleKey.bind(this), false);
1057     element.appendChild(container);
1058
1059     container.focus();
1060 };
1061
1062 /**
1063  * @param {!number} value
1064  * @return {!boolean}
1065  */
1066 CalendarPicker.prototype._stepMismatch = function(value) {
1067     return (value - this.stepBase) % this.step != 0;
1068 }
1069
1070 /**
1071  * @param {!number} value
1072  * @return {!boolean}
1073  */
1074 CalendarPicker.prototype._outOfRange = function(value) {
1075     return value < this._minimumValue || value > this._maximumValue;
1076 }
1077
1078 /**
1079  * @param {!Month|Day} range
1080  * @return {!boolean}
1081  */
1082 CalendarPicker.prototype.isValidDate = function(range) {
1083     var value = range.valueOf();
1084     return !this._outOfRange(value) && !this._stepMismatch(value);
1085 }
1086
1087 /**
1088  * @param {!Month} month
1089  */
1090 DaysTable.prototype._renderMonth = function(month) {
1091     var dayIterator = month.startDate();
1092     var monthStartDay = dayIterator.getUTCDay();
1093     var weekStartDay = global.params.weekStartDay || 0;
1094     var startOffset = weekStartDay - monthStartDay;
1095     if (startOffset >= 0)
1096         startOffset -= 7;
1097     dayIterator.setUTCDate(startOffset + 1);
1098     for (var w = 0; w < DaysTable._Weeks; w++) {
1099         for (var d = 0; d < 7; d++) {
1100             var iterMonth = Month.createFromDate(dayIterator);
1101             var time = dayIterator.getTime();
1102             var element = this._days[w][d];
1103             element.innerText = localizeNumber(dayIterator.getUTCDate());
1104             element.className = ClassNames.Day;
1105             element.dataset.submitValue = Day.createFromDate(dayIterator).toString();
1106             element.dataset.monthValue = iterMonth.toString();
1107             if (isNaN(time)) {
1108                 element.innerText = "-";
1109                 element.classList.add(ClassNames.Unavailable);
1110             } else if (!this.picker.isValidDate(this._rangeForNode(element)))
1111                 element.classList.add(ClassNames.Unavailable);
1112             else if (!iterMonth.equals(month)) {
1113                 element.classList.add(ClassNames.Available);
1114                 element.classList.add(ClassNames.NotThisMonth);
1115             } else
1116                 element.classList.add(ClassNames.Available);
1117             dayIterator.setUTCDate(dayIterator.getUTCDate() + 1);
1118         }
1119     }
1120 };
1121
1122 /**
1123  * @param {!Month} month
1124  * @param {!bool} animate
1125  * @param {!bool} keepSelectionPosition
1126  */
1127 DaysTable.prototype.navigateToMonth = function(month, animate, keepSelectionPosition) {
1128     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1129     if (animate) {
1130         var daysStyle = this._daysContainer.style;
1131         daysStyle.position = "relative";
1132         daysStyle.webkitTransition = "left 0.1s ease";
1133         daysStyle.left = (this.picker.currentMonth().valueOf() > month.valueOf() ? "" : "-") + this._daysContainer.offsetWidth + "px";
1134     }
1135     this._renderMonth(month);
1136     if (keepSelectionPosition && firstNodeInSelectedRange) {
1137         var x = parseInt(firstNodeInSelectedRange.dataset.positionX, 10);
1138         var y = parseInt(firstNodeInSelectedRange.dataset.positionY, 10);
1139         this._selectRangeAtPosition(x, y);
1140     }
1141 };
1142
1143 DaysTable.prototype._moveInDays = function() {
1144     var daysStyle = this._daysContainer.style;
1145     if (daysStyle.left == "0px")
1146         return;
1147     daysStyle.webkitTransition = "";
1148     daysStyle.left = (daysStyle.left.charAt(0) == "-" ? "" : "-") + this._daysContainer.offsetWidth + "px";
1149     this._daysContainer.offsetLeft; // Force to layout.
1150     daysStyle.webkitTransition = "left 0.1s ease";
1151     daysStyle.left = "0px";
1152 };
1153
1154 /**
1155  * @param {!Day} day
1156  */
1157 DaysTable.prototype._markRangeAsSelected = function(day) {
1158     var dateString = day.toString();
1159     for (var w = 0; w < DaysTable._Weeks; w++) {
1160         for (var d = 0; d < 7; d++) {
1161             if (this._days[w][d].dataset.submitValue == dateString) {
1162                 this._days[w][d].classList.add(ClassNames.Selected);
1163                 break;
1164             }
1165         }
1166     }
1167 };
1168
1169 /**
1170  * @param {!Day} day
1171  */
1172 DaysTable.prototype.selectRange = function(day) {
1173     this._deselect();
1174     if (this.startDate() > day.startDate() || this.endDate() < day.endDate())
1175         this.picker.showMonth(Month.createFromDate(day.startDate()), false);
1176     this._markRangeAsSelected(day);
1177 };
1178
1179 /**
1180  * @param {!Day} day
1181  */
1182 DaysTable.prototype.selectRangeAndShowEntireRange = function(day) {
1183     this.selectRange(day);
1184 };
1185
1186 /**
1187  * @param {!Element} dayNode
1188  */
1189 DaysTable.prototype._selectRangeContainingNode = function(dayNode) {
1190     var range = this._rangeForNode(dayNode);
1191     if (!range)
1192         return;
1193     this.selectRange(range);
1194 };
1195
1196 /**
1197  * @param {!Element} dayNode
1198  * @return {?Day}
1199  */
1200 DaysTable.prototype._rangeForNode = function(dayNode) {
1201     if (!dayNode)
1202         return null;
1203     return Day.parse(dayNode.dataset.submitValue);
1204 };
1205
1206 /**
1207  * @return {!Date}
1208  */
1209 DaysTable.prototype.startDate = function() {
1210     return Day.parse(this._days[0][0].dataset.submitValue).startDate();
1211 };
1212
1213 /**
1214  * @return {!Date}
1215  */
1216 DaysTable.prototype.endDate = function() {
1217     return Day.parse(this._days[DaysTable._Weeks - 1][7 - 1].dataset.submitValue).endDate();
1218 };
1219
1220 /**
1221  * @param {!number} x
1222  * @param {!number} y
1223  */
1224 DaysTable.prototype._selectRangeAtPosition = function(x, y) {
1225     this._selectRangeContainingNode(this._days[y][x]);
1226 };
1227
1228 /**
1229  * @return {!Element}
1230  */
1231 DaysTable.prototype._firstNodeInSelectedRange = function() {
1232     return this._daysContainer.getElementsByClassName(ClassNames.Selected)[0];
1233 };
1234
1235 DaysTable.prototype._deselect = function() {
1236     var selectedNodes = this._daysContainer.getElementsByClassName(ClassNames.Selected);
1237     for (var node = selectedNodes[0]; node; node = selectedNodes[0])
1238         node.classList.remove(ClassNames.Selected);
1239 };
1240
1241 /**
1242  * @return {!boolean}
1243  */
1244 DaysTable.prototype._maybeSetPreviousMonth = function() {
1245     var previousMonth = this.picker.currentMonth().previous();
1246     if (!this.picker.shouldShowMonth(previousMonth))
1247         return false;
1248     this.picker.showMonth(previousMonth, true);
1249     return true;
1250 };
1251
1252 /**
1253  * @return {!boolean}
1254  */
1255 DaysTable.prototype._maybeSetNextMonth = function() {
1256     var nextMonth = this.picker.currentMonth().next();
1257     if (!this.picker.shouldShowMonth(nextMonth))
1258         return false;
1259     this.picker.showMonth(nextMonth, true);
1260     return true;
1261 };
1262
1263 /**
1264  * @param {Event} event
1265  */
1266 DaysTable.prototype._handleDayClick = function(event) {
1267     if (event.target.classList.contains(ClassNames.Available))
1268         this.picker.submitValue(event.target.dataset.submitValue);
1269 };
1270
1271 /**
1272  * @param {Event} event
1273  */
1274 DaysTable.prototype._handleMouseOver = function(event) {
1275     var node = event.target;
1276     if (node.classList.contains(ClassNames.Selected))
1277         return;
1278     this._selectRangeContainingNode(node);
1279 };
1280
1281 /**
1282  * @param {Event} event
1283  */
1284 DaysTable.prototype._handleMouseOut = function(event) {
1285     this._deselect();
1286 };
1287
1288 /**
1289  * @param {Event} event
1290  */
1291 DaysTable.prototype._handleKey = function(event) {
1292     this.picker.maybeUpdateFocusStyle();
1293     var x = -1;
1294     var y = -1;
1295     var key = event.keyIdentifier;
1296     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1297     if (firstNodeInSelectedRange) {
1298         x = parseInt(firstNodeInSelectedRange.dataset.positionX, 10);
1299         y = parseInt(firstNodeInSelectedRange.dataset.positionY, 10);
1300     }
1301     if (!this._hasSelection() && (key == "Left" || key == "Up" || key == "Right" || key == "Down")) {
1302         // Put the selection on a center cell.
1303         this.updateSelection(event, 3, Math.floor(DaysTable._Weeks / 2 - 1));
1304         return;
1305     }
1306
1307     if (key == (global.params.isCalendarRTL ? "Right" : "Left")) {
1308         if (x == 0) {
1309             if (y == 0) {
1310                 if (!this._maybeSetPreviousMonth())
1311                     return;
1312                 y = DaysTable._Weeks - 1;
1313             } else
1314                 y--;
1315             x = 6;
1316         } else
1317             x--;
1318         this.updateSelection(event, x, y);
1319
1320     } else if (key == "Up") {
1321         if (y == 0) {
1322             if (!this._maybeSetPreviousMonth())
1323                 return;
1324             y = DaysTable._Weeks - 1;
1325         } else
1326             y--;
1327         this.updateSelection(event, x, y);
1328
1329     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right")) {
1330         if (x == 6) {
1331             if (y == DaysTable._Weeks - 1) {
1332                 if (!this._maybeSetNextMonth())
1333                     return;
1334                 y = 0;
1335             } else
1336                 y++;
1337             x = 0;
1338         } else
1339             x++;
1340         this.updateSelection(event, x, y);
1341
1342     } else if (key == "Down") {
1343         if (y == DaysTable._Weeks - 1) {
1344             if (!this._maybeSetNextMonth())
1345                 return;
1346             y = 0;
1347         } else
1348             y++;
1349         this.updateSelection(event, x, y);
1350
1351     } else if (key == "PageUp") {
1352         if (!this._maybeSetPreviousMonth())
1353             return;
1354         this.updateSelection(event, x, y);
1355
1356     } else if (key == "PageDown") {
1357         if (!this._maybeSetNextMonth())
1358             return;
1359         this.updateSelection(event, x, y);
1360
1361     } else if (this._hasSelection() && key == "Enter") {
1362         var dayNode = this._days[y][x];
1363         if (dayNode.classList.contains(ClassNames.Available)) {
1364             this.picker.submitValue(dayNode.dataset.submitValue);
1365             event.stopPropagation();
1366         }
1367
1368     } else if (key == "U+0054") { // 't'
1369         this.selectRangeAndShowEntireRange(Day.createFromToday());
1370         event.stopPropagation();
1371         event.preventDefault();
1372     }
1373 };
1374
1375 /**
1376  * @param {Event} event
1377  * @param {!number} x
1378  * @param {!number} y
1379  */
1380 DaysTable.prototype.updateSelection = function(event, x, y) {
1381     this._selectRangeAtPosition(x, y);
1382     event.stopPropagation();
1383     event.preventDefault();
1384 };
1385
1386 /**
1387  * @constructor
1388  * @param{!CalendarPicker} picker
1389  */
1390 function MonthPickerDaysTable(picker) {
1391     DaysTable.call(this, picker);
1392 }
1393 MonthPickerDaysTable.prototype = Object.create(DaysTable.prototype);
1394
1395 /**
1396  * @param {!Month} month
1397  */
1398 MonthPickerDaysTable.prototype._markRangeAsSelected = function(month) {
1399     var monthString = month.toString();
1400     for (var w = 0; w < DaysTable._Weeks; w++) {
1401         for (var d = 0; d < 7; d++) {
1402             if (this._days[w][d].dataset.monthValue == monthString) {
1403                 this._days[w][d].classList.add(ClassNames.Selected);
1404             }
1405         }
1406     }
1407 };
1408
1409 /**
1410  * @param {!Month} month
1411  */
1412 MonthPickerDaysTable.prototype.selectRange = function(month) {
1413     this._deselect();
1414     if (this.startDate() >= month.endDate() || this.endDate() <= month.startDate())
1415         this.picker.showMonth(month, true);
1416     this._markRangeAsSelected(month);
1417 };
1418
1419 /**
1420  * @param {!Month} month
1421  */
1422 MonthPickerDaysTable.prototype.selectRangeAndShowEntireRange = function(month) {
1423     this._deselect();
1424     this.picker.showMonth(month, true);
1425     this._markRangeAsSelected(month);
1426 };
1427
1428 /**
1429  * @param {!Element} dayNode
1430  * @return {?Month}
1431  */
1432 MonthPickerDaysTable.prototype._rangeForNode = function(dayNode) {
1433     if (!dayNode)
1434         return null;
1435     return Month.parse(dayNode.dataset.monthValue);
1436 };
1437
1438 /**
1439  * @param {Event} event
1440  */
1441 MonthPickerDaysTable.prototype._handleKey = function(event) {
1442     this.picker.maybeUpdateFocusStyle();
1443     var key = event.keyIdentifier;
1444     var eventHandled = false;
1445     var currentMonth = this.picker.currentMonth();
1446     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1447     if (!firstNodeInSelectedRange
1448         && (key == "Right" || key == "Left" || key == "Up" || key == "Down" || key == "PageUp" || key == "PageDown")) {
1449         this.selectRange(currentMonth);
1450         event.stopPropagation();
1451         event.preventDefault();
1452         return;
1453     }
1454     var selectedMonth = this._rangeForNode(firstNodeInSelectedRange);
1455     if (key == (global.params.isCalendarRTL ? "Right" : "Left") || key == "Up" || key == "PageUp") {
1456         if (selectedMonth.valueOf() > currentMonth.valueOf())
1457             this.selectRangeAndShowEntireRange(currentMonth);
1458         else
1459             this.selectRangeAndShowEntireRange(currentMonth.previous());
1460         eventHandled = true;
1461     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right") || key == "Down" || key == "PageDown") {
1462         if (selectedMonth.valueOf() < currentMonth.valueOf())
1463             this.selectRangeAndShowEntireRange(currentMonth);
1464         else
1465             this.selectRangeAndShowEntireRange(currentMonth.next());
1466         eventHandled = true;
1467     } else if (this._hasSelection() && key == "Enter") {
1468         if (currentSelection) {
1469             this.picker.submitValue(currentSelection.toString());
1470             eventHandled = true;
1471         }
1472     } else if (key == "U+0054") { // 't'
1473         this.selectRangeAndShowEntireRange(Month.createFromToday());
1474         eventHandled = true;
1475     }
1476     if (eventHandled) {
1477         event.stopPropagation();
1478         event.preventDefault();
1479     }
1480 };
1481
1482 /**
1483  * @param {!Event} event
1484  */
1485 CalendarPicker.prototype._handleBodyKeyDown = function(event) {
1486     this.maybeUpdateFocusStyle();
1487     var key = event.keyIdentifier;
1488     if (key == "U+0009") {
1489         if (!event.shiftKey && document.activeElement == this.lastFocusableControl) {
1490             event.stopPropagation();
1491             event.preventDefault();
1492             this.firstFocusableControl.focus();
1493         } else if (event.shiftKey && document.activeElement == this.firstFocusableControl) {
1494             event.stopPropagation();
1495             event.preventDefault();
1496             this.lastFocusableControl.focus();
1497         }
1498     } else if (key == "U+004D") { // 'm'
1499         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousMonth : YearMonthController.NextMonth);
1500     } else if (key == "U+0059") { // 'y'
1501         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousYear : YearMonthController.NextYear);
1502     } else if (key == "U+0044") { // 'd'
1503         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousTenYears : YearMonthController.NextTenYears);
1504     } else if (key == "U+001B") // ESC
1505         this.handleCancel();
1506 }
1507
1508 CalendarPicker.prototype.maybeUpdateFocusStyle = function() {
1509     if (this._hadKeyEvent)
1510         return;
1511     this._hadKeyEvent = true;
1512     this._element.classList.remove(ClassNames.NoFocusRing);
1513 }
1514
1515 if (window.dialogArguments) {
1516     initialize(dialogArguments);
1517 } else {
1518     window.addEventListener("message", handleMessage, false);
1519     window.setTimeout(handleArgumentsTimeout, 1000);
1520 }