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