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