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