Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / web / resources / 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
33 /**
34  * @enum {number}
35  */
36 var WeekDay = {
37     Sunday: 0,
38     Monday: 1,
39     Tuesday: 2,
40     Wednesday: 3,
41     Thursday: 4,
42     Friday: 5,
43     Saturday: 6
44 };
45
46 /**
47  * @type {Object}
48  */
49 var global = {
50     picker: null,
51     params: {
52         locale: "en_US",
53         weekStartDay: WeekDay.Sunday,
54         dayLabels: ["S", "M", "T", "W", "T", "F", "S"],
55         shortMonthLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"],
56         isLocaleRTL: false,
57         mode: "date",
58         weekLabel: "Week",
59         anchorRectInScreen: new Rectangle(0, 0, 0, 0),
60         currentValue: null
61     }
62 };
63
64 // ----------------------------------------------------------------
65 // Utility functions
66
67 /**
68  * @return {!boolean}
69  */
70 function hasInaccuratePointingDevice() {
71     return matchMedia("(pointer: coarse)").matches;
72 }
73
74 /**
75  * @return {!string} lowercase locale name. e.g. "en-us"
76  */
77 function getLocale() {
78     return (global.params.locale || "en-us").toLowerCase().replace(/_/g, '-');
79 }
80
81 /**
82  * @return {!string} lowercase language code. e.g. "en"
83  */
84 function getLanguage() {
85     var locale = getLocale();
86     var result = locale.match(/^([a-z]+)/);
87     if (!result)
88         return "en";
89     return result[1];
90 }
91
92 /**
93  * @param {!number} number
94  * @return {!string}
95  */
96 function localizeNumber(number) {
97     return window.pagePopupController.localizeNumberString(number);
98 }
99
100 /**
101  * @const
102  * @type {number}
103  */
104 var ImperialEraLimit = 2087;
105
106 /**
107  * @param {!number} year
108  * @param {!number} month
109  * @return {!string}
110  */
111 function formatJapaneseImperialEra(year, month) {
112     // We don't show an imperial era if it is greater than 99 becase of space
113     // limitation.
114     if (year > ImperialEraLimit)
115         return "";
116     if (year > 1989)
117         return "(平成" + localizeNumber(year - 1988) + "年)";
118     if (year == 1989)
119         return "(平成元年)";
120     if (year >= 1927)
121         return "(昭和" + localizeNumber(year - 1925) + "年)";
122     if (year > 1912)
123         return "(大正" + localizeNumber(year - 1911) + "年)";
124     if (year == 1912 && month >= 7)
125         return "(大正元年)";
126     if (year > 1868)
127         return "(明治" + localizeNumber(year - 1867) + "年)";
128     if (year == 1868)
129         return "(明治元年)";
130     return "";
131 }
132
133 function createUTCDate(year, month, date) {
134     var newDate = new Date(0);
135     newDate.setUTCFullYear(year);
136     newDate.setUTCMonth(month);
137     newDate.setUTCDate(date);
138     return newDate;
139 }
140
141 /**
142  * @param {string} dateString
143  * @return {?Day|Week|Month}
144  */
145 function parseDateString(dateString) {
146     var month = Month.parse(dateString);
147     if (month)
148         return month;
149     var week = Week.parse(dateString);
150     if (week)
151         return week;
152     return Day.parse(dateString);
153 }
154
155 /**
156  * @const
157  * @type {number}
158  */
159 var DaysPerWeek = 7;
160
161 /**
162  * @const
163  * @type {number}
164  */
165 var MonthsPerYear = 12;
166
167 /**
168  * @const
169  * @type {number}
170  */
171 var MillisecondsPerDay = 24 * 60 * 60 * 1000;
172
173 /**
174  * @const
175  * @type {number}
176  */
177 var MillisecondsPerWeek = DaysPerWeek * MillisecondsPerDay;
178
179 /**
180  * @constructor
181  */
182 function DateType() {
183 }
184
185 /**
186  * @constructor
187  * @extends DateType
188  * @param {!number} year
189  * @param {!number} month
190  * @param {!number} date
191  */
192 function Day(year, month, date) {
193     var dateObject = createUTCDate(year, month, date);
194     if (isNaN(dateObject.valueOf()))
195         throw "Invalid date";
196     /**
197      * @type {number}
198      * @const
199      */
200     this.year = dateObject.getUTCFullYear();   
201      /**
202      * @type {number}
203      * @const
204      */  
205     this.month = dateObject.getUTCMonth();
206     /**
207      * @type {number}
208      * @const
209      */
210     this.date = dateObject.getUTCDate();
211 };
212
213 Day.prototype = Object.create(DateType.prototype);
214
215 Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)/;
216
217 /**
218  * @param {!string} str
219  * @return {?Day}
220  */
221 Day.parse = function(str) {
222     var match = Day.ISOStringRegExp.exec(str);
223     if (!match)
224         return null;
225     var year = parseInt(match[1], 10);
226     var month = parseInt(match[2], 10) - 1;
227     var date = parseInt(match[3], 10);
228     return new Day(year, month, date);
229 };
230
231 /**
232  * @param {!number} value
233  * @return {!Day}
234  */
235 Day.createFromValue = function(millisecondsSinceEpoch) {
236     return Day.createFromDate(new Date(millisecondsSinceEpoch))
237 };
238
239 /**
240  * @param {!Date} date
241  * @return {!Day}
242  */
243 Day.createFromDate = function(date) {
244     if (isNaN(date.valueOf()))
245         throw "Invalid date";
246     return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
247 };
248
249 /**
250  * @param {!Day} day
251  * @return {!Day}
252  */
253 Day.createFromDay = function(day) {
254     return day;
255 };
256
257 /**
258  * @return {!Day}
259  */
260 Day.createFromToday = function() {
261     var now = new Date();
262     return new Day(now.getFullYear(), now.getMonth(), now.getDate());
263 };
264
265 /**
266  * @param {!DateType} other
267  * @return {!boolean}
268  */
269 Day.prototype.equals = function(other) {
270     return other instanceof Day && this.year === other.year && this.month === other.month && this.date === other.date;
271 };
272
273 /**
274  * @param {!number=} offset
275  * @return {!Day}
276  */
277 Day.prototype.previous = function(offset) {
278     if (typeof offset === "undefined")
279         offset = 1;
280     return new Day(this.year, this.month, this.date - offset);
281 };
282
283 /**
284  * @param {!number=} offset
285  * @return {!Day}
286  */
287 Day.prototype.next = function(offset) {
288  if (typeof offset === "undefined")
289      offset = 1;
290     return new Day(this.year, this.month, this.date + offset);
291 };
292
293 /**
294  * @return {!Date}
295  */
296 Day.prototype.startDate = function() {
297     return createUTCDate(this.year, this.month, this.date);
298 };
299
300 /**
301  * @return {!Date}
302  */
303 Day.prototype.endDate = function() {
304     return createUTCDate(this.year, this.month, this.date + 1);
305 };
306
307 /**
308  * @return {!Day}
309  */
310 Day.prototype.firstDay = function() {
311     return this;
312 };
313
314 /**
315  * @return {!Day}
316  */
317 Day.prototype.middleDay = function() {
318     return this;
319 };
320
321 /**
322  * @return {!Day}
323  */
324 Day.prototype.lastDay = function() {
325     return this;
326 };
327
328 /**
329  * @return {!number}
330  */
331 Day.prototype.valueOf = function() {
332     return createUTCDate(this.year, this.month, this.date).getTime();
333 };
334
335 /**
336  * @return {!WeekDay}
337  */
338 Day.prototype.weekDay = function() {
339     return createUTCDate(this.year, this.month, this.date).getUTCDay();
340 };
341
342 /**
343  * @return {!string}
344  */
345 Day.prototype.toString = function() {
346     var yearString = String(this.year);
347     if (yearString.length < 4)
348         yearString = ("000" + yearString).substr(-4, 4);
349     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2);
350 };
351
352 /**
353  * @return {!string}
354  */
355 Day.prototype.format = function() {
356     if (!Day.formatter) {
357         Day.formatter = new Intl.DateTimeFormat(getLocale(), {
358             weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC"
359         });
360     }
361     return Day.formatter.format(this.startDate());
362 };
363
364 // See WebCore/platform/DateComponents.h.
365 Day.Minimum = Day.createFromValue(-62135596800000.0);
366 Day.Maximum = Day.createFromValue(8640000000000000.0);
367
368 // See WebCore/html/DayInputType.cpp.
369 Day.DefaultStep = 86400000;
370 Day.DefaultStepBase = 0;
371
372 /**
373  * @constructor
374  * @extends DateType
375  * @param {!number} year
376  * @param {!number} week
377  */
378 function Week(year, week) { 
379     /**
380      * @type {number}
381      * @const
382      */
383     this.year = year;
384     /**
385      * @type {number}
386      * @const
387      */
388     this.week = week;
389     // Number of years per year is either 52 or 53.
390     if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) {
391         var normalizedWeek = Week.createFromDay(this.firstDay());
392         this.year = normalizedWeek.year;
393         this.week = normalizedWeek.week;
394     }
395 }
396
397 Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;
398
399 // See WebCore/platform/DateComponents.h.
400 Week.Minimum = new Week(1, 1);
401 Week.Maximum = new Week(275760, 37);
402
403 // See WebCore/html/WeekInputType.cpp.
404 Week.DefaultStep = 604800000;
405 Week.DefaultStepBase = -259200000;
406
407 Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay();
408
409 /**
410  * @param {!string} str
411  * @return {?Week}
412  */
413 Week.parse = function(str) {
414     var match = Week.ISOStringRegExp.exec(str);
415     if (!match)
416         return null;
417     var year = parseInt(match[1], 10);
418     var week = parseInt(match[2], 10);
419     return new Week(year, week);
420 };
421
422 /**
423  * @param {!number} millisecondsSinceEpoch
424  * @return {!Week}
425  */
426 Week.createFromValue = function(millisecondsSinceEpoch) {
427     return Week.createFromDate(new Date(millisecondsSinceEpoch))
428 };
429
430 /**
431  * @param {!Date} date
432  * @return {!Week}
433  */
434 Week.createFromDate = function(date) {
435     if (isNaN(date.valueOf()))
436         throw "Invalid date";
437     var year = date.getUTCFullYear();
438     if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime())
439         year++;
440     else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
441         year--;
442     var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
443     return new Week(year, week);
444 };
445
446 /**
447  * @param {!Day} day
448  * @return {!Week}
449  */
450 Week.createFromDay = function(day) {
451     var year = day.year;
452     if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day)
453         year++;
454     else if (year > 1 && Week.weekOneStartDayForYear(year) > day)
455         year--;
456     var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek);
457     return new Week(year, week);
458 };
459
460 /**
461  * @return {!Week}
462  */
463 Week.createFromToday = function() {
464     var now = new Date();
465     return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
466 };
467
468 /**
469  * @param {!number} year
470  * @return {!Date}
471  */
472 Week.weekOneStartDateForYear = function(year) {
473     if (year < 1)
474         return createUTCDate(1, 0, 1);
475     // The week containing January 4th is week one.
476     var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
477     return createUTCDate(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
478 };
479
480 /**
481  * @param {!number} year
482  * @return {!Day}
483  */
484 Week.weekOneStartDayForYear = function(year) {
485     if (year < 1)
486         return Day.Minimum;
487     // The week containing January 4th is week one.
488     var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
489     return new Day(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
490 };
491
492 /**
493  * @param {!number} year
494  * @return {!number}
495  */
496 Week.numberOfWeeksInYear = function(year) {
497     if (year < 1 || year > Week.Maximum.year)
498         return 0;
499     else if (year === Week.Maximum.year)
500         return Week.Maximum.week;
501     return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
502 };
503
504 /**
505  * @param {!Date} baseDate
506  * @param {!Date} date
507  * @return {!number}
508  */
509 Week._numberOfWeeksSinceDate = function(baseDate, date) {
510     return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek);
511 };
512
513 /**
514  * @param {!DateType} other
515  * @return {!boolean}
516  */
517 Week.prototype.equals = function(other) {
518     return other instanceof Week && this.year === other.year && this.week === other.week;
519 };
520
521 /**
522  * @param {!number=} offset
523  * @return {!Week}
524  */
525 Week.prototype.previous = function(offset) {
526     if (typeof offset === "undefined")
527         offset = 1;
528     return new Week(this.year, this.week - offset);
529 };
530
531 /**
532  * @param {!number=} offset
533  * @return {!Week}
534  */
535 Week.prototype.next = function(offset) {
536     if (typeof offset === "undefined")
537         offset = 1;
538     return new Week(this.year, this.week + offset);
539 };
540
541 /**
542  * @return {!Date}
543  */
544 Week.prototype.startDate = function() {
545     var weekStartDate = Week.weekOneStartDateForYear(this.year);
546     weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
547     return weekStartDate;
548 };
549
550 /**
551  * @return {!Date}
552  */
553 Week.prototype.endDate = function() {
554     if (this.equals(Week.Maximum))
555         return Day.Maximum.startDate();
556     return this.next().startDate();
557 };
558
559 /**
560  * @return {!Day}
561  */
562 Week.prototype.firstDay = function() {
563     var weekOneStartDay = Week.weekOneStartDayForYear(this.year);
564     return weekOneStartDay.next((this.week - 1) * DaysPerWeek);
565 };
566
567 /**
568  * @return {!Day}
569  */
570 Week.prototype.middleDay = function() {
571     return this.firstDay().next(3);
572 };
573
574 /**
575  * @return {!Day}
576  */
577 Week.prototype.lastDay = function() {
578     if (this.equals(Week.Maximum))
579         return Day.Maximum;
580     return this.next().firstDay().previous();
581 };
582
583 /**
584  * @return {!number}
585  */
586 Week.prototype.valueOf = function() {
587     return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime();
588 };
589
590 /**
591  * @return {!string}
592  */
593 Week.prototype.toString = function() {
594     var yearString = String(this.year);
595     if (yearString.length < 4)
596         yearString = ("000" + yearString).substr(-4, 4);
597     return yearString + "-W" + ("0" + this.week).substr(-2, 2);
598 };
599
600 /**
601  * @constructor
602  * @extends DateType
603  * @param {!number} year
604  * @param {!number} month
605  */
606 function Month(year, month) { 
607     /**
608      * @type {number}
609      * @const
610      */
611     this.year = year + Math.floor(month / MonthsPerYear);
612     /**
613      * @type {number}
614      * @const
615      */
616     this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear;
617 };
618
619 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
620
621 // See WebCore/platform/DateComponents.h.
622 Month.Minimum = new Month(1, 0);
623 Month.Maximum = new Month(275760, 8);
624
625 // See WebCore/html/MonthInputType.cpp.
626 Month.DefaultStep = 1;
627 Month.DefaultStepBase = 0;
628
629 /**
630  * @param {!string} str
631  * @return {?Month}
632  */
633 Month.parse = function(str) {
634     var match = Month.ISOStringRegExp.exec(str);
635     if (!match)
636         return null;
637     var year = parseInt(match[1], 10);
638     var month = parseInt(match[2], 10) - 1;
639     return new Month(year, month);
640 };
641
642 /**
643  * @param {!number} value
644  * @return {!Month}
645  */
646 Month.createFromValue = function(monthsSinceEpoch) {
647     return new Month(1970, monthsSinceEpoch)
648 };
649
650 /**
651  * @param {!Date} date
652  * @return {!Month}
653  */
654 Month.createFromDate = function(date) {
655     if (isNaN(date.valueOf()))
656         throw "Invalid date";
657     return new Month(date.getUTCFullYear(), date.getUTCMonth());
658 };
659
660 /**
661  * @param {!Day} day
662  * @return {!Month}
663  */
664 Month.createFromDay = function(day) {
665     return new Month(day.year, day.month);
666 };
667
668 /**
669  * @return {!Month}
670  */
671 Month.createFromToday = function() {
672     var now = new Date();
673     return new Month(now.getFullYear(), now.getMonth());
674 };
675
676 /**
677  * @return {!boolean}
678  */
679 Month.prototype.containsDay = function(day) {
680     return this.year === day.year && this.month === day.month;
681 };
682
683 /**
684  * @param {!Month} other
685  * @return {!boolean}
686  */
687 Month.prototype.equals = function(other) {
688     return other instanceof Month && this.year === other.year && this.month === other.month;
689 };
690
691 /**
692  * @param {!number=} offset
693  * @return {!Month}
694  */
695 Month.prototype.previous = function(offset) {
696     if (typeof offset === "undefined")
697         offset = 1;
698     return new Month(this.year, this.month - offset);
699 };
700
701 /**
702  * @param {!number=} offset
703  * @return {!Month}
704  */
705 Month.prototype.next = function(offset) {
706     if (typeof offset === "undefined")
707         offset = 1;
708     return new Month(this.year, this.month + offset);
709 };
710
711 /**
712  * @return {!Date}
713  */
714 Month.prototype.startDate = function() {
715     return createUTCDate(this.year, this.month, 1);
716 };
717
718 /**
719  * @return {!Date}
720  */
721 Month.prototype.endDate = function() {
722     if (this.equals(Month.Maximum))
723         return Day.Maximum.startDate();
724     return this.next().startDate();
725 };
726
727 /**
728  * @return {!Day}
729  */
730 Month.prototype.firstDay = function() {
731     return new Day(this.year, this.month, 1);
732 };
733
734 /**
735  * @return {!Day}
736  */
737 Month.prototype.middleDay = function() {
738     return new Day(this.year, this.month, this.month === 2 ? 14 : 15);
739 };
740
741 /**
742  * @return {!Day}
743  */
744 Month.prototype.lastDay = function() {
745     if (this.equals(Month.Maximum))
746         return Day.Maximum;
747     return this.next().firstDay().previous();
748 };
749
750 /**
751  * @return {!number}
752  */
753 Month.prototype.valueOf = function() {
754     return (this.year - 1970) * MonthsPerYear + this.month;
755 };
756
757 /**
758  * @return {!string}
759  */
760 Month.prototype.toString = function() {
761     var yearString = String(this.year);
762     if (yearString.length < 4)
763         yearString = ("000" + yearString).substr(-4, 4);
764     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
765 };
766
767 /**
768  * @return {!string}
769  */
770 Month.prototype.toLocaleString = function() {
771     if (global.params.locale === "ja")
772         return "" + this.year + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "月";
773     return window.pagePopupController.formatMonth(this.year, this.month);
774 };
775
776 /**
777  * @return {!string}
778  */
779 Month.prototype.toShortLocaleString = function() {
780     return window.pagePopupController.formatShortMonth(this.year, this.month);
781 };
782
783 // ----------------------------------------------------------------
784 // Initialization
785
786 /**
787  * @param {Event} event
788  */
789 function handleMessage(event) {
790     if (global.argumentsReceived)
791         return;
792     global.argumentsReceived = true;
793     initialize(JSON.parse(event.data));
794 }
795
796 /**
797  * @param {!Object} params
798  */
799 function setGlobalParams(params) {
800     var name;
801     for (name in global.params) {
802         if (typeof params[name] === "undefined")
803             console.warn("Missing argument: " + name);
804     }
805     for (name in params) {
806         global.params[name] = params[name];
807     }
808 };
809
810 /**
811  * @param {!Object} args
812  */
813 function initialize(args) { 
814     setGlobalParams(args);
815     if (global.params.suggestionValues && global.params.suggestionValues.length)
816         openSuggestionPicker();
817     else
818         openCalendarPicker();
819 }
820
821 function closePicker() {
822     if (global.picker)
823         global.picker.cleanup();
824     var main = $("main");
825     main.innerHTML = "";
826     main.className = "";
827 };
828
829 function openSuggestionPicker() {
830     closePicker();
831     global.picker = new SuggestionPicker($("main"), global.params);
832 };
833
834 function openCalendarPicker() {
835     closePicker();
836     global.picker = new CalendarPicker(global.params.mode, global.params);
837     global.picker.attachTo($("main"));
838 };
839
840 /**
841  * @constructor
842  */
843 function EventEmitter() {
844 };
845
846 /**
847  * @param {!string} type
848  * @param {!function({...*})} callback
849  */
850 EventEmitter.prototype.on = function(type, callback) {
851     console.assert(callback instanceof Function);
852     if (!this._callbacks)
853         this._callbacks = {};
854     if (!this._callbacks[type])
855         this._callbacks[type] = [];
856     this._callbacks[type].push(callback);
857 };
858
859 EventEmitter.prototype.hasListener = function(type) {
860     if (!this._callbacks)
861         return false;
862     var callbacksForType = this._callbacks[type];
863     if (!callbacksForType)
864         return false;
865     return callbacksForType.length > 0;
866 };
867
868 /**
869  * @param {!string} type
870  * @param {!function(Object)} callback
871  */
872 EventEmitter.prototype.removeListener = function(type, callback) {
873     if (!this._callbacks)
874         return;
875     var callbacksForType = this._callbacks[type];
876     if (!callbacksForType)
877         return;
878     callbacksForType.splice(callbacksForType.indexOf(callback), 1);
879     if (callbacksForType.length === 0)
880         delete this._callbacks[type];
881 };
882
883 /**
884  * @param {!string} type
885  * @param {...*} var_args
886  */
887 EventEmitter.prototype.dispatchEvent = function(type) {
888     if (!this._callbacks)
889         return;
890     var callbacksForType = this._callbacks[type];
891     if (!callbacksForType)
892         return;
893     callbacksForType = callbacksForType.slice(0);
894     for (var i = 0; i < callbacksForType.length; ++i) {
895         callbacksForType[i].apply(this, Array.prototype.slice.call(arguments, 1));
896     }
897 };
898
899 // Parameter t should be a number between 0 and 1.
900 var AnimationTimingFunction = {
901     Linear: function(t){
902         return t;
903     },
904     EaseInOut: function(t){
905         t *= 2;
906         if (t < 1)
907             return Math.pow(t, 3) / 2;
908         t -= 2;
909         return Math.pow(t, 3) / 2 + 1;
910     }
911 };
912
913 /**
914  * @constructor
915  * @extends EventEmitter
916  */
917 function AnimationManager() {
918     EventEmitter.call(this);
919
920     this._isRunning = false;
921     this._runningAnimatorCount = 0;
922     this._runningAnimators = {};
923     this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
924 }
925
926 AnimationManager.prototype = Object.create(EventEmitter.prototype);
927
928 AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";
929
930 AnimationManager.prototype._startAnimation = function() {
931     if (this._isRunning)
932         return;
933     this._isRunning = true;
934     window.requestAnimationFrame(this._animationFrameCallbackBound);
935 };
936
937 AnimationManager.prototype._stopAnimation = function() {
938     if (!this._isRunning)
939         return;
940     this._isRunning = false;
941 };
942
943 /**
944  * @param {!Animator} animator
945  */
946 AnimationManager.prototype.add = function(animator) {
947     if (this._runningAnimators[animator.id])
948         return;
949     this._runningAnimators[animator.id] = animator;
950     this._runningAnimatorCount++;
951     if (this._needsTimer())
952         this._startAnimation();
953 };
954
955 /**
956  * @param {!Animator} animator
957  */
958 AnimationManager.prototype.remove = function(animator) {
959     if (!this._runningAnimators[animator.id])
960         return;
961     delete this._runningAnimators[animator.id];
962     this._runningAnimatorCount--;
963     if (!this._needsTimer())
964         this._stopAnimation();
965 };
966
967 AnimationManager.prototype._animationFrameCallback = function(now) {
968     if (this._runningAnimatorCount > 0) {
969         for (var id in this._runningAnimators) {
970             this._runningAnimators[id].onAnimationFrame(now);
971         }
972     }
973     this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
974     if (this._isRunning)
975         window.requestAnimationFrame(this._animationFrameCallbackBound);
976 };
977
978 /**
979  * @return {!boolean}
980  */
981 AnimationManager.prototype._needsTimer = function() {
982     return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
983 };
984
985 /**
986  * @param {!string} type
987  * @param {!Function} callback
988  * @override
989  */
990 AnimationManager.prototype.on = function(type, callback) {
991     EventEmitter.prototype.on.call(this, type, callback);
992     if (this._needsTimer())
993         this._startAnimation();
994 };
995
996 /**
997  * @param {!string} type
998  * @param {!Function} callback
999  * @override
1000  */
1001 AnimationManager.prototype.removeListener = function(type, callback) {
1002     EventEmitter.prototype.removeListener.call(this, type, callback);
1003     if (!this._needsTimer())
1004         this._stopAnimation();
1005 };
1006
1007 AnimationManager.shared = new AnimationManager();
1008
1009 /**
1010  * @constructor
1011  * @extends EventEmitter
1012  */
1013 function Animator() {
1014     EventEmitter.call(this);
1015
1016     /**
1017      * @type {!number}
1018      * @const
1019      */
1020     this.id = Animator._lastId++;
1021     /**
1022      * @type {!number}
1023      */
1024     this.duration = 100;
1025     /**
1026      * @type {?function}
1027      */
1028     this.step = null;
1029     /**
1030      * @type {!boolean}
1031      * @protected
1032      */
1033     this._isRunning = false;
1034     /**
1035      * @type {!number}
1036      */
1037     this.currentValue = 0;
1038     /**
1039      * @type {!number}
1040      * @protected
1041      */
1042     this._lastStepTime = 0;
1043 }
1044
1045 Animator.prototype = Object.create(EventEmitter.prototype);
1046
1047 Animator._lastId = 0;
1048
1049 Animator.EventTypeDidAnimationStop = "didAnimationStop";
1050
1051 /**
1052  * @return {!boolean}
1053  */
1054 Animator.prototype.isRunning = function() {
1055     return this._isRunning;
1056 };
1057
1058 Animator.prototype.start = function() {
1059     this._lastStepTime = performance.now();
1060     this._isRunning = true;
1061     AnimationManager.shared.add(this);
1062 };
1063
1064 Animator.prototype.stop = function() {
1065     if (!this._isRunning)
1066         return;
1067     this._isRunning = false;
1068     AnimationManager.shared.remove(this);
1069     this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
1070 };
1071
1072 /**
1073  * @param {!number} now
1074  */
1075 Animator.prototype.onAnimationFrame = function(now) {
1076     this._lastStepTime = now;
1077     this.step(this);
1078 };
1079
1080 /**
1081  * @constructor
1082  * @extends Animator
1083  */
1084 function TransitionAnimator() {
1085     Animator.call(this);
1086     /**
1087      * @type {!number}
1088      * @protected
1089      */
1090     this._from = 0;
1091     /**
1092      * @type {!number}
1093      * @protected
1094      */
1095     this._to = 0;
1096     /**
1097      * @type {!number}
1098      * @protected
1099      */
1100     this._delta = 0;
1101     /**
1102      * @type {!number}
1103      */
1104     this.progress = 0.0;
1105     /**
1106      * @type {!function}
1107      */
1108     this.timingFunction = AnimationTimingFunction.Linear;
1109 }
1110
1111 TransitionAnimator.prototype = Object.create(Animator.prototype);
1112
1113 /**
1114  * @param {!number} value
1115  */
1116 TransitionAnimator.prototype.setFrom = function(value) {
1117     this._from = value;
1118     this._delta = this._to - this._from;
1119 };
1120
1121 TransitionAnimator.prototype.start = function() {
1122     console.assert(isFinite(this.duration));
1123     this.progress = 0.0;
1124     this.currentValue = this._from;
1125     Animator.prototype.start.call(this);
1126 };
1127
1128 /**
1129  * @param {!number} value
1130  */
1131 TransitionAnimator.prototype.setTo = function(value) {
1132     this._to = value;
1133     this._delta = this._to - this._from;
1134 };
1135
1136 /**
1137  * @param {!number} now
1138  */
1139 TransitionAnimator.prototype.onAnimationFrame = function(now) {
1140     this.progress += (now - this._lastStepTime) / this.duration;
1141     this.progress = Math.min(1.0, this.progress);
1142     this._lastStepTime = now;
1143     this.currentValue = this.timingFunction(this.progress) * this._delta + this._from;
1144     this.step(this);
1145     if (this.progress === 1.0) {
1146         this.stop();
1147         return;
1148     }
1149 };
1150
1151 /**
1152  * @constructor
1153  * @extends Animator
1154  * @param {!number} initialVelocity
1155  * @param {!number} initialValue
1156  */
1157 function FlingGestureAnimator(initialVelocity, initialValue) {
1158     Animator.call(this);
1159     /**
1160      * @type {!number}
1161      */
1162     this.initialVelocity = initialVelocity;
1163     /**
1164      * @type {!number}
1165      */
1166     this.initialValue = initialValue;
1167     /**
1168      * @type {!number}
1169      * @protected
1170      */
1171     this._elapsedTime = 0;
1172     var startVelocity = Math.abs(this.initialVelocity);
1173     if (startVelocity > this._velocityAtTime(0))
1174         startVelocity = this._velocityAtTime(0);
1175     if (startVelocity < 0)
1176         startVelocity = 0;
1177     /**
1178      * @type {!number}
1179      * @protected
1180      */
1181     this._timeOffset = this._timeAtVelocity(startVelocity);
1182     /**
1183      * @type {!number}
1184      * @protected
1185      */
1186     this._positionOffset = this._valueAtTime(this._timeOffset);
1187     /**
1188      * @type {!number}
1189      */
1190     this.duration = this._timeAtVelocity(0);
1191 }
1192
1193 FlingGestureAnimator.prototype = Object.create(Animator.prototype);
1194
1195 // Velocity is subject to exponential decay. These parameters are coefficients
1196 // that determine the curve.
1197 FlingGestureAnimator._P0 = -5707.62;
1198 FlingGestureAnimator._P1 = 0.172;
1199 FlingGestureAnimator._P2 = 0.0037;
1200
1201 /**
1202  * @param {!number} t
1203  */
1204 FlingGestureAnimator.prototype._valueAtTime = function(t) {
1205     return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
1206 };
1207
1208 /**
1209  * @param {!number} t
1210  */
1211 FlingGestureAnimator.prototype._velocityAtTime = function(t) {
1212     return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
1213 };
1214
1215 /**
1216  * @param {!number} v
1217  */
1218 FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
1219     return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
1220 };
1221
1222 FlingGestureAnimator.prototype.start = function() {
1223     this._lastStepTime = performance.now();
1224     Animator.prototype.start.call(this);
1225 };
1226
1227 /**
1228  * @param {!number} now
1229  */
1230 FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
1231     this._elapsedTime += now - this._lastStepTime;
1232     this._lastStepTime = now;
1233     if (this._elapsedTime + this._timeOffset >= this.duration) {
1234         this.stop();
1235         return;
1236     }
1237     var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset;
1238     if (this.initialVelocity < 0)
1239         position = -position;
1240     this.currentValue = position + this.initialValue;
1241     this.step(this);
1242 };
1243
1244 /**
1245  * @constructor
1246  * @extends EventEmitter
1247  * @param {?Element} element
1248  * View adds itself as a property on the element so we can access it from Event.target.
1249  */
1250 function View(element) {
1251     EventEmitter.call(this);
1252     /**
1253      * @type {Element}
1254      * @const
1255      */
1256     this.element = element || createElement("div");
1257     this.element.$view = this;
1258     this.bindCallbackMethods();
1259 }
1260
1261 View.prototype = Object.create(EventEmitter.prototype);
1262
1263 /**
1264  * @param {!Element} ancestorElement
1265  * @return {?Object}
1266  */
1267 View.prototype.offsetRelativeTo = function(ancestorElement) {
1268     var x = 0;
1269     var y = 0;
1270     var element = this.element;
1271     while (element) {
1272         x += element.offsetLeft  || 0;
1273         y += element.offsetTop || 0;
1274         element = element.offsetParent;
1275         if (element === ancestorElement)
1276             return {x: x, y: y};
1277     }
1278     return null;
1279 };
1280
1281 /**
1282  * @param {!View|Node} parent
1283  * @param {?View|Node=} before
1284  */
1285 View.prototype.attachTo = function(parent, before) {
1286     if (parent instanceof View)
1287         return this.attachTo(parent.element, before);
1288     if (typeof before === "undefined")
1289         before = null;
1290     if (before instanceof View)
1291         before = before.element;
1292     parent.insertBefore(this.element, before);
1293 };
1294
1295 View.prototype.bindCallbackMethods = function() {
1296     for (var methodName in this) {
1297         if (!/^on[A-Z]/.test(methodName))
1298             continue;
1299         if (this.hasOwnProperty(methodName))
1300             continue;
1301         var method = this[methodName];
1302         if (!(method instanceof Function))
1303             continue;
1304         this[methodName] = method.bind(this);
1305     }
1306 };
1307
1308 /**
1309  * @constructor
1310  * @extends View
1311  */
1312 function ScrollView() {
1313     View.call(this, createElement("div", ScrollView.ClassNameScrollView));
1314     /**
1315      * @type {Element}
1316      * @const
1317      */
1318     this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
1319     this.element.appendChild(this.contentElement);
1320     /**
1321      * @type {number}
1322      */
1323     this.minimumContentOffset = -Infinity;
1324     /**
1325      * @type {number}
1326      */
1327     this.maximumContentOffset = Infinity;
1328     /**
1329      * @type {number}
1330      * @protected
1331      */
1332     this._contentOffset = 0;
1333     /**
1334      * @type {number}
1335      * @protected
1336      */
1337     this._width = 0;
1338     /**
1339      * @type {number}
1340      * @protected
1341      */
1342     this._height = 0;
1343     /**
1344      * @type {Animator}
1345      * @protected
1346      */
1347     this._scrollAnimator = null;
1348     /**
1349      * @type {?Object}
1350      */
1351     this.delegate = null;
1352     /**
1353      * @type {!number}
1354      */
1355     this._lastTouchPosition = 0;
1356     /**
1357      * @type {!number}
1358      */
1359     this._lastTouchVelocity = 0;
1360     /**
1361      * @type {!number}
1362      */
1363     this._lastTouchTimeStamp = 0;
1364
1365     this.element.addEventListener("mousewheel", this.onMouseWheel, false);
1366     this.element.addEventListener("touchstart", this.onTouchStart, false);
1367
1368     /**
1369      * The content offset is partitioned so the it can go beyond the CSS limit
1370      * of 33554433px.
1371      * @type {number}
1372      * @protected
1373      */
1374     this._partitionNumber = 0;
1375 }
1376
1377 ScrollView.prototype = Object.create(View.prototype);
1378
1379 ScrollView.PartitionHeight = 100000;
1380 ScrollView.ClassNameScrollView = "scroll-view";
1381 ScrollView.ClassNameScrollViewContent = "scroll-view-content";
1382
1383 /**
1384  * @param {!Event} event
1385  */
1386 ScrollView.prototype.onTouchStart = function(event) {
1387     var touch = event.touches[0];
1388     this._lastTouchPosition = touch.clientY;
1389     this._lastTouchVelocity = 0;
1390     this._lastTouchTimeStamp = event.timeStamp;
1391     if (this._scrollAnimator)
1392         this._scrollAnimator.stop();
1393     window.addEventListener("touchmove", this.onWindowTouchMove, false);
1394     window.addEventListener("touchend", this.onWindowTouchEnd, false);
1395 };
1396
1397 /**
1398  * @param {!Event} event
1399  */
1400 ScrollView.prototype.onWindowTouchMove = function(event) {
1401     var touch = event.touches[0];
1402     var deltaTime = event.timeStamp - this._lastTouchTimeStamp;
1403     var deltaY = this._lastTouchPosition - touch.clientY;
1404     this.scrollBy(deltaY, false);
1405     this._lastTouchVelocity = deltaY / deltaTime;
1406     this._lastTouchPosition = touch.clientY;
1407     this._lastTouchTimeStamp = event.timeStamp;
1408     event.stopPropagation();
1409     event.preventDefault();
1410 };
1411
1412 /**
1413  * @param {!Event} event
1414  */
1415 ScrollView.prototype.onWindowTouchEnd = function(event) {
1416     if (Math.abs(this._lastTouchVelocity) > 0.01) {
1417         this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset);
1418         this._scrollAnimator.step = this.onFlingGestureAnimatorStep;
1419         this._scrollAnimator.start();
1420     }
1421     window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1422     window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1423 };
1424
1425 /**
1426  * @param {!Animator} animator
1427  */
1428 ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
1429     this.scrollTo(animator.currentValue, false);
1430 };
1431
1432 /**
1433  * @return {!Animator}
1434  */
1435 ScrollView.prototype.scrollAnimator = function() {
1436     return this._scrollAnimator;
1437 };
1438
1439 /**
1440  * @param {!number} width
1441  */
1442 ScrollView.prototype.setWidth = function(width) {
1443     console.assert(isFinite(width));
1444     if (this._width === width)
1445         return;
1446     this._width = width;
1447     this.element.style.width = this._width + "px";
1448 };
1449
1450 /**
1451  * @return {!number}
1452  */
1453 ScrollView.prototype.width = function() {
1454     return this._width;
1455 };
1456
1457 /**
1458  * @param {!number} height
1459  */
1460 ScrollView.prototype.setHeight = function(height) {
1461     console.assert(isFinite(height));
1462     if (this._height === height)
1463         return;
1464     this._height = height;
1465     this.element.style.height = height + "px";
1466     if (this.delegate)
1467         this.delegate.scrollViewDidChangeHeight(this);
1468 };
1469
1470 /**
1471  * @return {!number}
1472  */
1473 ScrollView.prototype.height = function() {
1474     return this._height;
1475 };
1476
1477 /**
1478  * @param {!Animator} animator
1479  */
1480 ScrollView.prototype.onScrollAnimatorStep = function(animator) {
1481     this.setContentOffset(animator.currentValue);
1482 };
1483
1484 /**
1485  * @param {!number} offset
1486  * @param {?boolean} animate
1487  */
1488 ScrollView.prototype.scrollTo = function(offset, animate) {
1489     console.assert(isFinite(offset));
1490     if (!animate) {
1491         this.setContentOffset(offset);
1492         return;
1493     }
1494     if (this._scrollAnimator)
1495         this._scrollAnimator.stop();
1496     this._scrollAnimator = new TransitionAnimator();
1497     this._scrollAnimator.step = this.onScrollAnimatorStep;
1498     this._scrollAnimator.setFrom(this._contentOffset);
1499     this._scrollAnimator.setTo(offset);
1500     this._scrollAnimator.duration = 300;
1501     this._scrollAnimator.start();
1502 };
1503
1504 /**
1505  * @param {!number} offset
1506  * @param {?boolean} animate
1507  */
1508 ScrollView.prototype.scrollBy = function(offset, animate) {
1509     this.scrollTo(this._contentOffset + offset, animate);
1510 };
1511
1512 /**
1513  * @return {!number}
1514  */
1515 ScrollView.prototype.contentOffset = function() {
1516     return this._contentOffset;
1517 };
1518
1519 /**
1520  * @param {?Event} event
1521  */
1522 ScrollView.prototype.onMouseWheel = function(event) {
1523     this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
1524     event.stopPropagation();
1525     event.preventDefault();
1526 };
1527
1528
1529 /**
1530  * @param {!number} value
1531  */
1532 ScrollView.prototype.setContentOffset = function(value) {
1533     console.assert(isFinite(value));
1534     value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value)));
1535     if (this._contentOffset === value)
1536         return;
1537     this._contentOffset = value;
1538     this._updateScrollContent();
1539     if (this.delegate)
1540         this.delegate.scrollViewDidChangeContentOffset(this);
1541 };
1542
1543 ScrollView.prototype._updateScrollContent = function() {
1544     var newPartitionNumber = Math.floor(this._contentOffset / ScrollView.PartitionHeight);
1545     var partitionChanged = this._partitionNumber !== newPartitionNumber;
1546     this._partitionNumber = newPartitionNumber;
1547     this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)";
1548     if (this.delegate && partitionChanged)
1549         this.delegate.scrollViewDidChangePartition(this);
1550 };
1551
1552 /**
1553  * @param {!View|Node} parent
1554  * @param {?View|Node=} before
1555  * @override
1556  */
1557 ScrollView.prototype.attachTo = function(parent, before) {
1558     View.prototype.attachTo.call(this, parent, before);
1559     this._updateScrollContent();
1560 };
1561
1562 /**
1563  * @param {!number} offset
1564  */
1565 ScrollView.prototype.contentPositionForContentOffset = function(offset) {
1566     return offset - this._partitionNumber * ScrollView.PartitionHeight;
1567 };
1568
1569 /**
1570  * @constructor
1571  * @extends View
1572  */
1573 function ListCell() {
1574     View.call(this, createElement("div", ListCell.ClassNameListCell));
1575     
1576     /**
1577      * @type {!number}
1578      */
1579     this.row = NaN;
1580     /**
1581      * @type {!number}
1582      */
1583     this._width = 0;
1584     /**
1585      * @type {!number}
1586      */
1587     this._position = 0;
1588 }
1589
1590 ListCell.prototype = Object.create(View.prototype);
1591
1592 ListCell.DefaultRecycleBinLimit = 64;
1593 ListCell.ClassNameListCell = "list-cell";
1594 ListCell.ClassNameHidden = "hidden";
1595
1596 /**
1597  * @return {!Array} An array to keep thrown away cells.
1598  */
1599 ListCell.prototype._recycleBin = function() {
1600     console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
1601     return [];
1602 };
1603
1604 ListCell.prototype.throwAway = function() {
1605     this.hide();
1606     var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit;
1607     var recycleBin = this._recycleBin();
1608     if (recycleBin.length < limit)
1609         recycleBin.push(this);
1610 };
1611
1612 ListCell.prototype.show = function() {
1613     this.element.classList.remove(ListCell.ClassNameHidden);
1614 };
1615
1616 ListCell.prototype.hide = function() {
1617     this.element.classList.add(ListCell.ClassNameHidden);
1618 };
1619
1620 /**
1621  * @return {!number} Width in pixels.
1622  */
1623 ListCell.prototype.width = function(){
1624     return this._width;
1625 };
1626
1627 /**
1628  * @param {!number} width Width in pixels.
1629  */
1630 ListCell.prototype.setWidth = function(width){
1631     if (this._width === width)
1632         return;
1633     this._width = width;
1634     this.element.style.width = this._width + "px";
1635 };
1636
1637 /**
1638  * @return {!number} Position in pixels.
1639  */
1640 ListCell.prototype.position = function(){
1641     return this._position;
1642 };
1643
1644 /**
1645  * @param {!number} y Position in pixels.
1646  */
1647 ListCell.prototype.setPosition = function(y) {
1648     if (this._position === y)
1649         return;
1650     this._position = y;
1651     this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
1652 };
1653
1654 /**
1655  * @param {!boolean} selected
1656  */
1657 ListCell.prototype.setSelected = function(selected) {
1658     if (this._selected === selected)
1659         return;
1660     this._selected = selected;
1661     if (this._selected)
1662         this.element.classList.add("selected");
1663     else
1664         this.element.classList.remove("selected");
1665 };
1666
1667 /**
1668  * @constructor
1669  * @extends View
1670  */
1671 function ListView() {
1672     View.call(this, createElement("div", ListView.ClassNameListView));
1673     this.element.tabIndex = 0;
1674     this.element.setAttribute("role", "grid");
1675
1676     /**
1677      * @type {!number}
1678      * @private
1679      */
1680     this._width = 0;
1681     /**
1682      * @type {!Object}
1683      * @private
1684      */
1685     this._cells = {};
1686
1687     /**
1688      * @type {!number}
1689      */
1690     this.selectedRow = ListView.NoSelection;
1691
1692     /**
1693      * @type {!ScrollView}
1694      */
1695     this.scrollView = new ScrollView();
1696     this.scrollView.delegate = this;
1697     this.scrollView.minimumContentOffset = 0;
1698     this.scrollView.setWidth(0);
1699     this.scrollView.setHeight(0);
1700     this.scrollView.attachTo(this);
1701
1702     this.element.addEventListener("click", this.onClick, false);
1703
1704     /**
1705      * @type {!boolean}
1706      * @private
1707      */
1708     this._needsUpdateCells = false;
1709 }
1710
1711 ListView.prototype = Object.create(View.prototype);
1712
1713 ListView.NoSelection = -1;
1714 ListView.ClassNameListView = "list-view";
1715
1716 ListView.prototype.onAnimationFrameWillFinish = function() {
1717     if (this._needsUpdateCells)
1718         this.updateCells();
1719 };
1720
1721 /**
1722  * @param {!boolean} needsUpdateCells
1723  */
1724 ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
1725     if (this._needsUpdateCells === needsUpdateCells)
1726         return;
1727     this._needsUpdateCells = needsUpdateCells;
1728     if (this._needsUpdateCells)
1729         AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1730     else
1731         AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1732 };
1733
1734 /**
1735  * @param {!number} row
1736  * @return {?ListCell}
1737  */
1738 ListView.prototype.cellAtRow = function(row) {
1739     return this._cells[row];
1740 };
1741
1742 /**
1743  * @param {!number} offset Scroll offset in pixels.
1744  * @return {!number}
1745  */
1746 ListView.prototype.rowAtScrollOffset = function(offset) {
1747     console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
1748     return 0;
1749 };
1750
1751 /**
1752  * @param {!number} row
1753  * @return {!number} Scroll offset in pixels.
1754  */
1755 ListView.prototype.scrollOffsetForRow = function(row) {
1756     console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
1757     return 0;
1758 };
1759
1760 /**
1761  * @param {!number} row
1762  * @return {!ListCell}
1763  */
1764 ListView.prototype.addCellIfNecessary = function(row) {
1765     var cell = this._cells[row];
1766     if (cell)
1767         return cell;
1768     cell = this.prepareNewCell(row);
1769     cell.attachTo(this.scrollView.contentElement);
1770     cell.setWidth(this._width);
1771     cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row)));
1772     this._cells[row] = cell;
1773     return cell;
1774 };
1775
1776 /**
1777  * @param {!number} row
1778  * @return {!ListCell}
1779  */
1780 ListView.prototype.prepareNewCell = function(row) {
1781     console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
1782     return new ListCell();
1783 };
1784
1785 /**
1786  * @param {!ListCell} cell
1787  */
1788 ListView.prototype.throwAwayCell = function(cell) {
1789     delete this._cells[cell.row];
1790     cell.throwAway();
1791 };
1792
1793 /**
1794  * @return {!number}
1795  */
1796 ListView.prototype.firstVisibleRow = function() {
1797     return this.rowAtScrollOffset(this.scrollView.contentOffset());
1798 };
1799
1800 /**
1801  * @return {!number}
1802  */
1803 ListView.prototype.lastVisibleRow = function() {
1804     return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
1805 };
1806
1807 /**
1808  * @param {!ScrollView} scrollView
1809  */
1810 ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
1811     this.setNeedsUpdateCells(true);
1812 };
1813
1814 /**
1815  * @param {!ScrollView} scrollView
1816  */
1817 ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
1818     this.setNeedsUpdateCells(true);
1819 };
1820
1821 /**
1822  * @param {!ScrollView} scrollView
1823  */
1824 ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
1825     this.setNeedsUpdateCells(true);
1826 };
1827
1828 ListView.prototype.updateCells = function() {
1829     var firstVisibleRow = this.firstVisibleRow();
1830     var lastVisibleRow = this.lastVisibleRow();
1831     console.assert(firstVisibleRow <= lastVisibleRow);
1832     for (var c in this._cells) {
1833         var cell = this._cells[c];
1834         if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
1835             this.throwAwayCell(cell);
1836     }
1837     for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
1838         var cell = this._cells[i];
1839         if (cell)
1840             cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
1841         else
1842             this.addCellIfNecessary(i);
1843     }
1844     this.setNeedsUpdateCells(false);
1845 };
1846
1847 /**
1848  * @return {!number} Width in pixels.
1849  */
1850 ListView.prototype.width = function() {
1851     return this._width;
1852 };
1853
1854 /**
1855  * @param {!number} width Width in pixels.
1856  */
1857 ListView.prototype.setWidth = function(width) {
1858     if (this._width === width)
1859         return;
1860     this._width = width;
1861     this.scrollView.setWidth(this._width);
1862     for (var c in this._cells) {
1863         this._cells[c].setWidth(this._width);
1864     }
1865     this.element.style.width = this._width + "px";
1866     this.setNeedsUpdateCells(true);
1867 };
1868
1869 /**
1870  * @return {!number} Height in pixels.
1871  */
1872 ListView.prototype.height = function() {
1873     return this.scrollView.height();
1874 };
1875
1876 /**
1877  * @param {!number} height Height in pixels.
1878  */
1879 ListView.prototype.setHeight = function(height) {
1880     this.scrollView.setHeight(height);
1881 };
1882
1883 /**
1884  * @param {?Event} event
1885  */
1886 ListView.prototype.onClick = function(event) {
1887     var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
1888     if (!clickedCellElement)
1889         return;
1890     var clickedCell = clickedCellElement.$view;
1891     if (clickedCell.row !== this.selectedRow)
1892         this.select(clickedCell.row);
1893 };
1894
1895 /**
1896  * @param {!number} row
1897  */
1898 ListView.prototype.select = function(row) {
1899     if (this.selectedRow === row)
1900         return;
1901     this.deselect();
1902     if (row === ListView.NoSelection)
1903         return;
1904     this.selectedRow = row;
1905     var selectedCell = this._cells[this.selectedRow];
1906     if (selectedCell)
1907         selectedCell.setSelected(true);
1908 };
1909
1910 ListView.prototype.deselect = function() {
1911     if (this.selectedRow === ListView.NoSelection)
1912         return;
1913     var selectedCell = this._cells[this.selectedRow];
1914     if (selectedCell)
1915         selectedCell.setSelected(false);
1916     this.selectedRow = ListView.NoSelection;
1917 };
1918
1919 /**
1920  * @param {!number} row
1921  * @param {!boolean} animate
1922  */
1923 ListView.prototype.scrollToRow = function(row, animate) {
1924     this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
1925 };
1926
1927 /**
1928  * @constructor
1929  * @extends View
1930  * @param {!ScrollView} scrollView
1931  */
1932 function ScrubbyScrollBar(scrollView) {
1933     View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));
1934
1935     /**
1936      * @type {!Element}
1937      * @const
1938      */
1939     this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
1940     this.element.appendChild(this.thumb);
1941
1942     /**
1943      * @type {!ScrollView}
1944      * @const
1945      */
1946     this.scrollView = scrollView;
1947
1948     /**
1949      * @type {!number}
1950      * @protected
1951      */
1952     this._height = 0;
1953     /**
1954      * @type {!number}
1955      * @protected
1956      */
1957     this._thumbHeight = 0;
1958     /**
1959      * @type {!number}
1960      * @protected
1961      */
1962     this._thumbPosition = 0;
1963
1964     this.setHeight(0);
1965     this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);
1966
1967     /**
1968      * @type {?Animator}
1969      * @protected
1970      */
1971     this._thumbStyleTopAnimator = null;
1972
1973     /** 
1974      * @type {?number}
1975      * @protected
1976      */
1977     this._timer = null;
1978     
1979     this.element.addEventListener("mousedown", this.onMouseDown, false);
1980     this.element.addEventListener("touchstart", this.onTouchStart, false);
1981 }
1982
1983 ScrubbyScrollBar.prototype = Object.create(View.prototype);
1984
1985 ScrubbyScrollBar.ScrollInterval = 16;
1986 ScrubbyScrollBar.ThumbMargin = 2;
1987 ScrubbyScrollBar.ThumbHeight = 30;
1988 ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
1989 ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";
1990
1991 /**
1992  * @param {?Event} event
1993  */
1994 ScrubbyScrollBar.prototype.onTouchStart = function(event) {
1995     var touch = event.touches[0];
1996     this._setThumbPositionFromEventPosition(touch.clientY);
1997     if (this._thumbStyleTopAnimator)
1998         this._thumbStyleTopAnimator.stop();
1999     this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2000     window.addEventListener("touchmove", this.onWindowTouchMove, false);
2001     window.addEventListener("touchend", this.onWindowTouchEnd, false);
2002     event.stopPropagation();
2003     event.preventDefault();
2004 };
2005
2006 /**
2007  * @param {?Event} event
2008  */
2009 ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
2010     var touch = event.touches[0];
2011     this._setThumbPositionFromEventPosition(touch.clientY);
2012     event.stopPropagation();
2013     event.preventDefault();
2014 };
2015
2016 /**
2017  * @param {?Event} event
2018  */
2019 ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) {
2020     this._thumbStyleTopAnimator = new TransitionAnimator();
2021     this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2022     this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2023     this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2024     this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2025     this._thumbStyleTopAnimator.duration = 100;
2026     this._thumbStyleTopAnimator.start();
2027
2028     window.removeEventListener("touchmove", this.onWindowTouchMove, false);
2029     window.removeEventListener("touchend", this.onWindowTouchEnd, false);
2030     clearInterval(this._timer);
2031 };
2032
2033 /**
2034  * @return {!number} Height of the view in pixels.
2035  */
2036 ScrubbyScrollBar.prototype.height = function() {
2037     return this._height;
2038 };
2039
2040 /**
2041  * @param {!number} height Height of the view in pixels.
2042  */
2043 ScrubbyScrollBar.prototype.setHeight = function(height) {
2044     if (this._height === height)
2045         return;
2046     this._height = height;
2047     this.element.style.height = this._height + "px";
2048     this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2049     this._thumbPosition = 0;
2050 };
2051
2052 /**
2053  * @param {!number} height Height of the scroll bar thumb in pixels.
2054  */
2055 ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
2056     if (this._thumbHeight === height)
2057         return;
2058     this._thumbHeight = height;
2059     this.thumb.style.height = this._thumbHeight + "px";
2060     this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2061     this._thumbPosition = 0;
2062 };
2063
2064 /**
2065  * @param {number} position
2066  */
2067 ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) {
2068     var thumbMin = ScrubbyScrollBar.ThumbMargin;
2069     var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2;
2070     var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop;
2071     var thumbPosition = y - this._thumbHeight / 2;
2072     thumbPosition = Math.max(thumbPosition, thumbMin);
2073     thumbPosition = Math.min(thumbPosition, thumbMax);
2074     this.thumb.style.top = thumbPosition + "px";
2075     this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2;
2076 };
2077
2078 /**
2079  * @param {?Event} event
2080  */
2081 ScrubbyScrollBar.prototype.onMouseDown = function(event) {
2082     this._setThumbPositionFromEventPosition(event.clientY);
2083
2084     window.addEventListener("mousemove", this.onWindowMouseMove, false);
2085     window.addEventListener("mouseup", this.onWindowMouseUp, false);
2086     if (this._thumbStyleTopAnimator)
2087         this._thumbStyleTopAnimator.stop();
2088     this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2089     event.stopPropagation();
2090     event.preventDefault();
2091 };
2092
2093 /**
2094  * @param {?Event} event
2095  */
2096 ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
2097     this._setThumbPositionFromEventPosition(event.clientY);
2098 };
2099
2100 /**
2101  * @param {?Event} event
2102  */
2103 ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) {
2104     this._thumbStyleTopAnimator = new TransitionAnimator();
2105     this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2106     this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2107     this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2108     this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2109     this._thumbStyleTopAnimator.duration = 100;
2110     this._thumbStyleTopAnimator.start();
2111     
2112     window.removeEventListener("mousemove", this.onWindowMouseMove, false);
2113     window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2114     clearInterval(this._timer);
2115 };
2116
2117 /**
2118  * @param {!Animator} animator
2119  */
2120 ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
2121     this.thumb.style.top = animator.currentValue + "px";
2122 };
2123
2124 ScrubbyScrollBar.prototype.onScrollTimer = function() {
2125     var scrollAmount = Math.pow(this._thumbPosition, 2) * 10;
2126     if (this._thumbPosition > 0)
2127         scrollAmount = -scrollAmount;
2128     this.scrollView.scrollBy(scrollAmount, false);
2129 };
2130
2131 /**
2132  * @constructor
2133  * @extends ListCell
2134  * @param {!Array} shortMonthLabels
2135  */
2136 function YearListCell(shortMonthLabels) {
2137     ListCell.call(this);
2138     this.element.classList.add(YearListCell.ClassNameYearListCell);
2139     this.element.style.height = YearListCell.Height + "px";
2140
2141     /**
2142      * @type {!Element}
2143      * @const
2144      */
2145     this.label = createElement("div", YearListCell.ClassNameLabel, "----");
2146     this.element.appendChild(this.label);
2147     this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2148     this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2149
2150     /**
2151      * @type {!Array} Array of the 12 month button elements.
2152      * @const
2153      */
2154     this.monthButtons = [];
2155     var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser);
2156     for (var r = 0; r < YearListCell.ButtonRows; ++r) {
2157         var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow);
2158         buttonsRow.setAttribute("role", "row");
2159         for (var c = 0; c < YearListCell.ButtonColumns; ++c) {
2160             var month = c + r * YearListCell.ButtonColumns;
2161             var button = createElement("div", YearListCell.ClassNameMonthButton, shortMonthLabels[month]);
2162             button.setAttribute("role", "gridcell");
2163             button.dataset.month = month;
2164             buttonsRow.appendChild(button);
2165             this.monthButtons.push(button);
2166         }
2167         monthChooserElement.appendChild(buttonsRow);
2168     }
2169     this.element.appendChild(monthChooserElement);
2170
2171     /**
2172      * @type {!boolean}
2173      * @private
2174      */
2175     this._selected = false;
2176     /**
2177      * @type {!number}
2178      * @private
2179      */
2180     this._height = 0;
2181 }
2182
2183 YearListCell.prototype = Object.create(ListCell.prototype);
2184
2185 YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25;
2186 YearListCell.BorderBottomWidth = 1;
2187 YearListCell.ButtonRows = 3;
2188 YearListCell.ButtonColumns = 4;
2189 YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121;
2190 YearListCell.ClassNameYearListCell = "year-list-cell";
2191 YearListCell.ClassNameLabel = "label";
2192 YearListCell.ClassNameMonthChooser = "month-chooser";
2193 YearListCell.ClassNameMonthButtonsRow = "month-buttons-row";
2194 YearListCell.ClassNameMonthButton = "month-button";
2195 YearListCell.ClassNameHighlighted = "highlighted";
2196
2197 YearListCell._recycleBin = [];
2198
2199 /**
2200  * @return {!Array}
2201  * @override
2202  */
2203 YearListCell.prototype._recycleBin = function() {
2204     return YearListCell._recycleBin;
2205 };
2206
2207 /**
2208  * @param {!number} row
2209  */
2210 YearListCell.prototype.reset = function(row) {
2211     this.row = row;
2212     this.label.textContent = row + 1;
2213     for (var i = 0; i < this.monthButtons.length; ++i) {
2214         this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted);
2215     }
2216     this.show();
2217 };
2218
2219 /**
2220  * @return {!number} The height in pixels.
2221  */
2222 YearListCell.prototype.height = function() {
2223     return this._height;
2224 };
2225
2226 /**
2227  * @param {!number} height Height in pixels.
2228  */
2229 YearListCell.prototype.setHeight = function(height) {
2230     if (this._height === height)
2231         return;
2232     this._height = height;
2233     this.element.style.height = this._height + "px";
2234 };
2235
2236 /**
2237  * @constructor
2238  * @extends ListView
2239  * @param {!Month} minimumMonth
2240  * @param {!Month} maximumMonth
2241  */
2242 function YearListView(minimumMonth, maximumMonth) {
2243     ListView.call(this);
2244     this.element.classList.add("year-list-view");
2245
2246     /**
2247      * @type {?Month}
2248      */
2249     this.highlightedMonth = null;
2250     /**
2251      * @type {!Month}
2252      * @const
2253      * @protected
2254      */
2255     this._minimumMonth = minimumMonth;
2256     /**
2257      * @type {!Month}
2258      * @const
2259      * @protected
2260      */
2261     this._maximumMonth = maximumMonth;
2262
2263     this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
2264     this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
2265     
2266     /**
2267      * @type {!Object}
2268      * @const
2269      * @protected
2270      */
2271     this._runningAnimators = {};
2272     /**
2273      * @type {!Array}
2274      * @const
2275      * @protected
2276      */
2277     this._animatingRows = [];
2278     /**
2279      * @type {!boolean}
2280      * @protected
2281      */
2282     this._ignoreMouseOutUntillNextMouseOver = false;
2283     
2284     /**
2285      * @type {!ScrubbyScrollBar}
2286      * @const
2287      */
2288     this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
2289     this.scrubbyScrollBar.attachTo(this);
2290     
2291     this.element.addEventListener("mouseover", this.onMouseOver, false);
2292     this.element.addEventListener("mouseout", this.onMouseOut, false);
2293     this.element.addEventListener("keydown", this.onKeyDown, false);
2294     this.element.addEventListener("touchstart", this.onTouchStart, false);
2295 }
2296
2297 YearListView.prototype = Object.create(ListView.prototype);
2298
2299 YearListView.Height = YearListCell.SelectedHeight - 1;
2300 YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
2301 YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";
2302
2303 /**
2304  * @param {?Event} event
2305  */
2306 YearListView.prototype.onTouchStart = function(event) {
2307     var touch = event.touches[0];
2308     var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
2309     if (!monthButtonElement)
2310         return;
2311     var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2312     var cell = cellElement.$view;
2313     this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2314 };
2315
2316 /**
2317  * @param {?Event} event
2318  */
2319 YearListView.prototype.onMouseOver = function(event) {
2320     var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2321     if (!monthButtonElement)
2322         return;
2323     var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2324     var cell = cellElement.$view;
2325     this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2326     this._ignoreMouseOutUntillNextMouseOver = false;
2327 };
2328
2329 /**
2330  * @param {?Event} event
2331  */
2332 YearListView.prototype.onMouseOut = function(event) {
2333     if (this._ignoreMouseOutUntillNextMouseOver)
2334         return;
2335     var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2336     if (!monthButtonElement) {
2337         this.dehighlightMonth();
2338     }
2339 };
2340
2341 /**
2342  * @param {!number} width Width in pixels.
2343  * @override
2344  */
2345 YearListView.prototype.setWidth = function(width) {
2346     ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
2347     this.element.style.width = width + "px";
2348 };
2349
2350 /**
2351  * @param {!number} height Height in pixels.
2352  * @override
2353  */
2354 YearListView.prototype.setHeight = function(height) {
2355     ListView.prototype.setHeight.call(this, height);
2356     this.scrubbyScrollBar.setHeight(height);
2357 };
2358
2359 /**
2360  * @enum {number}
2361  */
2362 YearListView.RowAnimationDirection = {
2363     Opening: 0,
2364     Closing: 1
2365 };
2366
2367 /**
2368  * @param {!number} row
2369  * @param {!YearListView.RowAnimationDirection} direction
2370  */
2371 YearListView.prototype._animateRow = function(row, direction) {
2372     var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
2373     var oldAnimator = this._runningAnimators[row];
2374     if (oldAnimator) {
2375         oldAnimator.stop();
2376         fromValue = oldAnimator.currentValue;
2377     }
2378     var cell = this.cellAtRow(row);
2379     var animator = new TransitionAnimator();
2380     animator.step = this.onCellHeightAnimatorStep;
2381     animator.setFrom(fromValue);
2382     animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height);
2383     animator.timingFunction = AnimationTimingFunction.EaseInOut;
2384     animator.duration = 300;
2385     animator.row = row;
2386     animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
2387     this._runningAnimators[row] = animator;
2388     this._animatingRows.push(row);
2389     this._animatingRows.sort();
2390     animator.start();
2391 };
2392
2393 /**
2394  * @param {?Animator} animator
2395  */
2396 YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) {
2397     delete this._runningAnimators[animator.row];
2398     var index = this._animatingRows.indexOf(animator.row);
2399     this._animatingRows.splice(index, 1);
2400 };
2401
2402 /**
2403  * @param {!Animator} animator
2404  */
2405 YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
2406     var cell = this.cellAtRow(animator.row);
2407     if (cell)
2408         cell.setHeight(animator.currentValue);
2409     this.updateCells();
2410 };
2411
2412 /**
2413  * @param {?Event} event
2414  */
2415 YearListView.prototype.onClick = function(event) {
2416     var oldSelectedRow = this.selectedRow;
2417     ListView.prototype.onClick.call(this, event);
2418     var year = this.selectedRow + 1;
2419     if (this.selectedRow !== oldSelectedRow) {
2420         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2421         this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2422         this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2423     } else {
2424         var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2425         if (!monthButton || monthButton.getAttribute("aria-disabled") == "true")
2426             return;
2427         var month = parseInt(monthButton.dataset.month, 10);
2428         this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2429         this.hide();
2430     }
2431 };
2432
2433 /**
2434  * @param {!number} scrollOffset
2435  * @return {!number}
2436  * @override
2437  */
2438 YearListView.prototype.rowAtScrollOffset = function(scrollOffset) {
2439     var remainingOffset = scrollOffset;
2440     var lastAnimatingRow = 0;
2441     var rowsWithIrregularHeight = this._animatingRows.slice();
2442     if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) {
2443         rowsWithIrregularHeight.push(this.selectedRow);
2444         rowsWithIrregularHeight.sort();
2445     }
2446     for (var i = 0; i < rowsWithIrregularHeight.length; ++i) {
2447         var row = rowsWithIrregularHeight[i];
2448         var animator = this._runningAnimators[row];
2449         var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight;
2450         if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) {
2451             return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2452         }
2453         remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
2454         if (remainingOffset <= (rowHeight - YearListCell.Height))
2455             return row;
2456         remainingOffset -= rowHeight - YearListCell.Height;
2457         lastAnimatingRow = row;
2458     }
2459     return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2460 };
2461
2462 /**
2463  * @param {!number} row
2464  * @return {!number}
2465  * @override
2466  */
2467 YearListView.prototype.scrollOffsetForRow = function(row) {
2468     var scrollOffset = row * YearListCell.Height;
2469     for (var i = 0; i < this._animatingRows.length; ++i) {
2470         var animatingRow = this._animatingRows[i];
2471         if (animatingRow >= row)
2472             break;
2473         var animator = this._runningAnimators[animatingRow];
2474         scrollOffset += animator.currentValue - YearListCell.Height;
2475     }
2476     if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
2477         scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
2478     }
2479     return scrollOffset;
2480 };
2481
2482 /**
2483  * @param {!number} row
2484  * @return {!YearListCell}
2485  * @override
2486  */
2487 YearListView.prototype.prepareNewCell = function(row) {
2488     var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
2489     cell.reset(row);
2490     cell.setSelected(this.selectedRow === row);
2491     for (var i = 0; i < cell.monthButtons.length; ++i) {
2492         var month = new Month(row + 1, i);
2493         cell.monthButtons[i].id = month.toString();
2494         cell.monthButtons[i].setAttribute("aria-disabled", this._minimumMonth > month || this._maximumMonth < month ? "true" : "false");
2495         cell.monthButtons[i].setAttribute("aria-label", month.toLocaleString());
2496     }
2497     if (this.highlightedMonth && row === this.highlightedMonth.year - 1) {
2498         var monthButton = cell.monthButtons[this.highlightedMonth.month];
2499         monthButton.classList.add(YearListCell.ClassNameHighlighted);
2500         // aira-activedescendant assumes both elements have renderers, and
2501         // |monthButton| might have no renderer yet.
2502         var element = this.element;
2503         setTimeout(function() {
2504             element.setAttribute("aria-activedescendant", monthButton.id);
2505         }, 0);
2506     }
2507     var animator = this._runningAnimators[row];
2508     if (animator)
2509         cell.setHeight(animator.currentValue);
2510     else if (row === this.selectedRow)
2511         cell.setHeight(YearListCell.SelectedHeight);
2512     else
2513         cell.setHeight(YearListCell.Height);
2514     return cell;
2515 };
2516
2517 /**
2518  * @override
2519  */
2520 YearListView.prototype.updateCells = function() {
2521     var firstVisibleRow = this.firstVisibleRow();
2522     var lastVisibleRow = this.lastVisibleRow();
2523     console.assert(firstVisibleRow <= lastVisibleRow);
2524     for (var c in this._cells) {
2525         var cell = this._cells[c];
2526         if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
2527             this.throwAwayCell(cell);
2528     }
2529     for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
2530         var cell = this._cells[i];
2531         if (cell)
2532             cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
2533         else
2534             this.addCellIfNecessary(i);
2535     }
2536     this.setNeedsUpdateCells(false);
2537 };
2538
2539 /**
2540  * @override
2541  */
2542 YearListView.prototype.deselect = function() {
2543     if (this.selectedRow === ListView.NoSelection)
2544         return;
2545     var selectedCell = this._cells[this.selectedRow];
2546     if (selectedCell)
2547         selectedCell.setSelected(false);
2548     this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
2549     this.selectedRow = ListView.NoSelection;
2550     this.setNeedsUpdateCells(true);
2551 };
2552
2553 YearListView.prototype.deselectWithoutAnimating = function() {
2554     if (this.selectedRow === ListView.NoSelection)
2555         return;
2556     var selectedCell = this._cells[this.selectedRow];
2557     if (selectedCell) {
2558         selectedCell.setSelected(false);
2559         selectedCell.setHeight(YearListCell.Height);
2560     }
2561     this.selectedRow = ListView.NoSelection;
2562     this.setNeedsUpdateCells(true);
2563 };
2564
2565 /**
2566  * @param {!number} row
2567  * @override
2568  */
2569 YearListView.prototype.select = function(row) {
2570     if (this.selectedRow === row)
2571         return;
2572     this.deselect();
2573     if (row === ListView.NoSelection)
2574         return;
2575     this.selectedRow = row;
2576     if (this.selectedRow !== ListView.NoSelection) {
2577         var selectedCell = this._cells[this.selectedRow];
2578         this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening);
2579         if (selectedCell)
2580             selectedCell.setSelected(true);
2581         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2582         this.highlightMonth(new Month(this.selectedRow + 1, month));
2583     }
2584     this.setNeedsUpdateCells(true);
2585 };
2586
2587 /**
2588  * @param {!number} row
2589  */
2590 YearListView.prototype.selectWithoutAnimating = function(row) {
2591     if (this.selectedRow === row)
2592         return;
2593     this.deselectWithoutAnimating();
2594     if (row === ListView.NoSelection)
2595         return;
2596     this.selectedRow = row;
2597     if (this.selectedRow !== ListView.NoSelection) {
2598         var selectedCell = this._cells[this.selectedRow];
2599         if (selectedCell) {
2600             selectedCell.setSelected(true);
2601             selectedCell.setHeight(YearListCell.SelectedHeight);
2602         }
2603         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2604         this.highlightMonth(new Month(this.selectedRow + 1, month));
2605     }
2606     this.setNeedsUpdateCells(true);
2607 };
2608
2609 /**
2610  * @param {!Month} month
2611  * @return {?HTMLDivElement}
2612  */
2613 YearListView.prototype.buttonForMonth = function(month) {
2614     if (!month)
2615         return null;
2616     var row = month.year - 1;
2617     var cell = this.cellAtRow(row);
2618     if (!cell)
2619         return null;
2620     return cell.monthButtons[month.month];
2621 };
2622
2623 YearListView.prototype.dehighlightMonth = function() {
2624     if (!this.highlightedMonth)
2625         return;
2626     var monthButton = this.buttonForMonth(this.highlightedMonth);
2627     if (monthButton) {
2628         monthButton.classList.remove(YearListCell.ClassNameHighlighted);
2629     }
2630     this.highlightedMonth = null;
2631     this.element.removeAttribute("aria-activedescendant");
2632 };
2633
2634 /**
2635  * @param {!Month} month
2636  */
2637 YearListView.prototype.highlightMonth = function(month) {
2638     if (this.highlightedMonth && this.highlightedMonth.equals(month))
2639         return;
2640     this.dehighlightMonth();
2641     this.highlightedMonth = month;
2642     if (!this.highlightedMonth)
2643         return;
2644     var monthButton = this.buttonForMonth(this.highlightedMonth);
2645     if (monthButton) {
2646         monthButton.classList.add(YearListCell.ClassNameHighlighted);
2647         this.element.setAttribute("aria-activedescendant", monthButton.id);
2648     }
2649 };
2650
2651 /**
2652  * @param {!Month} month
2653  */
2654 YearListView.prototype.show = function(month) {
2655     this._ignoreMouseOutUntillNextMouseOver = true;
2656     
2657     this.scrollToRow(month.year - 1, false);
2658     this.selectWithoutAnimating(month.year - 1);
2659     this.highlightMonth(month);
2660 };
2661
2662 YearListView.prototype.hide = function() {
2663     this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
2664 };
2665
2666 /**
2667  * @param {!Month} month
2668  */
2669 YearListView.prototype._moveHighlightTo = function(month) {
2670     this.highlightMonth(month);
2671     this.select(this.highlightedMonth.year - 1);
2672
2673     this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
2674     this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2675     return true;
2676 };
2677
2678 /**
2679  * @param {?Event} event
2680  */
2681 YearListView.prototype.onKeyDown = function(event) {
2682     var key = event.keyIdentifier;
2683     var eventHandled = false;
2684     if (key == "U+0054") // 't' key.
2685         eventHandled = this._moveHighlightTo(Month.createFromToday());
2686     else if (this.highlightedMonth) {
2687         if (global.params.isLocaleRTL ? key == "Right" : key == "Left")
2688             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous());
2689         else if (key == "Up")
2690             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns));
2691         else if (global.params.isLocaleRTL ? key == "Left" : key == "Right")
2692             eventHandled = this._moveHighlightTo(this.highlightedMonth.next());
2693         else if (key == "Down")
2694             eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns));
2695         else if (key == "PageUp")
2696             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear));
2697         else if (key == "PageDown")
2698             eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear));
2699         else if (key == "Enter") {
2700             this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth);
2701             this.hide();
2702             eventHandled = true;
2703         }
2704     } else if (key == "Up") {
2705         this.scrollView.scrollBy(-YearListCell.Height, true);
2706         eventHandled = true;
2707     } else if (key == "Down") {
2708         this.scrollView.scrollBy(YearListCell.Height, true);
2709         eventHandled = true;
2710     } else if (key == "PageUp") {
2711         this.scrollView.scrollBy(-this.scrollView.height(), true);
2712         eventHandled = true;
2713     } else if (key == "PageDown") {
2714         this.scrollView.scrollBy(this.scrollView.height(), true);
2715         eventHandled = true;
2716     }
2717
2718     if (eventHandled) {
2719         event.stopPropagation();
2720         event.preventDefault();
2721     }
2722 };
2723
2724 /**
2725  * @constructor
2726  * @extends View
2727  * @param {!Month} minimumMonth
2728  * @param {!Month} maximumMonth
2729  */
2730 function MonthPopupView(minimumMonth, maximumMonth) {
2731     View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));
2732
2733     /**
2734      * @type {!YearListView}
2735      * @const
2736      */
2737     this.yearListView = new YearListView(minimumMonth, maximumMonth);
2738     this.yearListView.attachTo(this);
2739
2740     /**
2741      * @type {!boolean}
2742      */
2743     this.isVisible = false;
2744
2745     this.element.addEventListener("click", this.onClick, false);
2746 }
2747
2748 MonthPopupView.prototype = Object.create(View.prototype);
2749
2750 MonthPopupView.ClassNameMonthPopupView = "month-popup-view";
2751
2752 MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) {
2753     this.isVisible = true;
2754     document.body.appendChild(this.element);
2755     this.yearListView.setWidth(calendarTableRect.width - 2);
2756     this.yearListView.setHeight(YearListView.Height);
2757     if (global.params.isLocaleRTL)
2758         this.yearListView.element.style.right = calendarTableRect.x + "px";
2759     else
2760         this.yearListView.element.style.left = calendarTableRect.x + "px";
2761     this.yearListView.element.style.top = calendarTableRect.y + "px";
2762     this.yearListView.show(initialMonth);
2763     this.yearListView.element.focus();
2764 };
2765
2766 MonthPopupView.prototype.hide = function() {
2767     if (!this.isVisible)
2768         return;
2769     this.isVisible = false;
2770     this.element.parentNode.removeChild(this.element);
2771     this.yearListView.hide();
2772 };
2773
2774 /**
2775  * @param {?Event} event
2776  */
2777 MonthPopupView.prototype.onClick = function(event) {
2778     if (event.target !== this.element)
2779         return;
2780     this.hide();
2781 };
2782
2783 /**
2784  * @constructor
2785  * @extends View
2786  * @param {!number} maxWidth Maximum width in pixels.
2787  */
2788 function MonthPopupButton(maxWidth) {
2789     View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));
2790     this.element.setAttribute("aria-label", global.params.axShowMonthSelector);
2791
2792     /**
2793      * @type {!Element}
2794      * @const
2795      */
2796     this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
2797     this.element.appendChild(this.labelElement);
2798
2799     /**
2800      * @type {!Element}
2801      * @const
2802      */
2803     this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle);
2804     this.disclosureTriangleIcon.innerHTML = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>";
2805     this.element.appendChild(this.disclosureTriangleIcon);
2806
2807     /**
2808      * @type {!boolean}
2809      * @protected
2810      */
2811     this._useShortMonth = this._shouldUseShortMonth(maxWidth);
2812     this.element.style.maxWidth = maxWidth + "px";
2813
2814     this.element.addEventListener("click", this.onClick, false);
2815 }
2816
2817 MonthPopupButton.prototype = Object.create(View.prototype);
2818
2819 MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
2820 MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
2821 MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
2822 MonthPopupButton.EventTypeButtonClick = "buttonClick";
2823
2824 /**
2825  * @param {!number} maxWidth Maximum available width in pixels.
2826  * @return {!boolean}
2827  */
2828 MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) {
2829     document.body.appendChild(this.element);
2830     var month = Month.Maximum;
2831     for (var i = 0; i < MonthsPerYear; ++i) {
2832         this.labelElement.textContent = month.toLocaleString();
2833         if (this.element.offsetWidth > maxWidth)
2834             return true;
2835         month = month.previous();
2836     }
2837     document.body.removeChild(this.element);
2838     return false;
2839 };
2840
2841 /**
2842  * @param {!Month} month
2843  */
2844 MonthPopupButton.prototype.setCurrentMonth = function(month) {
2845     this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
2846 };
2847
2848 /**
2849  * @param {?Event} event
2850  */
2851 MonthPopupButton.prototype.onClick = function(event) {
2852     this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
2853 };
2854
2855 /**
2856  * @constructor
2857  * @extends View
2858  */
2859 function CalendarNavigationButton() {
2860     View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
2861     /**
2862      * @type {number} Threshold for starting repeating clicks in milliseconds.
2863      */
2864     this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
2865     /**
2866      * @type {number} Interval between reapeating clicks in milliseconds.
2867      */
2868     this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
2869     /**
2870      * @type {?number} The ID for the timeout that triggers the repeating clicks.
2871      */
2872     this._timer = null;
2873     this.element.addEventListener("click", this.onClick, false);
2874     this.element.addEventListener("mousedown", this.onMouseDown, false);
2875     this.element.addEventListener("touchstart", this.onTouchStart, false);
2876 };
2877
2878 CalendarNavigationButton.prototype = Object.create(View.prototype);
2879
2880 CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600;
2881 CalendarNavigationButton.DefaultRepeatingClicksInterval = 300;
2882 CalendarNavigationButton.LeftMargin = 4;
2883 CalendarNavigationButton.Width = 24;
2884 CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button";
2885 CalendarNavigationButton.EventTypeButtonClick = "buttonClick";
2886 CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick";
2887
2888 /**
2889  * @param {!boolean} disabled
2890  */
2891 CalendarNavigationButton.prototype.setDisabled = function(disabled) {
2892     this.element.disabled = disabled;
2893 };
2894
2895 /**
2896  * @param {?Event} event
2897  */
2898 CalendarNavigationButton.prototype.onClick = function(event) {
2899     this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
2900 };
2901
2902 /**
2903  * @param {?Event} event
2904  */
2905 CalendarNavigationButton.prototype.onTouchStart = function(event) {
2906     if (this._timer !== null)
2907         return;
2908     this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2909     window.addEventListener("touchend", this.onWindowTouchEnd, false);
2910 };
2911
2912 /**
2913  * @param {?Event} event
2914  */
2915 CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
2916     if (this._timer === null)
2917         return;
2918     clearTimeout(this._timer);
2919     this._timer = null;
2920     window.removeEventListener("touchend", this.onWindowMouseUp, false);
2921 };
2922
2923 /**
2924  * @param {?Event} event
2925  */
2926 CalendarNavigationButton.prototype.onMouseDown = function(event) {
2927     if (this._timer !== null)
2928         return;
2929     this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2930     window.addEventListener("mouseup", this.onWindowMouseUp, false);
2931 };
2932
2933 /**
2934  * @param {?Event} event
2935  */
2936 CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
2937     if (this._timer === null)
2938         return;
2939     clearTimeout(this._timer);
2940     this._timer = null;
2941     window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2942 };
2943
2944 /**
2945  * @param {?Event} event
2946  */
2947 CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
2948     this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
2949     this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
2950 };
2951
2952 /**
2953  * @constructor
2954  * @extends View
2955  * @param {!CalendarPicker} calendarPicker
2956  */
2957 function CalendarHeaderView(calendarPicker) {
2958     View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
2959     this.calendarPicker = calendarPicker;
2960     this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
2961     
2962     var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
2963     this.element.appendChild(titleElement);
2964
2965     /**
2966      * @type {!MonthPopupButton}
2967      */
2968     this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
2969     this.monthPopupButton.attachTo(titleElement);
2970
2971     /**
2972      * @type {!CalendarNavigationButton}
2973      * @const
2974      */
2975     this._previousMonthButton = new CalendarNavigationButton();
2976     this._previousMonthButton.attachTo(this);
2977     this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2978     this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2979     this._previousMonthButton.element.setAttribute("aria-label", global.params.axShowPreviousMonth);
2980
2981     /**
2982      * @type {!CalendarNavigationButton}
2983      * @const
2984      */
2985     this._todayButton = new CalendarNavigationButton();
2986     this._todayButton.attachTo(this);
2987     this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2988     this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton);
2989     var monthContainingToday = Month.createFromToday();
2990     this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2991     this._todayButton.element.setAttribute("aria-label", global.params.todayLabel);
2992
2993     /**
2994      * @type {!CalendarNavigationButton}
2995      * @const
2996      */
2997     this._nextMonthButton = new CalendarNavigationButton();
2998     this._nextMonthButton.attachTo(this);
2999     this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
3000     this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
3001     this._nextMonthButton.element.setAttribute("aria-label", global.params.axShowNextMonth);
3002
3003     if (global.params.isLocaleRTL) {
3004         this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
3005         this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
3006     } else {
3007         this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
3008         this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
3009     }
3010 }
3011
3012 CalendarHeaderView.prototype = Object.create(View.prototype);
3013
3014 CalendarHeaderView.Height = 24;
3015 CalendarHeaderView.BottomMargin = 10;
3016 CalendarHeaderView._ForwardTriangle = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>";
3017 CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>";
3018 CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view";
3019 CalendarHeaderView.ClassNameCalendarTitle = "calendar-title";
3020 CalendarHeaderView.ClassNameTodayButton = "today-button";
3021
3022 CalendarHeaderView.prototype.onCurrentMonthChanged = function() {
3023     this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth());
3024     this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
3025     this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
3026 };
3027
3028 CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) {
3029     if (sender === this._previousMonthButton)
3030         this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation);
3031     else if (sender === this._nextMonthButton)
3032         this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation);
3033     else
3034         this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
3035 };
3036
3037 /**
3038  * @param {!boolean} disabled
3039  */
3040 CalendarHeaderView.prototype.setDisabled = function(disabled) {
3041     this.disabled = disabled;
3042     this.monthPopupButton.element.disabled = this.disabled;
3043     this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
3044     this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
3045     var monthContainingToday = Month.createFromToday();
3046     this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
3047 };
3048
3049 /**
3050  * @constructor
3051  * @extends ListCell
3052  */
3053 function DayCell() {
3054     ListCell.call(this);
3055     this.element.classList.add(DayCell.ClassNameDayCell);
3056     this.element.style.width = DayCell.Width + "px";
3057     this.element.style.height = DayCell.Height + "px";
3058     this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px";
3059     this.element.setAttribute("role", "gridcell");
3060     /**
3061      * @type {?Day}
3062      */
3063     this.day = null;
3064 };
3065
3066 DayCell.prototype = Object.create(ListCell.prototype);
3067
3068 DayCell.Width = 34;
3069 DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20;
3070 DayCell.PaddingSize = 1;
3071 DayCell.ClassNameDayCell = "day-cell";
3072 DayCell.ClassNameHighlighted = "highlighted";
3073 DayCell.ClassNameDisabled = "disabled";
3074 DayCell.ClassNameCurrentMonth = "current-month";
3075 DayCell.ClassNameToday = "today";
3076
3077 DayCell._recycleBin = [];
3078
3079 DayCell.recycleOrCreate = function() {
3080     return DayCell._recycleBin.pop() || new DayCell();
3081 };
3082
3083 /**
3084  * @return {!Array}
3085  * @override
3086  */
3087 DayCell.prototype._recycleBin = function() {
3088     return DayCell._recycleBin;
3089 };
3090
3091 /**
3092  * @override
3093  */
3094 DayCell.prototype.throwAway = function() {
3095     ListCell.prototype.throwAway.call(this);
3096     this.day = null;
3097 };
3098
3099 /**
3100  * @param {!boolean} highlighted
3101  */
3102 DayCell.prototype.setHighlighted = function(highlighted) {
3103     if (highlighted) {
3104         this.element.classList.add(DayCell.ClassNameHighlighted);
3105         this.element.setAttribute("aria-selected", "true");
3106     } else {
3107         this.element.classList.remove(DayCell.ClassNameHighlighted);
3108         this.element.setAttribute("aria-selected", "false");
3109     }
3110 };
3111
3112 /**
3113  * @param {!boolean} disabled
3114  */
3115 DayCell.prototype.setDisabled = function(disabled) {
3116     if (disabled)
3117         this.element.classList.add(DayCell.ClassNameDisabled);
3118     else
3119         this.element.classList.remove(DayCell.ClassNameDisabled);
3120 };
3121
3122 /**
3123  * @param {!boolean} selected
3124  */
3125 DayCell.prototype.setIsInCurrentMonth = function(selected) {
3126     if (selected)
3127         this.element.classList.add(DayCell.ClassNameCurrentMonth);
3128     else
3129         this.element.classList.remove(DayCell.ClassNameCurrentMonth);
3130 };
3131
3132 /**
3133  * @param {!boolean} selected
3134  */
3135 DayCell.prototype.setIsToday = function(selected) {
3136     if (selected)
3137         this.element.classList.add(DayCell.ClassNameToday);
3138     else
3139         this.element.classList.remove(DayCell.ClassNameToday);
3140 };
3141
3142 /**
3143  * @param {!Day} day
3144  */
3145 DayCell.prototype.reset = function(day) {
3146     this.day = day;
3147     this.element.textContent = localizeNumber(this.day.date.toString());
3148     this.element.setAttribute("aria-label", this.day.format());
3149     this.element.id = this.day.toString();
3150     this.show();
3151 };
3152
3153 /**
3154  * @constructor
3155  * @extends ListCell
3156  */
3157 function WeekNumberCell() {
3158     ListCell.call(this);
3159     this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell);
3160     this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px";
3161     this.element.style.height = WeekNumberCell.Height + "px";
3162     this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px";
3163     /**
3164      * @type {?Week}
3165      */
3166     this.week = null;
3167 };
3168
3169 WeekNumberCell.prototype = Object.create(ListCell.prototype);
3170
3171 WeekNumberCell.Width = 48;
3172 WeekNumberCell.Height = DayCell.Height;
3173 WeekNumberCell.SeparatorWidth = 1;
3174 WeekNumberCell.PaddingSize = 1;
3175 WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell";
3176 WeekNumberCell.ClassNameHighlighted = "highlighted";
3177 WeekNumberCell.ClassNameDisabled = "disabled";
3178
3179 WeekNumberCell._recycleBin = [];
3180
3181 /**
3182  * @return {!Array}
3183  * @override
3184  */
3185 WeekNumberCell.prototype._recycleBin = function() {
3186     return WeekNumberCell._recycleBin;
3187 };
3188
3189 /**
3190  * @return {!WeekNumberCell}
3191  */
3192 WeekNumberCell.recycleOrCreate = function() {
3193     return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
3194 };
3195
3196 /**
3197  * @param {!Week} week
3198  */
3199 WeekNumberCell.prototype.reset = function(week) {
3200     this.week = week;
3201     this.element.id = week.toString();
3202     this.element.setAttribute("role", "gridcell");
3203     this.element.setAttribute("aria-label", window.pagePopupController.formatWeek(week.year, week.week, week.firstDay().format()));
3204     this.element.textContent = localizeNumber(this.week.week.toString());
3205     this.show();
3206 };
3207
3208 /**
3209  * @override
3210  */
3211 WeekNumberCell.prototype.throwAway = function() {
3212     ListCell.prototype.throwAway.call(this);
3213     this.week = null;
3214 };
3215
3216 WeekNumberCell.prototype.setHighlighted = function(highlighted) {
3217     if (highlighted) {
3218         this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
3219         this.element.setAttribute("aria-selected", "true");
3220     } else {
3221         this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
3222         this.element.setAttribute("aria-selected", "false");
3223     }
3224 };
3225
3226 WeekNumberCell.prototype.setDisabled = function(disabled) {
3227     if (disabled)
3228         this.element.classList.add(WeekNumberCell.ClassNameDisabled);
3229     else
3230         this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
3231 };
3232
3233 /**
3234  * @constructor
3235  * @extends View
3236  * @param {!boolean} hasWeekNumberColumn
3237  */
3238 function CalendarTableHeaderView(hasWeekNumberColumn) {
3239     View.call(this, createElement("div", "calendar-table-header-view"));
3240     if (hasWeekNumberColumn) {
3241         var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel);
3242         weekNumberLabelElement.style.width = WeekNumberCell.Width + "px";
3243         this.element.appendChild(weekNumberLabelElement);
3244     }
3245     for (var i = 0; i < DaysPerWeek; ++i) {
3246         var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek;
3247         var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]);
3248         labelElement.style.width = DayCell.Width + "px";
3249         this.element.appendChild(labelElement);
3250         if (getLanguage() === "ja") {
3251             if (weekDayNumber === 0)
3252                 labelElement.style.color = "red";
3253             else if (weekDayNumber === 6)
3254                 labelElement.style.color = "blue";
3255         }
3256     }
3257 }
3258
3259 CalendarTableHeaderView.prototype = Object.create(View.prototype);
3260
3261 CalendarTableHeaderView.Height = 25;
3262
3263 /**
3264  * @constructor
3265  * @extends ListCell
3266  */
3267 function CalendarRowCell() {
3268     ListCell.call(this);
3269     this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
3270     this.element.style.height = CalendarRowCell.Height + "px";
3271     this.element.setAttribute("role", "row");
3272
3273     /**
3274      * @type {!Array}
3275      * @protected
3276      */
3277     this._dayCells = [];
3278     /**
3279      * @type {!number}
3280      */
3281     this.row = 0;
3282     /**
3283      * @type {?CalendarTableView}
3284      */
3285     this.calendarTableView = null;
3286 }
3287
3288 CalendarRowCell.prototype = Object.create(ListCell.prototype);
3289
3290 CalendarRowCell.Height = DayCell.Height;
3291 CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";
3292
3293 CalendarRowCell._recycleBin = [];
3294
3295 /**
3296  * @return {!Array}
3297  * @override
3298  */
3299 CalendarRowCell.prototype._recycleBin = function() {
3300     return CalendarRowCell._recycleBin;
3301 };
3302
3303 /**
3304  * @param {!number} row
3305  * @param {!CalendarTableView} calendarTableView
3306  */
3307 CalendarRowCell.prototype.reset = function(row, calendarTableView) {
3308     this.row = row;
3309     this.calendarTableView = calendarTableView;
3310     if (this.calendarTableView.hasWeekNumberColumn) {
3311         var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row);
3312         var week = Week.createFromDay(middleDay);
3313         this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week);
3314         this.weekNumberCell.attachTo(this);
3315     }
3316     var day = calendarTableView.dayAtColumnAndRow(0, row);
3317     for (var i = 0; i < DaysPerWeek; ++i) {
3318         var dayCell = this.calendarTableView.prepareNewDayCell(day);
3319         dayCell.attachTo(this);
3320         this._dayCells.push(dayCell);
3321         day = day.next();
3322     }
3323     this.show();
3324 };
3325
3326 /**
3327  * @override
3328  */
3329 CalendarRowCell.prototype.throwAway = function() {
3330     ListCell.prototype.throwAway.call(this);
3331     if (this.weekNumberCell)
3332         this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell);
3333     this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView);
3334     this._dayCells.length = 0;
3335 };
3336
3337 /**
3338  * @constructor
3339  * @extends ListView
3340  * @param {!CalendarPicker} calendarPicker
3341  */
3342 function CalendarTableView(calendarPicker) {
3343     ListView.call(this);
3344     this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
3345     this.element.tabIndex = 0;
3346
3347     /**
3348      * @type {!boolean}
3349      * @const
3350      */
3351     this.hasWeekNumberColumn = calendarPicker.type === "week";
3352     /**
3353      * @type {!CalendarPicker}
3354      * @const
3355      */
3356     this.calendarPicker = calendarPicker;
3357     /**
3358      * @type {!Object}
3359      * @const
3360      */
3361     this._dayCells = {};
3362     var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
3363     headerView.attachTo(this, this.scrollView);
3364
3365     if (this.hasWeekNumberColumn) {
3366         this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
3367         /**
3368          * @type {?Array}
3369          * @const
3370          */
3371         this._weekNumberCells = [];
3372     } else {
3373         this.setWidth(DayCell.Width * DaysPerWeek);
3374     }
3375     
3376     /**
3377      * @type {!boolean}
3378      * @protected
3379      */
3380     this._ignoreMouseOutUntillNextMouseOver = false;
3381
3382     this.element.addEventListener("click", this.onClick, false);
3383     this.element.addEventListener("mouseover", this.onMouseOver, false);
3384     this.element.addEventListener("mouseout", this.onMouseOut, false);
3385
3386     // You shouldn't be able to use the mouse wheel to scroll.
3387     this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false);
3388     // You shouldn't be able to do gesture scroll.
3389     this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false);
3390 }
3391
3392 CalendarTableView.prototype = Object.create(ListView.prototype);
3393
3394 CalendarTableView.BorderWidth = 1;
3395 CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";
3396
3397 /**
3398  * @param {!number} scrollOffset
3399  * @return {!number}
3400  */
3401 CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
3402     return Math.floor(scrollOffset / CalendarRowCell.Height);
3403 };
3404
3405 /**
3406  * @param {!number} row
3407  * @return {!number}
3408  */
3409 CalendarTableView.prototype.scrollOffsetForRow = function(row) {
3410     return row * CalendarRowCell.Height;
3411 };
3412
3413 /**
3414  * @param {?Event} event
3415  */
3416 CalendarTableView.prototype.onClick = function(event) {
3417     if (this.hasWeekNumberColumn) {
3418         var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3419         if (weekNumberCellElement) {
3420             var weekNumberCell = weekNumberCellElement.$view;
3421             this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay());
3422             return;
3423         }
3424     }
3425     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3426     if (!dayCellElement)
3427         return;
3428     var dayCell = dayCellElement.$view;
3429     this.calendarPicker.selectRangeContainingDay(dayCell.day);
3430 };
3431
3432 /**
3433  * @param {?Event} event
3434  */
3435 CalendarTableView.prototype.onMouseOver = function(event) {
3436     if (this.hasWeekNumberColumn) {
3437         var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3438         if (weekNumberCellElement) {
3439             var weekNumberCell = weekNumberCellElement.$view;
3440             this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay());
3441             this._ignoreMouseOutUntillNextMouseOver = false;
3442             return;
3443         }
3444     }
3445     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3446     if (!dayCellElement)
3447         return;
3448     var dayCell = dayCellElement.$view;
3449     this.calendarPicker.highlightRangeContainingDay(dayCell.day);
3450     this._ignoreMouseOutUntillNextMouseOver = false;
3451 };
3452
3453 /**
3454  * @param {?Event} event
3455  */
3456 CalendarTableView.prototype.onMouseOut = function(event) {
3457     if (this._ignoreMouseOutUntillNextMouseOver)
3458         return;
3459     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3460     if (!dayCellElement) {
3461         this.calendarPicker.highlightRangeContainingDay(null);
3462     }
3463 };
3464
3465 /**
3466  * @param {!number} row
3467  * @return {!CalendarRowCell}
3468  */
3469 CalendarTableView.prototype.prepareNewCell = function(row) {
3470     var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
3471     cell.reset(row, this);
3472     return cell;
3473 };
3474
3475 /**
3476  * @return {!number} Height in pixels.
3477  */
3478 CalendarTableView.prototype.height = function() {
3479     return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
3480 };
3481
3482 /**
3483  * @param {!number} height Height in pixels.
3484  */
3485 CalendarTableView.prototype.setHeight = function(height) {
3486     this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
3487 };
3488
3489 /**
3490  * @param {!Month} month
3491  * @param {!boolean} animate
3492  */
3493 CalendarTableView.prototype.scrollToMonth = function(month, animate) {
3494     var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
3495     this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
3496 };
3497
3498 /**
3499  * @param {!number} column
3500  * @param {!number} row
3501  * @return {!Day}
3502  */
3503 CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) {
3504     var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay;
3505     return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue);
3506 };
3507
3508 CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
3509 CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();
3510
3511 /**
3512  * @param {!Day} day
3513  * @return {!Object} Object with properties column and row.
3514  */
3515 CalendarTableView.prototype.columnAndRowForDay = function(day) {
3516     var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay;
3517     var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay;
3518     var row = Math.floor(offset / DaysPerWeek);
3519     var column = offset - row * DaysPerWeek;
3520     return {
3521         column: column,
3522         row: row
3523     };
3524 };
3525
3526 CalendarTableView.prototype.updateCells = function() {
3527     ListView.prototype.updateCells.call(this);
3528
3529     var selection = this.calendarPicker.selection();
3530     var firstDayInSelection;
3531     var lastDayInSelection;
3532     if (selection) {
3533         firstDayInSelection = selection.firstDay().valueOf();
3534         lastDayInSelection = selection.lastDay().valueOf();
3535     } else {
3536         firstDayInSelection = Infinity;
3537         lastDayInSelection = Infinity;
3538     }
3539     var highlight = this.calendarPicker.highlight();
3540     var firstDayInHighlight;
3541     var lastDayInHighlight;
3542     if (highlight) {
3543         firstDayInHighlight = highlight.firstDay().valueOf();
3544         lastDayInHighlight = highlight.lastDay().valueOf();
3545     } else {
3546         firstDayInHighlight = Infinity;
3547         lastDayInHighlight = Infinity;
3548     }
3549     var currentMonth = this.calendarPicker.currentMonth();
3550     var firstDayInCurrentMonth = currentMonth.firstDay().valueOf();
3551     var lastDayInCurrentMonth = currentMonth.lastDay().valueOf();
3552     var activeCell = null;
3553     for (var dayString in this._dayCells) {
3554         var dayCell = this._dayCells[dayString];
3555         var day = dayCell.day;
3556         dayCell.setIsToday(Day.createFromToday().equals(day));
3557         dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection);
3558         var isHighlighted = day >= firstDayInHighlight && day <= lastDayInHighlight;
3559         dayCell.setHighlighted(isHighlighted);
3560         if (isHighlighted) {
3561             if (firstDayInHighlight == lastDayInHighlight)
3562                 activeCell = dayCell;
3563             else if (this.calendarPicker.type == "month" && day == firstDayInHighlight)
3564                 activeCell = dayCell;
3565         }
3566         dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth);
3567         dayCell.setDisabled(!this.calendarPicker.isValidDay(day));
3568     }
3569     if (this.hasWeekNumberColumn) {
3570         for (var weekString in this._weekNumberCells) {
3571             var weekNumberCell = this._weekNumberCells[weekString];
3572             var week = weekNumberCell.week;
3573             var isWeekHighlighted = highlight && highlight.equals(week);
3574             weekNumberCell.setSelected(selection && selection.equals(week));
3575             weekNumberCell.setHighlighted(isWeekHighlighted);
3576             if (isWeekHighlighted)
3577                 activeCell = weekNumberCell;
3578             weekNumberCell.setDisabled(!this.calendarPicker.isValid(week));
3579         }
3580     }
3581     if (activeCell) {
3582         // Ensure a renderer because an element with no renderer doesn't post
3583         // activedescendant events. This shouldn't run in the above |for| loop
3584         // to avoid CSS transition.
3585         activeCell.element.offsetLeft;
3586         this.element.setAttribute("aria-activedescendant", activeCell.element.id);
3587     }
3588 };
3589
3590 /**
3591  * @param {!Day} day
3592  * @return {!DayCell}
3593  */
3594 CalendarTableView.prototype.prepareNewDayCell = function(day) {
3595     var dayCell = DayCell.recycleOrCreate();
3596     dayCell.reset(day);
3597     if (this.calendarPicker.type == "month")
3598         dayCell.element.setAttribute("aria-label", Month.createFromDay(day).toLocaleString());
3599     this._dayCells[dayCell.day.toString()] = dayCell;
3600     return dayCell;
3601 };
3602
3603 /**
3604  * @param {!Week} week
3605  * @return {!WeekNumberCell}
3606  */
3607 CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) {
3608     var weekNumberCell = WeekNumberCell.recycleOrCreate();
3609     weekNumberCell.reset(week);
3610     this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell;
3611     return weekNumberCell;
3612 };
3613
3614 /**
3615  * @param {!DayCell} dayCell
3616  */
3617 CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
3618     delete this._dayCells[dayCell.day.toString()];
3619     dayCell.throwAway();
3620 };
3621
3622 /**
3623  * @param {!WeekNumberCell} weekNumberCell
3624  */
3625 CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
3626     delete this._weekNumberCells[weekNumberCell.week.toString()];
3627     weekNumberCell.throwAway();
3628 };
3629
3630 /**
3631  * @constructor
3632  * @extends View
3633  * @param {!Object} config
3634  */
3635 function CalendarPicker(type, config) {
3636     View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
3637     this.element.classList.add(CalendarPicker.ClassNamePreparing);
3638
3639     /**
3640      * @type {!string}
3641      * @const
3642      */
3643     this.type = type;
3644     if (this.type === "week")
3645         this._dateTypeConstructor = Week;
3646     else if (this.type === "month")
3647         this._dateTypeConstructor = Month;
3648     else
3649         this._dateTypeConstructor = Day;
3650     /**
3651      * @type {!Object}
3652      * @const
3653      */
3654     this.config = {};
3655     this._setConfig(config);
3656     /**
3657      * @type {!Month}
3658      * @const
3659      */
3660     this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
3661     /**
3662      * @type {!Month}
3663      * @const
3664      */
3665     this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
3666     if (global.params.isLocaleRTL)
3667         this.element.classList.add("rtl");
3668     /**
3669      * @type {!CalendarTableView}
3670      * @const
3671      */
3672     this.calendarTableView = new CalendarTableView(this);
3673     this.calendarTableView.hasNumberColumn = this.type === "week";
3674     /**
3675      * @type {!CalendarHeaderView}
3676      * @const
3677      */
3678     this.calendarHeaderView = new CalendarHeaderView(this);
3679     this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
3680     /**
3681      * @type {!MonthPopupView}
3682      * @const
3683      */
3684     this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth);
3685     this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth);
3686     this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide);
3687     this.calendarHeaderView.attachTo(this);
3688     this.calendarTableView.attachTo(this);
3689     /**
3690      * @type {!Month}
3691      * @protected
3692      */
3693     this._currentMonth = new Month(NaN, NaN);
3694     /**
3695      * @type {?DateType}
3696      * @protected
3697      */
3698     this._selection = null;
3699     /**
3700      * @type {?DateType}
3701      * @protected
3702      */
3703     this._highlight = null;
3704     this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
3705     document.body.addEventListener("keydown", this.onBodyKeyDown, false);
3706
3707     window.addEventListener("resize", this.onWindowResize, false);
3708
3709     /**
3710      * @type {!number}
3711      * @protected
3712      */
3713     this._height = -1;
3714
3715     var initialSelection = parseDateString(config.currentValue);
3716     if (initialSelection) {
3717         this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
3718         this.setSelection(initialSelection);
3719     } else
3720         this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
3721 }
3722
3723 CalendarPicker.prototype = Object.create(View.prototype);
3724
3725 CalendarPicker.Padding = 10;
3726 CalendarPicker.BorderWidth = 1;
3727 CalendarPicker.ClassNameCalendarPicker = "calendar-picker";
3728 CalendarPicker.ClassNamePreparing = "preparing";
3729 CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged";
3730 CalendarPicker.commitDelayMs = 100;
3731
3732 /**
3733  * @param {!Event} event
3734  */
3735 CalendarPicker.prototype.onWindowResize = function(event) {
3736     this.element.classList.remove(CalendarPicker.ClassNamePreparing);
3737     window.removeEventListener("resize", this.onWindowResize, false);
3738 };
3739
3740 /**
3741  * @param {!YearListView} sender
3742  */
3743 CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
3744     this.monthPopupView.hide();
3745     this.calendarHeaderView.setDisabled(false);
3746     this.adjustHeight();
3747 };
3748
3749 /**
3750  * @param {!YearListView} sender
3751  * @param {!Month} month
3752  */
3753 CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
3754     this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
3755 };
3756
3757 /**
3758  * @param {!View|Node} parent
3759  * @param {?View|Node=} before
3760  * @override
3761  */
3762 CalendarPicker.prototype.attachTo = function(parent, before) {
3763     View.prototype.attachTo.call(this, parent, before);
3764     this.calendarTableView.element.focus();
3765 };
3766
3767 CalendarPicker.prototype.cleanup = function() {
3768     window.removeEventListener("resize", this.onWindowResize, false);
3769     this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false);
3770     // Month popup view might be attached to document.body.
3771     this.monthPopupView.hide();
3772 };
3773
3774 /**
3775  * @param {?MonthPopupButton} sender
3776  */
3777 CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) {
3778     var clientRect = this.calendarTableView.element.getBoundingClientRect();
3779     var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height);
3780     this.monthPopupView.show(this.currentMonth(), calendarTableRect);
3781     this.calendarHeaderView.setDisabled(true);
3782     this.adjustHeight();
3783 };
3784
3785 CalendarPicker.prototype._setConfig = function(config) {
3786     this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum;
3787     this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum;
3788     this.config.minimumValue = this.config.minimum.valueOf();
3789     this.config.maximumValue = this.config.maximum.valueOf();
3790     this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep;
3791     this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase;
3792 };
3793
3794 /**
3795  * @return {!Month}
3796  */
3797 CalendarPicker.prototype.currentMonth = function() {
3798     return this._currentMonth;
3799 };
3800
3801 /**
3802  * @enum {number}
3803  */
3804 CalendarPicker.NavigationBehavior = {
3805     None: 0,
3806     WithAnimation: 1
3807 };
3808
3809 /**
3810  * @param {!Month} month
3811  * @param {!CalendarPicker.NavigationBehavior} animate
3812  */
3813 CalendarPicker.prototype.setCurrentMonth = function(month, behavior) {
3814     if (month > this.maximumMonth)
3815         month = this.maximumMonth;
3816     else if (month < this.minimumMonth)
3817         month = this.minimumMonth;
3818     if (this._currentMonth.equals(month))
3819         return;
3820     this._currentMonth = month;
3821     this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation);
3822     this.adjustHeight();
3823     this.calendarTableView.setNeedsUpdateCells(true);
3824     this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, {
3825         target: this
3826     });
3827 };
3828
3829 CalendarPicker.prototype.adjustHeight = function() {
3830     var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row;
3831     var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row;
3832     var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1;
3833     var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2;
3834     var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2;
3835     this.setHeight(height);
3836 };
3837
3838 CalendarPicker.prototype.selection = function() {
3839     return this._selection;
3840 };
3841
3842 CalendarPicker.prototype.highlight = function() {
3843     return this._highlight;
3844 };
3845
3846 /**
3847  * @return {!Day}
3848  */
3849 CalendarPicker.prototype.firstVisibleDay = function() {
3850     var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
3851     var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3852     if (!firstVisibleDay)
3853         firstVisibleDay = Day.Minimum;
3854     return firstVisibleDay;
3855 };
3856
3857 /**
3858  * @return {!Day}
3859  */
3860 CalendarPicker.prototype.lastVisibleDay = function() { 
3861     var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row;
3862     var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3863     if (!lastVisibleDay)
3864         lastVisibleDay = Day.Maximum;
3865     return lastVisibleDay;
3866 };
3867
3868 /**
3869  * @param {?Day} day
3870  */
3871 CalendarPicker.prototype.selectRangeContainingDay = function(day) {
3872     var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
3873     this.setSelectionAndCommit(selection);
3874 };
3875
3876 /**
3877  * @param {?Day} day
3878  */
3879 CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
3880     var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
3881     this._setHighlight(highlight);
3882 };
3883
3884 /**
3885  * Select the specified date.
3886  * @param {?DateType} dayOrWeekOrMonth
3887  */
3888 CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
3889     if (!this._selection && !dayOrWeekOrMonth)
3890         return;
3891     if (this._selection && this._selection.equals(dayOrWeekOrMonth))
3892         return;
3893     var firstDayInSelection = dayOrWeekOrMonth.firstDay();    
3894     var lastDayInSelection = dayOrWeekOrMonth.lastDay();
3895     var candidateCurrentMonth = Month.createFromDay(firstDayInSelection);
3896     if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) {
3897         // Change current month if the selection is not visible at all.
3898         this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3899     } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) {
3900         // If the selection is partly visible, only change the current month if
3901         // doing so will make the whole selection visible.
3902         var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row;
3903         var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3904         var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row;
3905         var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3906         if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay)
3907             this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3908     }
3909     this._setHighlight(dayOrWeekOrMonth);
3910     if (!this.isValid(dayOrWeekOrMonth))
3911         return;
3912     this._selection = dayOrWeekOrMonth;
3913     this.calendarTableView.setNeedsUpdateCells(true);
3914 };
3915
3916 /**
3917  * Select the specified date, commit it, and close the popup.
3918  * @param {?DateType} dayOrWeekOrMonth
3919  */
3920 CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) {
3921     this.setSelection(dayOrWeekOrMonth);
3922     // Redraw the widget immidiately, and wait for some time to give feedback to
3923     // a user.
3924     this.element.offsetLeft;
3925     var value = this._selection.toString();
3926     if (CalendarPicker.commitDelayMs == 0) {
3927         // For testing.
3928         window.pagePopupController.setValueAndClosePopup(0, value);
3929     } else if (CalendarPicker.commitDelayMs < 0) {
3930         // For testing.
3931         window.pagePopupController.setValue(value);
3932     } else {
3933         setTimeout(function() {
3934             window.pagePopupController.setValueAndClosePopup(0, value);
3935         }, CalendarPicker.commitDelayMs);
3936     }
3937 };
3938
3939 /**
3940  * @param {?DateType} dayOrWeekOrMonth
3941  */
3942 CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
3943     if (!this._highlight && !dayOrWeekOrMonth)
3944         return;
3945     if (!dayOrWeekOrMonth && !this._highlight)
3946         return;
3947     if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
3948         return;
3949     this._highlight = dayOrWeekOrMonth;
3950     this.calendarTableView.setNeedsUpdateCells(true);
3951 };
3952
3953 /**
3954  * @param {!number} value
3955  * @return {!boolean}
3956  */
3957 CalendarPicker.prototype._stepMismatch = function(value) {
3958     var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase;
3959     return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep;
3960 };
3961
3962 /**
3963  * @param {!number} value
3964  * @return {!boolean}
3965  */
3966 CalendarPicker.prototype._outOfRange = function(value) {
3967     return value < this.config.minimumValue || value > this.config.maximumValue;
3968 };
3969
3970 /**
3971  * @param {!DateType} dayOrWeekOrMonth
3972  * @return {!boolean}
3973  */
3974 CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
3975     var value = dayOrWeekOrMonth.valueOf();
3976     return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
3977 };
3978
3979 /**
3980  * @param {!Day} day
3981  * @return {!boolean}
3982  */
3983 CalendarPicker.prototype.isValidDay = function(day) {
3984     return this.isValid(this._dateTypeConstructor.createFromDay(day));
3985 };
3986
3987 /**
3988  * @param {!DateType} dateRange
3989  * @return {!boolean} Returns true if the highlight was changed.
3990  */
3991 CalendarPicker.prototype._moveHighlight = function(dateRange) {
3992     if (!dateRange)
3993         return false;
3994     if (this._outOfRange(dateRange.valueOf()))
3995         return false;
3996     if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
3997         this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
3998     this._setHighlight(dateRange);
3999     return true;
4000 };
4001
4002 /**
4003  * @param {?Event} event
4004  */
4005 CalendarPicker.prototype.onCalendarTableKeyDown = function(event) {
4006     var key = event.keyIdentifier;
4007     var eventHandled = false;
4008     if (key == "U+0054") { // 't' key.
4009         this.selectRangeContainingDay(Day.createFromToday());
4010         eventHandled = true;
4011     } else if (key == "PageUp") {
4012         var previousMonth = this.currentMonth().previous();
4013         if (previousMonth && previousMonth >= this.config.minimumValue) {
4014             this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation);
4015             eventHandled = true;
4016         }
4017     } else if (key == "PageDown") {
4018         var nextMonth = this.currentMonth().next();
4019         if (nextMonth && nextMonth >= this.config.minimumValue) {
4020             this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation);
4021             eventHandled = true;
4022         }
4023     } else if (this._highlight) {
4024         if (global.params.isLocaleRTL ? key == "Right" : key == "Left") {
4025             eventHandled = this._moveHighlight(this._highlight.previous());
4026         } else if (key == "Up") {
4027             eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1));
4028         } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") {
4029             eventHandled = this._moveHighlight(this._highlight.next());
4030         } else if (key == "Down") {
4031             eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1));
4032         } else if (key == "Enter") {
4033             this.setSelectionAndCommit(this._highlight);
4034         }
4035     } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") {
4036         // Highlight range near the middle.
4037         this.highlightRangeContainingDay(this.currentMonth().middleDay());
4038         eventHandled = true;
4039     }
4040
4041     if (eventHandled) {
4042         event.stopPropagation();
4043         event.preventDefault();
4044     }
4045 };
4046
4047 /**
4048  * @return {!number} Width in pixels.
4049  */
4050 CalendarPicker.prototype.width = function() {
4051     return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
4052 };
4053
4054 /**
4055  * @return {!number} Height in pixels.
4056  */
4057 CalendarPicker.prototype.height = function() {
4058     return this._height;
4059 };
4060
4061 /**
4062  * @param {!number} height Height in pixels.
4063  */
4064 CalendarPicker.prototype.setHeight = function(height) {
4065     if (this._height === height)
4066         return;
4067     this._height = height;
4068     resizeWindow(this.width(), this._height);
4069     this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2);
4070 };
4071
4072 /**
4073  * @param {?Event} event
4074  */
4075 CalendarPicker.prototype.onBodyKeyDown = function(event) {
4076     var key = event.keyIdentifier;
4077     var eventHandled = false;
4078     var offset = 0;
4079     switch (key) {
4080     case "U+001B": // Esc key.
4081         window.pagePopupController.closePopup();
4082         eventHandled = true;
4083         break;
4084     case "U+004D": // 'm' key.
4085         offset = offset || 1; // Fall-through.
4086     case "U+0059": // 'y' key.
4087         offset = offset || MonthsPerYear; // Fall-through.
4088     case "U+0044": // 'd' key.
4089         offset = offset || MonthsPerYear * 10;
4090         var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4091         this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation);
4092         var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4093         if (this._highlight) {
4094             var highlightMiddleDay = this._highlight.middleDay();
4095             this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek));
4096         }
4097         eventHandled  =true;
4098         break;
4099     }
4100     if (eventHandled) {
4101         event.stopPropagation();
4102         event.preventDefault();
4103     }
4104 };
4105
4106 if (window.dialogArguments) {
4107     initialize(dialogArguments);
4108 } else {
4109     window.addEventListener("message", handleMessage, false);
4110 }