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