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