Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / web / resources / calendarPicker.js
blob0ba0d41d0e859dc0334e2d44c9ee3c7ce5f344ea
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  */
33 /**
34  * @enum {number}
35  */
36 var WeekDay = {
37     Sunday: 0,
38     Monday: 1,
39     Tuesday: 2,
40     Wednesday: 3,
41     Thursday: 4,
42     Friday: 5,
43     Saturday: 6
46 /**
47  * @type {Object}
48  */
49 var global = {
50     picker: null,
51     params: {
52         locale: "en-US",
53         weekStartDay: WeekDay.Sunday,
54         dayLabels: ["S", "M", "T", "W", "T", "F", "S"],
55         shortMonthLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"],
56         isLocaleRTL: false,
57         mode: "date",
58         weekLabel: "Week",
59         anchorRectInScreen: new Rectangle(0, 0, 0, 0),
60         currentValue: null
61     }
64 // ----------------------------------------------------------------
65 // Utility functions
67 /**
68  * @return {!boolean}
69  */
70 function hasInaccuratePointingDevice() {
71     return matchMedia("(pointer: coarse)").matches;
74 /**
75  * @return {!string} lowercase locale name. e.g. "en-us"
76  */
77 function getLocale() {
78     return (global.params.locale || "en-us").toLowerCase();
81 /**
82  * @return {!string} lowercase language code. e.g. "en"
83  */
84 function getLanguage() {
85     var locale = getLocale();
86     var result = locale.match(/^([a-z]+)/);
87     if (!result)
88         return "en";
89     return result[1];
92 /**
93  * @param {!number} number
94  * @return {!string}
95  */
96 function localizeNumber(number) {
97     return window.pagePopupController.localizeNumberString(number);
101  * @const
102  * @type {number}
103  */
104 var ImperialEraLimit = 2087;
107  * @param {!number} year
108  * @param {!number} month
109  * @return {!string}
110  */
111 function formatJapaneseImperialEra(year, month) {
112     // We don't show an imperial era if it is greater than 99 becase of space
113     // limitation.
114     if (year > ImperialEraLimit)
115         return "";
116     if (year > 1989)
117         return "(\u5e73\u6210" + localizeNumber(year - 1988) + "\u5e74)";
118     if (year == 1989)
119         return "(\u5e73\u6210\u5143\u5e74)";
120     if (year >= 1927)
121         return "(\u662d\u548c" + localizeNumber(year - 1925) + "\u5e74)";
122     if (year > 1912)
123         return "(\u5927\u6b63" + localizeNumber(year - 1911) + "\u5e74)";
124     if (year == 1912 && month >= 7)
125         return "(\u5927\u6b63\u5143\u5e74)";
126     if (year > 1868)
127         return "(\u660e\u6cbb" + localizeNumber(year - 1867) + "\u5e74)";
128     if (year == 1868)
129         return "(\u660e\u6cbb\u5143\u5e74)";
130     return "";
133 function createUTCDate(year, month, date) {
134     var newDate = new Date(0);
135     newDate.setUTCFullYear(year);
136     newDate.setUTCMonth(month);
137     newDate.setUTCDate(date);
138     return newDate;
142  * @param {string} dateString
143  * @return {?Day|Week|Month}
144  */
145 function parseDateString(dateString) {
146     var month = Month.parse(dateString);
147     if (month)
148         return month;
149     var week = Week.parse(dateString);
150     if (week)
151         return week;
152     return Day.parse(dateString);
156  * @const
157  * @type {number}
158  */
159 var DaysPerWeek = 7;
162  * @const
163  * @type {number}
164  */
165 var MonthsPerYear = 12;
168  * @const
169  * @type {number}
170  */
171 var MillisecondsPerDay = 24 * 60 * 60 * 1000;
174  * @const
175  * @type {number}
176  */
177 var MillisecondsPerWeek = DaysPerWeek * MillisecondsPerDay;
180  * @constructor
181  */
182 function DateType() {
186  * @constructor
187  * @extends DateType
188  * @param {!number} year
189  * @param {!number} month
190  * @param {!number} date
191  */
192 function Day(year, month, date) {
193     var dateObject = createUTCDate(year, month, date);
194     if (isNaN(dateObject.valueOf()))
195         throw "Invalid date";
196     /**
197      * @type {number}
198      * @const
199      */
200     this.year = dateObject.getUTCFullYear();   
201      /**
202      * @type {number}
203      * @const
204      */  
205     this.month = dateObject.getUTCMonth();
206     /**
207      * @type {number}
208      * @const
209      */
210     this.date = dateObject.getUTCDate();
213 Day.prototype = Object.create(DateType.prototype);
215 Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)/;
218  * @param {!string} str
219  * @return {?Day}
220  */
221 Day.parse = function(str) {
222     var match = Day.ISOStringRegExp.exec(str);
223     if (!match)
224         return null;
225     var year = parseInt(match[1], 10);
226     var month = parseInt(match[2], 10) - 1;
227     var date = parseInt(match[3], 10);
228     return new Day(year, month, date);
232  * @param {!number} value
233  * @return {!Day}
234  */
235 Day.createFromValue = function(millisecondsSinceEpoch) {
236     return Day.createFromDate(new Date(millisecondsSinceEpoch))
240  * @param {!Date} date
241  * @return {!Day}
242  */
243 Day.createFromDate = function(date) {
244     if (isNaN(date.valueOf()))
245         throw "Invalid date";
246     return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
250  * @param {!Day} day
251  * @return {!Day}
252  */
253 Day.createFromDay = function(day) {
254     return day;
258  * @return {!Day}
259  */
260 Day.createFromToday = function() {
261     var now = new Date();
262     return new Day(now.getFullYear(), now.getMonth(), now.getDate());
266  * @param {!DateType} other
267  * @return {!boolean}
268  */
269 Day.prototype.equals = function(other) {
270     return other instanceof Day && this.year === other.year && this.month === other.month && this.date === other.date;
274  * @param {!number=} offset
275  * @return {!Day}
276  */
277 Day.prototype.previous = function(offset) {
278     if (typeof offset === "undefined")
279         offset = 1;
280     return new Day(this.year, this.month, this.date - offset);
284  * @param {!number=} offset
285  * @return {!Day}
286  */
287 Day.prototype.next = function(offset) {
288  if (typeof offset === "undefined")
289      offset = 1;
290     return new Day(this.year, this.month, this.date + offset);
294  * @return {!Date}
295  */
296 Day.prototype.startDate = function() {
297     return createUTCDate(this.year, this.month, this.date);
301  * @return {!Date}
302  */
303 Day.prototype.endDate = function() {
304     return createUTCDate(this.year, this.month, this.date + 1);
308  * @return {!Day}
309  */
310 Day.prototype.firstDay = function() {
311     return this;
315  * @return {!Day}
316  */
317 Day.prototype.middleDay = function() {
318     return this;
322  * @return {!Day}
323  */
324 Day.prototype.lastDay = function() {
325     return this;
329  * @return {!number}
330  */
331 Day.prototype.valueOf = function() {
332     return createUTCDate(this.year, this.month, this.date).getTime();
336  * @return {!WeekDay}
337  */
338 Day.prototype.weekDay = function() {
339     return createUTCDate(this.year, this.month, this.date).getUTCDay();
343  * @return {!string}
344  */
345 Day.prototype.toString = function() {
346     var yearString = String(this.year);
347     if (yearString.length < 4)
348         yearString = ("000" + yearString).substr(-4, 4);
349     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2);
353  * @return {!string}
354  */
355 Day.prototype.format = function() {
356     if (!Day.formatter) {
357         Day.formatter = new Intl.DateTimeFormat(getLocale(), {
358             weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC"
359         });
360     }
361     return Day.formatter.format(this.startDate());
364 // See WebCore/platform/DateComponents.h.
365 Day.Minimum = Day.createFromValue(-62135596800000.0);
366 Day.Maximum = Day.createFromValue(8640000000000000.0);
368 // See WebCore/html/DayInputType.cpp.
369 Day.DefaultStep = 86400000;
370 Day.DefaultStepBase = 0;
373  * @constructor
374  * @extends DateType
375  * @param {!number} year
376  * @param {!number} week
377  */
378 function Week(year, week) { 
379     /**
380      * @type {number}
381      * @const
382      */
383     this.year = year;
384     /**
385      * @type {number}
386      * @const
387      */
388     this.week = week;
389     // Number of years per year is either 52 or 53.
390     if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) {
391         var normalizedWeek = Week.createFromDay(this.firstDay());
392         this.year = normalizedWeek.year;
393         this.week = normalizedWeek.week;
394     }
397 Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;
399 // See WebCore/platform/DateComponents.h.
400 Week.Minimum = new Week(1, 1);
401 Week.Maximum = new Week(275760, 37);
403 // See WebCore/html/WeekInputType.cpp.
404 Week.DefaultStep = 604800000;
405 Week.DefaultStepBase = -259200000;
407 Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay();
410  * @param {!string} str
411  * @return {?Week}
412  */
413 Week.parse = function(str) {
414     var match = Week.ISOStringRegExp.exec(str);
415     if (!match)
416         return null;
417     var year = parseInt(match[1], 10);
418     var week = parseInt(match[2], 10);
419     return new Week(year, week);
423  * @param {!number} millisecondsSinceEpoch
424  * @return {!Week}
425  */
426 Week.createFromValue = function(millisecondsSinceEpoch) {
427     return Week.createFromDate(new Date(millisecondsSinceEpoch))
431  * @param {!Date} date
432  * @return {!Week}
433  */
434 Week.createFromDate = function(date) {
435     if (isNaN(date.valueOf()))
436         throw "Invalid date";
437     var year = date.getUTCFullYear();
438     if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime())
439         year++;
440     else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
441         year--;
442     var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
443     return new Week(year, week);
447  * @param {!Day} day
448  * @return {!Week}
449  */
450 Week.createFromDay = function(day) {
451     var year = day.year;
452     if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day)
453         year++;
454     else if (year > 1 && Week.weekOneStartDayForYear(year) > day)
455         year--;
456     var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek);
457     return new Week(year, week);
461  * @return {!Week}
462  */
463 Week.createFromToday = function() {
464     var now = new Date();
465     return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
469  * @param {!number} year
470  * @return {!Date}
471  */
472 Week.weekOneStartDateForYear = function(year) {
473     if (year < 1)
474         return createUTCDate(1, 0, 1);
475     // The week containing January 4th is week one.
476     var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
477     return createUTCDate(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
481  * @param {!number} year
482  * @return {!Day}
483  */
484 Week.weekOneStartDayForYear = function(year) {
485     if (year < 1)
486         return Day.Minimum;
487     // The week containing January 4th is week one.
488     var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
489     return new Day(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
493  * @param {!number} year
494  * @return {!number}
495  */
496 Week.numberOfWeeksInYear = function(year) {
497     if (year < 1 || year > Week.Maximum.year)
498         return 0;
499     else if (year === Week.Maximum.year)
500         return Week.Maximum.week;
501     return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
505  * @param {!Date} baseDate
506  * @param {!Date} date
507  * @return {!number}
508  */
509 Week._numberOfWeeksSinceDate = function(baseDate, date) {
510     return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek);
514  * @param {!DateType} other
515  * @return {!boolean}
516  */
517 Week.prototype.equals = function(other) {
518     return other instanceof Week && this.year === other.year && this.week === other.week;
522  * @param {!number=} offset
523  * @return {!Week}
524  */
525 Week.prototype.previous = function(offset) {
526     if (typeof offset === "undefined")
527         offset = 1;
528     return new Week(this.year, this.week - offset);
532  * @param {!number=} offset
533  * @return {!Week}
534  */
535 Week.prototype.next = function(offset) {
536     if (typeof offset === "undefined")
537         offset = 1;
538     return new Week(this.year, this.week + offset);
542  * @return {!Date}
543  */
544 Week.prototype.startDate = function() {
545     var weekStartDate = Week.weekOneStartDateForYear(this.year);
546     weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
547     return weekStartDate;
551  * @return {!Date}
552  */
553 Week.prototype.endDate = function() {
554     if (this.equals(Week.Maximum))
555         return Day.Maximum.startDate();
556     return this.next().startDate();
560  * @return {!Day}
561  */
562 Week.prototype.firstDay = function() {
563     var weekOneStartDay = Week.weekOneStartDayForYear(this.year);
564     return weekOneStartDay.next((this.week - 1) * DaysPerWeek);
568  * @return {!Day}
569  */
570 Week.prototype.middleDay = function() {
571     return this.firstDay().next(3);
575  * @return {!Day}
576  */
577 Week.prototype.lastDay = function() {
578     if (this.equals(Week.Maximum))
579         return Day.Maximum;
580     return this.next().firstDay().previous();
584  * @return {!number}
585  */
586 Week.prototype.valueOf = function() {
587     return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime();
591  * @return {!string}
592  */
593 Week.prototype.toString = function() {
594     var yearString = String(this.year);
595     if (yearString.length < 4)
596         yearString = ("000" + yearString).substr(-4, 4);
597     return yearString + "-W" + ("0" + this.week).substr(-2, 2);
601  * @constructor
602  * @extends DateType
603  * @param {!number} year
604  * @param {!number} month
605  */
606 function Month(year, month) { 
607     /**
608      * @type {number}
609      * @const
610      */
611     this.year = year + Math.floor(month / MonthsPerYear);
612     /**
613      * @type {number}
614      * @const
615      */
616     this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear;
619 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
621 // See WebCore/platform/DateComponents.h.
622 Month.Minimum = new Month(1, 0);
623 Month.Maximum = new Month(275760, 8);
625 // See WebCore/html/MonthInputType.cpp.
626 Month.DefaultStep = 1;
627 Month.DefaultStepBase = 0;
630  * @param {!string} str
631  * @return {?Month}
632  */
633 Month.parse = function(str) {
634     var match = Month.ISOStringRegExp.exec(str);
635     if (!match)
636         return null;
637     var year = parseInt(match[1], 10);
638     var month = parseInt(match[2], 10) - 1;
639     return new Month(year, month);
643  * @param {!number} value
644  * @return {!Month}
645  */
646 Month.createFromValue = function(monthsSinceEpoch) {
647     return new Month(1970, monthsSinceEpoch)
651  * @param {!Date} date
652  * @return {!Month}
653  */
654 Month.createFromDate = function(date) {
655     if (isNaN(date.valueOf()))
656         throw "Invalid date";
657     return new Month(date.getUTCFullYear(), date.getUTCMonth());
661  * @param {!Day} day
662  * @return {!Month}
663  */
664 Month.createFromDay = function(day) {
665     return new Month(day.year, day.month);
669  * @return {!Month}
670  */
671 Month.createFromToday = function() {
672     var now = new Date();
673     return new Month(now.getFullYear(), now.getMonth());
677  * @return {!boolean}
678  */
679 Month.prototype.containsDay = function(day) {
680     return this.year === day.year && this.month === day.month;
684  * @param {!Month} other
685  * @return {!boolean}
686  */
687 Month.prototype.equals = function(other) {
688     return other instanceof Month && this.year === other.year && this.month === other.month;
692  * @param {!number=} offset
693  * @return {!Month}
694  */
695 Month.prototype.previous = function(offset) {
696     if (typeof offset === "undefined")
697         offset = 1;
698     return new Month(this.year, this.month - offset);
702  * @param {!number=} offset
703  * @return {!Month}
704  */
705 Month.prototype.next = function(offset) {
706     if (typeof offset === "undefined")
707         offset = 1;
708     return new Month(this.year, this.month + offset);
712  * @return {!Date}
713  */
714 Month.prototype.startDate = function() {
715     return createUTCDate(this.year, this.month, 1);
719  * @return {!Date}
720  */
721 Month.prototype.endDate = function() {
722     if (this.equals(Month.Maximum))
723         return Day.Maximum.startDate();
724     return this.next().startDate();
728  * @return {!Day}
729  */
730 Month.prototype.firstDay = function() {
731     return new Day(this.year, this.month, 1);
735  * @return {!Day}
736  */
737 Month.prototype.middleDay = function() {
738     return new Day(this.year, this.month, this.month === 2 ? 14 : 15);
742  * @return {!Day}
743  */
744 Month.prototype.lastDay = function() {
745     if (this.equals(Month.Maximum))
746         return Day.Maximum;
747     return this.next().firstDay().previous();
751  * @return {!number}
752  */
753 Month.prototype.valueOf = function() {
754     return (this.year - 1970) * MonthsPerYear + this.month;
758  * @return {!string}
759  */
760 Month.prototype.toString = function() {
761     var yearString = String(this.year);
762     if (yearString.length < 4)
763         yearString = ("000" + yearString).substr(-4, 4);
764     return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
768  * @return {!string}
769  */
770 Month.prototype.toLocaleString = function() {
771     if (global.params.locale === "ja")
772         return "" + this.year + "\u5e74" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "\u6708";
773     return window.pagePopupController.formatMonth(this.year, this.month);
777  * @return {!string}
778  */
779 Month.prototype.toShortLocaleString = function() {
780     return window.pagePopupController.formatShortMonth(this.year, this.month);
783 // ----------------------------------------------------------------
784 // Initialization
787  * @param {Event} event
788  */
789 function handleMessage(event) {
790     if (global.argumentsReceived)
791         return;
792     global.argumentsReceived = true;
793     initialize(JSON.parse(event.data));
797  * @param {!Object} params
798  */
799 function setGlobalParams(params) {
800     var name;
801     for (name in global.params) {
802         if (typeof params[name] === "undefined")
803             console.warn("Missing argument: " + name);
804     }
805     for (name in params) {
806         global.params[name] = params[name];
807     }
811  * @param {!Object} args
812  */
813 function initialize(args) { 
814     setGlobalParams(args);
815     if (global.params.suggestionValues && global.params.suggestionValues.length)
816         openSuggestionPicker();
817     else
818         openCalendarPicker();
821 function closePicker() {
822     if (global.picker)
823         global.picker.cleanup();
824     var main = $("main");
825     main.innerHTML = "";
826     main.className = "";
829 function openSuggestionPicker() {
830     closePicker();
831     global.picker = new SuggestionPicker($("main"), global.params);
834 function openCalendarPicker() {
835     closePicker();
836     global.picker = new CalendarPicker(global.params.mode, global.params);
837     global.picker.attachTo($("main"));
840 // Parameter t should be a number between 0 and 1.
841 var AnimationTimingFunction = {
842     Linear: function(t){
843         return t;
844     },
845     EaseInOut: function(t){
846         t *= 2;
847         if (t < 1)
848             return Math.pow(t, 3) / 2;
849         t -= 2;
850         return Math.pow(t, 3) / 2 + 1;
851     }
855  * @constructor
856  * @extends EventEmitter
857  */
858 function AnimationManager() {
859     EventEmitter.call(this);
861     this._isRunning = false;
862     this._runningAnimatorCount = 0;
863     this._runningAnimators = {};
864     this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
867 AnimationManager.prototype = Object.create(EventEmitter.prototype);
869 AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";
871 AnimationManager.prototype._startAnimation = function() {
872     if (this._isRunning)
873         return;
874     this._isRunning = true;
875     window.requestAnimationFrame(this._animationFrameCallbackBound);
878 AnimationManager.prototype._stopAnimation = function() {
879     if (!this._isRunning)
880         return;
881     this._isRunning = false;
885  * @param {!Animator} animator
886  */
887 AnimationManager.prototype.add = function(animator) {
888     if (this._runningAnimators[animator.id])
889         return;
890     this._runningAnimators[animator.id] = animator;
891     this._runningAnimatorCount++;
892     if (this._needsTimer())
893         this._startAnimation();
897  * @param {!Animator} animator
898  */
899 AnimationManager.prototype.remove = function(animator) {
900     if (!this._runningAnimators[animator.id])
901         return;
902     delete this._runningAnimators[animator.id];
903     this._runningAnimatorCount--;
904     if (!this._needsTimer())
905         this._stopAnimation();
908 AnimationManager.prototype._animationFrameCallback = function(now) {
909     if (this._runningAnimatorCount > 0) {
910         for (var id in this._runningAnimators) {
911             this._runningAnimators[id].onAnimationFrame(now);
912         }
913     }
914     this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
915     if (this._isRunning)
916         window.requestAnimationFrame(this._animationFrameCallbackBound);
920  * @return {!boolean}
921  */
922 AnimationManager.prototype._needsTimer = function() {
923     return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
927  * @param {!string} type
928  * @param {!Function} callback
929  * @override
930  */
931 AnimationManager.prototype.on = function(type, callback) {
932     EventEmitter.prototype.on.call(this, type, callback);
933     if (this._needsTimer())
934         this._startAnimation();
938  * @param {!string} type
939  * @param {!Function} callback
940  * @override
941  */
942 AnimationManager.prototype.removeListener = function(type, callback) {
943     EventEmitter.prototype.removeListener.call(this, type, callback);
944     if (!this._needsTimer())
945         this._stopAnimation();
948 AnimationManager.shared = new AnimationManager();
951  * @constructor
952  * @extends EventEmitter
953  */
954 function Animator() {
955     EventEmitter.call(this);
957     /**
958      * @type {!number}
959      * @const
960      */
961     this.id = Animator._lastId++;
962     /**
963      * @type {!number}
964      */
965     this.duration = 100;
966     /**
967      * @type {?function}
968      */
969     this.step = null;
970     /**
971      * @type {!boolean}
972      * @protected
973      */
974     this._isRunning = false;
975     /**
976      * @type {!number}
977      */
978     this.currentValue = 0;
979     /**
980      * @type {!number}
981      * @protected
982      */
983     this._lastStepTime = 0;
986 Animator.prototype = Object.create(EventEmitter.prototype);
988 Animator._lastId = 0;
990 Animator.EventTypeDidAnimationStop = "didAnimationStop";
993  * @return {!boolean}
994  */
995 Animator.prototype.isRunning = function() {
996     return this._isRunning;
999 Animator.prototype.start = function() {
1000     this._lastStepTime = performance.now();
1001     this._isRunning = true;
1002     AnimationManager.shared.add(this);
1005 Animator.prototype.stop = function() {
1006     if (!this._isRunning)
1007         return;
1008     this._isRunning = false;
1009     AnimationManager.shared.remove(this);
1010     this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
1014  * @param {!number} now
1015  */
1016 Animator.prototype.onAnimationFrame = function(now) {
1017     this._lastStepTime = now;
1018     this.step(this);
1022  * @constructor
1023  * @extends Animator
1024  */
1025 function TransitionAnimator() {
1026     Animator.call(this);
1027     /**
1028      * @type {!number}
1029      * @protected
1030      */
1031     this._from = 0;
1032     /**
1033      * @type {!number}
1034      * @protected
1035      */
1036     this._to = 0;
1037     /**
1038      * @type {!number}
1039      * @protected
1040      */
1041     this._delta = 0;
1042     /**
1043      * @type {!number}
1044      */
1045     this.progress = 0.0;
1046     /**
1047      * @type {!function}
1048      */
1049     this.timingFunction = AnimationTimingFunction.Linear;
1052 TransitionAnimator.prototype = Object.create(Animator.prototype);
1055  * @param {!number} value
1056  */
1057 TransitionAnimator.prototype.setFrom = function(value) {
1058     this._from = value;
1059     this._delta = this._to - this._from;
1062 TransitionAnimator.prototype.start = function() {
1063     console.assert(isFinite(this.duration));
1064     this.progress = 0.0;
1065     this.currentValue = this._from;
1066     Animator.prototype.start.call(this);
1070  * @param {!number} value
1071  */
1072 TransitionAnimator.prototype.setTo = function(value) {
1073     this._to = value;
1074     this._delta = this._to - this._from;
1078  * @param {!number} now
1079  */
1080 TransitionAnimator.prototype.onAnimationFrame = function(now) {
1081     this.progress += (now - this._lastStepTime) / this.duration;
1082     this.progress = Math.min(1.0, this.progress);
1083     this._lastStepTime = now;
1084     this.currentValue = this.timingFunction(this.progress) * this._delta + this._from;
1085     this.step(this);
1086     if (this.progress === 1.0) {
1087         this.stop();
1088         return;
1089     }
1093  * @constructor
1094  * @extends Animator
1095  * @param {!number} initialVelocity
1096  * @param {!number} initialValue
1097  */
1098 function FlingGestureAnimator(initialVelocity, initialValue) {
1099     Animator.call(this);
1100     /**
1101      * @type {!number}
1102      */
1103     this.initialVelocity = initialVelocity;
1104     /**
1105      * @type {!number}
1106      */
1107     this.initialValue = initialValue;
1108     /**
1109      * @type {!number}
1110      * @protected
1111      */
1112     this._elapsedTime = 0;
1113     var startVelocity = Math.abs(this.initialVelocity);
1114     if (startVelocity > this._velocityAtTime(0))
1115         startVelocity = this._velocityAtTime(0);
1116     if (startVelocity < 0)
1117         startVelocity = 0;
1118     /**
1119      * @type {!number}
1120      * @protected
1121      */
1122     this._timeOffset = this._timeAtVelocity(startVelocity);
1123     /**
1124      * @type {!number}
1125      * @protected
1126      */
1127     this._positionOffset = this._valueAtTime(this._timeOffset);
1128     /**
1129      * @type {!number}
1130      */
1131     this.duration = this._timeAtVelocity(0);
1134 FlingGestureAnimator.prototype = Object.create(Animator.prototype);
1136 // Velocity is subject to exponential decay. These parameters are coefficients
1137 // that determine the curve.
1138 FlingGestureAnimator._P0 = -5707.62;
1139 FlingGestureAnimator._P1 = 0.172;
1140 FlingGestureAnimator._P2 = 0.0037;
1143  * @param {!number} t
1144  */
1145 FlingGestureAnimator.prototype._valueAtTime = function(t) {
1146     return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
1150  * @param {!number} t
1151  */
1152 FlingGestureAnimator.prototype._velocityAtTime = function(t) {
1153     return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
1157  * @param {!number} v
1158  */
1159 FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
1160     return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
1163 FlingGestureAnimator.prototype.start = function() {
1164     this._lastStepTime = performance.now();
1165     Animator.prototype.start.call(this);
1169  * @param {!number} now
1170  */
1171 FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
1172     this._elapsedTime += now - this._lastStepTime;
1173     this._lastStepTime = now;
1174     if (this._elapsedTime + this._timeOffset >= this.duration) {
1175         this.stop();
1176         return;
1177     }
1178     var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset;
1179     if (this.initialVelocity < 0)
1180         position = -position;
1181     this.currentValue = position + this.initialValue;
1182     this.step(this);
1186  * @constructor
1187  * @extends EventEmitter
1188  * @param {?Element} element
1189  * View adds itself as a property on the element so we can access it from Event.target.
1190  */
1191 function View(element) {
1192     EventEmitter.call(this);
1193     /**
1194      * @type {Element}
1195      * @const
1196      */
1197     this.element = element || createElement("div");
1198     this.element.$view = this;
1199     this.bindCallbackMethods();
1202 View.prototype = Object.create(EventEmitter.prototype);
1205  * @param {!Element} ancestorElement
1206  * @return {?Object}
1207  */
1208 View.prototype.offsetRelativeTo = function(ancestorElement) {
1209     var x = 0;
1210     var y = 0;
1211     var element = this.element;
1212     while (element) {
1213         x += element.offsetLeft  || 0;
1214         y += element.offsetTop || 0;
1215         element = element.offsetParent;
1216         if (element === ancestorElement)
1217             return {x: x, y: y};
1218     }
1219     return null;
1223  * @param {!View|Node} parent
1224  * @param {?View|Node=} before
1225  */
1226 View.prototype.attachTo = function(parent, before) {
1227     if (parent instanceof View)
1228         return this.attachTo(parent.element, before);
1229     if (typeof before === "undefined")
1230         before = null;
1231     if (before instanceof View)
1232         before = before.element;
1233     parent.insertBefore(this.element, before);
1236 View.prototype.bindCallbackMethods = function() {
1237     for (var methodName in this) {
1238         if (!/^on[A-Z]/.test(methodName))
1239             continue;
1240         if (this.hasOwnProperty(methodName))
1241             continue;
1242         var method = this[methodName];
1243         if (!(method instanceof Function))
1244             continue;
1245         this[methodName] = method.bind(this);
1246     }
1250  * @constructor
1251  * @extends View
1252  */
1253 function ScrollView() {
1254     View.call(this, createElement("div", ScrollView.ClassNameScrollView));
1255     /**
1256      * @type {Element}
1257      * @const
1258      */
1259     this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
1260     this.element.appendChild(this.contentElement);
1261     /**
1262      * @type {number}
1263      */
1264     this.minimumContentOffset = -Infinity;
1265     /**
1266      * @type {number}
1267      */
1268     this.maximumContentOffset = Infinity;
1269     /**
1270      * @type {number}
1271      * @protected
1272      */
1273     this._contentOffset = 0;
1274     /**
1275      * @type {number}
1276      * @protected
1277      */
1278     this._width = 0;
1279     /**
1280      * @type {number}
1281      * @protected
1282      */
1283     this._height = 0;
1284     /**
1285      * @type {Animator}
1286      * @protected
1287      */
1288     this._scrollAnimator = null;
1289     /**
1290      * @type {?Object}
1291      */
1292     this.delegate = null;
1293     /**
1294      * @type {!number}
1295      */
1296     this._lastTouchPosition = 0;
1297     /**
1298      * @type {!number}
1299      */
1300     this._lastTouchVelocity = 0;
1301     /**
1302      * @type {!number}
1303      */
1304     this._lastTouchTimeStamp = 0;
1306     this.element.addEventListener("mousewheel", this.onMouseWheel, false);
1307     this.element.addEventListener("touchstart", this.onTouchStart, false);
1309     /**
1310      * The content offset is partitioned so the it can go beyond the CSS limit
1311      * of 33554433px.
1312      * @type {number}
1313      * @protected
1314      */
1315     this._partitionNumber = 0;
1318 ScrollView.prototype = Object.create(View.prototype);
1320 ScrollView.PartitionHeight = 100000;
1321 ScrollView.ClassNameScrollView = "scroll-view";
1322 ScrollView.ClassNameScrollViewContent = "scroll-view-content";
1325  * @param {!Event} event
1326  */
1327 ScrollView.prototype.onTouchStart = function(event) {
1328     var touch = event.touches[0];
1329     this._lastTouchPosition = touch.clientY;
1330     this._lastTouchVelocity = 0;
1331     this._lastTouchTimeStamp = event.timeStamp;
1332     if (this._scrollAnimator)
1333         this._scrollAnimator.stop();
1334     window.addEventListener("touchmove", this.onWindowTouchMove, false);
1335     window.addEventListener("touchend", this.onWindowTouchEnd, false);
1339  * @param {!Event} event
1340  */
1341 ScrollView.prototype.onWindowTouchMove = function(event) {
1342     var touch = event.touches[0];
1343     var deltaTime = event.timeStamp - this._lastTouchTimeStamp;
1344     var deltaY = this._lastTouchPosition - touch.clientY;
1345     this.scrollBy(deltaY, false);
1346     this._lastTouchVelocity = deltaY / deltaTime;
1347     this._lastTouchPosition = touch.clientY;
1348     this._lastTouchTimeStamp = event.timeStamp;
1349     event.stopPropagation();
1350     event.preventDefault();
1354  * @param {!Event} event
1355  */
1356 ScrollView.prototype.onWindowTouchEnd = function(event) {
1357     if (Math.abs(this._lastTouchVelocity) > 0.01) {
1358         this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset);
1359         this._scrollAnimator.step = this.onFlingGestureAnimatorStep;
1360         this._scrollAnimator.start();
1361     }
1362     window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1363     window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1367  * @param {!Animator} animator
1368  */
1369 ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
1370     this.scrollTo(animator.currentValue, false);
1374  * @return {!Animator}
1375  */
1376 ScrollView.prototype.scrollAnimator = function() {
1377     return this._scrollAnimator;
1381  * @param {!number} width
1382  */
1383 ScrollView.prototype.setWidth = function(width) {
1384     console.assert(isFinite(width));
1385     if (this._width === width)
1386         return;
1387     this._width = width;
1388     this.element.style.width = this._width + "px";
1392  * @return {!number}
1393  */
1394 ScrollView.prototype.width = function() {
1395     return this._width;
1399  * @param {!number} height
1400  */
1401 ScrollView.prototype.setHeight = function(height) {
1402     console.assert(isFinite(height));
1403     if (this._height === height)
1404         return;
1405     this._height = height;
1406     this.element.style.height = height + "px";
1407     if (this.delegate)
1408         this.delegate.scrollViewDidChangeHeight(this);
1412  * @return {!number}
1413  */
1414 ScrollView.prototype.height = function() {
1415     return this._height;
1419  * @param {!Animator} animator
1420  */
1421 ScrollView.prototype.onScrollAnimatorStep = function(animator) {
1422     this.setContentOffset(animator.currentValue);
1426  * @param {!number} offset
1427  * @param {?boolean} animate
1428  */
1429 ScrollView.prototype.scrollTo = function(offset, animate) {
1430     console.assert(isFinite(offset));
1431     if (!animate) {
1432         this.setContentOffset(offset);
1433         return;
1434     }
1435     if (this._scrollAnimator)
1436         this._scrollAnimator.stop();
1437     this._scrollAnimator = new TransitionAnimator();
1438     this._scrollAnimator.step = this.onScrollAnimatorStep;
1439     this._scrollAnimator.setFrom(this._contentOffset);
1440     this._scrollAnimator.setTo(offset);
1441     this._scrollAnimator.duration = 300;
1442     this._scrollAnimator.start();
1446  * @param {!number} offset
1447  * @param {?boolean} animate
1448  */
1449 ScrollView.prototype.scrollBy = function(offset, animate) {
1450     this.scrollTo(this._contentOffset + offset, animate);
1454  * @return {!number}
1455  */
1456 ScrollView.prototype.contentOffset = function() {
1457     return this._contentOffset;
1461  * @param {?Event} event
1462  */
1463 ScrollView.prototype.onMouseWheel = function(event) {
1464     this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
1465     event.stopPropagation();
1466     event.preventDefault();
1471  * @param {!number} value
1472  */
1473 ScrollView.prototype.setContentOffset = function(value) {
1474     console.assert(isFinite(value));
1475     value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value)));
1476     if (this._contentOffset === value)
1477         return;
1478     this._contentOffset = value;
1479     this._updateScrollContent();
1480     if (this.delegate)
1481         this.delegate.scrollViewDidChangeContentOffset(this);
1484 ScrollView.prototype._updateScrollContent = function() {
1485     var newPartitionNumber = Math.floor(this._contentOffset / ScrollView.PartitionHeight);
1486     var partitionChanged = this._partitionNumber !== newPartitionNumber;
1487     this._partitionNumber = newPartitionNumber;
1488     this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)";
1489     if (this.delegate && partitionChanged)
1490         this.delegate.scrollViewDidChangePartition(this);
1494  * @param {!View|Node} parent
1495  * @param {?View|Node=} before
1496  * @override
1497  */
1498 ScrollView.prototype.attachTo = function(parent, before) {
1499     View.prototype.attachTo.call(this, parent, before);
1500     this._updateScrollContent();
1504  * @param {!number} offset
1505  */
1506 ScrollView.prototype.contentPositionForContentOffset = function(offset) {
1507     return offset - this._partitionNumber * ScrollView.PartitionHeight;
1511  * @constructor
1512  * @extends View
1513  */
1514 function ListCell() {
1515     View.call(this, createElement("div", ListCell.ClassNameListCell));
1516     
1517     /**
1518      * @type {!number}
1519      */
1520     this.row = NaN;
1521     /**
1522      * @type {!number}
1523      */
1524     this._width = 0;
1525     /**
1526      * @type {!number}
1527      */
1528     this._position = 0;
1531 ListCell.prototype = Object.create(View.prototype);
1533 ListCell.DefaultRecycleBinLimit = 64;
1534 ListCell.ClassNameListCell = "list-cell";
1535 ListCell.ClassNameHidden = "hidden";
1538  * @return {!Array} An array to keep thrown away cells.
1539  */
1540 ListCell.prototype._recycleBin = function() {
1541     console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
1542     return [];
1545 ListCell.prototype.throwAway = function() {
1546     this.hide();
1547     var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit;
1548     var recycleBin = this._recycleBin();
1549     if (recycleBin.length < limit)
1550         recycleBin.push(this);
1553 ListCell.prototype.show = function() {
1554     this.element.classList.remove(ListCell.ClassNameHidden);
1557 ListCell.prototype.hide = function() {
1558     this.element.classList.add(ListCell.ClassNameHidden);
1562  * @return {!number} Width in pixels.
1563  */
1564 ListCell.prototype.width = function(){
1565     return this._width;
1569  * @param {!number} width Width in pixels.
1570  */
1571 ListCell.prototype.setWidth = function(width){
1572     if (this._width === width)
1573         return;
1574     this._width = width;
1575     this.element.style.width = this._width + "px";
1579  * @return {!number} Position in pixels.
1580  */
1581 ListCell.prototype.position = function(){
1582     return this._position;
1586  * @param {!number} y Position in pixels.
1587  */
1588 ListCell.prototype.setPosition = function(y) {
1589     if (this._position === y)
1590         return;
1591     this._position = y;
1592     this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
1596  * @param {!boolean} selected
1597  */
1598 ListCell.prototype.setSelected = function(selected) {
1599     if (this._selected === selected)
1600         return;
1601     this._selected = selected;
1602     if (this._selected)
1603         this.element.classList.add("selected");
1604     else
1605         this.element.classList.remove("selected");
1609  * @constructor
1610  * @extends View
1611  */
1612 function ListView() {
1613     View.call(this, createElement("div", ListView.ClassNameListView));
1614     this.element.tabIndex = 0;
1615     this.element.setAttribute("role", "grid");
1617     /**
1618      * @type {!number}
1619      * @private
1620      */
1621     this._width = 0;
1622     /**
1623      * @type {!Object}
1624      * @private
1625      */
1626     this._cells = {};
1628     /**
1629      * @type {!number}
1630      */
1631     this.selectedRow = ListView.NoSelection;
1633     /**
1634      * @type {!ScrollView}
1635      */
1636     this.scrollView = new ScrollView();
1637     this.scrollView.delegate = this;
1638     this.scrollView.minimumContentOffset = 0;
1639     this.scrollView.setWidth(0);
1640     this.scrollView.setHeight(0);
1641     this.scrollView.attachTo(this);
1643     this.element.addEventListener("click", this.onClick, false);
1645     /**
1646      * @type {!boolean}
1647      * @private
1648      */
1649     this._needsUpdateCells = false;
1652 ListView.prototype = Object.create(View.prototype);
1654 ListView.NoSelection = -1;
1655 ListView.ClassNameListView = "list-view";
1657 ListView.prototype.onAnimationFrameWillFinish = function() {
1658     if (this._needsUpdateCells)
1659         this.updateCells();
1663  * @param {!boolean} needsUpdateCells
1664  */
1665 ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
1666     if (this._needsUpdateCells === needsUpdateCells)
1667         return;
1668     this._needsUpdateCells = needsUpdateCells;
1669     if (this._needsUpdateCells)
1670         AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1671     else
1672         AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1676  * @param {!number} row
1677  * @return {?ListCell}
1678  */
1679 ListView.prototype.cellAtRow = function(row) {
1680     return this._cells[row];
1684  * @param {!number} offset Scroll offset in pixels.
1685  * @return {!number}
1686  */
1687 ListView.prototype.rowAtScrollOffset = function(offset) {
1688     console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
1689     return 0;
1693  * @param {!number} row
1694  * @return {!number} Scroll offset in pixels.
1695  */
1696 ListView.prototype.scrollOffsetForRow = function(row) {
1697     console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
1698     return 0;
1702  * @param {!number} row
1703  * @return {!ListCell}
1704  */
1705 ListView.prototype.addCellIfNecessary = function(row) {
1706     var cell = this._cells[row];
1707     if (cell)
1708         return cell;
1709     cell = this.prepareNewCell(row);
1710     cell.attachTo(this.scrollView.contentElement);
1711     cell.setWidth(this._width);
1712     cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row)));
1713     this._cells[row] = cell;
1714     return cell;
1718  * @param {!number} row
1719  * @return {!ListCell}
1720  */
1721 ListView.prototype.prepareNewCell = function(row) {
1722     console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
1723     return new ListCell();
1727  * @param {!ListCell} cell
1728  */
1729 ListView.prototype.throwAwayCell = function(cell) {
1730     delete this._cells[cell.row];
1731     cell.throwAway();
1735  * @return {!number}
1736  */
1737 ListView.prototype.firstVisibleRow = function() {
1738     return this.rowAtScrollOffset(this.scrollView.contentOffset());
1742  * @return {!number}
1743  */
1744 ListView.prototype.lastVisibleRow = function() {
1745     return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
1749  * @param {!ScrollView} scrollView
1750  */
1751 ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
1752     this.setNeedsUpdateCells(true);
1756  * @param {!ScrollView} scrollView
1757  */
1758 ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
1759     this.setNeedsUpdateCells(true);
1763  * @param {!ScrollView} scrollView
1764  */
1765 ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
1766     this.setNeedsUpdateCells(true);
1769 ListView.prototype.updateCells = function() {
1770     var firstVisibleRow = this.firstVisibleRow();
1771     var lastVisibleRow = this.lastVisibleRow();
1772     console.assert(firstVisibleRow <= lastVisibleRow);
1773     for (var c in this._cells) {
1774         var cell = this._cells[c];
1775         if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
1776             this.throwAwayCell(cell);
1777     }
1778     for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
1779         var cell = this._cells[i];
1780         if (cell)
1781             cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
1782         else
1783             this.addCellIfNecessary(i);
1784     }
1785     this.setNeedsUpdateCells(false);
1789  * @return {!number} Width in pixels.
1790  */
1791 ListView.prototype.width = function() {
1792     return this._width;
1796  * @param {!number} width Width in pixels.
1797  */
1798 ListView.prototype.setWidth = function(width) {
1799     if (this._width === width)
1800         return;
1801     this._width = width;
1802     this.scrollView.setWidth(this._width);
1803     for (var c in this._cells) {
1804         this._cells[c].setWidth(this._width);
1805     }
1806     this.element.style.width = this._width + "px";
1807     this.setNeedsUpdateCells(true);
1811  * @return {!number} Height in pixels.
1812  */
1813 ListView.prototype.height = function() {
1814     return this.scrollView.height();
1818  * @param {!number} height Height in pixels.
1819  */
1820 ListView.prototype.setHeight = function(height) {
1821     this.scrollView.setHeight(height);
1825  * @param {?Event} event
1826  */
1827 ListView.prototype.onClick = function(event) {
1828     var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
1829     if (!clickedCellElement)
1830         return;
1831     var clickedCell = clickedCellElement.$view;
1832     if (clickedCell.row !== this.selectedRow)
1833         this.select(clickedCell.row);
1837  * @param {!number} row
1838  */
1839 ListView.prototype.select = function(row) {
1840     if (this.selectedRow === row)
1841         return;
1842     this.deselect();
1843     if (row === ListView.NoSelection)
1844         return;
1845     this.selectedRow = row;
1846     var selectedCell = this._cells[this.selectedRow];
1847     if (selectedCell)
1848         selectedCell.setSelected(true);
1851 ListView.prototype.deselect = function() {
1852     if (this.selectedRow === ListView.NoSelection)
1853         return;
1854     var selectedCell = this._cells[this.selectedRow];
1855     if (selectedCell)
1856         selectedCell.setSelected(false);
1857     this.selectedRow = ListView.NoSelection;
1861  * @param {!number} row
1862  * @param {!boolean} animate
1863  */
1864 ListView.prototype.scrollToRow = function(row, animate) {
1865     this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
1869  * @constructor
1870  * @extends View
1871  * @param {!ScrollView} scrollView
1872  */
1873 function ScrubbyScrollBar(scrollView) {
1874     View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));
1876     /**
1877      * @type {!Element}
1878      * @const
1879      */
1880     this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
1881     this.element.appendChild(this.thumb);
1883     /**
1884      * @type {!ScrollView}
1885      * @const
1886      */
1887     this.scrollView = scrollView;
1889     /**
1890      * @type {!number}
1891      * @protected
1892      */
1893     this._height = 0;
1894     /**
1895      * @type {!number}
1896      * @protected
1897      */
1898     this._thumbHeight = 0;
1899     /**
1900      * @type {!number}
1901      * @protected
1902      */
1903     this._thumbPosition = 0;
1905     this.setHeight(0);
1906     this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);
1908     /**
1909      * @type {?Animator}
1910      * @protected
1911      */
1912     this._thumbStyleTopAnimator = null;
1914     /** 
1915      * @type {?number}
1916      * @protected
1917      */
1918     this._timer = null;
1919     
1920     this.element.addEventListener("mousedown", this.onMouseDown, false);
1921     this.element.addEventListener("touchstart", this.onTouchStart, false);
1924 ScrubbyScrollBar.prototype = Object.create(View.prototype);
1926 ScrubbyScrollBar.ScrollInterval = 16;
1927 ScrubbyScrollBar.ThumbMargin = 2;
1928 ScrubbyScrollBar.ThumbHeight = 30;
1929 ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
1930 ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";
1933  * @param {?Event} event
1934  */
1935 ScrubbyScrollBar.prototype.onTouchStart = function(event) {
1936     var touch = event.touches[0];
1937     this._setThumbPositionFromEventPosition(touch.clientY);
1938     if (this._thumbStyleTopAnimator)
1939         this._thumbStyleTopAnimator.stop();
1940     this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
1941     window.addEventListener("touchmove", this.onWindowTouchMove, false);
1942     window.addEventListener("touchend", this.onWindowTouchEnd, false);
1943     event.stopPropagation();
1944     event.preventDefault();
1948  * @param {?Event} event
1949  */
1950 ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
1951     var touch = event.touches[0];
1952     this._setThumbPositionFromEventPosition(touch.clientY);
1953     event.stopPropagation();
1954     event.preventDefault();
1958  * @param {?Event} event
1959  */
1960 ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) {
1961     this._thumbStyleTopAnimator = new TransitionAnimator();
1962     this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
1963     this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
1964     this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
1965     this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
1966     this._thumbStyleTopAnimator.duration = 100;
1967     this._thumbStyleTopAnimator.start();
1969     window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1970     window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1971     clearInterval(this._timer);
1975  * @return {!number} Height of the view in pixels.
1976  */
1977 ScrubbyScrollBar.prototype.height = function() {
1978     return this._height;
1982  * @param {!number} height Height of the view in pixels.
1983  */
1984 ScrubbyScrollBar.prototype.setHeight = function(height) {
1985     if (this._height === height)
1986         return;
1987     this._height = height;
1988     this.element.style.height = this._height + "px";
1989     this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
1990     this._thumbPosition = 0;
1994  * @param {!number} height Height of the scroll bar thumb in pixels.
1995  */
1996 ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
1997     if (this._thumbHeight === height)
1998         return;
1999     this._thumbHeight = height;
2000     this.thumb.style.height = this._thumbHeight + "px";
2001     this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2002     this._thumbPosition = 0;
2006  * @param {number} position
2007  */
2008 ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) {
2009     var thumbMin = ScrubbyScrollBar.ThumbMargin;
2010     var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2;
2011     var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop;
2012     var thumbPosition = y - this._thumbHeight / 2;
2013     thumbPosition = Math.max(thumbPosition, thumbMin);
2014     thumbPosition = Math.min(thumbPosition, thumbMax);
2015     this.thumb.style.top = thumbPosition + "px";
2016     this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2;
2020  * @param {?Event} event
2021  */
2022 ScrubbyScrollBar.prototype.onMouseDown = function(event) {
2023     this._setThumbPositionFromEventPosition(event.clientY);
2025     window.addEventListener("mousemove", this.onWindowMouseMove, false);
2026     window.addEventListener("mouseup", this.onWindowMouseUp, false);
2027     if (this._thumbStyleTopAnimator)
2028         this._thumbStyleTopAnimator.stop();
2029     this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2030     event.stopPropagation();
2031     event.preventDefault();
2035  * @param {?Event} event
2036  */
2037 ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
2038     this._setThumbPositionFromEventPosition(event.clientY);
2042  * @param {?Event} event
2043  */
2044 ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) {
2045     this._thumbStyleTopAnimator = new TransitionAnimator();
2046     this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2047     this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2048     this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2049     this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2050     this._thumbStyleTopAnimator.duration = 100;
2051     this._thumbStyleTopAnimator.start();
2052     
2053     window.removeEventListener("mousemove", this.onWindowMouseMove, false);
2054     window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2055     clearInterval(this._timer);
2059  * @param {!Animator} animator
2060  */
2061 ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
2062     this.thumb.style.top = animator.currentValue + "px";
2065 ScrubbyScrollBar.prototype.onScrollTimer = function() {
2066     var scrollAmount = Math.pow(this._thumbPosition, 2) * 10;
2067     if (this._thumbPosition > 0)
2068         scrollAmount = -scrollAmount;
2069     this.scrollView.scrollBy(scrollAmount, false);
2073  * @constructor
2074  * @extends ListCell
2075  * @param {!Array} shortMonthLabels
2076  */
2077 function YearListCell(shortMonthLabels) {
2078     ListCell.call(this);
2079     this.element.classList.add(YearListCell.ClassNameYearListCell);
2080     this.element.style.height = YearListCell.Height + "px";
2082     /**
2083      * @type {!Element}
2084      * @const
2085      */
2086     this.label = createElement("div", YearListCell.ClassNameLabel, "----");
2087     this.element.appendChild(this.label);
2088     this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2089     this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2091     /**
2092      * @type {!Array} Array of the 12 month button elements.
2093      * @const
2094      */
2095     this.monthButtons = [];
2096     var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser);
2097     for (var r = 0; r < YearListCell.ButtonRows; ++r) {
2098         var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow);
2099         buttonsRow.setAttribute("role", "row");
2100         for (var c = 0; c < YearListCell.ButtonColumns; ++c) {
2101             var month = c + r * YearListCell.ButtonColumns;
2102             var button = createElement("div", YearListCell.ClassNameMonthButton, shortMonthLabels[month]);
2103             button.setAttribute("role", "gridcell");
2104             button.dataset.month = month;
2105             buttonsRow.appendChild(button);
2106             this.monthButtons.push(button);
2107         }
2108         monthChooserElement.appendChild(buttonsRow);
2109     }
2110     this.element.appendChild(monthChooserElement);
2112     /**
2113      * @type {!boolean}
2114      * @private
2115      */
2116     this._selected = false;
2117     /**
2118      * @type {!number}
2119      * @private
2120      */
2121     this._height = 0;
2124 YearListCell.prototype = Object.create(ListCell.prototype);
2126 YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25;
2127 YearListCell.BorderBottomWidth = 1;
2128 YearListCell.ButtonRows = 3;
2129 YearListCell.ButtonColumns = 4;
2130 YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121;
2131 YearListCell.ClassNameYearListCell = "year-list-cell";
2132 YearListCell.ClassNameLabel = "label";
2133 YearListCell.ClassNameMonthChooser = "month-chooser";
2134 YearListCell.ClassNameMonthButtonsRow = "month-buttons-row";
2135 YearListCell.ClassNameMonthButton = "month-button";
2136 YearListCell.ClassNameHighlighted = "highlighted";
2138 YearListCell._recycleBin = [];
2141  * @return {!Array}
2142  * @override
2143  */
2144 YearListCell.prototype._recycleBin = function() {
2145     return YearListCell._recycleBin;
2149  * @param {!number} row
2150  */
2151 YearListCell.prototype.reset = function(row) {
2152     this.row = row;
2153     this.label.textContent = row + 1;
2154     for (var i = 0; i < this.monthButtons.length; ++i) {
2155         this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted);
2156     }
2157     this.show();
2161  * @return {!number} The height in pixels.
2162  */
2163 YearListCell.prototype.height = function() {
2164     return this._height;
2168  * @param {!number} height Height in pixels.
2169  */
2170 YearListCell.prototype.setHeight = function(height) {
2171     if (this._height === height)
2172         return;
2173     this._height = height;
2174     this.element.style.height = this._height + "px";
2178  * @constructor
2179  * @extends ListView
2180  * @param {!Month} minimumMonth
2181  * @param {!Month} maximumMonth
2182  */
2183 function YearListView(minimumMonth, maximumMonth) {
2184     ListView.call(this);
2185     this.element.classList.add("year-list-view");
2187     /**
2188      * @type {?Month}
2189      */
2190     this.highlightedMonth = null;
2191     /**
2192      * @type {!Month}
2193      * @const
2194      * @protected
2195      */
2196     this._minimumMonth = minimumMonth;
2197     /**
2198      * @type {!Month}
2199      * @const
2200      * @protected
2201      */
2202     this._maximumMonth = maximumMonth;
2204     this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
2205     this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
2206     
2207     /**
2208      * @type {!Object}
2209      * @const
2210      * @protected
2211      */
2212     this._runningAnimators = {};
2213     /**
2214      * @type {!Array}
2215      * @const
2216      * @protected
2217      */
2218     this._animatingRows = [];
2219     /**
2220      * @type {!boolean}
2221      * @protected
2222      */
2223     this._ignoreMouseOutUntillNextMouseOver = false;
2224     
2225     /**
2226      * @type {!ScrubbyScrollBar}
2227      * @const
2228      */
2229     this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
2230     this.scrubbyScrollBar.attachTo(this);
2231     
2232     this.element.addEventListener("mouseover", this.onMouseOver, false);
2233     this.element.addEventListener("mouseout", this.onMouseOut, false);
2234     this.element.addEventListener("keydown", this.onKeyDown, false);
2235     this.element.addEventListener("touchstart", this.onTouchStart, false);
2238 YearListView.prototype = Object.create(ListView.prototype);
2240 YearListView.Height = YearListCell.SelectedHeight - 1;
2241 YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
2242 YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";
2245  * @param {?Event} event
2246  */
2247 YearListView.prototype.onTouchStart = function(event) {
2248     var touch = event.touches[0];
2249     var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
2250     if (!monthButtonElement)
2251         return;
2252     var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2253     var cell = cellElement.$view;
2254     this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2258  * @param {?Event} event
2259  */
2260 YearListView.prototype.onMouseOver = function(event) {
2261     var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2262     if (!monthButtonElement)
2263         return;
2264     var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2265     var cell = cellElement.$view;
2266     this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2267     this._ignoreMouseOutUntillNextMouseOver = false;
2271  * @param {?Event} event
2272  */
2273 YearListView.prototype.onMouseOut = function(event) {
2274     if (this._ignoreMouseOutUntillNextMouseOver)
2275         return;
2276     var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2277     if (!monthButtonElement) {
2278         this.dehighlightMonth();
2279     }
2283  * @param {!number} width Width in pixels.
2284  * @override
2285  */
2286 YearListView.prototype.setWidth = function(width) {
2287     ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
2288     this.element.style.width = width + "px";
2292  * @param {!number} height Height in pixels.
2293  * @override
2294  */
2295 YearListView.prototype.setHeight = function(height) {
2296     ListView.prototype.setHeight.call(this, height);
2297     this.scrubbyScrollBar.setHeight(height);
2301  * @enum {number}
2302  */
2303 YearListView.RowAnimationDirection = {
2304     Opening: 0,
2305     Closing: 1
2309  * @param {!number} row
2310  * @param {!YearListView.RowAnimationDirection} direction
2311  */
2312 YearListView.prototype._animateRow = function(row, direction) {
2313     var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
2314     var oldAnimator = this._runningAnimators[row];
2315     if (oldAnimator) {
2316         oldAnimator.stop();
2317         fromValue = oldAnimator.currentValue;
2318     }
2319     var cell = this.cellAtRow(row);
2320     var animator = new TransitionAnimator();
2321     animator.step = this.onCellHeightAnimatorStep;
2322     animator.setFrom(fromValue);
2323     animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height);
2324     animator.timingFunction = AnimationTimingFunction.EaseInOut;
2325     animator.duration = 300;
2326     animator.row = row;
2327     animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
2328     this._runningAnimators[row] = animator;
2329     this._animatingRows.push(row);
2330     this._animatingRows.sort();
2331     animator.start();
2335  * @param {?Animator} animator
2336  */
2337 YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) {
2338     delete this._runningAnimators[animator.row];
2339     var index = this._animatingRows.indexOf(animator.row);
2340     this._animatingRows.splice(index, 1);
2344  * @param {!Animator} animator
2345  */
2346 YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
2347     var cell = this.cellAtRow(animator.row);
2348     if (cell)
2349         cell.setHeight(animator.currentValue);
2350     this.updateCells();
2354  * @param {?Event} event
2355  */
2356 YearListView.prototype.onClick = function(event) {
2357     var oldSelectedRow = this.selectedRow;
2358     ListView.prototype.onClick.call(this, event);
2359     var year = this.selectedRow + 1;
2360     if (this.selectedRow !== oldSelectedRow) {
2361         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2362         this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2363         this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2364     } else {
2365         var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2366         if (!monthButton || monthButton.getAttribute("aria-disabled") == "true")
2367             return;
2368         var month = parseInt(monthButton.dataset.month, 10);
2369         this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2370         this.hide();
2371     }
2375  * @param {!number} scrollOffset
2376  * @return {!number}
2377  * @override
2378  */
2379 YearListView.prototype.rowAtScrollOffset = function(scrollOffset) {
2380     var remainingOffset = scrollOffset;
2381     var lastAnimatingRow = 0;
2382     var rowsWithIrregularHeight = this._animatingRows.slice();
2383     if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) {
2384         rowsWithIrregularHeight.push(this.selectedRow);
2385         rowsWithIrregularHeight.sort();
2386     }
2387     for (var i = 0; i < rowsWithIrregularHeight.length; ++i) {
2388         var row = rowsWithIrregularHeight[i];
2389         var animator = this._runningAnimators[row];
2390         var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight;
2391         if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) {
2392             return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2393         }
2394         remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
2395         if (remainingOffset <= (rowHeight - YearListCell.Height))
2396             return row;
2397         remainingOffset -= rowHeight - YearListCell.Height;
2398         lastAnimatingRow = row;
2399     }
2400     return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2404  * @param {!number} row
2405  * @return {!number}
2406  * @override
2407  */
2408 YearListView.prototype.scrollOffsetForRow = function(row) {
2409     var scrollOffset = row * YearListCell.Height;
2410     for (var i = 0; i < this._animatingRows.length; ++i) {
2411         var animatingRow = this._animatingRows[i];
2412         if (animatingRow >= row)
2413             break;
2414         var animator = this._runningAnimators[animatingRow];
2415         scrollOffset += animator.currentValue - YearListCell.Height;
2416     }
2417     if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
2418         scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
2419     }
2420     return scrollOffset;
2424  * @param {!number} row
2425  * @return {!YearListCell}
2426  * @override
2427  */
2428 YearListView.prototype.prepareNewCell = function(row) {
2429     var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
2430     cell.reset(row);
2431     cell.setSelected(this.selectedRow === row);
2432     for (var i = 0; i < cell.monthButtons.length; ++i) {
2433         var month = new Month(row + 1, i);
2434         cell.monthButtons[i].id = month.toString();
2435         cell.monthButtons[i].setAttribute("aria-disabled", this._minimumMonth > month || this._maximumMonth < month ? "true" : "false");
2436         cell.monthButtons[i].setAttribute("aria-label", month.toLocaleString());
2437     }
2438     if (this.highlightedMonth && row === this.highlightedMonth.year - 1) {
2439         var monthButton = cell.monthButtons[this.highlightedMonth.month];
2440         monthButton.classList.add(YearListCell.ClassNameHighlighted);
2441         // aira-activedescendant assumes both elements have layoutObjects, and
2442         // |monthButton| might have no layoutObject yet.
2443         var element = this.element;
2444         setTimeout(function() {
2445             element.setAttribute("aria-activedescendant", monthButton.id);
2446         }, 0);
2447     }
2448     var animator = this._runningAnimators[row];
2449     if (animator)
2450         cell.setHeight(animator.currentValue);
2451     else if (row === this.selectedRow)
2452         cell.setHeight(YearListCell.SelectedHeight);
2453     else
2454         cell.setHeight(YearListCell.Height);
2455     return cell;
2459  * @override
2460  */
2461 YearListView.prototype.updateCells = function() {
2462     var firstVisibleRow = this.firstVisibleRow();
2463     var lastVisibleRow = this.lastVisibleRow();
2464     console.assert(firstVisibleRow <= lastVisibleRow);
2465     for (var c in this._cells) {
2466         var cell = this._cells[c];
2467         if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
2468             this.throwAwayCell(cell);
2469     }
2470     for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
2471         var cell = this._cells[i];
2472         if (cell)
2473             cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
2474         else
2475             this.addCellIfNecessary(i);
2476     }
2477     this.setNeedsUpdateCells(false);
2481  * @override
2482  */
2483 YearListView.prototype.deselect = function() {
2484     if (this.selectedRow === ListView.NoSelection)
2485         return;
2486     var selectedCell = this._cells[this.selectedRow];
2487     if (selectedCell)
2488         selectedCell.setSelected(false);
2489     this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
2490     this.selectedRow = ListView.NoSelection;
2491     this.setNeedsUpdateCells(true);
2494 YearListView.prototype.deselectWithoutAnimating = function() {
2495     if (this.selectedRow === ListView.NoSelection)
2496         return;
2497     var selectedCell = this._cells[this.selectedRow];
2498     if (selectedCell) {
2499         selectedCell.setSelected(false);
2500         selectedCell.setHeight(YearListCell.Height);
2501     }
2502     this.selectedRow = ListView.NoSelection;
2503     this.setNeedsUpdateCells(true);
2507  * @param {!number} row
2508  * @override
2509  */
2510 YearListView.prototype.select = function(row) {
2511     if (this.selectedRow === row)
2512         return;
2513     this.deselect();
2514     if (row === ListView.NoSelection)
2515         return;
2516     this.selectedRow = row;
2517     if (this.selectedRow !== ListView.NoSelection) {
2518         var selectedCell = this._cells[this.selectedRow];
2519         this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening);
2520         if (selectedCell)
2521             selectedCell.setSelected(true);
2522         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2523         this.highlightMonth(new Month(this.selectedRow + 1, month));
2524     }
2525     this.setNeedsUpdateCells(true);
2529  * @param {!number} row
2530  */
2531 YearListView.prototype.selectWithoutAnimating = function(row) {
2532     if (this.selectedRow === row)
2533         return;
2534     this.deselectWithoutAnimating();
2535     if (row === ListView.NoSelection)
2536         return;
2537     this.selectedRow = row;
2538     if (this.selectedRow !== ListView.NoSelection) {
2539         var selectedCell = this._cells[this.selectedRow];
2540         if (selectedCell) {
2541             selectedCell.setSelected(true);
2542             selectedCell.setHeight(YearListCell.SelectedHeight);
2543         }
2544         var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2545         this.highlightMonth(new Month(this.selectedRow + 1, month));
2546     }
2547     this.setNeedsUpdateCells(true);
2551  * @param {!Month} month
2552  * @return {?HTMLDivElement}
2553  */
2554 YearListView.prototype.buttonForMonth = function(month) {
2555     if (!month)
2556         return null;
2557     var row = month.year - 1;
2558     var cell = this.cellAtRow(row);
2559     if (!cell)
2560         return null;
2561     return cell.monthButtons[month.month];
2564 YearListView.prototype.dehighlightMonth = function() {
2565     if (!this.highlightedMonth)
2566         return;
2567     var monthButton = this.buttonForMonth(this.highlightedMonth);
2568     if (monthButton) {
2569         monthButton.classList.remove(YearListCell.ClassNameHighlighted);
2570     }
2571     this.highlightedMonth = null;
2572     this.element.removeAttribute("aria-activedescendant");
2576  * @param {!Month} month
2577  */
2578 YearListView.prototype.highlightMonth = function(month) {
2579     if (this.highlightedMonth && this.highlightedMonth.equals(month))
2580         return;
2581     this.dehighlightMonth();
2582     this.highlightedMonth = month;
2583     if (!this.highlightedMonth)
2584         return;
2585     var monthButton = this.buttonForMonth(this.highlightedMonth);
2586     if (monthButton) {
2587         monthButton.classList.add(YearListCell.ClassNameHighlighted);
2588         this.element.setAttribute("aria-activedescendant", monthButton.id);
2589     }
2593  * @param {!Month} month
2594  */
2595 YearListView.prototype.show = function(month) {
2596     this._ignoreMouseOutUntillNextMouseOver = true;
2597     
2598     this.scrollToRow(month.year - 1, false);
2599     this.selectWithoutAnimating(month.year - 1);
2600     this.highlightMonth(month);
2603 YearListView.prototype.hide = function() {
2604     this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
2608  * @param {!Month} month
2609  */
2610 YearListView.prototype._moveHighlightTo = function(month) {
2611     this.highlightMonth(month);
2612     this.select(this.highlightedMonth.year - 1);
2614     this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
2615     this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2616     return true;
2620  * @param {?Event} event
2621  */
2622 YearListView.prototype.onKeyDown = function(event) {
2623     var key = event.keyIdentifier;
2624     var eventHandled = false;
2625     if (key == "U+0054") // 't' key.
2626         eventHandled = this._moveHighlightTo(Month.createFromToday());
2627     else if (this.highlightedMonth) {
2628         if (global.params.isLocaleRTL ? key == "Right" : key == "Left")
2629             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous());
2630         else if (key == "Up")
2631             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns));
2632         else if (global.params.isLocaleRTL ? key == "Left" : key == "Right")
2633             eventHandled = this._moveHighlightTo(this.highlightedMonth.next());
2634         else if (key == "Down")
2635             eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns));
2636         else if (key == "PageUp")
2637             eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear));
2638         else if (key == "PageDown")
2639             eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear));
2640         else if (key == "Enter") {
2641             this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth);
2642             this.hide();
2643             eventHandled = true;
2644         }
2645     } else if (key == "Up") {
2646         this.scrollView.scrollBy(-YearListCell.Height, true);
2647         eventHandled = true;
2648     } else if (key == "Down") {
2649         this.scrollView.scrollBy(YearListCell.Height, true);
2650         eventHandled = true;
2651     } else if (key == "PageUp") {
2652         this.scrollView.scrollBy(-this.scrollView.height(), true);
2653         eventHandled = true;
2654     } else if (key == "PageDown") {
2655         this.scrollView.scrollBy(this.scrollView.height(), true);
2656         eventHandled = true;
2657     }
2659     if (eventHandled) {
2660         event.stopPropagation();
2661         event.preventDefault();
2662     }
2666  * @constructor
2667  * @extends View
2668  * @param {!Month} minimumMonth
2669  * @param {!Month} maximumMonth
2670  */
2671 function MonthPopupView(minimumMonth, maximumMonth) {
2672     View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));
2674     /**
2675      * @type {!YearListView}
2676      * @const
2677      */
2678     this.yearListView = new YearListView(minimumMonth, maximumMonth);
2679     this.yearListView.attachTo(this);
2681     /**
2682      * @type {!boolean}
2683      */
2684     this.isVisible = false;
2686     this.element.addEventListener("click", this.onClick, false);
2689 MonthPopupView.prototype = Object.create(View.prototype);
2691 MonthPopupView.ClassNameMonthPopupView = "month-popup-view";
2693 MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) {
2694     this.isVisible = true;
2695     document.body.appendChild(this.element);
2696     this.yearListView.setWidth(calendarTableRect.width - 2);
2697     this.yearListView.setHeight(YearListView.Height);
2698     if (global.params.isLocaleRTL)
2699         this.yearListView.element.style.right = calendarTableRect.x + "px";
2700     else
2701         this.yearListView.element.style.left = calendarTableRect.x + "px";
2702     this.yearListView.element.style.top = calendarTableRect.y + "px";
2703     this.yearListView.show(initialMonth);
2704     this.yearListView.element.focus();
2707 MonthPopupView.prototype.hide = function() {
2708     if (!this.isVisible)
2709         return;
2710     this.isVisible = false;
2711     this.element.parentNode.removeChild(this.element);
2712     this.yearListView.hide();
2716  * @param {?Event} event
2717  */
2718 MonthPopupView.prototype.onClick = function(event) {
2719     if (event.target !== this.element)
2720         return;
2721     this.hide();
2725  * @constructor
2726  * @extends View
2727  * @param {!number} maxWidth Maximum width in pixels.
2728  */
2729 function MonthPopupButton(maxWidth) {
2730     View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));
2731     this.element.setAttribute("aria-label", global.params.axShowMonthSelector);
2733     /**
2734      * @type {!Element}
2735      * @const
2736      */
2737     this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
2738     this.element.appendChild(this.labelElement);
2740     /**
2741      * @type {!Element}
2742      * @const
2743      */
2744     this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle);
2745     this.disclosureTriangleIcon.innerHTML = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>";
2746     this.element.appendChild(this.disclosureTriangleIcon);
2748     /**
2749      * @type {!boolean}
2750      * @protected
2751      */
2752     this._useShortMonth = this._shouldUseShortMonth(maxWidth);
2753     this.element.style.maxWidth = maxWidth + "px";
2755     this.element.addEventListener("click", this.onClick, false);
2758 MonthPopupButton.prototype = Object.create(View.prototype);
2760 MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
2761 MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
2762 MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
2763 MonthPopupButton.EventTypeButtonClick = "buttonClick";
2766  * @param {!number} maxWidth Maximum available width in pixels.
2767  * @return {!boolean}
2768  */
2769 MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) {
2770     document.body.appendChild(this.element);
2771     var month = Month.Maximum;
2772     for (var i = 0; i < MonthsPerYear; ++i) {
2773         this.labelElement.textContent = month.toLocaleString();
2774         if (this.element.offsetWidth > maxWidth)
2775             return true;
2776         month = month.previous();
2777     }
2778     document.body.removeChild(this.element);
2779     return false;
2783  * @param {!Month} month
2784  */
2785 MonthPopupButton.prototype.setCurrentMonth = function(month) {
2786     this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
2790  * @param {?Event} event
2791  */
2792 MonthPopupButton.prototype.onClick = function(event) {
2793     this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
2797  * @constructor
2798  * @extends View
2799  */
2800 function CalendarNavigationButton() {
2801     View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
2802     /**
2803      * @type {number} Threshold for starting repeating clicks in milliseconds.
2804      */
2805     this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
2806     /**
2807      * @type {number} Interval between reapeating clicks in milliseconds.
2808      */
2809     this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
2810     /**
2811      * @type {?number} The ID for the timeout that triggers the repeating clicks.
2812      */
2813     this._timer = null;
2814     this.element.addEventListener("click", this.onClick, false);
2815     this.element.addEventListener("mousedown", this.onMouseDown, false);
2816     this.element.addEventListener("touchstart", this.onTouchStart, false);
2819 CalendarNavigationButton.prototype = Object.create(View.prototype);
2821 CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600;
2822 CalendarNavigationButton.DefaultRepeatingClicksInterval = 300;
2823 CalendarNavigationButton.LeftMargin = 4;
2824 CalendarNavigationButton.Width = 24;
2825 CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button";
2826 CalendarNavigationButton.EventTypeButtonClick = "buttonClick";
2827 CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick";
2830  * @param {!boolean} disabled
2831  */
2832 CalendarNavigationButton.prototype.setDisabled = function(disabled) {
2833     this.element.disabled = disabled;
2837  * @param {?Event} event
2838  */
2839 CalendarNavigationButton.prototype.onClick = function(event) {
2840     this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
2844  * @param {?Event} event
2845  */
2846 CalendarNavigationButton.prototype.onTouchStart = function(event) {
2847     if (this._timer !== null)
2848         return;
2849     this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2850     window.addEventListener("touchend", this.onWindowTouchEnd, false);
2854  * @param {?Event} event
2855  */
2856 CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
2857     if (this._timer === null)
2858         return;
2859     clearTimeout(this._timer);
2860     this._timer = null;
2861     window.removeEventListener("touchend", this.onWindowMouseUp, false);
2865  * @param {?Event} event
2866  */
2867 CalendarNavigationButton.prototype.onMouseDown = function(event) {
2868     if (this._timer !== null)
2869         return;
2870     this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2871     window.addEventListener("mouseup", this.onWindowMouseUp, false);
2875  * @param {?Event} event
2876  */
2877 CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
2878     if (this._timer === null)
2879         return;
2880     clearTimeout(this._timer);
2881     this._timer = null;
2882     window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2886  * @param {?Event} event
2887  */
2888 CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
2889     this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
2890     this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
2894  * @constructor
2895  * @extends View
2896  * @param {!CalendarPicker} calendarPicker
2897  */
2898 function CalendarHeaderView(calendarPicker) {
2899     View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
2900     this.calendarPicker = calendarPicker;
2901     this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
2902     
2903     var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
2904     this.element.appendChild(titleElement);
2906     /**
2907      * @type {!MonthPopupButton}
2908      */
2909     this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
2910     this.monthPopupButton.attachTo(titleElement);
2912     /**
2913      * @type {!CalendarNavigationButton}
2914      * @const
2915      */
2916     this._previousMonthButton = new CalendarNavigationButton();
2917     this._previousMonthButton.attachTo(this);
2918     this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2919     this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2920     this._previousMonthButton.element.setAttribute("aria-label", global.params.axShowPreviousMonth);
2922     /**
2923      * @type {!CalendarNavigationButton}
2924      * @const
2925      */
2926     this._todayButton = new CalendarNavigationButton();
2927     this._todayButton.attachTo(this);
2928     this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2929     this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton);
2930     var monthContainingToday = Month.createFromToday();
2931     this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2932     this._todayButton.element.setAttribute("aria-label", global.params.todayLabel);
2934     /**
2935      * @type {!CalendarNavigationButton}
2936      * @const
2937      */
2938     this._nextMonthButton = new CalendarNavigationButton();
2939     this._nextMonthButton.attachTo(this);
2940     this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2941     this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2942     this._nextMonthButton.element.setAttribute("aria-label", global.params.axShowNextMonth);
2944     if (global.params.isLocaleRTL) {
2945         this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2946         this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2947     } else {
2948         this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2949         this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2950     }
2953 CalendarHeaderView.prototype = Object.create(View.prototype);
2955 CalendarHeaderView.Height = 24;
2956 CalendarHeaderView.BottomMargin = 10;
2957 CalendarHeaderView._ForwardTriangle = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>";
2958 CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>";
2959 CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view";
2960 CalendarHeaderView.ClassNameCalendarTitle = "calendar-title";
2961 CalendarHeaderView.ClassNameTodayButton = "today-button";
2963 CalendarHeaderView.prototype.onCurrentMonthChanged = function() {
2964     this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth());
2965     this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
2966     this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
2969 CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) {
2970     if (sender === this._previousMonthButton)
2971         this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation);
2972     else if (sender === this._nextMonthButton)
2973         this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation);
2974     else
2975         this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
2979  * @param {!boolean} disabled
2980  */
2981 CalendarHeaderView.prototype.setDisabled = function(disabled) {
2982     this.disabled = disabled;
2983     this.monthPopupButton.element.disabled = this.disabled;
2984     this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
2985     this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
2986     var monthContainingToday = Month.createFromToday();
2987     this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2991  * @constructor
2992  * @extends ListCell
2993  */
2994 function DayCell() {
2995     ListCell.call(this);
2996     this.element.classList.add(DayCell.ClassNameDayCell);
2997     this.element.style.width = DayCell.Width + "px";
2998     this.element.style.height = DayCell.Height + "px";
2999     this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px";
3000     this.element.setAttribute("role", "gridcell");
3001     /**
3002      * @type {?Day}
3003      */
3004     this.day = null;
3007 DayCell.prototype = Object.create(ListCell.prototype);
3009 DayCell.Width = 34;
3010 DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20;
3011 DayCell.PaddingSize = 1;
3012 DayCell.ClassNameDayCell = "day-cell";
3013 DayCell.ClassNameHighlighted = "highlighted";
3014 DayCell.ClassNameDisabled = "disabled";
3015 DayCell.ClassNameCurrentMonth = "current-month";
3016 DayCell.ClassNameToday = "today";
3018 DayCell._recycleBin = [];
3020 DayCell.recycleOrCreate = function() {
3021     return DayCell._recycleBin.pop() || new DayCell();
3025  * @return {!Array}
3026  * @override
3027  */
3028 DayCell.prototype._recycleBin = function() {
3029     return DayCell._recycleBin;
3033  * @override
3034  */
3035 DayCell.prototype.throwAway = function() {
3036     ListCell.prototype.throwAway.call(this);
3037     this.day = null;
3041  * @param {!boolean} highlighted
3042  */
3043 DayCell.prototype.setHighlighted = function(highlighted) {
3044     if (highlighted) {
3045         this.element.classList.add(DayCell.ClassNameHighlighted);
3046         this.element.setAttribute("aria-selected", "true");
3047     } else {
3048         this.element.classList.remove(DayCell.ClassNameHighlighted);
3049         this.element.setAttribute("aria-selected", "false");
3050     }
3054  * @param {!boolean} disabled
3055  */
3056 DayCell.prototype.setDisabled = function(disabled) {
3057     if (disabled)
3058         this.element.classList.add(DayCell.ClassNameDisabled);
3059     else
3060         this.element.classList.remove(DayCell.ClassNameDisabled);
3064  * @param {!boolean} selected
3065  */
3066 DayCell.prototype.setIsInCurrentMonth = function(selected) {
3067     if (selected)
3068         this.element.classList.add(DayCell.ClassNameCurrentMonth);
3069     else
3070         this.element.classList.remove(DayCell.ClassNameCurrentMonth);
3074  * @param {!boolean} selected
3075  */
3076 DayCell.prototype.setIsToday = function(selected) {
3077     if (selected)
3078         this.element.classList.add(DayCell.ClassNameToday);
3079     else
3080         this.element.classList.remove(DayCell.ClassNameToday);
3084  * @param {!Day} day
3085  */
3086 DayCell.prototype.reset = function(day) {
3087     this.day = day;
3088     this.element.textContent = localizeNumber(this.day.date.toString());
3089     this.element.setAttribute("aria-label", this.day.format());
3090     this.element.id = this.day.toString();
3091     this.show();
3095  * @constructor
3096  * @extends ListCell
3097  */
3098 function WeekNumberCell() {
3099     ListCell.call(this);
3100     this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell);
3101     this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px";
3102     this.element.style.height = WeekNumberCell.Height + "px";
3103     this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px";
3104     /**
3105      * @type {?Week}
3106      */
3107     this.week = null;
3110 WeekNumberCell.prototype = Object.create(ListCell.prototype);
3112 WeekNumberCell.Width = 48;
3113 WeekNumberCell.Height = DayCell.Height;
3114 WeekNumberCell.SeparatorWidth = 1;
3115 WeekNumberCell.PaddingSize = 1;
3116 WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell";
3117 WeekNumberCell.ClassNameHighlighted = "highlighted";
3118 WeekNumberCell.ClassNameDisabled = "disabled";
3120 WeekNumberCell._recycleBin = [];
3123  * @return {!Array}
3124  * @override
3125  */
3126 WeekNumberCell.prototype._recycleBin = function() {
3127     return WeekNumberCell._recycleBin;
3131  * @return {!WeekNumberCell}
3132  */
3133 WeekNumberCell.recycleOrCreate = function() {
3134     return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
3138  * @param {!Week} week
3139  */
3140 WeekNumberCell.prototype.reset = function(week) {
3141     this.week = week;
3142     this.element.id = week.toString();
3143     this.element.setAttribute("role", "gridcell");
3144     this.element.setAttribute("aria-label", window.pagePopupController.formatWeek(week.year, week.week, week.firstDay().format()));
3145     this.element.textContent = localizeNumber(this.week.week.toString());
3146     this.show();
3150  * @override
3151  */
3152 WeekNumberCell.prototype.throwAway = function() {
3153     ListCell.prototype.throwAway.call(this);
3154     this.week = null;
3157 WeekNumberCell.prototype.setHighlighted = function(highlighted) {
3158     if (highlighted) {
3159         this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
3160         this.element.setAttribute("aria-selected", "true");
3161     } else {
3162         this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
3163         this.element.setAttribute("aria-selected", "false");
3164     }
3167 WeekNumberCell.prototype.setDisabled = function(disabled) {
3168     if (disabled)
3169         this.element.classList.add(WeekNumberCell.ClassNameDisabled);
3170     else
3171         this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
3175  * @constructor
3176  * @extends View
3177  * @param {!boolean} hasWeekNumberColumn
3178  */
3179 function CalendarTableHeaderView(hasWeekNumberColumn) {
3180     View.call(this, createElement("div", "calendar-table-header-view"));
3181     if (hasWeekNumberColumn) {
3182         var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel);
3183         weekNumberLabelElement.style.width = WeekNumberCell.Width + "px";
3184         this.element.appendChild(weekNumberLabelElement);
3185     }
3186     for (var i = 0; i < DaysPerWeek; ++i) {
3187         var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek;
3188         var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]);
3189         labelElement.style.width = DayCell.Width + "px";
3190         this.element.appendChild(labelElement);
3191         if (getLanguage() === "ja") {
3192             if (weekDayNumber === 0)
3193                 labelElement.style.color = "red";
3194             else if (weekDayNumber === 6)
3195                 labelElement.style.color = "blue";
3196         }
3197     }
3200 CalendarTableHeaderView.prototype = Object.create(View.prototype);
3202 CalendarTableHeaderView.Height = 25;
3205  * @constructor
3206  * @extends ListCell
3207  */
3208 function CalendarRowCell() {
3209     ListCell.call(this);
3210     this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
3211     this.element.style.height = CalendarRowCell.Height + "px";
3212     this.element.setAttribute("role", "row");
3214     /**
3215      * @type {!Array}
3216      * @protected
3217      */
3218     this._dayCells = [];
3219     /**
3220      * @type {!number}
3221      */
3222     this.row = 0;
3223     /**
3224      * @type {?CalendarTableView}
3225      */
3226     this.calendarTableView = null;
3229 CalendarRowCell.prototype = Object.create(ListCell.prototype);
3231 CalendarRowCell.Height = DayCell.Height;
3232 CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";
3234 CalendarRowCell._recycleBin = [];
3237  * @return {!Array}
3238  * @override
3239  */
3240 CalendarRowCell.prototype._recycleBin = function() {
3241     return CalendarRowCell._recycleBin;
3245  * @param {!number} row
3246  * @param {!CalendarTableView} calendarTableView
3247  */
3248 CalendarRowCell.prototype.reset = function(row, calendarTableView) {
3249     this.row = row;
3250     this.calendarTableView = calendarTableView;
3251     if (this.calendarTableView.hasWeekNumberColumn) {
3252         var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row);
3253         var week = Week.createFromDay(middleDay);
3254         this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week);
3255         this.weekNumberCell.attachTo(this);
3256     }
3257     var day = calendarTableView.dayAtColumnAndRow(0, row);
3258     for (var i = 0; i < DaysPerWeek; ++i) {
3259         var dayCell = this.calendarTableView.prepareNewDayCell(day);
3260         dayCell.attachTo(this);
3261         this._dayCells.push(dayCell);
3262         day = day.next();
3263     }
3264     this.show();
3268  * @override
3269  */
3270 CalendarRowCell.prototype.throwAway = function() {
3271     ListCell.prototype.throwAway.call(this);
3272     if (this.weekNumberCell)
3273         this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell);
3274     this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView);
3275     this._dayCells.length = 0;
3279  * @constructor
3280  * @extends ListView
3281  * @param {!CalendarPicker} calendarPicker
3282  */
3283 function CalendarTableView(calendarPicker) {
3284     ListView.call(this);
3285     this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
3286     this.element.tabIndex = 0;
3288     /**
3289      * @type {!boolean}
3290      * @const
3291      */
3292     this.hasWeekNumberColumn = calendarPicker.type === "week";
3293     /**
3294      * @type {!CalendarPicker}
3295      * @const
3296      */
3297     this.calendarPicker = calendarPicker;
3298     /**
3299      * @type {!Object}
3300      * @const
3301      */
3302     this._dayCells = {};
3303     var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
3304     headerView.attachTo(this, this.scrollView);
3306     if (this.hasWeekNumberColumn) {
3307         this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
3308         /**
3309          * @type {?Array}
3310          * @const
3311          */
3312         this._weekNumberCells = [];
3313     } else {
3314         this.setWidth(DayCell.Width * DaysPerWeek);
3315     }
3316     
3317     /**
3318      * @type {!boolean}
3319      * @protected
3320      */
3321     this._ignoreMouseOutUntillNextMouseOver = false;
3323     this.element.addEventListener("click", this.onClick, false);
3324     this.element.addEventListener("mouseover", this.onMouseOver, false);
3325     this.element.addEventListener("mouseout", this.onMouseOut, false);
3327     // You shouldn't be able to use the mouse wheel to scroll.
3328     this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false);
3329     // You shouldn't be able to do gesture scroll.
3330     this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false);
3333 CalendarTableView.prototype = Object.create(ListView.prototype);
3335 CalendarTableView.BorderWidth = 1;
3336 CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";
3339  * @param {!number} scrollOffset
3340  * @return {!number}
3341  */
3342 CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
3343     return Math.floor(scrollOffset / CalendarRowCell.Height);
3347  * @param {!number} row
3348  * @return {!number}
3349  */
3350 CalendarTableView.prototype.scrollOffsetForRow = function(row) {
3351     return row * CalendarRowCell.Height;
3355  * @param {?Event} event
3356  */
3357 CalendarTableView.prototype.onClick = function(event) {
3358     if (this.hasWeekNumberColumn) {
3359         var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3360         if (weekNumberCellElement) {
3361             var weekNumberCell = weekNumberCellElement.$view;
3362             this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay());
3363             return;
3364         }
3365     }
3366     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3367     if (!dayCellElement)
3368         return;
3369     var dayCell = dayCellElement.$view;
3370     this.calendarPicker.selectRangeContainingDay(dayCell.day);
3374  * @param {?Event} event
3375  */
3376 CalendarTableView.prototype.onMouseOver = function(event) {
3377     if (this.hasWeekNumberColumn) {
3378         var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3379         if (weekNumberCellElement) {
3380             var weekNumberCell = weekNumberCellElement.$view;
3381             this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay());
3382             this._ignoreMouseOutUntillNextMouseOver = false;
3383             return;
3384         }
3385     }
3386     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3387     if (!dayCellElement)
3388         return;
3389     var dayCell = dayCellElement.$view;
3390     this.calendarPicker.highlightRangeContainingDay(dayCell.day);
3391     this._ignoreMouseOutUntillNextMouseOver = false;
3395  * @param {?Event} event
3396  */
3397 CalendarTableView.prototype.onMouseOut = function(event) {
3398     if (this._ignoreMouseOutUntillNextMouseOver)
3399         return;
3400     var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3401     if (!dayCellElement) {
3402         this.calendarPicker.highlightRangeContainingDay(null);
3403     }
3407  * @param {!number} row
3408  * @return {!CalendarRowCell}
3409  */
3410 CalendarTableView.prototype.prepareNewCell = function(row) {
3411     var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
3412     cell.reset(row, this);
3413     return cell;
3417  * @return {!number} Height in pixels.
3418  */
3419 CalendarTableView.prototype.height = function() {
3420     return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
3424  * @param {!number} height Height in pixels.
3425  */
3426 CalendarTableView.prototype.setHeight = function(height) {
3427     this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
3431  * @param {!Month} month
3432  * @param {!boolean} animate
3433  */
3434 CalendarTableView.prototype.scrollToMonth = function(month, animate) {
3435     var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
3436     this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
3440  * @param {!number} column
3441  * @param {!number} row
3442  * @return {!Day}
3443  */
3444 CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) {
3445     var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay;
3446     return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue);
3449 CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
3450 CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();
3453  * @param {!Day} day
3454  * @return {!Object} Object with properties column and row.
3455  */
3456 CalendarTableView.prototype.columnAndRowForDay = function(day) {
3457     var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay;
3458     var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay;
3459     var row = Math.floor(offset / DaysPerWeek);
3460     var column = offset - row * DaysPerWeek;
3461     return {
3462         column: column,
3463         row: row
3464     };
3467 CalendarTableView.prototype.updateCells = function() {
3468     ListView.prototype.updateCells.call(this);
3470     var selection = this.calendarPicker.selection();
3471     var firstDayInSelection;
3472     var lastDayInSelection;
3473     if (selection) {
3474         firstDayInSelection = selection.firstDay().valueOf();
3475         lastDayInSelection = selection.lastDay().valueOf();
3476     } else {
3477         firstDayInSelection = Infinity;
3478         lastDayInSelection = Infinity;
3479     }
3480     var highlight = this.calendarPicker.highlight();
3481     var firstDayInHighlight;
3482     var lastDayInHighlight;
3483     if (highlight) {
3484         firstDayInHighlight = highlight.firstDay().valueOf();
3485         lastDayInHighlight = highlight.lastDay().valueOf();
3486     } else {
3487         firstDayInHighlight = Infinity;
3488         lastDayInHighlight = Infinity;
3489     }
3490     var currentMonth = this.calendarPicker.currentMonth();
3491     var firstDayInCurrentMonth = currentMonth.firstDay().valueOf();
3492     var lastDayInCurrentMonth = currentMonth.lastDay().valueOf();
3493     var activeCell = null;
3494     for (var dayString in this._dayCells) {
3495         var dayCell = this._dayCells[dayString];
3496         var day = dayCell.day;
3497         dayCell.setIsToday(Day.createFromToday().equals(day));
3498         dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection);
3499         var isHighlighted = day >= firstDayInHighlight && day <= lastDayInHighlight;
3500         dayCell.setHighlighted(isHighlighted);
3501         if (isHighlighted) {
3502             if (firstDayInHighlight == lastDayInHighlight)
3503                 activeCell = dayCell;
3504             else if (this.calendarPicker.type == "month" && day == firstDayInHighlight)
3505                 activeCell = dayCell;
3506         }
3507         dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth);
3508         dayCell.setDisabled(!this.calendarPicker.isValidDay(day));
3509     }
3510     if (this.hasWeekNumberColumn) {
3511         for (var weekString in this._weekNumberCells) {
3512             var weekNumberCell = this._weekNumberCells[weekString];
3513             var week = weekNumberCell.week;
3514             var isWeekHighlighted = highlight && highlight.equals(week);
3515             weekNumberCell.setSelected(selection && selection.equals(week));
3516             weekNumberCell.setHighlighted(isWeekHighlighted);
3517             if (isWeekHighlighted)
3518                 activeCell = weekNumberCell;
3519             weekNumberCell.setDisabled(!this.calendarPicker.isValid(week));
3520         }
3521     }
3522     if (activeCell) {
3523         // Ensure a layoutObject because an element with no layoutObject doesn't post
3524         // activedescendant events. This shouldn't run in the above |for| loop
3525         // to avoid CSS transition.
3526         activeCell.element.offsetLeft;
3527         this.element.setAttribute("aria-activedescendant", activeCell.element.id);
3528     }
3532  * @param {!Day} day
3533  * @return {!DayCell}
3534  */
3535 CalendarTableView.prototype.prepareNewDayCell = function(day) {
3536     var dayCell = DayCell.recycleOrCreate();
3537     dayCell.reset(day);
3538     if (this.calendarPicker.type == "month")
3539         dayCell.element.setAttribute("aria-label", Month.createFromDay(day).toLocaleString());
3540     this._dayCells[dayCell.day.toString()] = dayCell;
3541     return dayCell;
3545  * @param {!Week} week
3546  * @return {!WeekNumberCell}
3547  */
3548 CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) {
3549     var weekNumberCell = WeekNumberCell.recycleOrCreate();
3550     weekNumberCell.reset(week);
3551     this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell;
3552     return weekNumberCell;
3556  * @param {!DayCell} dayCell
3557  */
3558 CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
3559     delete this._dayCells[dayCell.day.toString()];
3560     dayCell.throwAway();
3564  * @param {!WeekNumberCell} weekNumberCell
3565  */
3566 CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
3567     delete this._weekNumberCells[weekNumberCell.week.toString()];
3568     weekNumberCell.throwAway();
3572  * @constructor
3573  * @extends View
3574  * @param {!Object} config
3575  */
3576 function CalendarPicker(type, config) {
3577     View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
3578     this.element.classList.add(CalendarPicker.ClassNamePreparing);
3580     /**
3581      * @type {!string}
3582      * @const
3583      */
3584     this.type = type;
3585     if (this.type === "week")
3586         this._dateTypeConstructor = Week;
3587     else if (this.type === "month")
3588         this._dateTypeConstructor = Month;
3589     else
3590         this._dateTypeConstructor = Day;
3591     /**
3592      * @type {!Object}
3593      * @const
3594      */
3595     this.config = {};
3596     this._setConfig(config);
3597     /**
3598      * @type {!Month}
3599      * @const
3600      */
3601     this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
3602     /**
3603      * @type {!Month}
3604      * @const
3605      */
3606     this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
3607     if (global.params.isLocaleRTL)
3608         this.element.classList.add("rtl");
3609     /**
3610      * @type {!CalendarTableView}
3611      * @const
3612      */
3613     this.calendarTableView = new CalendarTableView(this);
3614     this.calendarTableView.hasNumberColumn = this.type === "week";
3615     /**
3616      * @type {!CalendarHeaderView}
3617      * @const
3618      */
3619     this.calendarHeaderView = new CalendarHeaderView(this);
3620     this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
3621     /**
3622      * @type {!MonthPopupView}
3623      * @const
3624      */
3625     this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth);
3626     this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth);
3627     this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide);
3628     this.calendarHeaderView.attachTo(this);
3629     this.calendarTableView.attachTo(this);
3630     /**
3631      * @type {!Month}
3632      * @protected
3633      */
3634     this._currentMonth = new Month(NaN, NaN);
3635     /**
3636      * @type {?DateType}
3637      * @protected
3638      */
3639     this._selection = null;
3640     /**
3641      * @type {?DateType}
3642      * @protected
3643      */
3644     this._highlight = null;
3645     this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
3646     document.body.addEventListener("keydown", this.onBodyKeyDown, false);
3648     window.addEventListener("resize", this.onWindowResize, false);
3650     /**
3651      * @type {!number}
3652      * @protected
3653      */
3654     this._height = -1;
3656     var initialSelection = parseDateString(config.currentValue);
3657     if (initialSelection) {
3658         this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
3659         this.setSelection(initialSelection);
3660     } else
3661         this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
3664 CalendarPicker.prototype = Object.create(View.prototype);
3666 CalendarPicker.Padding = 10;
3667 CalendarPicker.BorderWidth = 1;
3668 CalendarPicker.ClassNameCalendarPicker = "calendar-picker";
3669 CalendarPicker.ClassNamePreparing = "preparing";
3670 CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged";
3671 CalendarPicker.commitDelayMs = 100;
3674  * @param {!Event} event
3675  */
3676 CalendarPicker.prototype.onWindowResize = function(event) {
3677     this.element.classList.remove(CalendarPicker.ClassNamePreparing);
3678     window.removeEventListener("resize", this.onWindowResize, false);
3682  * @param {!YearListView} sender
3683  */
3684 CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
3685     this.monthPopupView.hide();
3686     this.calendarHeaderView.setDisabled(false);
3687     this.adjustHeight();
3691  * @param {!YearListView} sender
3692  * @param {!Month} month
3693  */
3694 CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
3695     this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
3699  * @param {!View|Node} parent
3700  * @param {?View|Node=} before
3701  * @override
3702  */
3703 CalendarPicker.prototype.attachTo = function(parent, before) {
3704     View.prototype.attachTo.call(this, parent, before);
3705     this.calendarTableView.element.focus();
3708 CalendarPicker.prototype.cleanup = function() {
3709     window.removeEventListener("resize", this.onWindowResize, false);
3710     this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false);
3711     // Month popup view might be attached to document.body.
3712     this.monthPopupView.hide();
3716  * @param {?MonthPopupButton} sender
3717  */
3718 CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) {
3719     var clientRect = this.calendarTableView.element.getBoundingClientRect();
3720     var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height);
3721     this.monthPopupView.show(this.currentMonth(), calendarTableRect);
3722     this.calendarHeaderView.setDisabled(true);
3723     this.adjustHeight();
3726 CalendarPicker.prototype._setConfig = function(config) {
3727     this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum;
3728     this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum;
3729     this.config.minimumValue = this.config.minimum.valueOf();
3730     this.config.maximumValue = this.config.maximum.valueOf();
3731     this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep;
3732     this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase;
3736  * @return {!Month}
3737  */
3738 CalendarPicker.prototype.currentMonth = function() {
3739     return this._currentMonth;
3743  * @enum {number}
3744  */
3745 CalendarPicker.NavigationBehavior = {
3746     None: 0,
3747     WithAnimation: 1
3751  * @param {!Month} month
3752  * @param {!CalendarPicker.NavigationBehavior} animate
3753  */
3754 CalendarPicker.prototype.setCurrentMonth = function(month, behavior) {
3755     if (month > this.maximumMonth)
3756         month = this.maximumMonth;
3757     else if (month < this.minimumMonth)
3758         month = this.minimumMonth;
3759     if (this._currentMonth.equals(month))
3760         return;
3761     this._currentMonth = month;
3762     this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation);
3763     this.adjustHeight();
3764     this.calendarTableView.setNeedsUpdateCells(true);
3765     this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, {
3766         target: this
3767     });
3770 CalendarPicker.prototype.adjustHeight = function() {
3771     var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row;
3772     var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row;
3773     var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1;
3774     var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2;
3775     var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2;
3776     this.setHeight(height);
3779 CalendarPicker.prototype.selection = function() {
3780     return this._selection;
3783 CalendarPicker.prototype.highlight = function() {
3784     return this._highlight;
3788  * @return {!Day}
3789  */
3790 CalendarPicker.prototype.firstVisibleDay = function() {
3791     var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
3792     var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3793     if (!firstVisibleDay)
3794         firstVisibleDay = Day.Minimum;
3795     return firstVisibleDay;
3799  * @return {!Day}
3800  */
3801 CalendarPicker.prototype.lastVisibleDay = function() { 
3802     var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row;
3803     var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3804     if (!lastVisibleDay)
3805         lastVisibleDay = Day.Maximum;
3806     return lastVisibleDay;
3810  * @param {?Day} day
3811  */
3812 CalendarPicker.prototype.selectRangeContainingDay = function(day) {
3813     var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
3814     this.setSelectionAndCommit(selection);
3818  * @param {?Day} day
3819  */
3820 CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
3821     var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
3822     this._setHighlight(highlight);
3826  * Select the specified date.
3827  * @param {?DateType} dayOrWeekOrMonth
3828  */
3829 CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
3830     if (!this._selection && !dayOrWeekOrMonth)
3831         return;
3832     if (this._selection && this._selection.equals(dayOrWeekOrMonth))
3833         return;
3834     var firstDayInSelection = dayOrWeekOrMonth.firstDay();    
3835     var lastDayInSelection = dayOrWeekOrMonth.lastDay();
3836     var candidateCurrentMonth = Month.createFromDay(firstDayInSelection);
3837     if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) {
3838         // Change current month if the selection is not visible at all.
3839         this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3840     } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) {
3841         // If the selection is partly visible, only change the current month if
3842         // doing so will make the whole selection visible.
3843         var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row;
3844         var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3845         var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row;
3846         var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3847         if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay)
3848             this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3849     }
3850     this._setHighlight(dayOrWeekOrMonth);
3851     if (!this.isValid(dayOrWeekOrMonth))
3852         return;
3853     this._selection = dayOrWeekOrMonth;
3854     this.calendarTableView.setNeedsUpdateCells(true);
3858  * Select the specified date, commit it, and close the popup.
3859  * @param {?DateType} dayOrWeekOrMonth
3860  */
3861 CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) {
3862     this.setSelection(dayOrWeekOrMonth);
3863     // Redraw the widget immidiately, and wait for some time to give feedback to
3864     // a user.
3865     this.element.offsetLeft;
3866     var value = this._selection.toString();
3867     if (CalendarPicker.commitDelayMs == 0) {
3868         // For testing.
3869         window.pagePopupController.setValueAndClosePopup(0, value);
3870     } else if (CalendarPicker.commitDelayMs < 0) {
3871         // For testing.
3872         window.pagePopupController.setValue(value);
3873     } else {
3874         setTimeout(function() {
3875             window.pagePopupController.setValueAndClosePopup(0, value);
3876         }, CalendarPicker.commitDelayMs);
3877     }
3881  * @param {?DateType} dayOrWeekOrMonth
3882  */
3883 CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
3884     if (!this._highlight && !dayOrWeekOrMonth)
3885         return;
3886     if (!dayOrWeekOrMonth && !this._highlight)
3887         return;
3888     if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
3889         return;
3890     this._highlight = dayOrWeekOrMonth;
3891     this.calendarTableView.setNeedsUpdateCells(true);
3895  * @param {!number} value
3896  * @return {!boolean}
3897  */
3898 CalendarPicker.prototype._stepMismatch = function(value) {
3899     var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase;
3900     return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep;
3904  * @param {!number} value
3905  * @return {!boolean}
3906  */
3907 CalendarPicker.prototype._outOfRange = function(value) {
3908     return value < this.config.minimumValue || value > this.config.maximumValue;
3912  * @param {!DateType} dayOrWeekOrMonth
3913  * @return {!boolean}
3914  */
3915 CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
3916     var value = dayOrWeekOrMonth.valueOf();
3917     return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
3921  * @param {!Day} day
3922  * @return {!boolean}
3923  */
3924 CalendarPicker.prototype.isValidDay = function(day) {
3925     return this.isValid(this._dateTypeConstructor.createFromDay(day));
3929  * @param {!DateType} dateRange
3930  * @return {!boolean} Returns true if the highlight was changed.
3931  */
3932 CalendarPicker.prototype._moveHighlight = function(dateRange) {
3933     if (!dateRange)
3934         return false;
3935     if (this._outOfRange(dateRange.valueOf()))
3936         return false;
3937     if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
3938         this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
3939     this._setHighlight(dateRange);
3940     return true;
3944  * @param {?Event} event
3945  */
3946 CalendarPicker.prototype.onCalendarTableKeyDown = function(event) {
3947     var key = event.keyIdentifier;
3948     var eventHandled = false;
3949     if (key == "U+0054") { // 't' key.
3950         this.selectRangeContainingDay(Day.createFromToday());
3951         eventHandled = true;
3952     } else if (key == "PageUp") {
3953         var previousMonth = this.currentMonth().previous();
3954         if (previousMonth && previousMonth >= this.config.minimumValue) {
3955             this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3956             eventHandled = true;
3957         }
3958     } else if (key == "PageDown") {
3959         var nextMonth = this.currentMonth().next();
3960         if (nextMonth && nextMonth >= this.config.minimumValue) {
3961             this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3962             eventHandled = true;
3963         }
3964     } else if (this._highlight) {
3965         if (global.params.isLocaleRTL ? key == "Right" : key == "Left") {
3966             eventHandled = this._moveHighlight(this._highlight.previous());
3967         } else if (key == "Up") {
3968             eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1));
3969         } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") {
3970             eventHandled = this._moveHighlight(this._highlight.next());
3971         } else if (key == "Down") {
3972             eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1));
3973         } else if (key == "Enter") {
3974             this.setSelectionAndCommit(this._highlight);
3975         }
3976     } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") {
3977         // Highlight range near the middle.
3978         this.highlightRangeContainingDay(this.currentMonth().middleDay());
3979         eventHandled = true;
3980     }
3982     if (eventHandled) {
3983         event.stopPropagation();
3984         event.preventDefault();
3985     }
3989  * @return {!number} Width in pixels.
3990  */
3991 CalendarPicker.prototype.width = function() {
3992     return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
3996  * @return {!number} Height in pixels.
3997  */
3998 CalendarPicker.prototype.height = function() {
3999     return this._height;
4003  * @param {!number} height Height in pixels.
4004  */
4005 CalendarPicker.prototype.setHeight = function(height) {
4006     if (this._height === height)
4007         return;
4008     this._height = height;
4009     resizeWindow(this.width(), this._height);
4010     this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2);
4014  * @param {?Event} event
4015  */
4016 CalendarPicker.prototype.onBodyKeyDown = function(event) {
4017     var key = event.keyIdentifier;
4018     var eventHandled = false;
4019     var offset = 0;
4020     switch (key) {
4021     case "U+001B": // Esc key.
4022         window.pagePopupController.closePopup();
4023         eventHandled = true;
4024         break;
4025     case "U+004D": // 'm' key.
4026         offset = offset || 1; // Fall-through.
4027     case "U+0059": // 'y' key.
4028         offset = offset || MonthsPerYear; // Fall-through.
4029     case "U+0044": // 'd' key.
4030         offset = offset || MonthsPerYear * 10;
4031         var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4032         this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation);
4033         var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4034         if (this._highlight) {
4035             var highlightMiddleDay = this._highlight.middleDay();
4036             this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek));
4037         }
4038         eventHandled  =true;
4039         break;
4040     }
4041     if (eventHandled) {
4042         event.stopPropagation();
4043         event.preventDefault();
4044     }
4047 if (window.dialogArguments) {
4048     initialize(dialogArguments);
4049 } else {
4050     window.addEventListener("message", handleMessage, false);