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