Implement week picking to calendar picker
[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     Monday: "monday",
50     MonthMode: "month-mode",
51     MonthSelector: "month-selector",
52     MonthSelectorBox: "month-selector-box",
53     MonthSelectorPopup: "month-selector-popup",
54     MonthSelectorPopupContents: "month-selector-popup-contents",
55     MonthSelectorPopupEntry: "month-selector-popup-entry",
56     MonthSelectorWall: "month-selector-wall",
57     NoFocusRing: "no-focus-ring",
58     NotThisMonth: "not-this-month",
59     Selected: "day-selected",
60     SelectedMonthYear: "selected-month-year",
61     Sunday: "sunday",
62     TodayButton: "today-button",
63     TodayClearArea: "today-clear-area",
64     Unavailable: "unavailable",
65     WeekContainer: "week-container",
66     WeekColumn: "week-column",
67     WeekMode: "week-mode",
68     YearMonthArea: "year-month-area",
69     YearMonthButton: "year-month-button",
70     YearMonthButtonLeft: "year-month-button-left",
71     YearMonthButtonRight: "year-month-button-right",
72     YearMonthUpper: "year-month-upper"
73 };
74
75 /**
76  * @type {Object}
77  */
78 var global = {
79     argumentsReceived: false,
80     params: null,
81     picker: null
82 };
83
84 // ----------------------------------------------------------------
85 // Utility functions
86
87 /**
88  * @return {!string} lowercase locale name. e.g. "en-us"
89  */
90 function getLocale() {
91     return (global.params.locale || "en-us").toLowerCase();
92 }
93
94 /**
95  * @return {!string} lowercase language code. e.g. "en"
96  */
97 function getLanguage() {
98     var locale = getLocale();
99     var result = locale.match(/^([a-z]+)/);
100     if (!result)
101         return "en";
102     return result[1];
103 }
104
105 /**
106  * @param {!number} number
107  * @return {!string}
108  */
109 function localizeNumber(number) {
110     return window.pagePopupController.localizeNumberString(number);
111 }
112
113 /*
114  * @const
115  * @type {number}
116  */
117 var ImperialEraLimit = 2087;
118
119 /**
120  * @param {!number} year
121  * @param {!number} month
122  * @return {!string}
123  */
124 function formatJapaneseImperialEra(year, month) {
125     // We don't show an imperial era if it is greater than 99 becase of space
126     // limitation.
127     if (year > ImperialEraLimit)
128         return "";
129     if (year > 1989)
130         return "(平成" + localizeNumber(year - 1988) + "年)";
131     if (year == 1989)
132         return "(平成元年)";
133     if (year >= 1927)
134         return "(昭和" + localizeNumber(year - 1925) + "年)";
135     if (year > 1912)
136         return "(大正" + localizeNumber(year - 1911) + "年)";
137     if (year == 1912 && month >= 7)
138         return "(大正元年)";
139     if (year > 1868)
140         return "(明治" + localizeNumber(year - 1867) + "年)";
141     if (year == 1868)
142         return "(明治元年)";
143     return "";
144 }
145
146 /**
147  * @return {!string}
148  */
149 Month.prototype.toLocaleString = function() {
150     if (isNaN(this.year) || isNaN(this.year))
151         return "Invalid Month";
152     var yearString = localizeNumber(this.year);
153     var monthString = global.params.monthLabels[this.month];
154     switch (getLanguage()) {
155     case "eu":
156     case "fil":
157     case "lt":
158     case "ml":
159     case "mt":
160     case "tl":
161     case "ur":
162         return yearString + " " + monthString;
163     case "hu":
164         return yearString + ". " + monthString;
165     case "ja":
166         return yearString + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + monthString;
167     case "zh":
168         return yearString + "年" + monthString;
169     case "ko":
170         return yearString + "년 " + monthString;
171     case "lv":
172         return yearString + ". g. " + monthString;
173     case "pt":
174         return monthString + " de " + yearString;
175     case "sr":
176         return monthString + ". " + yearString;
177     default:
178         return monthString + " " + yearString;
179     }
180 };
181
182 function createUTCDate(year, month, date) {
183     var newDate = new Date(0);
184     newDate.setUTCFullYear(year);
185     newDate.setUTCMonth(month);
186     newDate.setUTCDate(date);
187     return newDate;
188 };
189
190 /**
191  * @param {string} dateString
192  * @return {?Day|Week|Month}
193  */
194 function parseDateString(dateString) {
195     var month = Month.parse(dateString);
196     if (month)
197         return month;
198     var week = Week.parse(dateString);
199     if (week)
200         return week;
201     return Day.parse(dateString);
202 }
203
204 /**
205  * @param {!number|Day} valueOrDayOrYear
206  * @param {!number=} month
207  * @param {!number=} date
208  */
209 function Day(valueOrDayOrYear, month, date) {
210     var dateObject;
211     if (arguments.length == 3)
212         dateObject = createUTCDate(valueOrDayOrYear, month, date);
213     else if (valueOrDayOrYear instanceof Day)
214         dateObject = createUTCDate(valueOrDayOrYear.year, valueOrDayOrYear.month, valueOrDayOrYear.date);
215     else
216         dateObject = new Date(valueOrDayOrYear);
217     this.year = dateObject.getUTCFullYear();    
218     this.month = dateObject.getUTCMonth();
219     this.date = dateObject.getUTCDate();
220 };
221
222 Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)$/;
223
224 /**
225  * @param {!string} str
226  * @return {?Month}
227  */
228 Day.parse = function(str) {
229     var match = Day.ISOStringRegExp.exec(str);
230     if (!match)
231         return null;
232     var year = parseInt(match[1], 10);
233     var month = parseInt(match[2], 10) - 1;
234     var date = parseInt(match[3], 10);
235     return new Day(year, month, date);
236 };
237
238 /**
239  * @param {!Date} date
240  * @return {!Day}
241  */
242 Day.createFromDate = function(date) {
243     return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
244 };
245
246 /**
247  * @return {!Day}
248  */
249 Day.createFromToday = function() {
250     var now = new Date();
251     return new Day(now.getFullYear(), now.getMonth(), now.getDate());
252 };
253
254 /**
255  * @param {!Day} other
256  * @return {!bool}
257  */
258 Day.prototype.equals = function(other) {
259     return this.year === other.year && this.month === other.month && this.date === other.date;
260 };
261
262 /**
263  * @return {!Day}
264  */
265 Day.prototype.previous = function() {
266     return new Day(this.year, this.month, this.date - 1);
267 };
268
269 /**
270  * @return {!Day}
271  */
272 Day.prototype.next = function() {
273     return new Day(this.year, this.month, this.date + 1);
274 };
275
276 /**
277  * @return {!Date}
278  */
279 Day.prototype.startDate = function() {
280     return createUTCDate(this.year, this.month, this.date);
281 };
282
283 /**
284  * @return {!Date}
285  */
286 Day.prototype.endDate = function() {
287     return createUTCDate(this.year, this.month, this.date + 1);
288 };
289
290 /**
291  * @return {!number}
292  */
293 Day.prototype.valueOf = function() {
294     return this.startDate().getTime();
295 };
296
297 /**
298  * @return {!string}
299  */
300 Day.prototype.toString = function() {
301     var yearString = String(this.year);
302     if (yearString.length < 4)
303         yearString = ("000" + yearString).substr(-4, 4);
304     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2);
305 };
306
307 // See WebCore/platform/DateComponents.h.
308 Day.Minimum = new Day(-62135596800000.0);
309 Day.Maximum = new Day(8640000000000000.0);
310 // See WebCore/html/DayInputType.cpp.
311 Day.DefaultStep = 86400000;
312 Day.DefaultStepBase = 0;
313
314 /**
315  * @constructor
316  * @param {!number|Week} valueOrWeekOrYear
317  * @param {!number=} week
318  */
319 function Week(valueOrWeekOrYear, week) {
320     if (arguments.length === 2) {
321         this.year = valueOrWeekOrYear;
322         this.week = week;
323         // Number of years per year is either 52 or 53.
324         if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) {
325             var normalizedWeek = Week.createFromDate(this.startDate());
326             this.year = normalizedWeek.year;
327             this.week = normalizedWeek.week;
328         }
329     } else if (valueOrMonthOrYear instanceof Week) {
330         this.year = valueOrWeekOrYear.year;
331         this.week = valueOrWeekOrYear.week;
332     } else {
333         var week = Week.createFromDate(new Date(valueOrWeekOrYear));
334         this.year = week.year;
335         this.week = week.week;
336     }
337 }
338
339 Week.MillisecondsPerWeek = 7 * 24 * 60 * 60 * 1000;
340 Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;
341 // See WebCore/platform/DateComponents.h.
342 Week.Minimum = new Week(1, 1);
343 Week.Maximum = new Week(275760, 37);
344 // See WebCore/html/WeekInputType.cpp.
345 Week.DefaultStep = 604800000;
346 Week.DefaultStepBase = -259200000;
347
348 /**
349  * @param {!string} str
350  * @return {?Week}
351  */
352 Week.parse = function(str) {
353     var match = Week.ISOStringRegExp.exec(str);
354     if (!match)
355         return null;
356     var year = parseInt(match[1], 10);
357     var week = parseInt(match[2], 10);
358     return new Week(year, week);
359 };
360
361 /**
362  * @param {!Date} date
363  * @return {!Week}
364  */
365 Week.createFromDate = function(date) {
366     var year = date.getUTCFullYear();
367     if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime())
368         year++;
369     else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
370         year--;
371     var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
372     return new Week(year, week);
373 };
374
375 /**
376  * @return {!Week}
377  */
378 Week.createFromToday = function() {
379     var now = new Date();
380     return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
381 };
382
383 /**
384  * @param {!number} year
385  * @return {!Date}
386  */
387 Week.weekOneStartDateForYear = function(year) {
388     if (year < 1)
389         return createUTCDate(1, 0, 1);
390     // The week containing January 4th is week one.
391     var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
392     return createUTCDate(year, 0, 4 - (yearStartDay + 6) % 7);
393 };
394
395 /**
396  * @param {!number} year
397  * @return {!number}
398  */
399 Week.numberOfWeeksInYear = function(year) {
400     if (year < 1 || year > Week.Maximum.year)
401         return 0;
402     else if (year === Week.Maximum.year)
403         return Week.Maximum.week;
404     return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
405 };
406
407 /**
408  * @param {!Date} baseDate
409  * @param {!Date} date
410  * @return {!number}
411  */
412 Week._numberOfWeeksSinceDate = function(baseDate, date) {
413     return Math.floor((date.getTime() - baseDate.getTime()) / Week.MillisecondsPerWeek);
414 };
415
416 /**
417  * @param {!Week} other
418  * @return {!bool}
419  */
420 Week.prototype.equals = function(other) {
421     return this.year === other.year && this.week === other.week;
422 };
423
424 /**
425  * @return {!Week}
426  */
427 Week.prototype.previous = function() {
428     return new Week(this.year, this.week - 1);
429 };
430
431 /**
432  * @return {!Week}
433  */
434 Week.prototype.next = function() {
435     return new Week(this.year, this.week + 1);
436 };
437
438 /**
439  * @return {!Date}
440  */
441 Week.prototype.startDate = function() {
442     var weekStartDate = Week.weekOneStartDateForYear(this.year);
443     weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
444     return weekStartDate;
445 };
446
447 /**
448  * @return {!Date}
449  */
450 Week.prototype.endDate = function() {
451     if (this.equals(Week.Maximum))
452         return Day.Maximum.startDate();
453     return this.next().startDate();
454 };
455
456 /**
457  * @return {!number}
458  */
459 Week.prototype.valueOf = function() {
460     return this.startDate().getTime() - createUTCDate(1970, 0, 1).getTime();
461 };
462
463 /**
464  * @return {!string}
465  */
466 Week.prototype.toString = function() {
467     var yearString = String(this.year);
468     if (yearString.length < 4)
469         yearString = ("000" + yearString).substr(-4, 4);
470     return yearString + "-W" + ("0" + this.week).substr(-2, 2);
471 };
472
473 /**
474  * @param {!number|Month} valueOrMonthOrYear
475  * @param {!number=} month
476  */
477 function Month(valueOrMonthOrYear, month) {
478     if (arguments.length == 2) {
479         this.year = valueOrMonthOrYear;
480         this.month = month;
481     } else if (valueOrMonthOrYear instanceof Month) {
482         this.year = valueOrMonthOrYear.year;
483         this.month = valueOrMonthOrYear.month;
484     } else {
485         this.year = 1970;
486         this.month = valueOrMonthOrYear;
487     }
488     this.year = this.year + Math.floor(this.month / 12);
489     this.month = this.month < 0 ? this.month % 12 + 12 : this.month % 12;
490     if (this.year <= 0 || Month.Maximum < this) {
491         this.year = NaN;
492         this.month = NaN;
493     }
494 };
495
496 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
497
498 // See WebCore/platform/DateComponents.h.
499 Month.Minimum = new Month(1, 0);
500 Month.Maximum = new Month(275760, 8);
501 // See WebCore/html/MonthInputType.cpp.
502 Month.DefaultStep = 1;
503 Month.DefaultStepBase = 0;
504
505 /**
506  * @param {!string} str
507  * @return {?Month}
508  */
509 Month.parse = function(str) {
510     var match = Month.ISOStringRegExp.exec(str);
511     if (!match)
512         return null;
513     var year = parseInt(match[1], 10);
514     var month = parseInt(match[2], 10) - 1;
515     return new Month(year, month);
516 };
517
518 /**
519  * @param {!Date} date
520  * @return {!Month}
521  */
522 Month.createFromDate = function(date) {
523     return new Month(date.getUTCFullYear(), date.getUTCMonth());
524 };
525
526 /**
527  * @return {!Month}
528  */
529 Month.createFromToday = function() {
530     var now = new Date();
531     return new Month(now.getFullYear(), now.getMonth());
532 };
533
534 /**
535  * @param {!Month} other
536  * @return {!bool}
537  */
538 Month.prototype.equals = function(other) {
539     return this.year === other.year && this.month === other.month;
540 };
541
542 /**
543  * @return {!Month}
544  */
545 Month.prototype.previous = function() {
546     return new Month(this.year, this.month - 1);
547 };
548
549 /**
550  * @return {!Month}
551  */
552 Month.prototype.next = function() {
553     return new Month(this.year, this.month + 1);
554 };
555
556 /**
557  * @return {!Date}
558  */
559 Month.prototype.startDate = function() {
560     return createUTCDate(this.year, this.month, 1);
561 };
562
563 /**
564  * @return {!Date}
565  */
566 Month.prototype.endDate = function() {
567     if (this.equals(Month.Maximum))
568         return Day.Maximum.startDate();
569     return this.next().startDate();
570 };
571
572 /**
573  * @return {!number}
574  */
575 Month.prototype.valueOf = function() {
576     return (this.year - 1970) * 12 + this.month;
577 };
578
579 /**
580  * @return {!string}
581  */
582 Month.prototype.toString = function() {
583     var yearString = String(this.year);
584     if (yearString.length < 4)
585         yearString = ("000" + yearString).substr(-4, 4);
586     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
587 };
588
589 // ----------------------------------------------------------------
590 // Initialization
591
592 /**
593  * @param {Event} event
594  */
595 function handleMessage(event) {
596     if (global.argumentsReceived)
597         return;
598     global.argumentsReceived = true;
599     initialize(JSON.parse(event.data));
600 }
601
602 function handleArgumentsTimeout() {
603     if (global.argumentsReceived)
604         return;
605     var args = {
606         monthLabels : ["m1", "m2", "m3", "m4", "m5", "m6",
607                        "m7", "m8", "m9", "m10", "m11", "m12"],
608         dayLabels : ["d1", "d2", "d3", "d4", "d5", "d6", "d7"],
609         todayLabel : "Today",
610         clearLabel : "Clear",
611         cancelLabel : "Cancel",
612         currentValue : "",
613         weekStartDay : 0,
614         step : CalendarPicker.DefaultStepScaleFactor,
615         stepBase: CalendarPicker.DefaultStepBase
616     };
617     initialize(args);
618 }
619
620 /**
621  * @param {!Object} config
622  * @return {?string} An error message, or null if the argument has no errors.
623  */
624 CalendarPicker.validateConfig = function(config) {
625     if (!config.monthLabels)
626         return "No monthLabels.";
627     if (config.monthLabels.length != 12)
628         return "monthLabels is not an array with 12 elements.";
629     if (!config.dayLabels)
630         return "No dayLabels.";
631     if (config.dayLabels.length != 7)
632         return "dayLabels is not an array with 7 elements.";
633     if (!config.clearLabel)
634         return "No clearLabel.";
635     if (!config.todayLabel)
636         return "No todayLabel.";
637     if (config.weekStartDay) {
638         if (config.weekStartDay < 0 || config.weekStartDay > 6)
639             return "Invalid weekStartDay: " + config.weekStartDay;
640     }
641     return null;
642 }
643
644 /**
645  * @param {!Object} args
646  */
647 function initialize(args) { 
648     global.params = args;
649     var errorString = CalendarPicker.validateConfig(args);
650     if (args.suggestionValues)
651         errorString = errorString || SuggestionPicker.validateConfig(args)
652     if (errorString) {
653         var main = $("main");
654         main.textContent = "Internal error: " + errorString;
655         resizeWindow(main.offsetWidth, main.offsetHeight);
656     } else {
657         if (global.params.suggestionValues && global.params.suggestionValues.length)
658             openSuggestionPicker();
659         else
660             openCalendarPicker();
661     }
662 }
663
664 function closePicker() {
665     if (global.picker)
666         global.picker.cleanup();
667     var main = $("main");
668     main.innerHTML = "";
669     main.className = "";
670 };
671
672 function openSuggestionPicker() {
673     closePicker();
674     global.picker = new SuggestionPicker($("main"), global.params);
675 };
676
677 function openCalendarPicker() {
678     closePicker();
679     global.picker = new CalendarPicker($("main"), global.params);
680 };
681
682 /**
683  * @constructor
684  * @param {!Element} element
685  * @param {!Object} config
686  */
687 function CalendarPicker(element, config) {
688     Picker.call(this, element, config);
689     if (this._config.mode === "month") {
690         this.selectionConstructor = Month;
691         this._daysTable = new MonthPickerDaysTable(this);
692         this._element.classList.add(ClassNames.MonthMode);
693     } else if (this._config.mode === "week") {
694         this.selectionConstructor = Week;
695         this._daysTable = new WeekPickerDaysTable(this);
696         this._element.classList.add(ClassNames.WeekMode);
697     } else {
698         this.selectionConstructor = Day;
699         this._daysTable = new DaysTable(this);
700     }
701     this._element.classList.add("calendar-picker");
702     this._element.classList.add("preparing");
703     this._handleWindowResizeBound = this._handleWindowResize.bind(this);
704     window.addEventListener("resize", this._handleWindowResizeBound, false);
705     // We assume this._config.min/max are valid dates or months.
706     var minimum = (typeof this._config.min !== "undefined") ? parseDateString(this._config.min) : this.selectionConstructor.Minimum;
707     var maximum = (typeof this._config.max !== "undefined") ? parseDateString(this._config.max) : this.selectionConstructor.Maximum;
708     this._minimumValue = minimum.valueOf();
709     this._maximumValue = maximum.valueOf();
710     this.step = (typeof this._config.step !== undefined) ? Number(this._config.step) : this.selectionConstructor.DefaultStep;
711     this.stepBase = (typeof this._config.stepBase !== "undefined") ? Number(this._config.stepBase) : this.selectionConstructor.DefaultStepBase;
712     this._minimumMonth = Month.createFromDate(minimum.startDate());
713     this.maximumMonth = Month.createFromDate(maximum.startDate());
714     this._currentMonth = new Month(NaN, NaN);
715     this._yearMonthController = new YearMonthController(this);
716     this._hadKeyEvent = false;
717     this._layout();
718     var initialSelection = parseDateString(this._config.currentValue);
719     if (!initialSelection)
720         initialSelection = this.selectionConstructor.createFromToday();
721     if (initialSelection.valueOf() < this._minimumValue)
722         initialSelection = new this.selectionConstructor(this._minimumValue);
723     else if (initialSelection.valueOf() > this._maximumValue)
724         initialSelection = new this.selectionConstructor(this._maximumValue);
725     this.showMonth(Month.createFromDate(initialSelection.startDate()));
726     this._daysTable.selectRangeAndShowEntireRange(initialSelection);
727     this.fixWindowSize();
728     this._handleBodyKeyDownBound = this._handleBodyKeyDown.bind(this);
729     document.body.addEventListener("keydown", this._handleBodyKeyDownBound, false);
730 }
731 CalendarPicker.prototype = Object.create(Picker.prototype);
732
733 CalendarPicker.NavigationBehaviour = {
734     None: 0,
735     Animate: 1 << 0,
736     KeepSelectionPosition: 1 << 1
737 };
738
739 CalendarPicker.prototype._handleWindowResize = function() {
740     this._element.classList.remove("preparing");
741 };
742
743 CalendarPicker.prototype.cleanup = function() {
744     document.body.removeEventListener("keydown", this._handleBodyKeyDownBound, false);
745 };
746
747 CalendarPicker.prototype._layout = function() {
748     if (this._config.isCalendarRTL)
749         this._element.classList.add("rtl");
750     this._yearMonthController.attachTo(this._element);
751     this._daysTable.attachTo(this._element);
752     this._layoutButtons();
753     // DaysTable will have focus but we don't want to show its focus ring until the first key event.
754     this._element.classList.add(ClassNames.NoFocusRing);
755 };
756
757 CalendarPicker.prototype.handleToday = function() {
758     var today = this.selectionConstructor.createFromToday();
759     this._daysTable.selectRangeAndShowEntireRange(today);
760     this.submitValue(today.toString());
761 };
762
763 CalendarPicker.prototype.handleClear = function() {
764     this.submitValue("");
765 };
766
767 CalendarPicker.prototype.fixWindowSize = function() {
768     var yearMonthRightElement = this._element.getElementsByClassName(ClassNames.YearMonthButtonRight)[0];
769     var daysAreaElement = this._element.getElementsByClassName(ClassNames.DaysArea)[0];
770     var headers = daysAreaElement.getElementsByClassName(ClassNames.DayLabel);
771     var maxCellWidth = 0;
772     for (var i = 0; i < headers.length; ++i) {
773         if (maxCellWidth < headers[i].offsetWidth)
774             maxCellWidth = headers[i].offsetWidth;
775     }
776     var DaysAreaContainerBorder = 1;
777     var yearMonthEnd;
778     var daysAreaEnd;
779     if (global.params.isCalendarRTL) {
780         var startOffset = this._element.offsetLeft + this._element.offsetWidth;
781         yearMonthEnd = startOffset - yearMonthRightElement.offsetLeft;
782         daysAreaEnd = startOffset - (daysAreaElement.offsetLeft + daysAreaElement.offsetWidth) + maxCellWidth * 7 + DaysAreaContainerBorder;
783     } else {
784         yearMonthEnd = yearMonthRightElement.offsetLeft + yearMonthRightElement.offsetWidth;
785         daysAreaEnd = daysAreaElement.offsetLeft + maxCellWidth * 7 + DaysAreaContainerBorder;
786     }
787     var maxEnd = Math.max(yearMonthEnd, daysAreaEnd);
788     var MainPadding = 6; // FIXME: Fix name.
789     var MainBorder = 1;
790     var desiredBodyWidth = maxEnd + MainPadding + MainBorder;
791
792     var elementHeight = this._element.offsetHeight;
793     this._element.style.width = "auto";
794     daysAreaElement.style.width = "100%";
795     daysAreaElement.style.tableLayout = "fixed";
796     this._element.getElementsByClassName(ClassNames.YearMonthUpper)[0].style.display = "-webkit-box";
797     this._element.getElementsByClassName(ClassNames.MonthSelectorBox)[0].style.display = "block";
798     resizeWindow(desiredBodyWidth, elementHeight);
799 };
800
801 CalendarPicker.prototype._layoutButtons = function() {
802     var container = createElement("div", ClassNames.TodayClearArea);
803     this.today = createElement("input", ClassNames.TodayButton);
804     this.today.disabled = !this.isValidDate(this.selectionConstructor.createFromToday());
805     this.today.type = "button";
806     this.today.value = this._config.todayLabel;
807     this.today.addEventListener("click", this.handleToday.bind(this), false);
808     container.appendChild(this.today);
809     this.clear = null;
810     if (!this._config.required) {
811         this.clear = createElement("input", ClassNames.ClearButton);
812         this.clear.type = "button";
813         this.clear.value = this._config.clearLabel;
814         this.clear.addEventListener("click", this.handleClear.bind(this), false);
815         container.appendChild(this.clear);
816     }
817     this._element.appendChild(container);
818
819     this.lastFocusableControl = this.clear || this.today;
820 };
821
822 /**
823  * @param {!Month} month
824  * @return {!bool}
825  */
826 CalendarPicker.prototype.shouldShowMonth = function(month) {
827     return this._minimumMonth.valueOf() <= month.valueOf() && this.maximumMonth.valueOf() >= month.valueOf();
828 };
829
830 /**
831  * @param {!Month} month
832  * @param {!CalendarPicker.NavigationBehaviour=} navigationBehaviour
833  */
834 CalendarPicker.prototype.showMonth = function(month, navigationBehaviour) {
835     if (this._currentMonth.equals(month))
836         return;
837     else if (month.valueOf() < this._minimumMonth.valueOf())
838         month = this._minimumMonth;
839     else if (month.valueOf() > this.maximumMonth.valueOf())
840         month = this.maximumMonth;
841     this._yearMonthController.setMonth(month);
842     this._daysTable.navigateToMonth(month, navigationBehaviour || CalendarPicker.NavigationBehaviour.None);
843     this._currentMonth = month;
844 };
845
846 /**
847  * @return {!Month}
848  */
849 CalendarPicker.prototype.currentMonth = function() {
850     return this._currentMonth;
851 };
852
853 // ----------------------------------------------------------------
854
855 /**
856  * @constructor
857  * @param {!CalendarPicker} picker
858  */
859 function YearMonthController(picker) {
860     this.picker = picker;
861 }
862
863 /**
864  * @param {!Element} element
865  */
866 YearMonthController.prototype.attachTo = function(element) {
867     var outerContainer = createElement("div", ClassNames.YearMonthArea);
868
869     var innerContainer = createElement("div", ClassNames.YearMonthUpper);
870     outerContainer.appendChild(innerContainer);
871
872     this._attachLeftButtonsTo(innerContainer);
873
874     var box = createElement("div", ClassNames.MonthSelectorBox);
875     innerContainer.appendChild(box);
876     // We can't use <select> popup in PagePopup.
877     this._monthPopup = createElement("div", ClassNames.MonthSelectorPopup);
878     this._monthPopup.addEventListener("click", this._handleYearMonthChange.bind(this), false);
879     this._monthPopup.addEventListener("keydown", this._handleMonthPopupKey.bind(this), false);
880     this._monthPopup.addEventListener("mousemove", this._handleMouseMove.bind(this), false);
881     this._updateSelectionOnMouseMove = true;
882     this._monthPopup.tabIndex = 0;
883     this._monthPopupContents = createElement("div", ClassNames.MonthSelectorPopupContents);
884     this._monthPopup.appendChild(this._monthPopupContents);
885     box.appendChild(this._monthPopup);
886     this._month = createElement("div", ClassNames.MonthSelector);
887     this._month.addEventListener("click", this._showPopup.bind(this), false);
888     box.appendChild(this._month);
889
890     this._attachRightButtonsTo(innerContainer);
891     element.appendChild(outerContainer);
892
893     this._wall = createElement("div", ClassNames.MonthSelectorWall);
894     this._wall.addEventListener("click", this._closePopup.bind(this), false);
895     element.appendChild(this._wall);
896
897     var month = this.picker.maximumMonth;
898     var maxWidth = 0;
899     for (var m = 0; m < 12; ++m) {
900         this._month.textContent = month.toLocaleString();
901         maxWidth = Math.max(maxWidth, this._month.offsetWidth);
902         month = month.previous();
903     }
904     if (getLanguage() == "ja" && ImperialEraLimit < this.picker.maximumMonth.year) {
905         for (var m = 0; m < 12; ++m) {
906             this._month.textContent = new Month(ImperialEraLimit, m).toLocaleString();
907             maxWidth = Math.max(maxWidth, this._month.offsetWidth);
908         }
909     }
910     this._month.style.minWidth = maxWidth + 'px';
911
912     this.picker.firstFocusableControl = this._left2; // FIXME: Should it be this.month?
913 };
914
915 YearMonthController.addTenYearsButtons = false;
916
917 /**
918  * @param {!Element} parent
919  */
920 YearMonthController.prototype._attachLeftButtonsTo = function(parent) {
921     var container = createElement("div", ClassNames.YearMonthButtonLeft);
922     parent.appendChild(container);
923
924     if (YearMonthController.addTenYearsButtons) {
925         this._left3 = createElement("input", ClassNames.YearMonthButton);
926         this._left3.type = "button";
927         this._left3.value = "<<<";
928         this._left3.addEventListener("click", this._handleButtonClick.bind(this), false);
929         container.appendChild(this._left3);
930     }
931
932     this._left2 = createElement("input", ClassNames.YearMonthButton);
933     this._left2.type = "button";
934     this._left2.value = "<<";
935     this._left2.addEventListener("click", this._handleButtonClick.bind(this), false);
936     container.appendChild(this._left2);
937
938     this._left1 = createElement("input", ClassNames.YearMonthButton);
939     this._left1.type = "button";
940     this._left1.value = "<";
941     this._left1.addEventListener("click", this._handleButtonClick.bind(this), false);
942     container.appendChild(this._left1);
943 };
944
945 /**
946  * @param {!Element} parent
947  */
948 YearMonthController.prototype._attachRightButtonsTo = function(parent) {
949     var container = createElement("div", ClassNames.YearMonthButtonRight);
950     parent.appendChild(container);
951     this._right1 = createElement("input", ClassNames.YearMonthButton);
952     this._right1.type = "button";
953     this._right1.value = ">";
954     this._right1.addEventListener("click", this._handleButtonClick.bind(this), false);
955     container.appendChild(this._right1);
956
957     this._right2 = createElement("input", ClassNames.YearMonthButton);
958     this._right2.type = "button";
959     this._right2.value = ">>";
960     this._right2.addEventListener("click", this._handleButtonClick.bind(this), false);
961     container.appendChild(this._right2);
962
963     if (YearMonthController.addTenYearsButtons) {
964         this._right3 = createElement("input", ClassNames.YearMonthButton);
965         this._right3.type = "button";
966         this._right3.value = ">>>";
967         this._right3.addEventListener("click", this._handleButtonClick.bind(this), false);
968         container.appendChild(this._right3);
969     }
970 };
971
972 /**
973  * @param {!Month} month
974  */
975 YearMonthController.prototype.setMonth = function(month) {
976     var monthValue = month.valueOf();
977     if (this._left3)
978         this._left3.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 13));
979     this._left2.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 2));
980     this._left1.disabled = !this.picker.shouldShowMonth(new Month(monthValue - 1));
981     this._right1.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 1));
982     this._right2.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 2));
983     if (this._right3)
984         this._left3.disabled = !this.picker.shouldShowMonth(new Month(monthValue + 13));
985     this._month.innerText = month.toLocaleString();
986     while (this._monthPopupContents.hasChildNodes())
987         this._monthPopupContents.removeChild(this._monthPopupContents.firstChild);
988
989     for (var m = monthValue - 6; m <= monthValue + 6; m++) {
990         var month = new Month(m);
991         if (!this.picker.shouldShowMonth(month))
992             continue;
993         var option = createElement("div", ClassNames.MonthSelectorPopupEntry, month.toLocaleString());
994         option.dataset.value = month.toString();
995         this._monthPopupContents.appendChild(option);
996         if (m == monthValue)
997             option.classList.add(ClassNames.SelectedMonthYear);
998     }
999 };
1000
1001 YearMonthController.prototype._showPopup = function() {
1002     this._monthPopup.style.display = "block";
1003     this._monthPopup.style.zIndex = "1000"; // Larger than the days area.
1004     this._monthPopup.style.left = this._month.offsetLeft + (this._month.offsetWidth - this._monthPopup.offsetWidth) / 2 + "px";
1005     this._monthPopup.style.top = this._month.offsetTop + this._month.offsetHeight + "px";
1006
1007     this._wall.style.display = "block";
1008     this._wall.style.zIndex = "999"; // This should be smaller than the z-index of monthPopup.
1009
1010     var popupHeight = this._monthPopup.clientHeight;
1011     var fullHeight = this._monthPopupContents.clientHeight;
1012     if (fullHeight > popupHeight) {
1013         var selected = this._getSelection();
1014         if (selected) {
1015            var bottom = selected.offsetTop + selected.clientHeight;
1016            if (bottom > popupHeight)
1017                this._monthPopup.scrollTop = bottom - popupHeight;
1018         }
1019         this._monthPopup.style.webkitPaddingEnd = getScrollbarWidth() + 'px';
1020     }
1021     this._monthPopup.focus();
1022 };
1023
1024 YearMonthController.prototype._closePopup = function() {
1025     this._monthPopup.style.display = "none";
1026     this._wall.style.display = "none";
1027     var container = document.querySelector("." + ClassNames.DaysAreaContainer);
1028     container.focus();
1029 };
1030
1031 /**
1032  * @return {Element} Selected element in the month-year popup.
1033  */
1034 YearMonthController.prototype._getSelection = function()
1035 {
1036     return document.querySelector("." + ClassNames.SelectedMonthYear);
1037 }
1038
1039 /**
1040  * @param {Event} event
1041  */
1042 YearMonthController.prototype._handleMouseMove = function(event)
1043 {
1044     if (!this._updateSelectionOnMouseMove) {
1045         // Selection update turned off while navigating with keyboard to prevent a mouse
1046         // move trigged during a scroll from resetting the selection. Automatically
1047         // rearm control to enable mouse-based selection.
1048         this._updateSelectionOnMouseMove = true;
1049     } else {
1050         var target = event.target;
1051         var selection = this._getSelection();
1052         if (target && target != selection && target.classList.contains(ClassNames.MonthSelectorPopupEntry)) {
1053             if (selection)
1054                 selection.classList.remove(ClassNames.SelectedMonthYear);
1055             target.classList.add(ClassNames.SelectedMonthYear);
1056         }
1057     }
1058     event.stopPropagation();
1059     event.preventDefault();
1060 }
1061
1062 /**
1063  * @param {Event} event
1064  */
1065 YearMonthController.prototype._handleMonthPopupKey = function(event)
1066 {
1067     var key = event.keyIdentifier;
1068     if (key == "Down") {
1069         var selected = this._getSelection();
1070         if (selected) {
1071             var next = selected.nextSibling;
1072             if (next) {
1073                 selected.classList.remove(ClassNames.SelectedMonthYear);
1074                 next.classList.add(ClassNames.SelectedMonthYear);
1075                 var bottom = next.offsetTop + next.clientHeight;
1076                 if (bottom > this._monthPopup.scrollTop + this._monthPopup.clientHeight) {
1077                     this._updateSelectionOnMouseMove = false;
1078                     this._monthPopup.scrollTop = bottom - this._monthPopup.clientHeight;
1079                 }
1080             }
1081         }
1082         event.stopPropagation();
1083         event.preventDefault();
1084     } else if (key == "Up") {
1085         var selected = this._getSelection();
1086         if (selected) {
1087             var previous = selected.previousSibling;
1088             if (previous) {
1089                 selected.classList.remove(ClassNames.SelectedMonthYear);
1090                 previous.classList.add(ClassNames.SelectedMonthYear);
1091                 if (previous.offsetTop < this._monthPopup.scrollTop) {
1092                     this._updateSelectionOnMouseMove = false;
1093                     this._monthPopup.scrollTop = previous.offsetTop;
1094                 }
1095             }
1096         }
1097         event.stopPropagation();
1098         event.preventDefault();
1099     } else if (key == "U+001B") {
1100         this._closePopup();
1101         event.stopPropagation();
1102         event.preventDefault();
1103     } else if (key == "Enter") {
1104         this._handleYearMonthChange();
1105         event.stopPropagation();
1106         event.preventDefault();
1107     }
1108 }
1109
1110 YearMonthController.prototype._handleYearMonthChange = function() {
1111     this._closePopup();
1112     var selection = this._getSelection();
1113     if (!selection)
1114         return;
1115     this.picker.showMonth(Month.parse(selection.dataset.value));
1116 };
1117
1118 /*
1119  * @const
1120  * @type {number}
1121  */
1122 YearMonthController.PreviousTenYears = -120;
1123 /*
1124  * @const
1125  * @type {number}
1126  */
1127 YearMonthController.PreviousYear = -12;
1128 /*
1129  * @const
1130  * @type {number}
1131  */
1132 YearMonthController.PreviousMonth = -1;
1133 /*
1134  * @const
1135  * @type {number}
1136  */
1137 YearMonthController.NextMonth = 1;
1138 /*
1139  * @const
1140  * @type {number}
1141  */
1142 YearMonthController.NextYear = 12;
1143 /*
1144  * @const
1145  * @type {number}
1146  */
1147 YearMonthController.NextTenYears = 120;
1148
1149 /**
1150  * @param {Event} event
1151  */
1152 YearMonthController.prototype._handleButtonClick = function(event) {
1153     if (event.target == this._left3)
1154         this.moveRelatively(YearMonthController.PreviousTenYears);
1155     else if (event.target == this._left2)
1156         this.moveRelatively(YearMonthController.PreviousYear);
1157     else if (event.target == this._left1)
1158         this.moveRelatively(YearMonthController.PreviousMonth);
1159     else if (event.target == this._right1)
1160         this.moveRelatively(YearMonthController.NextMonth)
1161     else if (event.target == this._right2)
1162         this.moveRelatively(YearMonthController.NextYear);
1163     else if (event.target == this._right3)
1164         this.moveRelatively(YearMonthController.NextTenYears);
1165     else
1166         return;
1167 };
1168
1169 /**
1170  * @param {!number} amount
1171  */
1172 YearMonthController.prototype.moveRelatively = function(amount) {
1173     var current = this.picker.currentMonth().valueOf();
1174     var updated = new Month(current + amount);
1175     this.picker.showMonth(updated, CalendarPicker.NavigationBehaviour.Animate | CalendarPicker.NavigationBehaviour.KeepSelectionPosition);
1176 };
1177
1178 // ----------------------------------------------------------------
1179
1180 /**
1181  * @constructor
1182  * @param {!CalendarPicker} picker
1183  */
1184 function DaysTable(picker) {
1185     this.picker = picker;
1186 }
1187
1188 /**
1189  * @return {!boolean}
1190  */
1191 DaysTable.prototype._hasSelection = function() {
1192     return !!this._firstNodeInSelectedRange();
1193 }
1194
1195 /**
1196  * The number of week lines in the screen.
1197  * @const
1198  * @type {number}
1199  */
1200 DaysTable._Weeks = 6;
1201
1202 /**
1203  * @param {!Element} element
1204  */
1205 DaysTable.prototype.attachTo = function(element) {
1206     this._daysContainer = createElement("table", ClassNames.DaysArea);
1207     this._daysContainer.addEventListener("click", this._handleDayClick.bind(this), false);
1208     this._daysContainer.addEventListener("mouseover", this._handleMouseOver.bind(this), false);
1209     this._daysContainer.addEventListener("mouseout", this._handleMouseOut.bind(this), false);
1210     this._daysContainer.addEventListener("webkitTransitionEnd", this._moveInDays.bind(this), false);
1211     var container = createElement("tr", ClassNames.DayLabelContainer);
1212     var weekStartDay = global.params.weekStartDay || 0;
1213     container.appendChild(createElement("th", ClassNames.DayLabel + " " + ClassNames.WeekColumn, global.params.weekLabel));
1214     for (var i = 0; i < 7; i++)
1215         container.appendChild(createElement("th", ClassNames.DayLabel, global.params.dayLabels[(weekStartDay + i) % 7]));
1216     this._daysContainer.appendChild(container);
1217     this._days = [];
1218     this._weekNumbers = [];
1219     for (var w = 0; w < DaysTable._Weeks; w++) {
1220         container = createElement("tr", ClassNames.WeekContainer);
1221         var week = [];
1222         var weekNumberNode = createElement("td", ClassNames.Day + " " + ClassNames.WeekColumn, " ");
1223         weekNumberNode.dataset.positionX = -1;
1224         weekNumberNode.dataset.positionY = w;
1225         this._weekNumbers.push(weekNumberNode);
1226         container.appendChild(weekNumberNode);
1227         for (var d = 0; d < 7; d++) {
1228             var day = createElement("td", ClassNames.Day, " ");
1229             day.setAttribute("data-position-x", String(d));
1230             day.setAttribute("data-position-y", String(w));
1231             week.push(day);
1232             container.appendChild(day);
1233         }
1234         this._days.push(week);
1235         this._daysContainer.appendChild(container);
1236     }
1237     container = createElement("div", ClassNames.DaysAreaContainer);
1238     container.appendChild(this._daysContainer);
1239     container.tabIndex = 0;
1240     container.addEventListener("keydown", this._handleKey.bind(this), false);
1241     element.appendChild(container);
1242
1243     container.focus();
1244 };
1245
1246 /**
1247  * @param {!number} value
1248  * @return {!boolean}
1249  */
1250 CalendarPicker.prototype._stepMismatch = function(value) {
1251     return (value - this.stepBase) % this.step != 0;
1252 }
1253
1254 /**
1255  * @param {!number} value
1256  * @return {!boolean}
1257  */
1258 CalendarPicker.prototype._outOfRange = function(value) {
1259     return value < this._minimumValue || value > this._maximumValue;
1260 }
1261
1262 /**
1263  * @param {!Month|Day} range
1264  * @return {!boolean}
1265  */
1266 CalendarPicker.prototype.isValidDate = function(range) {
1267     var value = range.valueOf();
1268     return !this._outOfRange(value) && !this._stepMismatch(value);
1269 }
1270
1271 /**
1272  * @param {!Month} month
1273  */
1274 DaysTable.prototype._renderMonth = function(month) {
1275     var dayIterator = month.startDate();
1276     var monthStartDay = dayIterator.getUTCDay();
1277     var weekStartDay = global.params.weekStartDay || 0;
1278     var startOffset = weekStartDay - monthStartDay;
1279     if (startOffset >= 0)
1280         startOffset -= 7;
1281     dayIterator.setUTCDate(startOffset + 1);
1282     var mondayOffset = (8 - weekStartDay) % 7;
1283     var sundayOffset = weekStartDay % 7;
1284     for (var w = 0; w < DaysTable._Weeks; w++) {
1285         for (var d = 0; d < 7; d++) {
1286             var iterMonth = Month.createFromDate(dayIterator);
1287             var iterWeek = Week.createFromDate(dayIterator);
1288             var time = dayIterator.getTime();
1289             var element = this._days[w][d];
1290             element.innerText = localizeNumber(dayIterator.getUTCDate());
1291             element.className = ClassNames.Day;
1292             element.dataset.submitValue = Day.createFromDate(dayIterator).toString();
1293             element.dataset.weekValue = iterWeek.toString();
1294             element.dataset.monthValue = iterMonth.toString();
1295             if (isNaN(time)) {
1296                 element.innerText = "-";
1297                 element.classList.add(ClassNames.Unavailable);
1298             } else if (!this.picker.isValidDate(this._rangeForNode(element)))
1299                 element.classList.add(ClassNames.Unavailable);
1300             else if (!iterMonth.equals(month)) {
1301                 element.classList.add(ClassNames.Available);
1302                 element.classList.add(ClassNames.NotThisMonth);
1303             } else
1304                 element.classList.add(ClassNames.Available);
1305             if (d === mondayOffset) {
1306                 element.classList.add(ClassNames.Monday);
1307                 if (this._weekNumbers[w]) {
1308                     this._weekNumbers[w].dataset.weekValue = iterWeek.toString();
1309                     this._weekNumbers[w].innerText = localizeNumber(iterWeek.week);
1310                     if (element.classList.contains(ClassNames.Available))
1311                         this._weekNumbers[w].classList.add(ClassNames.Available);
1312                     else
1313                         this._weekNumbers[w].classList.add(ClassNames.Unavailable);
1314                 }
1315             } else if (d === sundayOffset)
1316                 element.classList.add(ClassNames.Sunday);
1317             dayIterator.setUTCDate(dayIterator.getUTCDate() + 1);
1318         }
1319     }
1320 };
1321
1322 /**
1323  * @param {!Month} month
1324  * @param {!CalendarPicker.NavigationBehaviour} navigationBehaviour
1325  */
1326 DaysTable.prototype.navigateToMonth = function(month, navigationBehaviour) {
1327     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1328     if (navigationBehaviour & CalendarPicker.NavigationBehaviour.Animate) {
1329         var daysStyle = this._daysContainer.style;
1330         daysStyle.position = "relative";
1331         daysStyle.webkitTransition = "left 0.1s ease";
1332         daysStyle.left = (this.picker.currentMonth().valueOf() > month.valueOf() ? "" : "-") + this._daysContainer.offsetWidth + "px";
1333     }
1334     this._renderMonth(month);
1335     if (navigationBehaviour & CalendarPicker.NavigationBehaviour.KeepSelectionPosition && firstNodeInSelectedRange) {
1336         var x = parseInt(firstNodeInSelectedRange.dataset.positionX, 10);
1337         var y = parseInt(firstNodeInSelectedRange.dataset.positionY, 10);
1338         this._selectRangeAtPosition(x, y);
1339     }
1340 };
1341
1342 DaysTable.prototype._moveInDays = function() {
1343     var daysStyle = this._daysContainer.style;
1344     if (daysStyle.left == "0px")
1345         return;
1346     daysStyle.webkitTransition = "";
1347     daysStyle.left = (daysStyle.left.charAt(0) == "-" ? "" : "-") + this._daysContainer.offsetWidth + "px";
1348     this._daysContainer.offsetLeft; // Force to layout.
1349     daysStyle.webkitTransition = "left 0.1s ease";
1350     daysStyle.left = "0px";
1351 };
1352
1353 /**
1354  * @param {!Day} day
1355  */
1356 DaysTable.prototype._markRangeAsSelected = function(day) {
1357     var dateString = day.toString();
1358     for (var w = 0; w < DaysTable._Weeks; w++) {
1359         for (var d = 0; d < 7; d++) {
1360             if (this._days[w][d].dataset.submitValue == dateString) {
1361                 this._days[w][d].classList.add(ClassNames.Selected);
1362                 break;
1363             }
1364         }
1365     }
1366 };
1367
1368 /**
1369  * @param {!Day} day
1370  */
1371 DaysTable.prototype.selectRange = function(day) {
1372     this._deselect();
1373     if (this.startDate() > day.startDate() || this.endDate() < day.endDate())
1374         this.picker.showMonth(Month.createFromDate(day.startDate()));
1375     this._markRangeAsSelected(day);
1376 };
1377
1378 /**
1379  * @param {!Day} day
1380  */
1381 DaysTable.prototype.selectRangeAndShowEntireRange = function(day) {
1382     this.selectRange(day);
1383 };
1384
1385 /**
1386  * @param {!Element} dayNode
1387  */
1388 DaysTable.prototype._selectRangeContainingNode = function(dayNode) {
1389     var range = this._rangeForNode(dayNode);
1390     if (!range)
1391         return;
1392     this.selectRange(range);
1393 };
1394
1395 /**
1396  * @param {!Element} dayNode
1397  * @return {?Day}
1398  */
1399 DaysTable.prototype._rangeForNode = function(dayNode) {
1400     if (!dayNode)
1401         return null;
1402     return Day.parse(dayNode.dataset.submitValue);
1403 };
1404
1405 /**
1406  * @return {!Date}
1407  */
1408 DaysTable.prototype.startDate = function() {
1409     return Day.parse(this._days[0][0].dataset.submitValue).startDate();
1410 };
1411
1412 /**
1413  * @return {!Date}
1414  */
1415 DaysTable.prototype.endDate = function() {
1416     return Day.parse(this._days[DaysTable._Weeks - 1][7 - 1].dataset.submitValue).endDate();
1417 };
1418
1419 /**
1420  * @param {!number} x
1421  * @param {!number} y
1422  */
1423 DaysTable.prototype._selectRangeAtPosition = function(x, y) {
1424     var node = x === -1 ? this._weekNumbers[y] : this._days[y][x];
1425     this._selectRangeContainingNode(node);
1426 };
1427
1428 /**
1429  * @return {!Element}
1430  */
1431 DaysTable.prototype._firstNodeInSelectedRange = function() {
1432     return this._daysContainer.getElementsByClassName(ClassNames.Selected)[0];
1433 };
1434
1435 DaysTable.prototype._deselect = function() {
1436     var selectedNodes = this._daysContainer.getElementsByClassName(ClassNames.Selected);
1437     for (var node = selectedNodes[0]; node; node = selectedNodes[0])
1438         node.classList.remove(ClassNames.Selected);
1439 };
1440
1441 /**
1442  * @param {!CalendarPicker.NavigationBehaviour=} navigationBehaviour
1443  * @return {!boolean}
1444  */
1445 DaysTable.prototype._maybeSetPreviousMonth = function(navigationBehaviour) {
1446     if (typeof navigationBehaviour === "undefined")
1447         navigationBehaviour = CalendarPicker.NavigationBehaviour.Animate;
1448     var previousMonth = this.picker.currentMonth().previous();
1449     if (!this.picker.shouldShowMonth(previousMonth))
1450         return false;
1451     this.picker.showMonth(previousMonth, navigationBehaviour);
1452     return true;
1453 };
1454
1455 /**
1456  * @param {!CalendarPicker.NavigationBehaviour=} navigationBehaviour
1457  * @return {!boolean}
1458  */
1459 DaysTable.prototype._maybeSetNextMonth = function(navigationBehaviour) {
1460     if (typeof navigationBehaviour === "undefined")
1461         navigationBehaviour = CalendarPicker.NavigationBehaviour.Animate;
1462     var nextMonth = this.picker.currentMonth().next();
1463     if (!this.picker.shouldShowMonth(nextMonth))
1464         return false;
1465     this.picker.showMonth(nextMonth, navigationBehaviour);
1466     return true;
1467 };
1468
1469 /**
1470  * @param {Event} event
1471  */
1472 DaysTable.prototype._handleDayClick = function(event) {
1473     if (event.target.classList.contains(ClassNames.Available))
1474         this.picker.submitValue(event.target.dataset.submitValue);
1475 };
1476
1477 /**
1478  * @param {Event} event
1479  */
1480 DaysTable.prototype._handleMouseOver = function(event) {
1481     var node = event.target;
1482     if (node.classList.contains(ClassNames.Selected))
1483         return;
1484     this._selectRangeContainingNode(node);
1485 };
1486
1487 /**
1488  * @param {Event} event
1489  */
1490 DaysTable.prototype._handleMouseOut = function(event) {
1491     this._deselect();
1492 };
1493
1494 /**
1495  * @param {Event} event
1496  */
1497 DaysTable.prototype._handleKey = function(event) {
1498     this.picker.maybeUpdateFocusStyle();
1499     var x = -1;
1500     var y = -1;
1501     var key = event.keyIdentifier;
1502     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1503     if (firstNodeInSelectedRange) {
1504         x = parseInt(firstNodeInSelectedRange.dataset.positionX, 10);
1505         y = parseInt(firstNodeInSelectedRange.dataset.positionY, 10);
1506     }
1507     if (!this._hasSelection() && (key == "Left" || key == "Up" || key == "Right" || key == "Down")) {
1508         // Put the selection on a center cell.
1509         this.updateSelection(event, 3, Math.floor(DaysTable._Weeks / 2 - 1));
1510         return;
1511     }
1512
1513     if (key == (global.params.isCalendarRTL ? "Right" : "Left")) {
1514         if (x == 0) {
1515             if (y == 0) {
1516                 if (!this._maybeSetPreviousMonth())
1517                     return;
1518                 y = DaysTable._Weeks - 1;
1519             } else
1520                 y--;
1521             x = 6;
1522         } else
1523             x--;
1524         this.updateSelection(event, x, y);
1525
1526     } else if (key == "Up") {
1527         if (y == 0) {
1528             if (!this._maybeSetPreviousMonth())
1529                 return;
1530             y = DaysTable._Weeks - 1;
1531         } else
1532             y--;
1533         this.updateSelection(event, x, y);
1534
1535     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right")) {
1536         if (x == 6) {
1537             if (y == DaysTable._Weeks - 1) {
1538                 if (!this._maybeSetNextMonth())
1539                     return;
1540                 y = 0;
1541             } else
1542                 y++;
1543             x = 0;
1544         } else
1545             x++;
1546         this.updateSelection(event, x, y);
1547
1548     } else if (key == "Down") {
1549         if (y == DaysTable._Weeks - 1) {
1550             if (!this._maybeSetNextMonth())
1551                 return;
1552             y = 0;
1553         } else
1554             y++;
1555         this.updateSelection(event, x, y);
1556
1557     } else if (key == "PageUp") {
1558         if (!this._maybeSetPreviousMonth())
1559             return;
1560         this.updateSelection(event, x, y);
1561
1562     } else if (key == "PageDown") {
1563         if (!this._maybeSetNextMonth())
1564             return;
1565         this.updateSelection(event, x, y);
1566
1567     } else if (this._hasSelection() && key == "Enter") {
1568         var dayNode = this._days[y][x];
1569         if (dayNode.classList.contains(ClassNames.Available)) {
1570             this.picker.submitValue(dayNode.dataset.submitValue);
1571             event.stopPropagation();
1572         }
1573
1574     } else if (key == "U+0054") { // 't'
1575         this.selectRangeAndShowEntireRange(Day.createFromToday());
1576         event.stopPropagation();
1577         event.preventDefault();
1578     }
1579 };
1580
1581 /**
1582  * @param {Event} event
1583  * @param {!number} x
1584  * @param {!number} y
1585  */
1586 DaysTable.prototype.updateSelection = function(event, x, y) {
1587     this._selectRangeAtPosition(x, y);
1588     event.stopPropagation();
1589     event.preventDefault();
1590 };
1591
1592 /**
1593  * @constructor
1594  * @param{!CalendarPicker} picker
1595  */
1596 function MonthPickerDaysTable(picker) {
1597     DaysTable.call(this, picker);
1598 }
1599 MonthPickerDaysTable.prototype = Object.create(DaysTable.prototype);
1600
1601 /**
1602  * @param {!Month} month
1603  */
1604 MonthPickerDaysTable.prototype._markRangeAsSelected = function(month) {
1605     var monthString = month.toString();
1606     for (var w = 0; w < DaysTable._Weeks; w++) {
1607         for (var d = 0; d < 7; d++) {
1608             if (this._days[w][d].dataset.monthValue == monthString) {
1609                 this._days[w][d].classList.add(ClassNames.Selected);
1610             }
1611         }
1612     }
1613 };
1614
1615 /**
1616  * @param {!Month} month
1617  */
1618 MonthPickerDaysTable.prototype.selectRange = function(month) {
1619     this._deselect();
1620     if (this.startDate() >= month.endDate() || this.endDate() <= month.startDate())
1621         this.picker.showMonth(month, CalendarPicker.NavigationBehaviour.Animate);
1622     this._markRangeAsSelected(month);
1623 };
1624
1625 /**
1626  * @param {!Month} month
1627  */
1628 MonthPickerDaysTable.prototype.selectRangeAndShowEntireRange = function(month) {
1629     this._deselect();
1630     this.picker.showMonth(month, CalendarPicker.NavigationBehaviour.Animate);
1631     this._markRangeAsSelected(month);
1632 };
1633
1634 /**
1635  * @param {!Element} dayNode
1636  * @return {?Month}
1637  */
1638 MonthPickerDaysTable.prototype._rangeForNode = function(dayNode) {
1639     if (!dayNode)
1640         return null;
1641     return Month.parse(dayNode.dataset.monthValue);
1642 };
1643
1644 /**
1645  * @param {Event} event
1646  */
1647 MonthPickerDaysTable.prototype._handleKey = function(event) {
1648     this.picker.maybeUpdateFocusStyle();
1649     var key = event.keyIdentifier;
1650     var eventHandled = false;
1651     var currentMonth = this.picker.currentMonth();
1652     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1653     var selectedMonth = this._rangeForNode(firstNodeInSelectedRange);
1654     if (!firstNodeInSelectedRange
1655         && (key == "Right" || key == "Left" || key == "Up" || key == "Down" || key == "PageUp" || key == "PageDown")) {
1656         this.selectRange(currentMonth);
1657         eventHandled = true;
1658     } else if (key == (global.params.isCalendarRTL ? "Right" : "Left") || key == "Up" || key == "PageUp") {
1659         if (selectedMonth.valueOf() > currentMonth.valueOf())
1660             this.selectRangeAndShowEntireRange(currentMonth);
1661         else
1662             this.selectRangeAndShowEntireRange(currentMonth.previous());
1663         eventHandled = true;
1664     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right") || key == "Down" || key == "PageDown") {
1665         if (selectedMonth.valueOf() < currentMonth.valueOf())
1666             this.selectRangeAndShowEntireRange(currentMonth);
1667         else
1668             this.selectRangeAndShowEntireRange(currentMonth.next());
1669         eventHandled = true;
1670     } else if (selectedMonth && key == "Enter") {
1671         if (firstNodeInSelectedRange.classList.contains(ClassNames.Available)) {
1672             this.picker.submitValue(selectedMonth.toString());
1673             eventHandled = true;
1674         }
1675     } else if (key == "U+0054") { // 't'
1676         this.selectRangeAndShowEntireRange(Month.createFromToday());
1677         eventHandled = true;
1678     }
1679     if (eventHandled) {
1680         event.stopPropagation();
1681         event.preventDefault();
1682     }
1683 };
1684
1685 /**
1686  * @constructor
1687  * @param{!CalendarPicker} picker
1688  */
1689 function WeekPickerDaysTable(picker) {
1690     DaysTable.call(this, picker);
1691 }
1692 WeekPickerDaysTable.prototype = Object.create(DaysTable.prototype);
1693
1694 /**
1695  * @param {!Week} week
1696  */
1697 WeekPickerDaysTable.prototype._markRangeAsSelected = function(week) {
1698     var weekString = week.toString();
1699     for (var w = 0; w < DaysTable._Weeks; w++) {
1700         for (var d = 0; d < 7; d++) {
1701             if (this._days[w][d].dataset.weekValue == weekString) {
1702                 this._days[w][d].classList.add(ClassNames.Selected);
1703             }
1704         }
1705     }
1706     for (var i = 0; i < this._weekNumbers.length; ++i) {
1707         if (this._weekNumbers[i].dataset.weekValue === weekString) {
1708             this._weekNumbers[i].classList.add(ClassNames.Selected);
1709             break;
1710         }
1711     }
1712 };
1713
1714 /**
1715  * @param {!Week} week
1716  */
1717 WeekPickerDaysTable.prototype.selectRange = function(week) {
1718     this._deselect();
1719     var weekStartDate = week.startDate();
1720     var weekEndDate = week.endDate();
1721     if (this.startDate() >= weekEndDate)
1722         this.picker.showMonth(Month.createFromDate(weekEndDate), CalendarPicker.NavigationBehaviour.Animate);
1723     else if (this.endDate() <= weekStartDate)
1724         this.picker.showMonth(Month.createFromDate(weekStartDate), CalendarPicker.NavigationBehaviour.Animate);
1725     this._markRangeAsSelected(week);
1726 };
1727
1728 /**
1729  * @param {!Week} week
1730  */
1731 WeekPickerDaysTable.prototype.selectRangeAndShowEntireRange = function(week) {
1732     this._deselect();
1733     var weekStartDate = week.startDate();
1734     var weekEndDate = week.endDate();
1735     if (this.startDate() > weekStartDate)
1736         this.picker.showMonth(Month.createFromDate(weekStartDate), CalendarPicker.NavigationBehaviour.Animate);
1737     else if (this.endDate() < weekEndDate)
1738         this.picker.showMonth(Month.createFromDate(weekEndDate), CalendarPicker.NavigationBehaviour.Animate);
1739     this._markRangeAsSelected(week);
1740 };
1741
1742 /**
1743  * @param {!Element} dayNode
1744  * @return {?Week}
1745  */
1746 WeekPickerDaysTable.prototype._rangeForNode = function(dayNode) {
1747     if (!dayNode)
1748         return null;
1749     return Week.parse(dayNode.dataset.weekValue);
1750 };
1751
1752 /**
1753  * @param {!Event} event
1754  */
1755 WeekPickerDaysTable.prototype._handleKey = function(event) {
1756     this.picker.maybeUpdateFocusStyle();
1757     var key = event.keyIdentifier;
1758     var eventHandled = false;
1759     var currentMonth = this.picker.currentMonth();
1760     var firstNodeInSelectedRange = this._firstNodeInSelectedRange();
1761     var selectedWeek = this._rangeForNode(firstNodeInSelectedRange);
1762     if (!firstNodeInSelectedRange
1763         && (key == "Right" || key == "Left" || key == "Up" || key == "Down" || key == "PageUp" || key == "PageDown")) {
1764         // Put the selection on a center cell.
1765         this._selectRangeAtPosition(3, Math.floor(DaysTable._Weeks / 2 - 1));
1766     } else if (key == (global.params.isCalendarRTL ? "Right" : "Left") || key == "Up") {
1767         this.selectRangeAndShowEntireRange(selectedWeek.previous());
1768         eventHandled = true;
1769     } else if (key == (global.params.isCalendarRTL ? "Left" : "Right") || key == "Down") {
1770         this.selectRangeAndShowEntireRange(selectedWeek.next());
1771         eventHandled = true;
1772     } else if (key == "PageUp") {
1773         if (!this._maybeSetPreviousMonth(CalendarPicker.NavigationBehaviour.Animate | CalendarPicker.NavigationBehaviour.KeepSelectionPosition))
1774             return;
1775         eventHandled = true;
1776     } else if (key == "PageDown") {
1777         if (!this._maybeSetNextMonth(CalendarPicker.NavigationBehaviour.Animate | CalendarPicker.NavigationBehaviour.KeepSelectionPosition))
1778             return;
1779         eventHandled = true;
1780     } else if (selectedWeek && key == "Enter") {
1781         if (firstNodeInSelectedRange.classList.contains(ClassNames.Available)) {
1782             this.picker.submitValue(selectedWeek.toString());
1783             eventHandled = true;
1784         }
1785     } else if (key == "U+0054") { // 't'
1786         this.selectRangeAndShowEntireRange(Week.createFromToday());
1787         eventHandled = true;
1788     }
1789     if (eventHandled) {
1790         event.stopPropagation();
1791         event.preventDefault();
1792     }
1793 };
1794
1795 /**
1796  * @param {!Event} event
1797  */
1798 CalendarPicker.prototype._handleBodyKeyDown = function(event) {
1799     this.maybeUpdateFocusStyle();
1800     var key = event.keyIdentifier;
1801     if (key == "U+0009") {
1802         if (!event.shiftKey && document.activeElement == this.lastFocusableControl) {
1803             event.stopPropagation();
1804             event.preventDefault();
1805             this.firstFocusableControl.focus();
1806         } else if (event.shiftKey && document.activeElement == this.firstFocusableControl) {
1807             event.stopPropagation();
1808             event.preventDefault();
1809             this.lastFocusableControl.focus();
1810         }
1811     } else if (key == "U+004D") { // 'm'
1812         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousMonth : YearMonthController.NextMonth);
1813     } else if (key == "U+0059") { // 'y'
1814         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousYear : YearMonthController.NextYear);
1815     } else if (key == "U+0044") { // 'd'
1816         this._yearMonthController.moveRelatively(event.shiftKey ? YearMonthController.PreviousTenYears : YearMonthController.NextTenYears);
1817     } else if (key == "U+001B") // ESC
1818         this.handleCancel();
1819 }
1820
1821 CalendarPicker.prototype.maybeUpdateFocusStyle = function() {
1822     if (this._hadKeyEvent)
1823         return;
1824     this._hadKeyEvent = true;
1825     this._element.classList.remove(ClassNames.NoFocusRing);
1826 }
1827
1828 if (window.dialogArguments) {
1829     initialize(dialogArguments);
1830 } else {
1831     window.addEventListener("message", handleMessage, false);
1832     window.setTimeout(handleArgumentsTimeout, 1000);
1833 }