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