REGRESSION (r129738): Calendar picker is too wide when the input is rtl
[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     MonthSelector: "month-selector",
50     MonthSelectorBox: "month-selector-box",
51     MonthSelectorPopup: "month-selector-popup",
52     MonthSelectorPopupContents: "month-selector-popup-contents",
53     MonthSelectorPopupEntry: "month-selector-popup-entry",
54     MonthSelectorWall: "month-selector-wall",
55     NoFocusRing: "no-focus-ring",
56     NotThisMonth: "not-this-month",
57     Selected: "day-selected",
58     SelectedMonthYear: "selected-month-year",
59     TodayButton: "today-button",
60     TodayClearArea: "today-clear-area",
61     Unavailable: "unavailable",
62     WeekContainer: "week-container",
63     YearMonthArea: "year-month-area",
64     YearMonthButton: "year-month-button",
65     YearMonthButtonLeft: "year-month-button-left",
66     YearMonthButtonRight: "year-month-button-right",
67     YearMonthUpper: "year-month-upper"
68 };
69
70 /**
71  * @type {Object}
72  */
73 var global = {
74     argumentsReceived: false,
75     params: null,
76     picker: null
77 };
78
79 // ----------------------------------------------------------------
80 // Utility functions
81
82 /**
83  * @return {!string} lowercase locale name. e.g. "en-us"
84  */
85 function getLocale() {
86     return (global.params.locale || "en-us").toLowerCase();
87 }
88
89 /**
90  * @return {!string} lowercase language code. e.g. "en"
91  */
92 function getLanguage() {
93     var locale = getLocale();
94     var result = locale.match(/^([a-z]+)/);
95     if (!result)
96         return "en";
97     return result[1];
98 }
99
100 /**
101  * @param {!number} number
102  * @return {!string}
103  */
104 function localizeNumber(number) {
105     return window.pagePopupController.localizeNumberString(number);
106 }
107
108 /*
109  * @const
110  * @type {number}
111  */
112 var ImperialEraLimit = 2087;
113
114 /**
115  * @param {!number} year
116  * @param {!number} month
117  * @return {!string}
118  */
119 function formatJapaneseImperialEra(year, month) {
120     // We don't show an imperial era if it is greater than 99 becase of space
121     // limitation.
122     if (year > ImperialEraLimit)
123         return "";
124     if (year > 1989)
125         return "(平成" + localizeNumber(year - 1988) + "年)";
126     if (year == 1989)
127         return "(平成元年)";
128     if (year >= 1927)
129         return "(昭和" + localizeNumber(year - 1925) + "年)";
130     if (year > 1912)
131         return "(大正" + localizeNumber(year - 1911) + "年)";
132     if (year == 1912 && month >= 7)
133         return "(大正元年)";
134     if (year > 1868)
135         return "(明治" + localizeNumber(year - 1867) + "年)";
136     if (year == 1868)
137         return "(明治元年)";
138     return "";
139 }
140
141 /**
142  * @param {!number} year
143  * @param {!number} month
144  * @return {!string}
145  */
146 function formatYearMonth(year, month) {
147     var yearString = localizeNumber(year);
148     var monthString = global.params.monthLabels[month];
149     switch (getLanguage()) {
150     case "eu":
151     case "fil":
152     case "lt":
153     case "ml":
154     case "mt":
155     case "tl":
156     case "ur":
157         return yearString + " " + monthString;
158     case "hu":
159         return yearString + ". " + monthString;
160     case "ja":
161         return yearString + "年" + formatJapaneseImperialEra(year, month) + " " + monthString;
162     case "zh":
163         return yearString + "年" + monthString;
164     case "ko":
165         return yearString + "년 " + monthString;
166     case "lv":
167         return yearString + ". g. " + monthString;
168     case "pt":
169         return monthString + " de " + yearString;
170     case "sr":
171         return monthString + ". " + yearString;
172     default:
173         return monthString + " " + yearString;
174     }
175 }
176
177 function createUTCDate(year, month, date) {
178     var newDate = new Date(Date.UTC(year, month, date));
179     if (year < 100)
180         newDate.setUTCFullYear(year);
181     return newDate;
182 };
183
184 /**
185  * @param {string=} opt_current
186  * @return {!Date}
187  */
188 function parseDateString(opt_current) {
189     if (opt_current) {
190         var result = opt_current.match(/(\d+)-(\d+)-(\d+)/);
191         if (result)
192             return createUTCDate(Number(result[1]), Number(result[2]) - 1, Number(result[3]));
193     }
194     var now = new Date();
195     // Create UTC date with same numbers as local date.
196     return createUTCDate(now.getFullYear(), now.getMonth(), now.getDate());
197 }
198
199 /**
200  * @param {!number} year
201  * @param {!number} month
202  * @param {!number} day
203  * @return {!string}
204  */
205 function serializeDate(year, month, day) {
206     var yearString = String(year);
207     if (yearString.length < 4)
208         yearString = ("000" + yearString).substr(-4, 4);
209     return yearString + "-" + ("0" + (month + 1)).substr(-2, 2) + "-" + ("0" + day).substr(-2, 2);
210 }
211
212 // ----------------------------------------------------------------
213 // Initialization
214
215 /**
216  * @param {Event} event
217  */
218 function handleMessage(event) {
219     if (global.argumentsReceived)
220         return;
221     global.argumentsReceived = true;
222     initialize(JSON.parse(event.data));
223 }
224
225 function handleArgumentsTimeout() {
226     if (global.argumentsReceived)
227         return;
228     var args = {
229         monthLabels : ["m1", "m2", "m3", "m4", "m5", "m6",
230                        "m7", "m8", "m9", "m10", "m11", "m12"],
231         dayLabels : ["d1", "d2", "d3", "d4", "d5", "d6", "d7"],
232         todayLabel : "Today",
233         clearLabel : "Clear",
234         cancelLabel : "Cancel",
235         currentValue : "",
236         weekStartDay : 0,
237         step : CalendarPicker.DefaultStepScaleFactor,
238         stepBase: CalendarPicker.DefaultStepBase
239     };
240     initialize(args);
241 }
242
243 /**
244  * @param {!Object} config
245  * @return {?string} An error message, or null if the argument has no errors.
246  */
247 CalendarPicker.validateConfig = function(config) {
248     if (!config.monthLabels)
249         return "No monthLabels.";
250     if (config.monthLabels.length != 12)
251         return "monthLabels is not an array with 12 elements.";
252     if (!config.dayLabels)
253         return "No dayLabels.";
254     if (config.dayLabels.length != 7)
255         return "dayLabels is not an array with 7 elements.";
256     if (!config.clearLabel)
257         return "No clearLabel.";
258     if (!config.todayLabel)
259         return "No todayLabel.";
260     if (config.weekStartDay) {
261         if (config.weekStartDay < 0 || config.weekStartDay > 6)
262             return "Invalid weekStartDay: " + config.weekStartDay;
263     }
264     return null;
265 }
266
267 /**
268  * @param {!Object} args
269  */
270 function initialize(args) { 
271     global.params = args;
272     var errorString = CalendarPicker.validateConfig(args);
273     if (args.suggestionValues)
274         errorString = errorString || SuggestionPicker.validateConfig(args)
275     if (errorString) {
276         var main = $("main");
277         main.textContent = "Internal error: " + errorString;
278         resizeWindow(main.offsetWidth, main.offsetHeight);
279     } else {
280         if (global.params.suggestionValues && global.params.suggestionValues.length)
281             openSuggestionPicker();
282         else
283             openCalendarPicker();
284     }
285 }
286
287 function closePicker() {
288     if (global.picker)
289         global.picker.cleanup();
290     var main = $("main");
291     main.innerHTML = "";
292     main.className = "";
293 };
294
295 function openSuggestionPicker() {
296     closePicker();
297     global.picker = new SuggestionPicker($("main"), global.params);
298 };
299
300 function openCalendarPicker() {
301     closePicker();
302     global.picker = new CalendarPicker($("main"), global.params);
303 };
304
305 /**
306  * @constructor
307  * @param {!Element} element
308  * @param {!Object} config
309  */
310 function CalendarPicker(element, config) {
311     Picker.call(this, element, config);
312     this._element.classList.add("calendar-picker");
313     // We assume this._config.min is a valid date.
314     this.minimumDate = (typeof this._config.min !== "undefined") ? parseDateString(this._config.min) : CalendarPicker.MinimumPossibleDate;
315     // We assume this._config.max is a valid date.
316     this.maximumDate = (typeof this._config.max !== "undefined") ? parseDateString(this._config.max) : CalendarPicker.MaximumPossibleDate;
317     this.step = (typeof this._config.step !== undefined) ? Number(this._config.step) : CalendarPicker.DefaultStepScaleFactor;
318     this.stepBase = (typeof this._config.stepBase !== "undefined") ? Number(this._config.stepBase) : CalendarPicker.DefaultStepBase;
319     this.yearMonthController = new YearMonthController(this);
320     this.daysTable = new DaysTable(this);
321     this._hadKeyEvent = false;
322     this._layout();
323     var initialDate = parseDateString(this._config.currentValue);
324     if (initialDate < this.minimumDate)
325         initialDate = this.minimumDate;
326     else if (initialDate > this.maximumDate)
327         initialDate = this.maximumDate;
328     this.daysTable.selectDate(initialDate);
329     this.fixWindowSize();
330     this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
331     document.body.addEventListener("keydown", this._handleBodyKeyDownBound, false);
332 }
333 CalendarPicker.prototype = Object.create(Picker.prototype);
334
335 // Hard limits of type=date. See WebCore/platform/DateComponents.h.
336 CalendarPicker.MinimumPossibleDate = new Date(-62135596800000.0);
337 CalendarPicker.MaximumPossibleDate = new Date(8640000000000000.0);
338 // See WebCore/html/DateInputType.cpp.
339 CalendarPicker.DefaultStepScaleFactor = 86400000;
340 CalendarPicker.DefaultStepBase = 0.0;
341
342 CalendarPicker.prototype.cleanup = function() {
343     document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
344 };
345
346 CalendarPicker.prototype._layout = function() {
347     if (this._config.isCalendarRTL)
348         this._element.classList.add("rtl");
349     this.yearMonthController.attachTo(this._element);
350     this.daysTable.attachTo(this._element);
351     this._layoutButtons();
352     // DaysTable will have focus but we don't want to show its focus ring until the first key event.
353     this._element.classList.add(ClassNames.NoFocusRing);
354 };
355
356 CalendarPicker.prototype.handleToday = function() {
357     var date = new Date();
358     this.daysTable.selectDate(date);
359     this.submitValue(serializeDate(date.getFullYear(), date.getMonth(), date.getDate()));
360 };
361
362 CalendarPicker.prototype.handleClear = function() {
363     this.submitValue("");
364 };
365
366 CalendarPicker.prototype.fixWindowSize = function() {
367     var yearMonthRightElement = this._element.getElementsByClassName(ClassNames.YearMonthButtonRight)[0];
368     var daysAreaElement = this._element.getElementsByClassName(ClassNames.DaysArea)[0];
369     var headers = daysAreaElement.getElementsByClassName(ClassNames.DayLabel);
370     var maxCellWidth = 0;
371     for (var i = 0; i < headers.length; ++i) {
372         if (maxCellWidth < headers[i].offsetWidth)
373             maxCellWidth = headers[i].offsetWidth;
374     }
375     var DaysAreaContainerBorder = 1;
376     var yearMonthEnd;
377     var daysAreaEnd;
378     if (global.params.isCalendarRTL) {
379         var startOffset = this._element.offsetLeft + this._element.offsetWidth;
380         yearMonthEnd = startOffset - yearMonthRightElement.offsetLeft;
381         daysAreaEnd = startOffset - (daysAreaElement.offsetLeft + daysAreaElement.offsetWidth) + maxCellWidth * 7 + DaysAreaContainerBorder;
382     } else {
383         yearMonthEnd = yearMonthRightElement.offsetLeft + yearMonthRightElement.offsetWidth;
384         daysAreaEnd = daysAreaElement.offsetLeft + maxCellWidth * 7 + DaysAreaContainerBorder;
385     }
386     var maxEnd = Math.max(yearMonthEnd, daysAreaEnd);
387     var MainPadding = 6; // FIXME: Fix name.
388     var MainBorder = 1;
389     var desiredBodyWidth = maxEnd + MainPadding + MainBorder;
390
391     var elementHeight = this._element.offsetHeight;
392     this._element.style.width = "auto";
393     daysAreaElement.style.width = "100%";
394     daysAreaElement.style.tableLayout = "fixed";
395     this._element.getElementsByClassName(ClassNames.YearMonthUpper)[0].style.display = "-webkit-box";
396     this._element.getElementsByClassName(ClassNames.MonthSelectorBox)[0].style.display = "block";
397     resizeWindow(desiredBodyWidth, elementHeight);
398 };
399
400 CalendarPicker.prototype._layoutButtons = function() {
401     var container = createElement("div", ClassNames.TodayClearArea);
402     this.today = createElement("input", ClassNames.TodayButton);
403     this.today.type = "button";
404     this.today.value = this._config.todayLabel;
405     this.today.addEventListener("click", this.handleToday.bind(this), false);
406     container.appendChild(this.today);
407     this.clear = null;
408     if (!this._config.required) {
409         this.clear = createElement("input", ClassNames.ClearButton);
410         this.clear.type = "button";
411         this.clear.value = this._config.clearLabel;
412         this.clear.addEventListener("click", this.handleClear.bind(this), false);
413         container.appendChild(this.clear);
414     }
415     this._element.appendChild(container);
416
417     this.lastFocusableControl = this.clear || this.today;
418 };
419
420 // ----------------------------------------------------------------
421
422 /**
423  * @constructor
424  * @param {!CalendarPicker} picker
425  */
426 function YearMonthController(picker) {
427     this.picker = picker;
428     /**
429      * @type {!number}
430      */
431     this._currentYear = -1;
432     /**
433      * @type {!number}
434      */
435     this._currentMonth = -1;
436 }
437
438 /**
439  * @param {!Element} element
440  */
441 YearMonthController.prototype.attachTo = function(element) {
442     var outerContainer = createElement("div", ClassNames.YearMonthArea);
443
444     var innerContainer = createElement("div", ClassNames.YearMonthUpper);
445     outerContainer.appendChild(innerContainer);
446
447     this._attachLeftButtonsTo(innerContainer);
448
449     var box = createElement("div", ClassNames.MonthSelectorBox);
450     innerContainer.appendChild(box);
451     // We can't use <select> popup in PagePopup.
452     this._monthPopup = createElement("div", ClassNames.MonthSelectorPopup);
453     this._monthPopup.addEventListener("click", this._handleYearMonthChange.bind(this), false);
454     this._monthPopup.addEventListener("keydown", this._handleMonthPopupKey.bind(this), false);
455     this._monthPopup.addEventListener("mousemove", this._handleMouseMove.bind(this), false);
456     this._updateSelectionOnMouseMove = true;
457     this._monthPopup.tabIndex = 0;
458     this._monthPopupContents = createElement("div", ClassNames.MonthSelectorPopupContents);
459     this._monthPopup.appendChild(this._monthPopupContents);
460     box.appendChild(this._monthPopup);
461     this._month = createElement("div", ClassNames.MonthSelector);
462     this._month.addEventListener("click", this._showPopup.bind(this), false);
463     box.appendChild(this._month);
464
465     this._attachRightButtonsTo(innerContainer);
466     element.appendChild(outerContainer);
467
468     this._wall = createElement("div", ClassNames.MonthSelectorWall);
469     this._wall.addEventListener("click", this._closePopup.bind(this), false);
470     element.appendChild(this._wall);
471
472     var maximumYear = this.picker.maximumDate.getUTCFullYear();
473     var maxWidth = 0;
474     for (var m = 0; m < 12; ++m) {
475         this._month.textContent = formatYearMonth(maximumYear, m);
476         maxWidth = Math.max(maxWidth, this._month.offsetWidth);
477     }
478     if (getLanguage() == "ja" && ImperialEraLimit < maximumYear) {
479         for (var m = 0; m < 12; ++m) {
480             this._month.textContent = formatYearMonth(ImperialEraLimit, m);
481             maxWidth = Math.max(maxWidth, this._month.offsetWidth);
482         }
483     }
484     this._month.style.minWidth = maxWidth + 'px';
485
486     this.picker.firstFocusableControl = this._left2; // FIXME: Should it be this.month?
487 };
488
489 YearMonthController.addTenYearsButtons = false;
490
491 /**
492  * @param {!Element} parent
493  */
494 YearMonthController.prototype._attachLeftButtonsTo = function(parent) {
495     var container = createElement("div", ClassNames.YearMonthButtonLeft);
496     parent.appendChild(container);
497
498     if (YearMonthController.addTenYearsButtons) {
499         this._left3 = createElement("input", ClassNames.YearMonthButton);
500         this._left3.type = "button";
501         this._left3.value = "<<<";
502         this._left3.addEventListener("click", this._handleButtonClick.bind(this), false);
503         container.appendChild(this._left3);
504     }
505
506     this._left2 = createElement("input", ClassNames.YearMonthButton);
507     this._left2.type = "button";
508     this._left2.value = "<<";
509     this._left2.addEventListener("click", this._handleButtonClick.bind(this), false);
510     container.appendChild(this._left2);
511
512     this._left1 = createElement("input", ClassNames.YearMonthButton);
513     this._left1.type = "button";
514     this._left1.value = "<";
515     this._left1.addEventListener("click", this._handleButtonClick.bind(this), false);
516     container.appendChild(this._left1);
517 };
518
519 /**
520  * @param {!Element} parent
521  */
522 YearMonthController.prototype._attachRightButtonsTo = function(parent) {
523     var container = createElement("div", ClassNames.YearMonthButtonRight);
524     parent.appendChild(container);
525     this._right1 = createElement("input", ClassNames.YearMonthButton);
526     this._right1.type = "button";
527     this._right1.value = ">";
528     this._right1.addEventListener("click", this._handleButtonClick.bind(this), false);
529     container.appendChild(this._right1);
530
531     this._right2 = createElement("input", ClassNames.YearMonthButton);
532     this._right2.type = "button";
533     this._right2.value = ">>";
534     this._right2.addEventListener("click", this._handleButtonClick.bind(this), false);
535     container.appendChild(this._right2);
536
537     if (YearMonthController.addTenYearsButtons) {
538         this._right3 = createElement("input", ClassNames.YearMonthButton);
539         this._right3.type = "button";
540         this._right3.value = ">>>";
541         this._right3.addEventListener("click", this._handleButtonClick.bind(this), false);
542         container.appendChild(this._right3);
543     }
544 };
545
546 /**
547  * @return {!number}
548  */
549 YearMonthController.prototype.year = function() {
550     return this._currentYear;
551 };
552
553 /**
554  * @return {!number}
555  */
556 YearMonthController.prototype.month = function() {
557     return this._currentMonth;
558 };
559
560 /**
561  * @param {!number} year
562  * @param {!number} month
563  */
564 YearMonthController.prototype.setYearMonth = function(year, month) {
565     this._currentYear = year;
566     this._currentMonth = month;
567     this._redraw();
568 };
569
570 YearMonthController.prototype._redraw = function() {
571     var min = this.picker.minimumDate.getUTCFullYear() * 12 + this.picker.minimumDate.getUTCMonth();
572     var max = this.picker.maximumDate.getUTCFullYear() * 12 + this.picker.maximumDate.getUTCMonth();
573     var current = this._currentYear * 12 + this._currentMonth;
574     if (this._left3)
575         this._left3.disabled = current - 13 < min;
576     this._left2.disabled = current - 2 < min;
577     this._left1.disabled = current - 1 < min;
578     this._right1.disabled = current + 1 > max;
579     this._right2.disabled = current + 2 > max;
580     if (this._right3)
581         this._right3.disabled = current + 13 > max;
582     this._month.innerText = formatYearMonth(this._currentYear, this._currentMonth);
583     while (this._monthPopupContents.hasChildNodes())
584         this._monthPopupContents.removeChild(this._monthPopupContents.firstChild);
585
586     for (var m = current - 6; m <= current + 6; m++) {
587         if (m < min || m > max)
588             continue;
589         var option = createElement("div", ClassNames.MonthSelectorPopupEntry, formatYearMonth(Math.floor(m / 12), m % 12));
590         option.dataset.value = String(Math.floor(m / 12)) + "-" + String(m % 12);
591         this._monthPopupContents.appendChild(option);
592         if (m == current)
593             option.classList.add(ClassNames.SelectedMonthYear);
594     }
595 };
596
597 YearMonthController.prototype._showPopup = function() {
598     this._monthPopup.style.display = "block";
599     this._monthPopup.style.zIndex = "1000"; // Larger than the days area.
600     this._monthPopup.style.left = this._month.offsetLeft + (this._month.offsetWidth - this._monthPopup.offsetWidth) / 2 + "px";
601     this._monthPopup.style.top = this._month.offsetTop + this._month.offsetHeight + "px";
602
603     this._wall.style.display = "block";
604     this._wall.style.zIndex = "999"; // This should be smaller than the z-index of monthPopup.
605
606     var popupHeight = this._monthPopup.clientHeight;
607     var fullHeight = this._monthPopupContents.clientHeight;
608     if (fullHeight > popupHeight) {
609         var selected = this._getSelection();
610         if (selected) {
611            var bottom = selected.offsetTop + selected.clientHeight;
612            if (bottom > popupHeight)
613                this._monthPopup.scrollTop = bottom - popupHeight;
614         }
615         this._monthPopup.style.webkitPaddingEnd = getScrollbarWidth() + 'px';
616     }
617     this._monthPopup.focus();
618 };
619
620 YearMonthController.prototype._closePopup = function() {
621     this._monthPopup.style.display = "none";
622     this._wall.style.display = "none";
623     var container = document.querySelector("." + ClassNames.DaysAreaContainer);
624     container.focus();
625 };
626
627 /**
628  * @return {Element} Selected element in the month-year popup.
629  */
630 YearMonthController.prototype._getSelection = function()
631 {
632     return document.querySelector("." + ClassNames.SelectedMonthYear);
633 }
634
635 /**
636  * @param {Event} event
637  */
638 YearMonthController.prototype._handleMouseMove = function(event)
639 {
640     if (!this._updateSelectionOnMouseMove) {
641         // Selection update turned off while navigating with keyboard to prevent a mouse
642         // move trigged during a scroll from resetting the selection. Automatically
643         // rearm control to enable mouse-based selection.
644         this._updateSelectionOnMouseMove = true;
645     } else {
646         var target = event.target;
647         var selection = this._getSelection();
648         if (target && target != selection && target.classList.contains(ClassNames.MonthSelectorPopupEntry)) {
649             if (selection)
650                 selection.classList.remove(ClassNames.SelectedMonthYear);
651             target.classList.add(ClassNames.SelectedMonthYear);
652         }
653     }
654     event.stopPropagation();
655     event.preventDefault();
656 }
657
658 /**
659  * @param {Event} event
660  */
661 YearMonthController.prototype._handleMonthPopupKey = function(event)
662 {
663     var key = event.keyIdentifier;
664     if (key == "Down") {
665         var selected = this._getSelection();
666         if (selected) {
667             var next = selected.nextSibling;
668             if (next) {
669                 selected.classList.remove(ClassNames.SelectedMonthYear);
670                 next.classList.add(ClassNames.SelectedMonthYear);
671                 var bottom = next.offsetTop + next.clientHeight;
672                 if (bottom > this._monthPopup.scrollTop + this._monthPopup.clientHeight) {
673                     this._updateSelectionOnMouseMove = false;
674                     this._monthPopup.scrollTop = bottom - this._monthPopup.clientHeight;
675                 }
676             }
677         }
678         event.stopPropagation();
679         event.preventDefault();
680     } else if (key == "Up") {
681         var selected = this._getSelection();
682         if (selected) {
683             var previous = selected.previousSibling;
684             if (previous) {
685                 selected.classList.remove(ClassNames.SelectedMonthYear);
686                 previous.classList.add(ClassNames.SelectedMonthYear);
687                 if (previous.offsetTop < this._monthPopup.scrollTop) {
688                     this._updateSelectionOnMouseMove = false;
689                     this._monthPopup.scrollTop = previous.offsetTop;
690                 }
691             }
692         }
693         event.stopPropagation();
694         event.preventDefault();
695     } else if (key == "U+001B") {
696         this._closePopup();
697         event.stopPropagation();
698         event.preventDefault();
699     } else if (key == "Enter") {
700         this._handleYearMonthChange();
701         event.stopPropagation();
702         event.preventDefault();
703     }
704 }
705
706 YearMonthController.prototype._handleYearMonthChange = function() {
707     this._closePopup();
708     var selection = this._getSelection();
709     if (!selection)
710         return;
711     var value = selection.dataset.value;
712     var result  = value.match(/(\d+)-(\d+)/);
713     if (!result)
714         return;
715     var newYear = Number(result[1]);
716     var newMonth = Number(result[2]);
717     this.picker.daysTable.navigateToMonthAndKeepSelectionPosition(newYear, newMonth);
718 };
719
720 /*
721  * @const
722  * @type {number}
723  */
724 YearMonthController.PreviousTenYears = -120;
725 /*
726  * @const
727  * @type {number}
728  */
729 YearMonthController.PreviousYear = -12;
730 /*
731  * @const
732  * @type {number}
733  */
734 YearMonthController.PreviousMonth = -1;
735 /*
736  * @const
737  * @type {number}
738  */
739 YearMonthController.NextMonth = 1;
740 /*
741  * @const
742  * @type {number}
743  */
744 YearMonthController.NextYear = 12;
745 /*
746  * @const
747  * @type {number}
748  */
749 YearMonthController.NextTenYears = 120;
750
751 /**
752  * @param {Event} event
753  */
754 YearMonthController.prototype._handleButtonClick = function(event) {
755     if (event.target == this._left3)
756         this.moveRelatively(YearMonthController.PreviousTenYears);
757     else if (event.target == this._left2)
758         this.moveRelatively(YearMonthController.PreviousYear);
759     else if (event.target == this._left1)
760         this.moveRelatively(YearMonthController.PreviousMonth);
761     else if (event.target == this._right1)
762         this.moveRelatively(YearMonthController.NextMonth)
763     else if (event.target == this._right2)
764         this.moveRelatively(YearMonthController.NextYear);
765     else if (event.target == this._right3)
766         this.moveRelatively(YearMonthController.NextTenYears);
767     else
768         return;
769 };
770
771 /**
772  * @param {!number} amount
773  */
774 YearMonthController.prototype.moveRelatively = function(amount) {
775     var min = this.picker.minimumDate.getUTCFullYear() * 12 + this.picker.minimumDate.getUTCMonth();
776     var max = this.picker.maximumDate.getUTCFullYear() * 12 + this.picker.maximumDate.getUTCMonth();
777     var current = this._currentYear * 12 + this._currentMonth;
778     var updated = current;
779     if (amount < 0)
780         updated = current + amount >= min ? current + amount : min;
781     else
782         updated = current + amount <= max ? current + amount : max;
783     if (updated == current)
784         return;
785     this.picker.daysTable.navigateToMonthAndKeepSelectionPosition(Math.floor(updated / 12), updated % 12);
786 };
787
788 // ----------------------------------------------------------------
789
790 /**
791  * @constructor
792  * @param {!CalendarPicker} picker
793  */
794 function DaysTable(picker) {
795     this.picker = picker;
796     /**
797      * @type {!number}
798      */
799     this._x = -1;
800     /**
801      * @type {!number}
802      */
803     this._y = -1;
804     /**
805      * @type {!number}
806      */
807     this._currentYear = -1;
808     /**
809      * @type {!number}
810      */
811     this._currentMonth = -1;
812 }
813
814 /**
815  * @return {!boolean}
816  */
817 DaysTable.prototype._hasSelection = function() {
818     return this._x >= 0 && this._y >= 0;
819 }
820
821 /**
822  * The number of week lines in the screen.
823  * @const
824  * @type {number}
825  */
826 DaysTable._Weeks = 6;
827
828 /**
829  * @param {!Element} element
830  */
831 DaysTable.prototype.attachTo = function(element) {
832     this._daysContainer = createElement("table", ClassNames.DaysArea);
833     this._daysContainer.addEventListener("click", this._handleDayClick.bind(this), false);
834     this._daysContainer.addEventListener("mouseover", this._handleMouseOver.bind(this), false);
835     this._daysContainer.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
836     this._daysContainer.addEventListener("webkitTransitionEnd", this._moveInDays.bind(this), false);
837     var container = createElement("tr", ClassNames.DayLabelContainer);
838     var weekStartDay = global.params.weekStartDay || 0;
839     for (var i = 0; i < 7; i++)
840         container.appendChild(createElement("th", ClassNames.DayLabel, global.params.dayLabels[(weekStartDay + i) % 7]));
841     this._daysContainer.appendChild(container);
842     this._days = [];
843     for (var w = 0; w < DaysTable._Weeks; w++) {
844         container = createElement("tr", ClassNames.WeekContainer);
845         var week = [];
846         for (var d = 0; d < 7; d++) {
847             var day = createElement("td", ClassNames.Day, " ");
848             day.setAttribute("data-position-x", String(d));
849             day.setAttribute("data-position-y", String(w));
850             week.push(day);
851             container.appendChild(day);
852         }
853         this._days.push(week);
854         this._daysContainer.appendChild(container);
855     }
856     container = createElement("div", ClassNames.DaysAreaContainer);
857     container.appendChild(this._daysContainer);
858     container.tabIndex = 0;
859     container.addEventListener("keydown", this._handleKey.bind(this), false);
860     element.appendChild(container);
861
862     container.focus();
863 };
864
865 /**
866  * @param {!number} time date in millisecond.
867  * @return {!boolean}
868  */
869 CalendarPicker.prototype.stepMismatch = function(time) {
870     return (time - this.stepBase) % this.step != 0;
871 }
872
873 /**
874  * @param {!number} time date in millisecond.
875  * @return {!boolean}
876  */
877 CalendarPicker.prototype.outOfRange = function(time) {
878     return time < this.minimumDate.getTime() || time > this.maximumDate.getTime();
879 }
880
881 /**
882  * @param {!number} time date in millisecond.
883  * @return {!boolean}
884  */
885 CalendarPicker.prototype.isValidDate = function(time) {
886     return !this.outOfRange(time) && !this.stepMismatch(time);
887 }
888
889 /**
890  * @param {!number} year
891  * @param {!number} month
892  */
893 DaysTable.prototype._renderMonth = function(year, month) {
894     this._currentYear = year;
895     this._currentMonth = month;
896     var dayIterator = createUTCDate(year, month, 1);
897     var monthStartDay = dayIterator.getUTCDay();
898     var weekStartDay = global.params.weekStartDay || 0;
899     var startOffset = weekStartDay - monthStartDay;
900     if (startOffset >= 0)
901         startOffset -= 7;
902     dayIterator.setUTCDate(startOffset + 1);
903     for (var w = 0; w < DaysTable._Weeks; w++) {
904         for (var d = 0; d < 7; d++) {
905             var iterYear = dayIterator.getUTCFullYear();
906             var iterMonth = dayIterator.getUTCMonth();
907             var time = dayIterator.getTime();
908             var element = this._days[w][d];
909             element.innerText = localizeNumber(dayIterator.getUTCDate());
910             element.className = ClassNames.Day;
911             element.dataset.submitValue = serializeDate(iterYear, iterMonth, dayIterator.getUTCDate());
912             if (this.picker.outOfRange(time))
913                 element.classList.add(ClassNames.Unavailable);
914             else if (this.picker.stepMismatch(time))
915                 element.classList.add(ClassNames.Unavailable);
916             else if ((iterYear == year && dayIterator.getUTCMonth() < month) || (month == 0 && iterMonth == 11)) {
917                 element.classList.add(ClassNames.Available);
918                 element.classList.add(ClassNames.NotThisMonth);
919             } else if ((iterYear == year && dayIterator.getUTCMonth() > month) || (month == 11 && iterMonth == 0)) {
920                 element.classList.add(ClassNames.Available);
921                 element.classList.add(ClassNames.NotThisMonth);
922             } else if (isNaN(time)) {
923                 element.innerText = "-";
924                 element.classList.add(ClassNames.Unavailable);
925             } else
926                 element.classList.add(ClassNames.Available);
927             dayIterator.setUTCDate(dayIterator.getUTCDate() + 1);
928         }
929     }
930
931     this.picker.today.disabled = !this.picker.isValidDate(parseDateString().getTime());
932 };
933
934 /**
935  * @param {!number} year
936  * @param {!number} month
937  */
938 DaysTable.prototype._navigateToMonth = function(year, month) {
939     this.picker.yearMonthController.setYearMonth(year, month);
940     this._renderMonth(year, month);
941 };
942
943 /**
944  * @param {!number} year
945  * @param {!number} month
946  */
947 DaysTable.prototype._navigateToMonthWithAnimation = function(year, month) {
948     if (this._currentYear >= 0 && this._currentMonth >= 0) {
949         if (year == this._currentYear && month == this._currentMonth)
950             return;
951         var decreasing = false;
952         if (year < this._currentYear)
953             decreasing = true;
954         else if (year > this._currentYear)
955             decreasing = false;
956         else
957             decreasing = month < this._currentMonth;
958         var daysStyle = this._daysContainer.style;
959         daysStyle.position = "relative";
960         daysStyle.webkitTransition = "left 0.1s ease";
961         daysStyle.left = (decreasing ? "" : "-") + this._daysContainer.offsetWidth + "px";
962     }
963     this._navigateToMonth(year, month);
964 };
965
966 DaysTable.prototype._moveInDays = function() {
967     var daysStyle = this._daysContainer.style;
968     if (daysStyle.left == "0px")
969         return;
970     daysStyle.webkitTransition = "";
971     daysStyle.left = (daysStyle.left.charAt(0) == "-" ? "" : "-") + this._daysContainer.offsetWidth + "px";
972     this._daysContainer.offsetLeft; // Force to layout.
973     daysStyle.webkitTransition = "left 0.1s ease";
974     daysStyle.left = "0px";
975 };
976
977 /**
978  * @param {!number} year
979  * @param {!number} month
980  */
981 DaysTable.prototype.navigateToMonthAndKeepSelectionPosition = function(year, month) {
982     if (year == this._currentYear && month == this._currentMonth)
983         return;
984     this._navigateToMonthWithAnimation(year, month);
985     if (this._hasSelection())
986         this._days[this._y][this._x].classList.add(ClassNames.Selected);
987 };
988
989 /**
990  * @param {!Date} date
991  */
992 DaysTable.prototype.selectDate = function(date) {
993     this._navigateToMonthWithAnimation(date.getUTCFullYear(), date.getUTCMonth());
994     var dateString = serializeDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
995     for (var w = 0; w < DaysTable._Weeks; w++) {
996         for (var d = 0; d < 7; d++) {
997             if (this._days[w][d].dataset.submitValue == dateString) {
998                 this._days[w][d].classList.add(ClassNames.Selected);
999                 this._x = d;
1000                 this._y = w;
1001                 break;
1002             }
1003         }
1004     }
1005 };
1006
1007 /**
1008  * @return {!boolean}
1009  */
1010 DaysTable.prototype._maybeSetPreviousMonth = function() {
1011     var year = this.picker.yearMonthController.year();
1012     var month = this.picker.yearMonthController.month();
1013     var thisMonthStartTime = createUTCDate(year, month, 1).getTime();
1014     if (this.picker.minimumDate.getTime() >= thisMonthStartTime)
1015         return false;
1016     if (month == 0) {
1017         year--;
1018         month = 11;
1019     } else
1020         month--;
1021     this._navigateToMonthWithAnimation(year, month);
1022     return true;
1023 };
1024
1025 /**
1026  * @return {!boolean}
1027  */
1028 DaysTable.prototype._maybeSetNextMonth = function() {
1029     var year = this.picker.yearMonthController.year();
1030     var month = this.picker.yearMonthController.month();
1031     if (month == 11) {
1032         year++;
1033         month = 0;
1034     } else
1035         month++;
1036     var nextMonthStartTime = createUTCDate(year, month, 1).getTime();
1037     if (this.picker.maximumDate.getTime() < nextMonthStartTime)
1038         return false;
1039     this._navigateToMonthWithAnimation(year, month);
1040     return true;
1041 };
1042
1043 /**
1044  * @param {Event} event
1045  */
1046 DaysTable.prototype._handleDayClick = function(event) {
1047     if (event.target.classList.contains(ClassNames.Available))
1048         this.picker.submitValue(event.target.dataset.submitValue);
1049 };
1050
1051 /**
1052  * @param {Event} event
1053  */
1054 DaysTable.prototype._handleMouseOver = function(event) {
1055     var node = event.target;
1056     if (this._hasSelection())
1057         this._days[this._y][this._x].classList.remove(ClassNames.Selected);
1058     if (!node.classList.contains(ClassNames.Day)) {
1059         this._x = -1;
1060         this._y = -1;
1061         return;
1062     }
1063     node.classList.add(ClassNames.Selected);
1064     this._x = Number(node.dataset.positionX);
1065     this._y = Number(node.dataset.positionY);
1066 };
1067
1068 /**
1069  * @param {Event} event
1070  */
1071 DaysTable.prototype._handleMouseOut = function(event) {
1072     if (this._hasSelection())
1073         this._days[this._y][this._x].classList.remove(ClassNames.Selected);
1074     this._x = -1;
1075     this._y = -1;
1076 };
1077
1078 /**
1079  * @param {Event} event
1080  */
1081 DaysTable.prototype._handleKey = function(event) {
1082     this.picker.maybeUpdateFocusStyle();
1083     var x = this._x;
1084     var y = this._y;
1085     var key = event.keyIdentifier;
1086     if (!this._hasSelection() && (key == "Left" || key == "Up" || key == "Right" || key == "Down")) {
1087         // Put the selection on a center cell.
1088         this.updateSelection(event, 3, Math.floor(DaysTable._Weeks / 2 - 1));
1089         return;
1090     }
1091
1092     if (key == (global.params.isCalendarRTL ? "Right" : "Left")) {
1093         if (x == 0) {
1094             if (y == 0) {
1095                 if (!this._maybeSetPreviousMonth())
1096                     return;
1097                 y = DaysTable._Weeks - 1;
1098             } else
1099                 y--;
1100             x = 6;
1101         } else
1102             x--;
1103         this.updateSelection(event, x, y);
1104
1105     } else if (key == "Up") {
1106         if (y == 0) {
1107             if (!this._maybeSetPreviousMonth())
1108                 return;
1109             y = DaysTable._Weeks - 1;
1110         } else
1111             y--;
1112         this.updateSelection(event, x, y);
1113
1114     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right")) {
1115         if (x == 6) {
1116             if (y == DaysTable._Weeks - 1) {
1117                 if (!this._maybeSetNextMonth())
1118                     return;
1119                 y = 0;
1120             } else
1121                 y++;
1122             x = 0;
1123         } else
1124             x++;
1125         this.updateSelection(event, x, y);
1126
1127     } else if (key == "Down") {
1128         if (y == DaysTable._Weeks - 1) {
1129             if (!this._maybeSetNextMonth())
1130                 return;
1131             y = 0;
1132         } else
1133             y++;
1134         this.updateSelection(event, x, y);
1135
1136     } else if (key == "PageUp") {
1137         if (!this._maybeSetPreviousMonth())
1138             return;
1139         this.updateSelection(event, x, y);
1140
1141     } else if (key == "PageDown") {
1142         if (!this._maybeSetNextMonth())
1143             return;
1144         this.updateSelection(event, x, y);
1145
1146     } else if (this._hasSelection() && key == "Enter") {
1147         var dayNode = this._days[y][x];
1148         if (dayNode.classList.contains(ClassNames.Available)) {
1149             this.picker.submitValue(dayNode.dataset.submitValue);
1150             event.stopPropagation();
1151         }
1152
1153     } else if (key == "U+0054") { // 't'
1154         this._days[this._y][this._x].classList.remove(ClassNames.Selected);
1155         this.selectDate(new Date());
1156         event.stopPropagation();
1157         event.preventDefault();
1158     }
1159 };
1160
1161 /**
1162  * @param {Event} event
1163  * @param {!number} x
1164  * @param {!number} y
1165  */
1166 DaysTable.prototype.updateSelection = function(event, x, y) {
1167     if (this._hasSelection())
1168         this._days[this._y][this._x].classList.remove(ClassNames.Selected);
1169     if (x >= 0 && y >= 0) {
1170         this._days[y][x].classList.add(ClassNames.Selected);
1171         this._x = x;
1172         this._y = y;
1173     }
1174     event.stopPropagation();
1175     event.preventDefault();
1176 };
1177
1178 /**
1179  * @param {!Event} event
1180  */
1181 CalendarPicker.prototype._handleBodyKeyDown = function(event) {
1182     this.maybeUpdateFocusStyle();
1183     var key = event.keyIdentifier;
1184     if (key == "U+0009") {
1185         if (!event.shiftKey && document.activeElement == this.lastFocusableControl) {
1186             event.stopPropagation();
1187             event.preventDefault();
1188             this.firstFocusableControl.focus();
1189         } else if (event.shiftKey && document.activeElement == this.firstFocusableControl) {
1190             event.stopPropagation();
1191             event.preventDefault();
1192             this.lastFocusableControl.focus();
1193         }
1194     } else if (key == "U+004D") { // 'm'
1195         this.yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousMonth : YearMonthController.NextMonth);
1196     } else if (key == "U+0059") { // 'y'
1197         this.yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousYear : YearMonthController.NextYear);
1198     } else if (key == "U+0044") { // 'd'
1199         this.yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousTenYears : YearMonthController.NextTenYears);
1200     } else if (key == "U+001B") // ESC
1201         this.handleCancel();
1202 }
1203
1204 CalendarPicker.prototype.maybeUpdateFocusStyle = function() {
1205     if (this._hadKeyEvent)
1206         return;
1207     this._hadKeyEvent = true;
1208     this._element.classList.remove(ClassNames.NoFocusRing);
1209 }
1210
1211 if (window.dialogArguments) {
1212     initialize(dialogArguments);
1213 } else {
1214     window.addEventListener("message", handleMessage, false);
1215     window.setTimeout(handleArgumentsTimeout, 1000);
1216 }