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