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