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