3 * Copyright (C) 2012 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
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
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.
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.
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"],
59 anchorRectInScreen: new Rectangle(0, 0, 0, 0),
64 // ----------------------------------------------------------------
70 function hasInaccuratePointingDevice() {
71 return matchMedia("(pointer: coarse)").matches;
75 * @return {!string} lowercase locale name. e.g. "en-us"
77 function getLocale() {
78 return (global.params.locale || "en-us").toLowerCase();
82 * @return {!string} lowercase language code. e.g. "en"
84 function getLanguage() {
85 var locale = getLocale();
86 var result = locale.match(/^([a-z]+)/);
93 * @param {!number} number
96 function localizeNumber(number) {
97 return window.pagePopupController.localizeNumberString(number);
104 var ImperialEraLimit = 2087;
107 * @param {!number} year
108 * @param {!number} month
111 function formatJapaneseImperialEra(year, month) {
112 // We don't show an imperial era if it is greater than 99 becase of space
114 if (year > ImperialEraLimit)
117 return "(平成" + localizeNumber(year - 1988) + "年)";
121 return "(昭和" + localizeNumber(year - 1925) + "年)";
123 return "(大正" + localizeNumber(year - 1911) + "年)";
124 if (year == 1912 && month >= 7)
127 return "(明治" + localizeNumber(year - 1867) + "年)";
133 function createUTCDate(year, month, date) {
134 var newDate = new Date(0);
135 newDate.setUTCFullYear(year);
136 newDate.setUTCMonth(month);
137 newDate.setUTCDate(date);
142 * @param {string} dateString
143 * @return {?Day|Week|Month}
145 function parseDateString(dateString) {
146 var month = Month.parse(dateString);
149 var week = Week.parse(dateString);
152 return Day.parse(dateString);
165 var MonthsPerYear = 12;
171 var MillisecondsPerDay = 24 * 60 * 60 * 1000;
177 var MillisecondsPerWeek = DaysPerWeek * MillisecondsPerDay;
182 function DateType() {
188 * @param {!number} year
189 * @param {!number} month
190 * @param {!number} date
192 function Day(year, month, date) {
193 var dateObject = createUTCDate(year, month, date);
194 if (isNaN(dateObject.valueOf()))
195 throw "Invalid date";
200 this.year = dateObject.getUTCFullYear();
205 this.month = dateObject.getUTCMonth();
210 this.date = dateObject.getUTCDate();
213 Day.prototype = Object.create(DateType.prototype);
215 Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)/;
218 * @param {!string} str
221 Day.parse = function(str) {
222 var match = Day.ISOStringRegExp.exec(str);
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);
232 * @param {!number} value
235 Day.createFromValue = function(millisecondsSinceEpoch) {
236 return Day.createFromDate(new Date(millisecondsSinceEpoch))
240 * @param {!Date} date
243 Day.createFromDate = function(date) {
244 if (isNaN(date.valueOf()))
245 throw "Invalid date";
246 return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
253 Day.createFromDay = function(day) {
260 Day.createFromToday = function() {
261 var now = new Date();
262 return new Day(now.getFullYear(), now.getMonth(), now.getDate());
266 * @param {!DateType} other
269 Day.prototype.equals = function(other) {
270 return other instanceof Day && this.year === other.year && this.month === other.month && this.date === other.date;
274 * @param {!number=} offset
277 Day.prototype.previous = function(offset) {
278 if (typeof offset === "undefined")
280 return new Day(this.year, this.month, this.date - offset);
284 * @param {!number=} offset
287 Day.prototype.next = function(offset) {
288 if (typeof offset === "undefined")
290 return new Day(this.year, this.month, this.date + offset);
296 Day.prototype.startDate = function() {
297 return createUTCDate(this.year, this.month, this.date);
303 Day.prototype.endDate = function() {
304 return createUTCDate(this.year, this.month, this.date + 1);
310 Day.prototype.firstDay = function() {
317 Day.prototype.middleDay = function() {
324 Day.prototype.lastDay = function() {
331 Day.prototype.valueOf = function() {
332 return createUTCDate(this.year, this.month, this.date).getTime();
338 Day.prototype.weekDay = function() {
339 return createUTCDate(this.year, this.month, this.date).getUTCDay();
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);
352 // See WebCore/platform/DateComponents.h.
353 Day.Minimum = Day.createFromValue(-62135596800000.0);
354 Day.Maximum = Day.createFromValue(8640000000000000.0);
356 // See WebCore/html/DayInputType.cpp.
357 Day.DefaultStep = 86400000;
358 Day.DefaultStepBase = 0;
363 * @param {!number} year
364 * @param {!number} week
366 function Week(year, 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;
385 Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;
387 // See WebCore/platform/DateComponents.h.
388 Week.Minimum = new Week(1, 1);
389 Week.Maximum = new Week(275760, 37);
391 // See WebCore/html/WeekInputType.cpp.
392 Week.DefaultStep = 604800000;
393 Week.DefaultStepBase = -259200000;
395 Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay();
398 * @param {!string} str
401 Week.parse = function(str) {
402 var match = Week.ISOStringRegExp.exec(str);
405 var year = parseInt(match[1], 10);
406 var week = parseInt(match[2], 10);
407 return new Week(year, week);
411 * @param {!number} millisecondsSinceEpoch
414 Week.createFromValue = function(millisecondsSinceEpoch) {
415 return Week.createFromDate(new Date(millisecondsSinceEpoch))
419 * @param {!Date} date
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())
428 else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
430 var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
431 return new Week(year, week);
438 Week.createFromDay = function(day) {
440 if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day)
442 else if (year > 1 && Week.weekOneStartDayForYear(year) > day)
444 var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek);
445 return new Week(year, week);
451 Week.createFromToday = function() {
452 var now = new Date();
453 return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
457 * @param {!number} year
460 Week.weekOneStartDateForYear = function(year) {
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);
469 * @param {!number} year
472 Week.weekOneStartDayForYear = function(year) {
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);
481 * @param {!number} year
484 Week.numberOfWeeksInYear = function(year) {
485 if (year < 1 || year > Week.Maximum.year)
487 else if (year === Week.Maximum.year)
488 return Week.Maximum.week;
489 return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
493 * @param {!Date} baseDate
494 * @param {!Date} date
497 Week._numberOfWeeksSinceDate = function(baseDate, date) {
498 return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek);
502 * @param {!DateType} other
505 Week.prototype.equals = function(other) {
506 return other instanceof Week && this.year === other.year && this.week === other.week;
510 * @param {!number=} offset
513 Week.prototype.previous = function(offset) {
514 if (typeof offset === "undefined")
516 return new Week(this.year, this.week - offset);
520 * @param {!number=} offset
523 Week.prototype.next = function(offset) {
524 if (typeof offset === "undefined")
526 return new Week(this.year, this.week + offset);
532 Week.prototype.startDate = function() {
533 var weekStartDate = Week.weekOneStartDateForYear(this.year);
534 weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
535 return weekStartDate;
541 Week.prototype.endDate = function() {
542 if (this.equals(Week.Maximum))
543 return Day.Maximum.startDate();
544 return this.next().startDate();
550 Week.prototype.firstDay = function() {
551 var weekOneStartDay = Week.weekOneStartDayForYear(this.year);
552 return weekOneStartDay.next((this.week - 1) * DaysPerWeek);
558 Week.prototype.middleDay = function() {
559 return this.firstDay().next(3);
565 Week.prototype.lastDay = function() {
566 if (this.equals(Week.Maximum))
568 return this.next().firstDay().previous();
574 Week.prototype.valueOf = function() {
575 return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime();
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);
591 * @param {!number} year
592 * @param {!number} month
594 function Month(year, month) {
599 this.year = year + Math.floor(month / MonthsPerYear);
604 this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear;
607 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
609 // See WebCore/platform/DateComponents.h.
610 Month.Minimum = new Month(1, 0);
611 Month.Maximum = new Month(275760, 8);
613 // See WebCore/html/MonthInputType.cpp.
614 Month.DefaultStep = 1;
615 Month.DefaultStepBase = 0;
618 * @param {!string} str
621 Month.parse = function(str) {
622 var match = Month.ISOStringRegExp.exec(str);
625 var year = parseInt(match[1], 10);
626 var month = parseInt(match[2], 10) - 1;
627 return new Month(year, month);
631 * @param {!number} value
634 Month.createFromValue = function(monthsSinceEpoch) {
635 return new Month(1970, monthsSinceEpoch)
639 * @param {!Date} date
642 Month.createFromDate = function(date) {
643 if (isNaN(date.valueOf()))
644 throw "Invalid date";
645 return new Month(date.getUTCFullYear(), date.getUTCMonth());
652 Month.createFromDay = function(day) {
653 return new Month(day.year, day.month);
659 Month.createFromToday = function() {
660 var now = new Date();
661 return new Month(now.getFullYear(), now.getMonth());
667 Month.prototype.containsDay = function(day) {
668 return this.year === day.year && this.month === day.month;
672 * @param {!Month} other
675 Month.prototype.equals = function(other) {
676 return other instanceof Month && this.year === other.year && this.month === other.month;
680 * @param {!number=} offset
683 Month.prototype.previous = function(offset) {
684 if (typeof offset === "undefined")
686 return new Month(this.year, this.month - offset);
690 * @param {!number=} offset
693 Month.prototype.next = function(offset) {
694 if (typeof offset === "undefined")
696 return new Month(this.year, this.month + offset);
702 Month.prototype.startDate = function() {
703 return createUTCDate(this.year, this.month, 1);
709 Month.prototype.endDate = function() {
710 if (this.equals(Month.Maximum))
711 return Day.Maximum.startDate();
712 return this.next().startDate();
718 Month.prototype.firstDay = function() {
719 return new Day(this.year, this.month, 1);
725 Month.prototype.middleDay = function() {
726 return new Day(this.year, this.month, this.month === 2 ? 14 : 15);
732 Month.prototype.lastDay = function() {
733 if (this.equals(Month.Maximum))
735 return this.next().firstDay().previous();
741 Month.prototype.valueOf = function() {
742 return (this.year - 1970) * MonthsPerYear + this.month;
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);
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);
767 Month.prototype.toShortLocaleString = function() {
768 return window.pagePopupController.formatShortMonth(this.year, this.month);
771 // ----------------------------------------------------------------
775 * @param {Event} event
777 function handleMessage(event) {
778 if (global.argumentsReceived)
780 global.argumentsReceived = true;
781 initialize(JSON.parse(event.data));
785 * @param {!Object} params
787 function setGlobalParams(params) {
789 for (name in global.params) {
790 if (typeof params[name] === "undefined")
791 console.warn("Missing argument: " + name);
793 for (name in params) {
794 global.params[name] = params[name];
799 * @param {!Object} args
801 function initialize(args) {
802 setGlobalParams(args);
803 if (global.params.suggestionValues && global.params.suggestionValues.length)
804 openSuggestionPicker();
806 openCalendarPicker();
809 function closePicker() {
811 global.picker.cleanup();
812 var main = $("main");
817 function openSuggestionPicker() {
819 global.picker = new SuggestionPicker($("main"), global.params);
822 function openCalendarPicker() {
824 global.picker = new CalendarPicker(global.params.mode, global.params);
825 global.picker.attachTo($("main"));
831 function EventEmitter() {
835 * @param {!string} type
836 * @param {!function({...*})} callback
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);
847 EventEmitter.prototype.hasListener = function(type) {
848 if (!this._callbacks)
850 var callbacksForType = this._callbacks[type];
851 if (!callbacksForType)
853 return callbacksForType.length > 0;
857 * @param {!string} type
858 * @param {!function(Object)} callback
860 EventEmitter.prototype.removeListener = function(type, callback) {
861 if (!this._callbacks)
863 var callbacksForType = this._callbacks[type];
864 if (!callbacksForType)
866 callbacksForType.splice(callbacksForType.indexOf(callback), 1);
867 if (callbacksForType.length === 0)
868 delete this._callbacks[type];
872 * @param {!string} type
873 * @param {...*} var_args
875 EventEmitter.prototype.dispatchEvent = function(type) {
876 if (!this._callbacks)
878 var callbacksForType = this._callbacks[type];
879 if (!callbacksForType)
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));
887 // Parameter t should be a number between 0 and 1.
888 var AnimationTimingFunction = {
892 EaseInOut: function(t){
895 return Math.pow(t, 3) / 2;
897 return Math.pow(t, 3) / 2 + 1;
903 * @extends EventEmitter
905 function AnimationManager() {
906 EventEmitter.call(this);
908 this._isRunning = false;
909 this._runningAnimatorCount = 0;
910 this._runningAnimators = {};
911 this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
914 AnimationManager.prototype = Object.create(EventEmitter.prototype);
916 AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";
918 AnimationManager.prototype._startAnimation = function() {
921 this._isRunning = true;
922 window.requestAnimationFrame(this._animationFrameCallbackBound);
925 AnimationManager.prototype._stopAnimation = function() {
926 if (!this._isRunning)
928 this._isRunning = false;
932 * @param {!Animator} animator
934 AnimationManager.prototype.add = function(animator) {
935 if (this._runningAnimators[animator.id])
937 this._runningAnimators[animator.id] = animator;
938 this._runningAnimatorCount++;
939 if (this._needsTimer())
940 this._startAnimation();
944 * @param {!Animator} animator
946 AnimationManager.prototype.remove = function(animator) {
947 if (!this._runningAnimators[animator.id])
949 delete this._runningAnimators[animator.id];
950 this._runningAnimatorCount--;
951 if (!this._needsTimer())
952 this._stopAnimation();
955 AnimationManager.prototype._animationFrameCallback = function(now) {
956 if (this._runningAnimatorCount > 0) {
957 for (var id in this._runningAnimators) {
958 this._runningAnimators[id].onAnimationFrame(now);
961 this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
963 window.requestAnimationFrame(this._animationFrameCallbackBound);
969 AnimationManager.prototype._needsTimer = function() {
970 return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
974 * @param {!string} type
975 * @param {!Function} callback
978 AnimationManager.prototype.on = function(type, callback) {
979 EventEmitter.prototype.on.call(this, type, callback);
980 if (this._needsTimer())
981 this._startAnimation();
985 * @param {!string} type
986 * @param {!Function} callback
989 AnimationManager.prototype.removeListener = function(type, callback) {
990 EventEmitter.prototype.removeListener.call(this, type, callback);
991 if (!this._needsTimer())
992 this._stopAnimation();
995 AnimationManager.shared = new AnimationManager();
999 * @extends EventEmitter
1001 function Animator() {
1002 EventEmitter.call(this);
1008 this.id = Animator._lastId++;
1012 this.duration = 100;
1021 this._isRunning = false;
1025 this.currentValue = 0;
1030 this._lastStepTime = 0;
1033 Animator.prototype = Object.create(EventEmitter.prototype);
1035 Animator._lastId = 0;
1037 Animator.EventTypeDidAnimationStop = "didAnimationStop";
1040 * @return {!boolean}
1042 Animator.prototype.isRunning = function() {
1043 return this._isRunning;
1046 Animator.prototype.start = function() {
1047 this._lastStepTime = performance.now();
1048 this._isRunning = true;
1049 AnimationManager.shared.add(this);
1052 Animator.prototype.stop = function() {
1053 if (!this._isRunning)
1055 this._isRunning = false;
1056 AnimationManager.shared.remove(this);
1057 this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
1061 * @param {!number} now
1063 Animator.prototype.onAnimationFrame = function(now) {
1064 this._lastStepTime = now;
1072 function TransitionAnimator() {
1073 Animator.call(this);
1092 this.progress = 0.0;
1096 this.timingFunction = AnimationTimingFunction.Linear;
1099 TransitionAnimator.prototype = Object.create(Animator.prototype);
1102 * @param {!number} value
1104 TransitionAnimator.prototype.setFrom = function(value) {
1106 this._delta = this._to - this._from;
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);
1117 * @param {!number} value
1119 TransitionAnimator.prototype.setTo = function(value) {
1121 this._delta = this._to - this._from;
1125 * @param {!number} now
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;
1133 if (this.progress === 1.0) {
1142 * @param {!number} initialVelocity
1143 * @param {!number} initialValue
1145 function FlingGestureAnimator(initialVelocity, initialValue) {
1146 Animator.call(this);
1150 this.initialVelocity = initialVelocity;
1154 this.initialValue = initialValue;
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)
1169 this._timeOffset = this._timeAtVelocity(startVelocity);
1174 this._positionOffset = this._valueAtTime(this._timeOffset);
1178 this.duration = this._timeAtVelocity(0);
1181 FlingGestureAnimator.prototype = Object.create(Animator.prototype);
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;
1190 * @param {!number} t
1192 FlingGestureAnimator.prototype._valueAtTime = function(t) {
1193 return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
1197 * @param {!number} t
1199 FlingGestureAnimator.prototype._velocityAtTime = function(t) {
1200 return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
1204 * @param {!number} v
1206 FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
1207 return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
1210 FlingGestureAnimator.prototype.start = function() {
1211 this._lastStepTime = performance.now();
1212 Animator.prototype.start.call(this);
1216 * @param {!number} now
1218 FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
1219 this._elapsedTime += now - this._lastStepTime;
1220 this._lastStepTime = now;
1221 if (this._elapsedTime + this._timeOffset >= this.duration) {
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;
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.
1238 function View(element) {
1239 EventEmitter.call(this);
1244 this.element = element || createElement("div");
1245 this.element.$view = this;
1246 this.bindCallbackMethods();
1249 View.prototype = Object.create(EventEmitter.prototype);
1252 * @param {!Element} ancestorElement
1255 View.prototype.offsetRelativeTo = function(ancestorElement) {
1258 var element = this.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};
1270 * @param {!View|Node} parent
1271 * @param {?View|Node=} before
1273 View.prototype.attachTo = function(parent, before) {
1274 if (parent instanceof View)
1275 return this.attachTo(parent.element, before);
1276 if (typeof before === "undefined")
1278 if (before instanceof View)
1279 before = before.element;
1280 parent.insertBefore(this.element, before);
1283 View.prototype.bindCallbackMethods = function() {
1284 for (var methodName in this) {
1285 if (!/^on[A-Z]/.test(methodName))
1287 if (this.hasOwnProperty(methodName))
1289 var method = this[methodName];
1290 if (!(method instanceof Function))
1292 this[methodName] = method.bind(this);
1300 function ScrollView() {
1301 View.call(this, createElement("div", ScrollView.ClassNameScrollView));
1306 this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
1307 this.element.appendChild(this.contentElement);
1311 this.minimumContentOffset = -Infinity;
1315 this.maximumContentOffset = Infinity;
1320 this._contentOffset = 0;
1335 this._scrollAnimator = null;
1339 this.delegate = null;
1343 this._lastTouchPosition = 0;
1347 this._lastTouchVelocity = 0;
1351 this._lastTouchTimeStamp = 0;
1353 this.element.addEventListener("mousewheel", this.onMouseWheel, false);
1354 this.element.addEventListener("touchstart", this.onTouchStart, false);
1357 * The content offset is partitioned so the it can go beyond the CSS limit
1362 this._partitionNumber = 0;
1365 ScrollView.prototype = Object.create(View.prototype);
1367 ScrollView.PartitionHeight = 100000;
1368 ScrollView.ClassNameScrollView = "scroll-view";
1369 ScrollView.ClassNameScrollViewContent = "scroll-view-content";
1372 * @param {!Event} event
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);
1386 * @param {!Event} event
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();
1401 * @param {!Event} event
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();
1409 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1410 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1414 * @param {!Animator} animator
1416 ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
1417 this.scrollTo(animator.currentValue, false);
1421 * @return {!Animator}
1423 ScrollView.prototype.scrollAnimator = function() {
1424 return this._scrollAnimator;
1428 * @param {!number} width
1430 ScrollView.prototype.setWidth = function(width) {
1431 console.assert(isFinite(width));
1432 if (this._width === width)
1434 this._width = width;
1435 this.element.style.width = this._width + "px";
1441 ScrollView.prototype.width = function() {
1446 * @param {!number} height
1448 ScrollView.prototype.setHeight = function(height) {
1449 console.assert(isFinite(height));
1450 if (this._height === height)
1452 this._height = height;
1453 this.element.style.height = height + "px";
1455 this.delegate.scrollViewDidChangeHeight(this);
1461 ScrollView.prototype.height = function() {
1462 return this._height;
1466 * @param {!Animator} animator
1468 ScrollView.prototype.onScrollAnimatorStep = function(animator) {
1469 this.setContentOffset(animator.currentValue);
1473 * @param {!number} offset
1474 * @param {?boolean} animate
1476 ScrollView.prototype.scrollTo = function(offset, animate) {
1477 console.assert(isFinite(offset));
1479 this.setContentOffset(offset);
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();
1493 * @param {!number} offset
1494 * @param {?boolean} animate
1496 ScrollView.prototype.scrollBy = function(offset, animate) {
1497 this.scrollTo(this._contentOffset + offset, animate);
1503 ScrollView.prototype.contentOffset = function() {
1504 return this._contentOffset;
1508 * @param {?Event} event
1510 ScrollView.prototype.onMouseWheel = function(event) {
1511 this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
1512 event.stopPropagation();
1513 event.preventDefault();
1518 * @param {!number} value
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)
1525 this._contentOffset = value;
1526 this._updateScrollContent();
1528 this.delegate.scrollViewDidChangeContentOffset(this);
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);
1541 * @param {!View|Node} parent
1542 * @param {?View|Node=} before
1545 ScrollView.prototype.attachTo = function(parent, before) {
1546 View.prototype.attachTo.call(this, parent, before);
1547 this._updateScrollContent();
1551 * @param {!number} offset
1553 ScrollView.prototype.contentPositionForContentOffset = function(offset) {
1554 return offset - this._partitionNumber * ScrollView.PartitionHeight;
1561 function ListCell() {
1562 View.call(this, createElement("div", ListCell.ClassNameListCell));
1578 ListCell.prototype = Object.create(View.prototype);
1580 ListCell.DefaultRecycleBinLimit = 64;
1581 ListCell.ClassNameListCell = "list-cell";
1582 ListCell.ClassNameHidden = "hidden";
1585 * @return {!Array} An array to keep thrown away cells.
1587 ListCell.prototype._recycleBin = function() {
1588 console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
1592 ListCell.prototype.throwAway = function() {
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);
1600 ListCell.prototype.show = function() {
1601 this.element.classList.remove(ListCell.ClassNameHidden);
1604 ListCell.prototype.hide = function() {
1605 this.element.classList.add(ListCell.ClassNameHidden);
1609 * @return {!number} Width in pixels.
1611 ListCell.prototype.width = function(){
1616 * @param {!number} width Width in pixels.
1618 ListCell.prototype.setWidth = function(width){
1619 if (this._width === width)
1621 this._width = width;
1622 this.element.style.width = this._width + "px";
1626 * @return {!number} Position in pixels.
1628 ListCell.prototype.position = function(){
1629 return this._position;
1633 * @param {!number} y Position in pixels.
1635 ListCell.prototype.setPosition = function(y) {
1636 if (this._position === y)
1639 this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
1643 * @param {!boolean} selected
1645 ListCell.prototype.setSelected = function(selected) {
1646 if (this._selected === selected)
1648 this._selected = selected;
1650 this.element.classList.add("selected");
1652 this.element.classList.remove("selected");
1659 function ListView() {
1660 View.call(this, createElement("div", ListView.ClassNameListView));
1661 this.element.tabIndex = 0;
1677 this.selectedRow = ListView.NoSelection;
1680 * @type {!ScrollView}
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);
1689 this.element.addEventListener("click", this.onClick, false);
1695 this._needsUpdateCells = false;
1698 ListView.prototype = Object.create(View.prototype);
1700 ListView.NoSelection = -1;
1701 ListView.ClassNameListView = "list-view";
1703 ListView.prototype.onAnimationFrameWillFinish = function() {
1704 if (this._needsUpdateCells)
1709 * @param {!boolean} needsUpdateCells
1711 ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
1712 if (this._needsUpdateCells === needsUpdateCells)
1714 this._needsUpdateCells = needsUpdateCells;
1715 if (this._needsUpdateCells)
1716 AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1718 AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1722 * @param {!number} row
1723 * @return {?ListCell}
1725 ListView.prototype.cellAtRow = function(row) {
1726 return this._cells[row];
1730 * @param {!number} offset Scroll offset in pixels.
1733 ListView.prototype.rowAtScrollOffset = function(offset) {
1734 console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
1739 * @param {!number} row
1740 * @return {!number} Scroll offset in pixels.
1742 ListView.prototype.scrollOffsetForRow = function(row) {
1743 console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
1748 * @param {!number} row
1749 * @return {!ListCell}
1751 ListView.prototype.addCellIfNecessary = function(row) {
1752 var cell = this._cells[row];
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;
1764 * @param {!number} row
1765 * @return {!ListCell}
1767 ListView.prototype.prepareNewCell = function(row) {
1768 console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
1769 return new ListCell();
1773 * @param {!ListCell} cell
1775 ListView.prototype.throwAwayCell = function(cell) {
1776 delete this._cells[cell.row];
1783 ListView.prototype.firstVisibleRow = function() {
1784 return this.rowAtScrollOffset(this.scrollView.contentOffset());
1790 ListView.prototype.lastVisibleRow = function() {
1791 return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
1795 * @param {!ScrollView} scrollView
1797 ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
1798 this.setNeedsUpdateCells(true);
1802 * @param {!ScrollView} scrollView
1804 ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
1805 this.setNeedsUpdateCells(true);
1809 * @param {!ScrollView} scrollView
1811 ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
1812 this.setNeedsUpdateCells(true);
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);
1824 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
1825 var cell = this._cells[i];
1827 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
1829 this.addCellIfNecessary(i);
1831 this.setNeedsUpdateCells(false);
1835 * @return {!number} Width in pixels.
1837 ListView.prototype.width = function() {
1842 * @param {!number} width Width in pixels.
1844 ListView.prototype.setWidth = function(width) {
1845 if (this._width === width)
1847 this._width = width;
1848 this.scrollView.setWidth(this._width);
1849 for (var c in this._cells) {
1850 this._cells[c].setWidth(this._width);
1852 this.element.style.width = this._width + "px";
1853 this.setNeedsUpdateCells(true);
1857 * @return {!number} Height in pixels.
1859 ListView.prototype.height = function() {
1860 return this.scrollView.height();
1864 * @param {!number} height Height in pixels.
1866 ListView.prototype.setHeight = function(height) {
1867 this.scrollView.setHeight(height);
1871 * @param {?Event} event
1873 ListView.prototype.onClick = function(event) {
1874 var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
1875 if (!clickedCellElement)
1877 var clickedCell = clickedCellElement.$view;
1878 if (clickedCell.row !== this.selectedRow)
1879 this.select(clickedCell.row);
1883 * @param {!number} row
1885 ListView.prototype.select = function(row) {
1886 if (this.selectedRow === row)
1889 if (row === ListView.NoSelection)
1891 this.selectedRow = row;
1892 var selectedCell = this._cells[this.selectedRow];
1894 selectedCell.setSelected(true);
1897 ListView.prototype.deselect = function() {
1898 if (this.selectedRow === ListView.NoSelection)
1900 var selectedCell = this._cells[this.selectedRow];
1902 selectedCell.setSelected(false);
1903 this.selectedRow = ListView.NoSelection;
1907 * @param {!number} row
1908 * @param {!boolean} animate
1910 ListView.prototype.scrollToRow = function(row, animate) {
1911 this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
1917 * @param {!ScrollView} scrollView
1919 function ScrubbyScrollBar(scrollView) {
1920 View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));
1926 this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
1927 this.element.appendChild(this.thumb);
1930 * @type {!ScrollView}
1933 this.scrollView = scrollView;
1944 this._thumbHeight = 0;
1949 this._thumbPosition = 0;
1952 this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);
1958 this._thumbStyleTopAnimator = null;
1966 this.element.addEventListener("mousedown", this.onMouseDown, false);
1967 this.element.addEventListener("touchstart", this.onTouchStart, false);
1970 ScrubbyScrollBar.prototype = Object.create(View.prototype);
1972 ScrubbyScrollBar.ScrollInterval = 16;
1973 ScrubbyScrollBar.ThumbMargin = 2;
1974 ScrubbyScrollBar.ThumbHeight = 30;
1975 ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
1976 ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";
1979 * @param {?Event} event
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();
1994 * @param {?Event} event
1996 ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
1997 var touch = event.touches[0];
1998 this._setThumbPositionFromEventPosition(touch.clientY);
1999 event.stopPropagation();
2000 event.preventDefault();
2004 * @param {?Event} event
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();
2015 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
2016 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
2017 clearInterval(this._timer);
2021 * @return {!number} Height of the view in pixels.
2023 ScrubbyScrollBar.prototype.height = function() {
2024 return this._height;
2028 * @param {!number} height Height of the view in pixels.
2030 ScrubbyScrollBar.prototype.setHeight = function(height) {
2031 if (this._height === height)
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;
2040 * @param {!number} height Height of the scroll bar thumb in pixels.
2042 ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
2043 if (this._thumbHeight === height)
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;
2052 * @param {number} position
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;
2066 * @param {?Event} event
2068 ScrubbyScrollBar.prototype.onMouseDown = function(event) {
2069 this._setThumbPositionFromEventPosition(event.clientY);
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();
2081 * @param {?Event} event
2083 ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
2084 this._setThumbPositionFromEventPosition(event.clientY);
2088 * @param {?Event} event
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();
2099 window.removeEventListener("mousemove", this.onWindowMouseMove, false);
2100 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2101 clearInterval(this._timer);
2105 * @param {!Animator} animator
2107 ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
2108 this.thumb.style.top = animator.currentValue + "px";
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);
2121 * @param {!Array} shortMonthLabels
2123 function YearListCell(shortMonthLabels) {
2124 ListCell.call(this);
2125 this.element.classList.add(YearListCell.ClassNameYearListCell);
2126 this.element.style.height = YearListCell.Height + "px";
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";
2138 * @type {!Array} Array of the 12 month button elements.
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);
2152 monthChooserElement.appendChild(buttonsRow);
2154 this.element.appendChild(monthChooserElement);
2160 this._selected = false;
2168 YearListCell.prototype = Object.create(ListCell.prototype);
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";
2182 YearListCell._recycleBin = [];
2188 YearListCell.prototype._recycleBin = function() {
2189 return YearListCell._recycleBin;
2193 * @param {!number} row
2195 YearListCell.prototype.reset = function(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);
2205 * @return {!number} The height in pixels.
2207 YearListCell.prototype.height = function() {
2208 return this._height;
2212 * @param {!number} height Height in pixels.
2214 YearListCell.prototype.setHeight = function(height) {
2215 if (this._height === height)
2217 this._height = height;
2218 this.element.style.height = this._height + "px";
2224 * @param {!Month} minimumMonth
2225 * @param {!Month} maximumMonth
2227 function YearListView(minimumMonth, maximumMonth) {
2228 ListView.call(this);
2229 this.element.classList.add("year-list-view");
2234 this.highlightedMonth = null;
2240 this._minimumMonth = minimumMonth;
2246 this._maximumMonth = maximumMonth;
2248 this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
2249 this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
2256 this._runningAnimators = {};
2262 this._animatingRows = [];
2267 this._ignoreMouseOutUntillNextMouseOver = false;
2270 * @type {!ScrubbyScrollBar}
2273 this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
2274 this.scrubbyScrollBar.attachTo(this);
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);
2282 YearListView.prototype = Object.create(ListView.prototype);
2284 YearListView.Height = YearListCell.SelectedHeight - 1;
2285 YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
2286 YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";
2289 * @param {?Event} event
2291 YearListView.prototype.onTouchStart = function(event) {
2292 var touch = event.touches[0];
2293 var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
2294 if (!monthButtonElement)
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)));
2302 * @param {?Event} event
2304 YearListView.prototype.onMouseOver = function(event) {
2305 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2306 if (!monthButtonElement)
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;
2315 * @param {?Event} event
2317 YearListView.prototype.onMouseOut = function(event) {
2318 if (this._ignoreMouseOutUntillNextMouseOver)
2320 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2321 if (!monthButtonElement) {
2322 this.dehighlightMonth();
2327 * @param {!number} width Width in pixels.
2330 YearListView.prototype.setWidth = function(width) {
2331 ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
2332 this.element.style.width = width + "px";
2336 * @param {!number} height Height in pixels.
2339 YearListView.prototype.setHeight = function(height) {
2340 ListView.prototype.setHeight.call(this, height);
2341 this.scrubbyScrollBar.setHeight(height);
2347 YearListView.RowAnimationDirection = {
2353 * @param {!number} row
2354 * @param {!YearListView.RowAnimationDirection} direction
2356 YearListView.prototype._animateRow = function(row, direction) {
2357 var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
2358 var oldAnimator = this._runningAnimators[row];
2361 fromValue = oldAnimator.currentValue;
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;
2371 animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
2372 this._runningAnimators[row] = animator;
2373 this._animatingRows.push(row);
2374 this._animatingRows.sort();
2379 * @param {?Animator} animator
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);
2388 * @param {!Animator} animator
2390 YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
2391 var cell = this.cellAtRow(animator.row);
2393 cell.setHeight(animator.currentValue);
2398 * @param {?Event} event
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);
2409 var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2412 var month = parseInt(monthButton.dataset.month, 10);
2413 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2419 * @param {!number} scrollOffset
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();
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);
2438 remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
2439 if (remainingOffset <= (rowHeight - YearListCell.Height))
2441 remainingOffset -= rowHeight - YearListCell.Height;
2442 lastAnimatingRow = row;
2444 return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2448 * @param {!number} row
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)
2458 var animator = this._runningAnimators[animatingRow];
2459 scrollOffset += animator.currentValue - YearListCell.Height;
2461 if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
2462 scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
2464 return scrollOffset;
2468 * @param {!number} row
2469 * @return {!YearListCell}
2472 YearListView.prototype.prepareNewCell = function(row) {
2473 var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
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);
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;
2483 var animator = this._runningAnimators[row];
2485 cell.setHeight(animator.currentValue);
2486 else if (row === this.selectedRow)
2487 cell.setHeight(YearListCell.SelectedHeight);
2489 cell.setHeight(YearListCell.Height);
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);
2505 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
2506 var cell = this._cells[i];
2508 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
2510 this.addCellIfNecessary(i);
2512 this.setNeedsUpdateCells(false);
2518 YearListView.prototype.deselect = function() {
2519 if (this.selectedRow === ListView.NoSelection)
2521 var selectedCell = this._cells[this.selectedRow];
2523 selectedCell.setSelected(false);
2524 this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
2525 this.selectedRow = ListView.NoSelection;
2526 this.setNeedsUpdateCells(true);
2529 YearListView.prototype.deselectWithoutAnimating = function() {
2530 if (this.selectedRow === ListView.NoSelection)
2532 var selectedCell = this._cells[this.selectedRow];
2534 selectedCell.setSelected(false);
2535 selectedCell.setHeight(YearListCell.Height);
2537 this.selectedRow = ListView.NoSelection;
2538 this.setNeedsUpdateCells(true);
2542 * @param {!number} row
2545 YearListView.prototype.select = function(row) {
2546 if (this.selectedRow === row)
2549 if (row === ListView.NoSelection)
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);
2556 selectedCell.setSelected(true);
2557 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2558 this.highlightMonth(new Month(this.selectedRow + 1, month));
2560 this.setNeedsUpdateCells(true);
2564 * @param {!number} row
2566 YearListView.prototype.selectWithoutAnimating = function(row) {
2567 if (this.selectedRow === row)
2569 this.deselectWithoutAnimating();
2570 if (row === ListView.NoSelection)
2572 this.selectedRow = row;
2573 if (this.selectedRow !== ListView.NoSelection) {
2574 var selectedCell = this._cells[this.selectedRow];
2576 selectedCell.setSelected(true);
2577 selectedCell.setHeight(YearListCell.SelectedHeight);
2579 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2580 this.highlightMonth(new Month(this.selectedRow + 1, month));
2582 this.setNeedsUpdateCells(true);
2586 * @param {!Month} month
2587 * @return {?HTMLButtonElement}
2589 YearListView.prototype.buttonForMonth = function(month) {
2592 var row = month.year - 1;
2593 var cell = this.cellAtRow(row);
2596 return cell.monthButtons[month.month];
2599 YearListView.prototype.dehighlightMonth = function() {
2600 if (!this.highlightedMonth)
2602 var monthButton = this.buttonForMonth(this.highlightedMonth);
2604 monthButton.classList.remove(YearListCell.ClassNameHighlighted);
2606 this.highlightedMonth = null;
2610 * @param {!Month} month
2612 YearListView.prototype.highlightMonth = function(month) {
2613 if (this.highlightedMonth && this.highlightedMonth.equals(month))
2615 this.dehighlightMonth();
2616 this.highlightedMonth = month;
2617 if (!this.highlightedMonth)
2619 var monthButton = this.buttonForMonth(this.highlightedMonth);
2621 monthButton.classList.add(YearListCell.ClassNameHighlighted);
2626 * @param {!Month} month
2628 YearListView.prototype.show = function(month) {
2629 this._ignoreMouseOutUntillNextMouseOver = true;
2631 this.scrollToRow(month.year - 1, false);
2632 this.selectWithoutAnimating(month.year - 1);
2633 this.highlightMonth(month);
2636 YearListView.prototype.hide = function() {
2637 this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
2641 * @param {!Month} month
2643 YearListView.prototype._moveHighlightTo = function(month) {
2644 this.highlightMonth(month);
2645 this.select(this.highlightedMonth.year - 1);
2647 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
2648 this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2653 * @param {?Event} event
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);
2676 eventHandled = true;
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;
2693 event.stopPropagation();
2694 event.preventDefault();
2701 * @param {!Month} minimumMonth
2702 * @param {!Month} maximumMonth
2704 function MonthPopupView(minimumMonth, maximumMonth) {
2705 View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));
2708 * @type {!YearListView}
2711 this.yearListView = new YearListView(minimumMonth, maximumMonth);
2712 this.yearListView.attachTo(this);
2717 this.isVisible = false;
2719 this.element.addEventListener("click", this.onClick, false);
2722 MonthPopupView.prototype = Object.create(View.prototype);
2724 MonthPopupView.ClassNameMonthPopupView = "month-popup-view";
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";
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();
2740 MonthPopupView.prototype.hide = function() {
2741 if (!this.isVisible)
2743 this.isVisible = false;
2744 this.element.parentNode.removeChild(this.element);
2745 this.yearListView.hide();
2749 * @param {?Event} event
2751 MonthPopupView.prototype.onClick = function(event) {
2752 if (event.target !== this.element)
2760 * @param {!number} maxWidth Maximum width in pixels.
2762 function MonthPopupButton(maxWidth) {
2763 View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));
2769 this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
2770 this.element.appendChild(this.labelElement);
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);
2784 this._useShortMonth = this._shouldUseShortMonth(maxWidth);
2785 this.element.style.maxWidth = maxWidth + "px";
2787 this.element.addEventListener("click", this.onClick, false);
2790 MonthPopupButton.prototype = Object.create(View.prototype);
2792 MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
2793 MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
2794 MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
2795 MonthPopupButton.EventTypeButtonClick = "buttonClick";
2798 * @param {!number} maxWidth Maximum available width in pixels.
2799 * @return {!boolean}
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)
2808 month = month.previous();
2810 document.body.removeChild(this.element);
2815 * @param {!Month} month
2817 MonthPopupButton.prototype.setCurrentMonth = function(month) {
2818 this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
2822 * @param {?Event} event
2824 MonthPopupButton.prototype.onClick = function(event) {
2825 this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
2832 function CalendarNavigationButton() {
2833 View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
2835 * @type {number} Threshold for starting repeating clicks in milliseconds.
2837 this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
2839 * @type {number} Interval between reapeating clicks in milliseconds.
2841 this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
2843 * @type {?number} The ID for the timeout that triggers the repeating clicks.
2846 this.element.addEventListener("click", this.onClick, false);
2847 this.element.addEventListener("mousedown", this.onMouseDown, false);
2848 this.element.addEventListener("touchstart", this.onTouchStart, false);
2851 CalendarNavigationButton.prototype = Object.create(View.prototype);
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";
2862 * @param {!boolean} disabled
2864 CalendarNavigationButton.prototype.setDisabled = function(disabled) {
2865 this.element.disabled = disabled;
2869 * @param {?Event} event
2871 CalendarNavigationButton.prototype.onClick = function(event) {
2872 this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
2876 * @param {?Event} event
2878 CalendarNavigationButton.prototype.onTouchStart = function(event) {
2879 if (this._timer !== null)
2881 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2882 window.addEventListener("touchend", this.onWindowTouchEnd, false);
2886 * @param {?Event} event
2888 CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
2889 if (this._timer === null)
2891 clearTimeout(this._timer);
2893 window.removeEventListener("touchend", this.onWindowMouseUp, false);
2897 * @param {?Event} event
2899 CalendarNavigationButton.prototype.onMouseDown = function(event) {
2900 if (this._timer !== null)
2902 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2903 window.addEventListener("mouseup", this.onWindowMouseUp, false);
2907 * @param {?Event} event
2909 CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
2910 if (this._timer === null)
2912 clearTimeout(this._timer);
2914 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2918 * @param {?Event} event
2920 CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
2921 this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
2922 this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
2928 * @param {!CalendarPicker} calendarPicker
2930 function CalendarHeaderView(calendarPicker) {
2931 View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
2932 this.calendarPicker = calendarPicker;
2933 this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
2935 var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
2936 this.element.appendChild(titleElement);
2939 * @type {!MonthPopupButton}
2941 this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
2942 this.monthPopupButton.attachTo(titleElement);
2945 * @type {!CalendarNavigationButton}
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);
2954 * @type {!CalendarNavigationButton}
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);
2965 * @type {!CalendarNavigationButton}
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);
2973 if (global.params.isLocaleRTL) {
2974 this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2975 this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2977 this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2978 this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2982 CalendarHeaderView.prototype = Object.create(View.prototype);
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";
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);
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);
3004 this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
3008 * @param {!boolean} disabled
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);
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";
3035 DayCell.prototype = Object.create(ListCell.prototype);
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";
3046 DayCell._recycleBin = [];
3048 DayCell.recycleOrCreate = function() {
3049 return DayCell._recycleBin.pop() || new DayCell();
3056 DayCell.prototype._recycleBin = function() {
3057 return DayCell._recycleBin;
3063 DayCell.prototype.throwAway = function() {
3064 ListCell.prototype.throwAway.call(this);
3069 * @param {!boolean} highlighted
3071 DayCell.prototype.setHighlighted = function(highlighted) {
3073 this.element.classList.add(DayCell.ClassNameHighlighted);
3075 this.element.classList.remove(DayCell.ClassNameHighlighted);
3079 * @param {!boolean} disabled
3081 DayCell.prototype.setDisabled = function(disabled) {
3083 this.element.classList.add(DayCell.ClassNameDisabled);
3085 this.element.classList.remove(DayCell.ClassNameDisabled);
3089 * @param {!boolean} selected
3091 DayCell.prototype.setIsInCurrentMonth = function(selected) {
3093 this.element.classList.add(DayCell.ClassNameCurrentMonth);
3095 this.element.classList.remove(DayCell.ClassNameCurrentMonth);
3099 * @param {!boolean} selected
3101 DayCell.prototype.setIsToday = function(selected) {
3103 this.element.classList.add(DayCell.ClassNameToday);
3105 this.element.classList.remove(DayCell.ClassNameToday);
3111 DayCell.prototype.reset = function(day) {
3113 this.element.textContent = localizeNumber(this.day.date.toString());
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";
3133 WeekNumberCell.prototype = Object.create(ListCell.prototype);
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";
3143 WeekNumberCell._recycleBin = [];
3149 WeekNumberCell.prototype._recycleBin = function() {
3150 return WeekNumberCell._recycleBin;
3154 * @return {!WeekNumberCell}
3156 WeekNumberCell.recycleOrCreate = function() {
3157 return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
3161 * @param {!Week} week
3163 WeekNumberCell.prototype.reset = function(week) {
3165 this.element.textContent = localizeNumber(this.week.week.toString());
3172 WeekNumberCell.prototype.throwAway = function() {
3173 ListCell.prototype.throwAway.call(this);
3177 WeekNumberCell.prototype.setHighlighted = function(highlighted) {
3179 this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
3181 this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
3184 WeekNumberCell.prototype.setDisabled = function(disabled) {
3186 this.element.classList.add(WeekNumberCell.ClassNameDisabled);
3188 this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
3194 * @param {!boolean} hasWeekNumberColumn
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);
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";
3217 CalendarTableHeaderView.prototype = Object.create(View.prototype);
3219 CalendarTableHeaderView.Height = 25;
3225 function CalendarRowCell() {
3226 ListCell.call(this);
3227 this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
3228 this.element.style.height = CalendarRowCell.Height + "px";
3234 this._dayCells = [];
3240 * @type {?CalendarTableView}
3242 this.calendarTableView = null;
3245 CalendarRowCell.prototype = Object.create(ListCell.prototype);
3247 CalendarRowCell.Height = DayCell.Height;
3248 CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";
3250 CalendarRowCell._recycleBin = [];
3256 CalendarRowCell.prototype._recycleBin = function() {
3257 return CalendarRowCell._recycleBin;
3261 * @param {!number} row
3262 * @param {!CalendarTableView} calendarTableView
3264 CalendarRowCell.prototype.reset = function(row, calendarTableView) {
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);
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);
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;
3297 * @param {!CalendarPicker} calendarPicker
3299 function CalendarTableView(calendarPicker) {
3300 ListView.call(this);
3301 this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
3302 this.element.tabIndex = 0;
3308 this.hasWeekNumberColumn = calendarPicker.type === "week";
3310 * @type {!CalendarPicker}
3313 this.calendarPicker = calendarPicker;
3318 this._dayCells = {};
3319 var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
3320 headerView.attachTo(this, this.scrollView);
3322 if (this.hasWeekNumberColumn) {
3323 this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
3328 this._weekNumberCells = [];
3330 this.setWidth(DayCell.Width * DaysPerWeek);
3337 this._ignoreMouseOutUntillNextMouseOver = false;
3339 this.element.addEventListener("click", this.onClick, false);
3340 this.element.addEventListener("mouseover", this.onMouseOver, false);
3341 this.element.addEventListener("mouseout", this.onMouseOut, false);
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);
3349 CalendarTableView.prototype = Object.create(ListView.prototype);
3351 CalendarTableView.BorderWidth = 1;
3352 CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";
3355 * @param {!number} scrollOffset
3358 CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
3359 return Math.floor(scrollOffset / CalendarRowCell.Height);
3363 * @param {!number} row
3366 CalendarTableView.prototype.scrollOffsetForRow = function(row) {
3367 return row * CalendarRowCell.Height;
3371 * @param {?Event} event
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());
3382 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3383 if (!dayCellElement)
3385 var dayCell = dayCellElement.$view;
3386 this.calendarPicker.selectRangeContainingDay(dayCell.day);
3390 * @param {?Event} event
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;
3402 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3403 if (!dayCellElement)
3405 var dayCell = dayCellElement.$view;
3406 this.calendarPicker.highlightRangeContainingDay(dayCell.day);
3407 this._ignoreMouseOutUntillNextMouseOver = false;
3411 * @param {?Event} event
3413 CalendarTableView.prototype.onMouseOut = function(event) {
3414 if (this._ignoreMouseOutUntillNextMouseOver)
3416 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3417 if (!dayCellElement) {
3418 this.calendarPicker.highlightRangeContainingDay(null);
3423 * @param {!number} row
3424 * @return {!CalendarRowCell}
3426 CalendarTableView.prototype.prepareNewCell = function(row) {
3427 var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
3428 cell.reset(row, this);
3433 * @return {!number} Height in pixels.
3435 CalendarTableView.prototype.height = function() {
3436 return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
3440 * @param {!number} height Height in pixels.
3442 CalendarTableView.prototype.setHeight = function(height) {
3443 this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
3447 * @param {!Month} month
3448 * @param {!boolean} animate
3450 CalendarTableView.prototype.scrollToMonth = function(month, animate) {
3451 var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
3452 this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
3456 * @param {!number} column
3457 * @param {!number} row
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);
3465 CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
3466 CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();
3470 * @return {!Object} Object with properties column and row.
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;
3483 CalendarTableView.prototype.updateCells = function() {
3484 ListView.prototype.updateCells.call(this);
3486 var selection = this.calendarPicker.selection();
3487 var firstDayInSelection;
3488 var lastDayInSelection;
3490 firstDayInSelection = selection.firstDay().valueOf();
3491 lastDayInSelection = selection.lastDay().valueOf();
3493 firstDayInSelection = Infinity;
3494 lastDayInSelection = Infinity;
3496 var highlight = this.calendarPicker.highlight();
3497 var firstDayInHighlight;
3498 var lastDayInHighlight;
3500 firstDayInHighlight = highlight.firstDay().valueOf();
3501 lastDayInHighlight = highlight.lastDay().valueOf();
3503 firstDayInHighlight = Infinity;
3504 lastDayInHighlight = Infinity;
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));
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));
3531 * @return {!DayCell}
3533 CalendarTableView.prototype.prepareNewDayCell = function(day) {
3534 var dayCell = DayCell.recycleOrCreate();
3536 this._dayCells[dayCell.day.toString()] = dayCell;
3541 * @param {!Week} week
3542 * @return {!WeekNumberCell}
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;
3552 * @param {!DayCell} dayCell
3554 CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
3555 delete this._dayCells[dayCell.day.toString()];
3556 dayCell.throwAway();
3560 * @param {!WeekNumberCell} weekNumberCell
3562 CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
3563 delete this._weekNumberCells[weekNumberCell.week.toString()];
3564 weekNumberCell.throwAway();
3570 * @param {!Object} config
3572 function CalendarPicker(type, config) {
3573 View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
3574 this.element.classList.add(CalendarPicker.ClassNamePreparing);
3581 if (this.type === "week")
3582 this._dateTypeConstructor = Week;
3583 else if (this.type === "month")
3584 this._dateTypeConstructor = Month;
3586 this._dateTypeConstructor = Day;
3592 this._setConfig(config);
3597 this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
3602 this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
3603 if (global.params.isLocaleRTL)
3604 this.element.classList.add("rtl");
3606 * @type {!CalendarTableView}
3609 this.calendarTableView = new CalendarTableView(this);
3610 this.calendarTableView.hasNumberColumn = this.type === "week";
3612 * @type {!CalendarHeaderView}
3615 this.calendarHeaderView = new CalendarHeaderView(this);
3616 this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
3618 * @type {!MonthPopupView}
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);
3630 this._currentMonth = new Month(NaN, NaN);
3635 this._selection = null;
3640 this._highlight = null;
3641 this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
3642 document.body.addEventListener("keydown", this.onBodyKeyDown, false);
3644 window.addEventListener("resize", this.onWindowResize, false);
3652 var initialSelection = parseDateString(config.currentValue);
3653 if (initialSelection) {
3654 this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
3655 this.setSelection(initialSelection);
3657 this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
3660 CalendarPicker.prototype = Object.create(View.prototype);
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;
3670 * @param {!Event} event
3672 CalendarPicker.prototype.onWindowResize = function(event) {
3673 this.element.classList.remove(CalendarPicker.ClassNamePreparing);
3674 window.removeEventListener("resize", this.onWindowResize, false);
3678 * @param {!YearListView} sender
3680 CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
3681 this.monthPopupView.hide();
3682 this.calendarHeaderView.setDisabled(false);
3683 this.adjustHeight();
3687 * @param {!YearListView} sender
3688 * @param {!Month} month
3690 CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
3691 this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
3695 * @param {!View|Node} parent
3696 * @param {?View|Node=} before
3699 CalendarPicker.prototype.attachTo = function(parent, before) {
3700 View.prototype.attachTo.call(this, parent, before);
3701 this.calendarTableView.element.focus();
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();
3712 * @param {?MonthPopupButton} sender
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();
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;
3734 CalendarPicker.prototype.currentMonth = function() {
3735 return this._currentMonth;
3741 CalendarPicker.NavigationBehavior = {
3747 * @param {!Month} month
3748 * @param {!CalendarPicker.NavigationBehavior} animate
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))
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, {
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);
3775 CalendarPicker.prototype.selection = function() {
3776 return this._selection;
3779 CalendarPicker.prototype.highlight = function() {
3780 return this._highlight;
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;
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;
3808 CalendarPicker.prototype.selectRangeContainingDay = function(day) {
3809 var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
3810 this.setSelectionAndCommit(selection);
3816 CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
3817 var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
3818 this._setHighlight(highlight);
3822 * Select the specified date.
3823 * @param {?DateType} dayOrWeekOrMonth
3825 CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
3826 if (!this._selection && !dayOrWeekOrMonth)
3828 if (this._selection && this._selection.equals(dayOrWeekOrMonth))
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);
3846 this._setHighlight(dayOrWeekOrMonth);
3847 if (!this.isValid(dayOrWeekOrMonth))
3849 this._selection = dayOrWeekOrMonth;
3850 this.calendarTableView.setNeedsUpdateCells(true);
3854 * Select the specified date, commit it, and close the popup.
3855 * @param {?DateType} dayOrWeekOrMonth
3857 CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) {
3858 this.setSelection(dayOrWeekOrMonth);
3859 // Redraw the widget immidiately, and wait for some time to give feedback to
3861 this.element.offsetLeft;
3862 var value = this._selection.toString();
3863 if (CalendarPicker.commitDelayMs == 0) {
3865 window.pagePopupController.setValueAndClosePopup(0, value);
3866 } else if (CalendarPicker.commitDelayMs < 0) {
3868 window.pagePopupController.setValue(value);
3870 setTimeout(function() {
3871 window.pagePopupController.setValueAndClosePopup(0, value);
3872 }, CalendarPicker.commitDelayMs);
3877 * @param {?DateType} dayOrWeekOrMonth
3879 CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
3880 if (!this._highlight && !dayOrWeekOrMonth)
3882 if (!dayOrWeekOrMonth && !this._highlight)
3884 if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
3886 this._highlight = dayOrWeekOrMonth;
3887 this.calendarTableView.setNeedsUpdateCells(true);
3891 * @param {!number} value
3892 * @return {!boolean}
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;
3900 * @param {!number} value
3901 * @return {!boolean}
3903 CalendarPicker.prototype._outOfRange = function(value) {
3904 return value < this.config.minimumValue || value > this.config.maximumValue;
3908 * @param {!DateType} dayOrWeekOrMonth
3909 * @return {!boolean}
3911 CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
3912 var value = dayOrWeekOrMonth.valueOf();
3913 return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
3918 * @return {!boolean}
3920 CalendarPicker.prototype.isValidDay = function(day) {
3921 return this.isValid(this._dateTypeConstructor.createFromDay(day));
3925 * @param {!DateType} dateRange
3926 * @return {!boolean} Returns true if the highlight was changed.
3928 CalendarPicker.prototype._moveHighlight = function(dateRange) {
3931 if (this._outOfRange(dateRange.valueOf()))
3933 if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
3934 this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
3935 this._setHighlight(dateRange);
3940 * @param {?Event} event
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;
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;
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);
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;
3979 event.stopPropagation();
3980 event.preventDefault();
3985 * @return {!number} Width in pixels.
3987 CalendarPicker.prototype.width = function() {
3988 return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
3992 * @return {!number} Height in pixels.
3994 CalendarPicker.prototype.height = function() {
3995 return this._height;
3999 * @param {!number} height Height in pixels.
4001 CalendarPicker.prototype.setHeight = function(height) {
4002 if (this._height === height)
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);
4010 * @param {?Event} event
4012 CalendarPicker.prototype.onBodyKeyDown = function(event) {
4013 var key = event.keyIdentifier;
4014 var eventHandled = false;
4017 case "U+001B": // Esc key.
4018 window.pagePopupController.closePopup();
4019 eventHandled = true;
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));
4038 event.stopPropagation();
4039 event.preventDefault();
4043 if (window.dialogArguments) {
4044 initialize(dialogArguments);
4046 window.addEventListener("message", handleMessage, false);