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