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