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().replace(/_/g, '-');
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);
355 Day.prototype.format = function() {
356 if (!Day.formatter) {
357 Day.formatter = new Intl.DateTimeFormat(getLocale(), {
358 weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC"
361 return Day.formatter.format(this.startDate());
364 // See WebCore/platform/DateComponents.h.
365 Day.Minimum = Day.createFromValue(-62135596800000.0);
366 Day.Maximum = Day.createFromValue(8640000000000000.0);
368 // See WebCore/html/DayInputType.cpp.
369 Day.DefaultStep = 86400000;
370 Day.DefaultStepBase = 0;
375 * @param {!number} year
376 * @param {!number} week
378 function Week(year, week) {
389 // Number of years per year is either 52 or 53.
390 if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) {
391 var normalizedWeek = Week.createFromDay(this.firstDay());
392 this.year = normalizedWeek.year;
393 this.week = normalizedWeek.week;
397 Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/;
399 // See WebCore/platform/DateComponents.h.
400 Week.Minimum = new Week(1, 1);
401 Week.Maximum = new Week(275760, 37);
403 // See WebCore/html/WeekInputType.cpp.
404 Week.DefaultStep = 604800000;
405 Week.DefaultStepBase = -259200000;
407 Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay();
410 * @param {!string} str
413 Week.parse = function(str) {
414 var match = Week.ISOStringRegExp.exec(str);
417 var year = parseInt(match[1], 10);
418 var week = parseInt(match[2], 10);
419 return new Week(year, week);
423 * @param {!number} millisecondsSinceEpoch
426 Week.createFromValue = function(millisecondsSinceEpoch) {
427 return Week.createFromDate(new Date(millisecondsSinceEpoch))
431 * @param {!Date} date
434 Week.createFromDate = function(date) {
435 if (isNaN(date.valueOf()))
436 throw "Invalid date";
437 var year = date.getUTCFullYear();
438 if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime())
440 else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime())
442 var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date);
443 return new Week(year, week);
450 Week.createFromDay = function(day) {
452 if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day)
454 else if (year > 1 && Week.weekOneStartDayForYear(year) > day)
456 var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek);
457 return new Week(year, week);
463 Week.createFromToday = function() {
464 var now = new Date();
465 return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate()));
469 * @param {!number} year
472 Week.weekOneStartDateForYear = function(year) {
474 return createUTCDate(1, 0, 1);
475 // The week containing January 4th is week one.
476 var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
477 return createUTCDate(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
481 * @param {!number} year
484 Week.weekOneStartDayForYear = function(year) {
487 // The week containing January 4th is week one.
488 var yearStartDay = createUTCDate(year, 0, 4).getUTCDay();
489 return new Day(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek);
493 * @param {!number} year
496 Week.numberOfWeeksInYear = function(year) {
497 if (year < 1 || year > Week.Maximum.year)
499 else if (year === Week.Maximum.year)
500 return Week.Maximum.week;
501 return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1));
505 * @param {!Date} baseDate
506 * @param {!Date} date
509 Week._numberOfWeeksSinceDate = function(baseDate, date) {
510 return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek);
514 * @param {!DateType} other
517 Week.prototype.equals = function(other) {
518 return other instanceof Week && this.year === other.year && this.week === other.week;
522 * @param {!number=} offset
525 Week.prototype.previous = function(offset) {
526 if (typeof offset === "undefined")
528 return new Week(this.year, this.week - offset);
532 * @param {!number=} offset
535 Week.prototype.next = function(offset) {
536 if (typeof offset === "undefined")
538 return new Week(this.year, this.week + offset);
544 Week.prototype.startDate = function() {
545 var weekStartDate = Week.weekOneStartDateForYear(this.year);
546 weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7);
547 return weekStartDate;
553 Week.prototype.endDate = function() {
554 if (this.equals(Week.Maximum))
555 return Day.Maximum.startDate();
556 return this.next().startDate();
562 Week.prototype.firstDay = function() {
563 var weekOneStartDay = Week.weekOneStartDayForYear(this.year);
564 return weekOneStartDay.next((this.week - 1) * DaysPerWeek);
570 Week.prototype.middleDay = function() {
571 return this.firstDay().next(3);
577 Week.prototype.lastDay = function() {
578 if (this.equals(Week.Maximum))
580 return this.next().firstDay().previous();
586 Week.prototype.valueOf = function() {
587 return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime();
593 Week.prototype.toString = function() {
594 var yearString = String(this.year);
595 if (yearString.length < 4)
596 yearString = ("000" + yearString).substr(-4, 4);
597 return yearString + "-W" + ("0" + this.week).substr(-2, 2);
603 * @param {!number} year
604 * @param {!number} month
606 function Month(year, month) {
611 this.year = year + Math.floor(month / MonthsPerYear);
616 this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear;
619 Month.ISOStringRegExp = /^(\d+)-(\d+)$/;
621 // See WebCore/platform/DateComponents.h.
622 Month.Minimum = new Month(1, 0);
623 Month.Maximum = new Month(275760, 8);
625 // See WebCore/html/MonthInputType.cpp.
626 Month.DefaultStep = 1;
627 Month.DefaultStepBase = 0;
630 * @param {!string} str
633 Month.parse = function(str) {
634 var match = Month.ISOStringRegExp.exec(str);
637 var year = parseInt(match[1], 10);
638 var month = parseInt(match[2], 10) - 1;
639 return new Month(year, month);
643 * @param {!number} value
646 Month.createFromValue = function(monthsSinceEpoch) {
647 return new Month(1970, monthsSinceEpoch)
651 * @param {!Date} date
654 Month.createFromDate = function(date) {
655 if (isNaN(date.valueOf()))
656 throw "Invalid date";
657 return new Month(date.getUTCFullYear(), date.getUTCMonth());
664 Month.createFromDay = function(day) {
665 return new Month(day.year, day.month);
671 Month.createFromToday = function() {
672 var now = new Date();
673 return new Month(now.getFullYear(), now.getMonth());
679 Month.prototype.containsDay = function(day) {
680 return this.year === day.year && this.month === day.month;
684 * @param {!Month} other
687 Month.prototype.equals = function(other) {
688 return other instanceof Month && this.year === other.year && this.month === other.month;
692 * @param {!number=} offset
695 Month.prototype.previous = function(offset) {
696 if (typeof offset === "undefined")
698 return new Month(this.year, this.month - offset);
702 * @param {!number=} offset
705 Month.prototype.next = function(offset) {
706 if (typeof offset === "undefined")
708 return new Month(this.year, this.month + offset);
714 Month.prototype.startDate = function() {
715 return createUTCDate(this.year, this.month, 1);
721 Month.prototype.endDate = function() {
722 if (this.equals(Month.Maximum))
723 return Day.Maximum.startDate();
724 return this.next().startDate();
730 Month.prototype.firstDay = function() {
731 return new Day(this.year, this.month, 1);
737 Month.prototype.middleDay = function() {
738 return new Day(this.year, this.month, this.month === 2 ? 14 : 15);
744 Month.prototype.lastDay = function() {
745 if (this.equals(Month.Maximum))
747 return this.next().firstDay().previous();
753 Month.prototype.valueOf = function() {
754 return (this.year - 1970) * MonthsPerYear + this.month;
760 Month.prototype.toString = function() {
761 var yearString = String(this.year);
762 if (yearString.length < 4)
763 yearString = ("000" + yearString).substr(-4, 4);
764 return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2);
770 Month.prototype.toLocaleString = function() {
771 if (global.params.locale === "ja")
772 return "" + this.year + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "月";
773 return window.pagePopupController.formatMonth(this.year, this.month);
779 Month.prototype.toShortLocaleString = function() {
780 return window.pagePopupController.formatShortMonth(this.year, this.month);
783 // ----------------------------------------------------------------
787 * @param {Event} event
789 function handleMessage(event) {
790 if (global.argumentsReceived)
792 global.argumentsReceived = true;
793 initialize(JSON.parse(event.data));
797 * @param {!Object} params
799 function setGlobalParams(params) {
801 for (name in global.params) {
802 if (typeof params[name] === "undefined")
803 console.warn("Missing argument: " + name);
805 for (name in params) {
806 global.params[name] = params[name];
811 * @param {!Object} args
813 function initialize(args) {
814 setGlobalParams(args);
815 if (global.params.suggestionValues && global.params.suggestionValues.length)
816 openSuggestionPicker();
818 openCalendarPicker();
821 function closePicker() {
823 global.picker.cleanup();
824 var main = $("main");
829 function openSuggestionPicker() {
831 global.picker = new SuggestionPicker($("main"), global.params);
834 function openCalendarPicker() {
836 global.picker = new CalendarPicker(global.params.mode, global.params);
837 global.picker.attachTo($("main"));
843 function EventEmitter() {
847 * @param {!string} type
848 * @param {!function({...*})} callback
850 EventEmitter.prototype.on = function(type, callback) {
851 console.assert(callback instanceof Function);
852 if (!this._callbacks)
853 this._callbacks = {};
854 if (!this._callbacks[type])
855 this._callbacks[type] = [];
856 this._callbacks[type].push(callback);
859 EventEmitter.prototype.hasListener = function(type) {
860 if (!this._callbacks)
862 var callbacksForType = this._callbacks[type];
863 if (!callbacksForType)
865 return callbacksForType.length > 0;
869 * @param {!string} type
870 * @param {!function(Object)} callback
872 EventEmitter.prototype.removeListener = function(type, callback) {
873 if (!this._callbacks)
875 var callbacksForType = this._callbacks[type];
876 if (!callbacksForType)
878 callbacksForType.splice(callbacksForType.indexOf(callback), 1);
879 if (callbacksForType.length === 0)
880 delete this._callbacks[type];
884 * @param {!string} type
885 * @param {...*} var_args
887 EventEmitter.prototype.dispatchEvent = function(type) {
888 if (!this._callbacks)
890 var callbacksForType = this._callbacks[type];
891 if (!callbacksForType)
893 callbacksForType = callbacksForType.slice(0);
894 for (var i = 0; i < callbacksForType.length; ++i) {
895 callbacksForType[i].apply(this, Array.prototype.slice.call(arguments, 1));
899 // Parameter t should be a number between 0 and 1.
900 var AnimationTimingFunction = {
904 EaseInOut: function(t){
907 return Math.pow(t, 3) / 2;
909 return Math.pow(t, 3) / 2 + 1;
915 * @extends EventEmitter
917 function AnimationManager() {
918 EventEmitter.call(this);
920 this._isRunning = false;
921 this._runningAnimatorCount = 0;
922 this._runningAnimators = {};
923 this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
926 AnimationManager.prototype = Object.create(EventEmitter.prototype);
928 AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";
930 AnimationManager.prototype._startAnimation = function() {
933 this._isRunning = true;
934 window.requestAnimationFrame(this._animationFrameCallbackBound);
937 AnimationManager.prototype._stopAnimation = function() {
938 if (!this._isRunning)
940 this._isRunning = false;
944 * @param {!Animator} animator
946 AnimationManager.prototype.add = function(animator) {
947 if (this._runningAnimators[animator.id])
949 this._runningAnimators[animator.id] = animator;
950 this._runningAnimatorCount++;
951 if (this._needsTimer())
952 this._startAnimation();
956 * @param {!Animator} animator
958 AnimationManager.prototype.remove = function(animator) {
959 if (!this._runningAnimators[animator.id])
961 delete this._runningAnimators[animator.id];
962 this._runningAnimatorCount--;
963 if (!this._needsTimer())
964 this._stopAnimation();
967 AnimationManager.prototype._animationFrameCallback = function(now) {
968 if (this._runningAnimatorCount > 0) {
969 for (var id in this._runningAnimators) {
970 this._runningAnimators[id].onAnimationFrame(now);
973 this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
975 window.requestAnimationFrame(this._animationFrameCallbackBound);
981 AnimationManager.prototype._needsTimer = function() {
982 return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
986 * @param {!string} type
987 * @param {!Function} callback
990 AnimationManager.prototype.on = function(type, callback) {
991 EventEmitter.prototype.on.call(this, type, callback);
992 if (this._needsTimer())
993 this._startAnimation();
997 * @param {!string} type
998 * @param {!Function} callback
1001 AnimationManager.prototype.removeListener = function(type, callback) {
1002 EventEmitter.prototype.removeListener.call(this, type, callback);
1003 if (!this._needsTimer())
1004 this._stopAnimation();
1007 AnimationManager.shared = new AnimationManager();
1011 * @extends EventEmitter
1013 function Animator() {
1014 EventEmitter.call(this);
1020 this.id = Animator._lastId++;
1024 this.duration = 100;
1033 this._isRunning = false;
1037 this.currentValue = 0;
1042 this._lastStepTime = 0;
1045 Animator.prototype = Object.create(EventEmitter.prototype);
1047 Animator._lastId = 0;
1049 Animator.EventTypeDidAnimationStop = "didAnimationStop";
1052 * @return {!boolean}
1054 Animator.prototype.isRunning = function() {
1055 return this._isRunning;
1058 Animator.prototype.start = function() {
1059 this._lastStepTime = performance.now();
1060 this._isRunning = true;
1061 AnimationManager.shared.add(this);
1064 Animator.prototype.stop = function() {
1065 if (!this._isRunning)
1067 this._isRunning = false;
1068 AnimationManager.shared.remove(this);
1069 this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
1073 * @param {!number} now
1075 Animator.prototype.onAnimationFrame = function(now) {
1076 this._lastStepTime = now;
1084 function TransitionAnimator() {
1085 Animator.call(this);
1104 this.progress = 0.0;
1108 this.timingFunction = AnimationTimingFunction.Linear;
1111 TransitionAnimator.prototype = Object.create(Animator.prototype);
1114 * @param {!number} value
1116 TransitionAnimator.prototype.setFrom = function(value) {
1118 this._delta = this._to - this._from;
1121 TransitionAnimator.prototype.start = function() {
1122 console.assert(isFinite(this.duration));
1123 this.progress = 0.0;
1124 this.currentValue = this._from;
1125 Animator.prototype.start.call(this);
1129 * @param {!number} value
1131 TransitionAnimator.prototype.setTo = function(value) {
1133 this._delta = this._to - this._from;
1137 * @param {!number} now
1139 TransitionAnimator.prototype.onAnimationFrame = function(now) {
1140 this.progress += (now - this._lastStepTime) / this.duration;
1141 this.progress = Math.min(1.0, this.progress);
1142 this._lastStepTime = now;
1143 this.currentValue = this.timingFunction(this.progress) * this._delta + this._from;
1145 if (this.progress === 1.0) {
1154 * @param {!number} initialVelocity
1155 * @param {!number} initialValue
1157 function FlingGestureAnimator(initialVelocity, initialValue) {
1158 Animator.call(this);
1162 this.initialVelocity = initialVelocity;
1166 this.initialValue = initialValue;
1171 this._elapsedTime = 0;
1172 var startVelocity = Math.abs(this.initialVelocity);
1173 if (startVelocity > this._velocityAtTime(0))
1174 startVelocity = this._velocityAtTime(0);
1175 if (startVelocity < 0)
1181 this._timeOffset = this._timeAtVelocity(startVelocity);
1186 this._positionOffset = this._valueAtTime(this._timeOffset);
1190 this.duration = this._timeAtVelocity(0);
1193 FlingGestureAnimator.prototype = Object.create(Animator.prototype);
1195 // Velocity is subject to exponential decay. These parameters are coefficients
1196 // that determine the curve.
1197 FlingGestureAnimator._P0 = -5707.62;
1198 FlingGestureAnimator._P1 = 0.172;
1199 FlingGestureAnimator._P2 = 0.0037;
1202 * @param {!number} t
1204 FlingGestureAnimator.prototype._valueAtTime = function(t) {
1205 return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
1209 * @param {!number} t
1211 FlingGestureAnimator.prototype._velocityAtTime = function(t) {
1212 return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
1216 * @param {!number} v
1218 FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
1219 return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
1222 FlingGestureAnimator.prototype.start = function() {
1223 this._lastStepTime = performance.now();
1224 Animator.prototype.start.call(this);
1228 * @param {!number} now
1230 FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
1231 this._elapsedTime += now - this._lastStepTime;
1232 this._lastStepTime = now;
1233 if (this._elapsedTime + this._timeOffset >= this.duration) {
1237 var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset;
1238 if (this.initialVelocity < 0)
1239 position = -position;
1240 this.currentValue = position + this.initialValue;
1246 * @extends EventEmitter
1247 * @param {?Element} element
1248 * View adds itself as a property on the element so we can access it from Event.target.
1250 function View(element) {
1251 EventEmitter.call(this);
1256 this.element = element || createElement("div");
1257 this.element.$view = this;
1258 this.bindCallbackMethods();
1261 View.prototype = Object.create(EventEmitter.prototype);
1264 * @param {!Element} ancestorElement
1267 View.prototype.offsetRelativeTo = function(ancestorElement) {
1270 var element = this.element;
1272 x += element.offsetLeft || 0;
1273 y += element.offsetTop || 0;
1274 element = element.offsetParent;
1275 if (element === ancestorElement)
1276 return {x: x, y: y};
1282 * @param {!View|Node} parent
1283 * @param {?View|Node=} before
1285 View.prototype.attachTo = function(parent, before) {
1286 if (parent instanceof View)
1287 return this.attachTo(parent.element, before);
1288 if (typeof before === "undefined")
1290 if (before instanceof View)
1291 before = before.element;
1292 parent.insertBefore(this.element, before);
1295 View.prototype.bindCallbackMethods = function() {
1296 for (var methodName in this) {
1297 if (!/^on[A-Z]/.test(methodName))
1299 if (this.hasOwnProperty(methodName))
1301 var method = this[methodName];
1302 if (!(method instanceof Function))
1304 this[methodName] = method.bind(this);
1312 function ScrollView() {
1313 View.call(this, createElement("div", ScrollView.ClassNameScrollView));
1318 this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
1319 this.element.appendChild(this.contentElement);
1323 this.minimumContentOffset = -Infinity;
1327 this.maximumContentOffset = Infinity;
1332 this._contentOffset = 0;
1347 this._scrollAnimator = null;
1351 this.delegate = null;
1355 this._lastTouchPosition = 0;
1359 this._lastTouchVelocity = 0;
1363 this._lastTouchTimeStamp = 0;
1365 this.element.addEventListener("mousewheel", this.onMouseWheel, false);
1366 this.element.addEventListener("touchstart", this.onTouchStart, false);
1369 * The content offset is partitioned so the it can go beyond the CSS limit
1374 this._partitionNumber = 0;
1377 ScrollView.prototype = Object.create(View.prototype);
1379 ScrollView.PartitionHeight = 100000;
1380 ScrollView.ClassNameScrollView = "scroll-view";
1381 ScrollView.ClassNameScrollViewContent = "scroll-view-content";
1384 * @param {!Event} event
1386 ScrollView.prototype.onTouchStart = function(event) {
1387 var touch = event.touches[0];
1388 this._lastTouchPosition = touch.clientY;
1389 this._lastTouchVelocity = 0;
1390 this._lastTouchTimeStamp = event.timeStamp;
1391 if (this._scrollAnimator)
1392 this._scrollAnimator.stop();
1393 window.addEventListener("touchmove", this.onWindowTouchMove, false);
1394 window.addEventListener("touchend", this.onWindowTouchEnd, false);
1398 * @param {!Event} event
1400 ScrollView.prototype.onWindowTouchMove = function(event) {
1401 var touch = event.touches[0];
1402 var deltaTime = event.timeStamp - this._lastTouchTimeStamp;
1403 var deltaY = this._lastTouchPosition - touch.clientY;
1404 this.scrollBy(deltaY, false);
1405 this._lastTouchVelocity = deltaY / deltaTime;
1406 this._lastTouchPosition = touch.clientY;
1407 this._lastTouchTimeStamp = event.timeStamp;
1408 event.stopPropagation();
1409 event.preventDefault();
1413 * @param {!Event} event
1415 ScrollView.prototype.onWindowTouchEnd = function(event) {
1416 if (Math.abs(this._lastTouchVelocity) > 0.01) {
1417 this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset);
1418 this._scrollAnimator.step = this.onFlingGestureAnimatorStep;
1419 this._scrollAnimator.start();
1421 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1422 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1426 * @param {!Animator} animator
1428 ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
1429 this.scrollTo(animator.currentValue, false);
1433 * @return {!Animator}
1435 ScrollView.prototype.scrollAnimator = function() {
1436 return this._scrollAnimator;
1440 * @param {!number} width
1442 ScrollView.prototype.setWidth = function(width) {
1443 console.assert(isFinite(width));
1444 if (this._width === width)
1446 this._width = width;
1447 this.element.style.width = this._width + "px";
1453 ScrollView.prototype.width = function() {
1458 * @param {!number} height
1460 ScrollView.prototype.setHeight = function(height) {
1461 console.assert(isFinite(height));
1462 if (this._height === height)
1464 this._height = height;
1465 this.element.style.height = height + "px";
1467 this.delegate.scrollViewDidChangeHeight(this);
1473 ScrollView.prototype.height = function() {
1474 return this._height;
1478 * @param {!Animator} animator
1480 ScrollView.prototype.onScrollAnimatorStep = function(animator) {
1481 this.setContentOffset(animator.currentValue);
1485 * @param {!number} offset
1486 * @param {?boolean} animate
1488 ScrollView.prototype.scrollTo = function(offset, animate) {
1489 console.assert(isFinite(offset));
1491 this.setContentOffset(offset);
1494 if (this._scrollAnimator)
1495 this._scrollAnimator.stop();
1496 this._scrollAnimator = new TransitionAnimator();
1497 this._scrollAnimator.step = this.onScrollAnimatorStep;
1498 this._scrollAnimator.setFrom(this._contentOffset);
1499 this._scrollAnimator.setTo(offset);
1500 this._scrollAnimator.duration = 300;
1501 this._scrollAnimator.start();
1505 * @param {!number} offset
1506 * @param {?boolean} animate
1508 ScrollView.prototype.scrollBy = function(offset, animate) {
1509 this.scrollTo(this._contentOffset + offset, animate);
1515 ScrollView.prototype.contentOffset = function() {
1516 return this._contentOffset;
1520 * @param {?Event} event
1522 ScrollView.prototype.onMouseWheel = function(event) {
1523 this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
1524 event.stopPropagation();
1525 event.preventDefault();
1530 * @param {!number} value
1532 ScrollView.prototype.setContentOffset = function(value) {
1533 console.assert(isFinite(value));
1534 value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value)));
1535 if (this._contentOffset === value)
1537 this._contentOffset = value;
1538 this._updateScrollContent();
1540 this.delegate.scrollViewDidChangeContentOffset(this);
1543 ScrollView.prototype._updateScrollContent = function() {
1544 var newPartitionNumber = Math.floor(this._contentOffset / ScrollView.PartitionHeight);
1545 var partitionChanged = this._partitionNumber !== newPartitionNumber;
1546 this._partitionNumber = newPartitionNumber;
1547 this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)";
1548 if (this.delegate && partitionChanged)
1549 this.delegate.scrollViewDidChangePartition(this);
1553 * @param {!View|Node} parent
1554 * @param {?View|Node=} before
1557 ScrollView.prototype.attachTo = function(parent, before) {
1558 View.prototype.attachTo.call(this, parent, before);
1559 this._updateScrollContent();
1563 * @param {!number} offset
1565 ScrollView.prototype.contentPositionForContentOffset = function(offset) {
1566 return offset - this._partitionNumber * ScrollView.PartitionHeight;
1573 function ListCell() {
1574 View.call(this, createElement("div", ListCell.ClassNameListCell));
1590 ListCell.prototype = Object.create(View.prototype);
1592 ListCell.DefaultRecycleBinLimit = 64;
1593 ListCell.ClassNameListCell = "list-cell";
1594 ListCell.ClassNameHidden = "hidden";
1597 * @return {!Array} An array to keep thrown away cells.
1599 ListCell.prototype._recycleBin = function() {
1600 console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
1604 ListCell.prototype.throwAway = function() {
1606 var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit;
1607 var recycleBin = this._recycleBin();
1608 if (recycleBin.length < limit)
1609 recycleBin.push(this);
1612 ListCell.prototype.show = function() {
1613 this.element.classList.remove(ListCell.ClassNameHidden);
1616 ListCell.prototype.hide = function() {
1617 this.element.classList.add(ListCell.ClassNameHidden);
1621 * @return {!number} Width in pixels.
1623 ListCell.prototype.width = function(){
1628 * @param {!number} width Width in pixels.
1630 ListCell.prototype.setWidth = function(width){
1631 if (this._width === width)
1633 this._width = width;
1634 this.element.style.width = this._width + "px";
1638 * @return {!number} Position in pixels.
1640 ListCell.prototype.position = function(){
1641 return this._position;
1645 * @param {!number} y Position in pixels.
1647 ListCell.prototype.setPosition = function(y) {
1648 if (this._position === y)
1651 this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
1655 * @param {!boolean} selected
1657 ListCell.prototype.setSelected = function(selected) {
1658 if (this._selected === selected)
1660 this._selected = selected;
1662 this.element.classList.add("selected");
1664 this.element.classList.remove("selected");
1671 function ListView() {
1672 View.call(this, createElement("div", ListView.ClassNameListView));
1673 this.element.tabIndex = 0;
1674 this.element.setAttribute("role", "grid");
1690 this.selectedRow = ListView.NoSelection;
1693 * @type {!ScrollView}
1695 this.scrollView = new ScrollView();
1696 this.scrollView.delegate = this;
1697 this.scrollView.minimumContentOffset = 0;
1698 this.scrollView.setWidth(0);
1699 this.scrollView.setHeight(0);
1700 this.scrollView.attachTo(this);
1702 this.element.addEventListener("click", this.onClick, false);
1708 this._needsUpdateCells = false;
1711 ListView.prototype = Object.create(View.prototype);
1713 ListView.NoSelection = -1;
1714 ListView.ClassNameListView = "list-view";
1716 ListView.prototype.onAnimationFrameWillFinish = function() {
1717 if (this._needsUpdateCells)
1722 * @param {!boolean} needsUpdateCells
1724 ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
1725 if (this._needsUpdateCells === needsUpdateCells)
1727 this._needsUpdateCells = needsUpdateCells;
1728 if (this._needsUpdateCells)
1729 AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1731 AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1735 * @param {!number} row
1736 * @return {?ListCell}
1738 ListView.prototype.cellAtRow = function(row) {
1739 return this._cells[row];
1743 * @param {!number} offset Scroll offset in pixels.
1746 ListView.prototype.rowAtScrollOffset = function(offset) {
1747 console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
1752 * @param {!number} row
1753 * @return {!number} Scroll offset in pixels.
1755 ListView.prototype.scrollOffsetForRow = function(row) {
1756 console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
1761 * @param {!number} row
1762 * @return {!ListCell}
1764 ListView.prototype.addCellIfNecessary = function(row) {
1765 var cell = this._cells[row];
1768 cell = this.prepareNewCell(row);
1769 cell.attachTo(this.scrollView.contentElement);
1770 cell.setWidth(this._width);
1771 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row)));
1772 this._cells[row] = cell;
1777 * @param {!number} row
1778 * @return {!ListCell}
1780 ListView.prototype.prepareNewCell = function(row) {
1781 console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
1782 return new ListCell();
1786 * @param {!ListCell} cell
1788 ListView.prototype.throwAwayCell = function(cell) {
1789 delete this._cells[cell.row];
1796 ListView.prototype.firstVisibleRow = function() {
1797 return this.rowAtScrollOffset(this.scrollView.contentOffset());
1803 ListView.prototype.lastVisibleRow = function() {
1804 return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
1808 * @param {!ScrollView} scrollView
1810 ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
1811 this.setNeedsUpdateCells(true);
1815 * @param {!ScrollView} scrollView
1817 ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
1818 this.setNeedsUpdateCells(true);
1822 * @param {!ScrollView} scrollView
1824 ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
1825 this.setNeedsUpdateCells(true);
1828 ListView.prototype.updateCells = function() {
1829 var firstVisibleRow = this.firstVisibleRow();
1830 var lastVisibleRow = this.lastVisibleRow();
1831 console.assert(firstVisibleRow <= lastVisibleRow);
1832 for (var c in this._cells) {
1833 var cell = this._cells[c];
1834 if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
1835 this.throwAwayCell(cell);
1837 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
1838 var cell = this._cells[i];
1840 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
1842 this.addCellIfNecessary(i);
1844 this.setNeedsUpdateCells(false);
1848 * @return {!number} Width in pixels.
1850 ListView.prototype.width = function() {
1855 * @param {!number} width Width in pixels.
1857 ListView.prototype.setWidth = function(width) {
1858 if (this._width === width)
1860 this._width = width;
1861 this.scrollView.setWidth(this._width);
1862 for (var c in this._cells) {
1863 this._cells[c].setWidth(this._width);
1865 this.element.style.width = this._width + "px";
1866 this.setNeedsUpdateCells(true);
1870 * @return {!number} Height in pixels.
1872 ListView.prototype.height = function() {
1873 return this.scrollView.height();
1877 * @param {!number} height Height in pixels.
1879 ListView.prototype.setHeight = function(height) {
1880 this.scrollView.setHeight(height);
1884 * @param {?Event} event
1886 ListView.prototype.onClick = function(event) {
1887 var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
1888 if (!clickedCellElement)
1890 var clickedCell = clickedCellElement.$view;
1891 if (clickedCell.row !== this.selectedRow)
1892 this.select(clickedCell.row);
1896 * @param {!number} row
1898 ListView.prototype.select = function(row) {
1899 if (this.selectedRow === row)
1902 if (row === ListView.NoSelection)
1904 this.selectedRow = row;
1905 var selectedCell = this._cells[this.selectedRow];
1907 selectedCell.setSelected(true);
1910 ListView.prototype.deselect = function() {
1911 if (this.selectedRow === ListView.NoSelection)
1913 var selectedCell = this._cells[this.selectedRow];
1915 selectedCell.setSelected(false);
1916 this.selectedRow = ListView.NoSelection;
1920 * @param {!number} row
1921 * @param {!boolean} animate
1923 ListView.prototype.scrollToRow = function(row, animate) {
1924 this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
1930 * @param {!ScrollView} scrollView
1932 function ScrubbyScrollBar(scrollView) {
1933 View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));
1939 this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
1940 this.element.appendChild(this.thumb);
1943 * @type {!ScrollView}
1946 this.scrollView = scrollView;
1957 this._thumbHeight = 0;
1962 this._thumbPosition = 0;
1965 this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);
1971 this._thumbStyleTopAnimator = null;
1979 this.element.addEventListener("mousedown", this.onMouseDown, false);
1980 this.element.addEventListener("touchstart", this.onTouchStart, false);
1983 ScrubbyScrollBar.prototype = Object.create(View.prototype);
1985 ScrubbyScrollBar.ScrollInterval = 16;
1986 ScrubbyScrollBar.ThumbMargin = 2;
1987 ScrubbyScrollBar.ThumbHeight = 30;
1988 ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
1989 ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";
1992 * @param {?Event} event
1994 ScrubbyScrollBar.prototype.onTouchStart = function(event) {
1995 var touch = event.touches[0];
1996 this._setThumbPositionFromEventPosition(touch.clientY);
1997 if (this._thumbStyleTopAnimator)
1998 this._thumbStyleTopAnimator.stop();
1999 this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2000 window.addEventListener("touchmove", this.onWindowTouchMove, false);
2001 window.addEventListener("touchend", this.onWindowTouchEnd, false);
2002 event.stopPropagation();
2003 event.preventDefault();
2007 * @param {?Event} event
2009 ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
2010 var touch = event.touches[0];
2011 this._setThumbPositionFromEventPosition(touch.clientY);
2012 event.stopPropagation();
2013 event.preventDefault();
2017 * @param {?Event} event
2019 ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) {
2020 this._thumbStyleTopAnimator = new TransitionAnimator();
2021 this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2022 this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2023 this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2024 this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2025 this._thumbStyleTopAnimator.duration = 100;
2026 this._thumbStyleTopAnimator.start();
2028 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
2029 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
2030 clearInterval(this._timer);
2034 * @return {!number} Height of the view in pixels.
2036 ScrubbyScrollBar.prototype.height = function() {
2037 return this._height;
2041 * @param {!number} height Height of the view in pixels.
2043 ScrubbyScrollBar.prototype.setHeight = function(height) {
2044 if (this._height === height)
2046 this._height = height;
2047 this.element.style.height = this._height + "px";
2048 this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2049 this._thumbPosition = 0;
2053 * @param {!number} height Height of the scroll bar thumb in pixels.
2055 ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
2056 if (this._thumbHeight === height)
2058 this._thumbHeight = height;
2059 this.thumb.style.height = this._thumbHeight + "px";
2060 this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2061 this._thumbPosition = 0;
2065 * @param {number} position
2067 ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) {
2068 var thumbMin = ScrubbyScrollBar.ThumbMargin;
2069 var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2;
2070 var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop;
2071 var thumbPosition = y - this._thumbHeight / 2;
2072 thumbPosition = Math.max(thumbPosition, thumbMin);
2073 thumbPosition = Math.min(thumbPosition, thumbMax);
2074 this.thumb.style.top = thumbPosition + "px";
2075 this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2;
2079 * @param {?Event} event
2081 ScrubbyScrollBar.prototype.onMouseDown = function(event) {
2082 this._setThumbPositionFromEventPosition(event.clientY);
2084 window.addEventListener("mousemove", this.onWindowMouseMove, false);
2085 window.addEventListener("mouseup", this.onWindowMouseUp, false);
2086 if (this._thumbStyleTopAnimator)
2087 this._thumbStyleTopAnimator.stop();
2088 this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2089 event.stopPropagation();
2090 event.preventDefault();
2094 * @param {?Event} event
2096 ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
2097 this._setThumbPositionFromEventPosition(event.clientY);
2101 * @param {?Event} event
2103 ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) {
2104 this._thumbStyleTopAnimator = new TransitionAnimator();
2105 this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2106 this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2107 this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2108 this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2109 this._thumbStyleTopAnimator.duration = 100;
2110 this._thumbStyleTopAnimator.start();
2112 window.removeEventListener("mousemove", this.onWindowMouseMove, false);
2113 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2114 clearInterval(this._timer);
2118 * @param {!Animator} animator
2120 ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
2121 this.thumb.style.top = animator.currentValue + "px";
2124 ScrubbyScrollBar.prototype.onScrollTimer = function() {
2125 var scrollAmount = Math.pow(this._thumbPosition, 2) * 10;
2126 if (this._thumbPosition > 0)
2127 scrollAmount = -scrollAmount;
2128 this.scrollView.scrollBy(scrollAmount, false);
2134 * @param {!Array} shortMonthLabels
2136 function YearListCell(shortMonthLabels) {
2137 ListCell.call(this);
2138 this.element.classList.add(YearListCell.ClassNameYearListCell);
2139 this.element.style.height = YearListCell.Height + "px";
2145 this.label = createElement("div", YearListCell.ClassNameLabel, "----");
2146 this.element.appendChild(this.label);
2147 this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2148 this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2151 * @type {!Array} Array of the 12 month button elements.
2154 this.monthButtons = [];
2155 var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser);
2156 for (var r = 0; r < YearListCell.ButtonRows; ++r) {
2157 var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow);
2158 buttonsRow.setAttribute("role", "row");
2159 for (var c = 0; c < YearListCell.ButtonColumns; ++c) {
2160 var month = c + r * YearListCell.ButtonColumns;
2161 var button = createElement("div", YearListCell.ClassNameMonthButton, shortMonthLabels[month]);
2162 button.setAttribute("role", "gridcell");
2163 button.dataset.month = month;
2164 buttonsRow.appendChild(button);
2165 this.monthButtons.push(button);
2167 monthChooserElement.appendChild(buttonsRow);
2169 this.element.appendChild(monthChooserElement);
2175 this._selected = false;
2183 YearListCell.prototype = Object.create(ListCell.prototype);
2185 YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25;
2186 YearListCell.BorderBottomWidth = 1;
2187 YearListCell.ButtonRows = 3;
2188 YearListCell.ButtonColumns = 4;
2189 YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121;
2190 YearListCell.ClassNameYearListCell = "year-list-cell";
2191 YearListCell.ClassNameLabel = "label";
2192 YearListCell.ClassNameMonthChooser = "month-chooser";
2193 YearListCell.ClassNameMonthButtonsRow = "month-buttons-row";
2194 YearListCell.ClassNameMonthButton = "month-button";
2195 YearListCell.ClassNameHighlighted = "highlighted";
2197 YearListCell._recycleBin = [];
2203 YearListCell.prototype._recycleBin = function() {
2204 return YearListCell._recycleBin;
2208 * @param {!number} row
2210 YearListCell.prototype.reset = function(row) {
2212 this.label.textContent = row + 1;
2213 for (var i = 0; i < this.monthButtons.length; ++i) {
2214 this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted);
2220 * @return {!number} The height in pixels.
2222 YearListCell.prototype.height = function() {
2223 return this._height;
2227 * @param {!number} height Height in pixels.
2229 YearListCell.prototype.setHeight = function(height) {
2230 if (this._height === height)
2232 this._height = height;
2233 this.element.style.height = this._height + "px";
2239 * @param {!Month} minimumMonth
2240 * @param {!Month} maximumMonth
2242 function YearListView(minimumMonth, maximumMonth) {
2243 ListView.call(this);
2244 this.element.classList.add("year-list-view");
2249 this.highlightedMonth = null;
2255 this._minimumMonth = minimumMonth;
2261 this._maximumMonth = maximumMonth;
2263 this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
2264 this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
2271 this._runningAnimators = {};
2277 this._animatingRows = [];
2282 this._ignoreMouseOutUntillNextMouseOver = false;
2285 * @type {!ScrubbyScrollBar}
2288 this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
2289 this.scrubbyScrollBar.attachTo(this);
2291 this.element.addEventListener("mouseover", this.onMouseOver, false);
2292 this.element.addEventListener("mouseout", this.onMouseOut, false);
2293 this.element.addEventListener("keydown", this.onKeyDown, false);
2294 this.element.addEventListener("touchstart", this.onTouchStart, false);
2297 YearListView.prototype = Object.create(ListView.prototype);
2299 YearListView.Height = YearListCell.SelectedHeight - 1;
2300 YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
2301 YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";
2304 * @param {?Event} event
2306 YearListView.prototype.onTouchStart = function(event) {
2307 var touch = event.touches[0];
2308 var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
2309 if (!monthButtonElement)
2311 var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2312 var cell = cellElement.$view;
2313 this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2317 * @param {?Event} event
2319 YearListView.prototype.onMouseOver = function(event) {
2320 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2321 if (!monthButtonElement)
2323 var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2324 var cell = cellElement.$view;
2325 this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2326 this._ignoreMouseOutUntillNextMouseOver = false;
2330 * @param {?Event} event
2332 YearListView.prototype.onMouseOut = function(event) {
2333 if (this._ignoreMouseOutUntillNextMouseOver)
2335 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2336 if (!monthButtonElement) {
2337 this.dehighlightMonth();
2342 * @param {!number} width Width in pixels.
2345 YearListView.prototype.setWidth = function(width) {
2346 ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
2347 this.element.style.width = width + "px";
2351 * @param {!number} height Height in pixels.
2354 YearListView.prototype.setHeight = function(height) {
2355 ListView.prototype.setHeight.call(this, height);
2356 this.scrubbyScrollBar.setHeight(height);
2362 YearListView.RowAnimationDirection = {
2368 * @param {!number} row
2369 * @param {!YearListView.RowAnimationDirection} direction
2371 YearListView.prototype._animateRow = function(row, direction) {
2372 var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
2373 var oldAnimator = this._runningAnimators[row];
2376 fromValue = oldAnimator.currentValue;
2378 var cell = this.cellAtRow(row);
2379 var animator = new TransitionAnimator();
2380 animator.step = this.onCellHeightAnimatorStep;
2381 animator.setFrom(fromValue);
2382 animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height);
2383 animator.timingFunction = AnimationTimingFunction.EaseInOut;
2384 animator.duration = 300;
2386 animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
2387 this._runningAnimators[row] = animator;
2388 this._animatingRows.push(row);
2389 this._animatingRows.sort();
2394 * @param {?Animator} animator
2396 YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) {
2397 delete this._runningAnimators[animator.row];
2398 var index = this._animatingRows.indexOf(animator.row);
2399 this._animatingRows.splice(index, 1);
2403 * @param {!Animator} animator
2405 YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
2406 var cell = this.cellAtRow(animator.row);
2408 cell.setHeight(animator.currentValue);
2413 * @param {?Event} event
2415 YearListView.prototype.onClick = function(event) {
2416 var oldSelectedRow = this.selectedRow;
2417 ListView.prototype.onClick.call(this, event);
2418 var year = this.selectedRow + 1;
2419 if (this.selectedRow !== oldSelectedRow) {
2420 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2421 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2422 this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2424 var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2425 if (!monthButton || monthButton.getAttribute("aria-disabled") == "true")
2427 var month = parseInt(monthButton.dataset.month, 10);
2428 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2434 * @param {!number} scrollOffset
2438 YearListView.prototype.rowAtScrollOffset = function(scrollOffset) {
2439 var remainingOffset = scrollOffset;
2440 var lastAnimatingRow = 0;
2441 var rowsWithIrregularHeight = this._animatingRows.slice();
2442 if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) {
2443 rowsWithIrregularHeight.push(this.selectedRow);
2444 rowsWithIrregularHeight.sort();
2446 for (var i = 0; i < rowsWithIrregularHeight.length; ++i) {
2447 var row = rowsWithIrregularHeight[i];
2448 var animator = this._runningAnimators[row];
2449 var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight;
2450 if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) {
2451 return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2453 remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
2454 if (remainingOffset <= (rowHeight - YearListCell.Height))
2456 remainingOffset -= rowHeight - YearListCell.Height;
2457 lastAnimatingRow = row;
2459 return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2463 * @param {!number} row
2467 YearListView.prototype.scrollOffsetForRow = function(row) {
2468 var scrollOffset = row * YearListCell.Height;
2469 for (var i = 0; i < this._animatingRows.length; ++i) {
2470 var animatingRow = this._animatingRows[i];
2471 if (animatingRow >= row)
2473 var animator = this._runningAnimators[animatingRow];
2474 scrollOffset += animator.currentValue - YearListCell.Height;
2476 if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
2477 scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
2479 return scrollOffset;
2483 * @param {!number} row
2484 * @return {!YearListCell}
2487 YearListView.prototype.prepareNewCell = function(row) {
2488 var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
2490 cell.setSelected(this.selectedRow === row);
2491 for (var i = 0; i < cell.monthButtons.length; ++i) {
2492 var month = new Month(row + 1, i);
2493 cell.monthButtons[i].id = month.toString();
2494 cell.monthButtons[i].setAttribute("aria-disabled", this._minimumMonth > month || this._maximumMonth < month ? "true" : "false");
2495 cell.monthButtons[i].setAttribute("aria-label", month.toLocaleString());
2497 if (this.highlightedMonth && row === this.highlightedMonth.year - 1) {
2498 var monthButton = cell.monthButtons[this.highlightedMonth.month];
2499 monthButton.classList.add(YearListCell.ClassNameHighlighted);
2500 // aira-activedescendant assumes both elements have renderers, and
2501 // |monthButton| might have no renderer yet.
2502 var element = this.element;
2503 setTimeout(function() {
2504 element.setAttribute("aria-activedescendant", monthButton.id);
2507 var animator = this._runningAnimators[row];
2509 cell.setHeight(animator.currentValue);
2510 else if (row === this.selectedRow)
2511 cell.setHeight(YearListCell.SelectedHeight);
2513 cell.setHeight(YearListCell.Height);
2520 YearListView.prototype.updateCells = function() {
2521 var firstVisibleRow = this.firstVisibleRow();
2522 var lastVisibleRow = this.lastVisibleRow();
2523 console.assert(firstVisibleRow <= lastVisibleRow);
2524 for (var c in this._cells) {
2525 var cell = this._cells[c];
2526 if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
2527 this.throwAwayCell(cell);
2529 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
2530 var cell = this._cells[i];
2532 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
2534 this.addCellIfNecessary(i);
2536 this.setNeedsUpdateCells(false);
2542 YearListView.prototype.deselect = function() {
2543 if (this.selectedRow === ListView.NoSelection)
2545 var selectedCell = this._cells[this.selectedRow];
2547 selectedCell.setSelected(false);
2548 this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
2549 this.selectedRow = ListView.NoSelection;
2550 this.setNeedsUpdateCells(true);
2553 YearListView.prototype.deselectWithoutAnimating = function() {
2554 if (this.selectedRow === ListView.NoSelection)
2556 var selectedCell = this._cells[this.selectedRow];
2558 selectedCell.setSelected(false);
2559 selectedCell.setHeight(YearListCell.Height);
2561 this.selectedRow = ListView.NoSelection;
2562 this.setNeedsUpdateCells(true);
2566 * @param {!number} row
2569 YearListView.prototype.select = function(row) {
2570 if (this.selectedRow === row)
2573 if (row === ListView.NoSelection)
2575 this.selectedRow = row;
2576 if (this.selectedRow !== ListView.NoSelection) {
2577 var selectedCell = this._cells[this.selectedRow];
2578 this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening);
2580 selectedCell.setSelected(true);
2581 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2582 this.highlightMonth(new Month(this.selectedRow + 1, month));
2584 this.setNeedsUpdateCells(true);
2588 * @param {!number} row
2590 YearListView.prototype.selectWithoutAnimating = function(row) {
2591 if (this.selectedRow === row)
2593 this.deselectWithoutAnimating();
2594 if (row === ListView.NoSelection)
2596 this.selectedRow = row;
2597 if (this.selectedRow !== ListView.NoSelection) {
2598 var selectedCell = this._cells[this.selectedRow];
2600 selectedCell.setSelected(true);
2601 selectedCell.setHeight(YearListCell.SelectedHeight);
2603 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2604 this.highlightMonth(new Month(this.selectedRow + 1, month));
2606 this.setNeedsUpdateCells(true);
2610 * @param {!Month} month
2611 * @return {?HTMLDivElement}
2613 YearListView.prototype.buttonForMonth = function(month) {
2616 var row = month.year - 1;
2617 var cell = this.cellAtRow(row);
2620 return cell.monthButtons[month.month];
2623 YearListView.prototype.dehighlightMonth = function() {
2624 if (!this.highlightedMonth)
2626 var monthButton = this.buttonForMonth(this.highlightedMonth);
2628 monthButton.classList.remove(YearListCell.ClassNameHighlighted);
2630 this.highlightedMonth = null;
2631 this.element.removeAttribute("aria-activedescendant");
2635 * @param {!Month} month
2637 YearListView.prototype.highlightMonth = function(month) {
2638 if (this.highlightedMonth && this.highlightedMonth.equals(month))
2640 this.dehighlightMonth();
2641 this.highlightedMonth = month;
2642 if (!this.highlightedMonth)
2644 var monthButton = this.buttonForMonth(this.highlightedMonth);
2646 monthButton.classList.add(YearListCell.ClassNameHighlighted);
2647 this.element.setAttribute("aria-activedescendant", monthButton.id);
2652 * @param {!Month} month
2654 YearListView.prototype.show = function(month) {
2655 this._ignoreMouseOutUntillNextMouseOver = true;
2657 this.scrollToRow(month.year - 1, false);
2658 this.selectWithoutAnimating(month.year - 1);
2659 this.highlightMonth(month);
2662 YearListView.prototype.hide = function() {
2663 this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
2667 * @param {!Month} month
2669 YearListView.prototype._moveHighlightTo = function(month) {
2670 this.highlightMonth(month);
2671 this.select(this.highlightedMonth.year - 1);
2673 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
2674 this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2679 * @param {?Event} event
2681 YearListView.prototype.onKeyDown = function(event) {
2682 var key = event.keyIdentifier;
2683 var eventHandled = false;
2684 if (key == "U+0054") // 't' key.
2685 eventHandled = this._moveHighlightTo(Month.createFromToday());
2686 else if (this.highlightedMonth) {
2687 if (global.params.isLocaleRTL ? key == "Right" : key == "Left")
2688 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous());
2689 else if (key == "Up")
2690 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns));
2691 else if (global.params.isLocaleRTL ? key == "Left" : key == "Right")
2692 eventHandled = this._moveHighlightTo(this.highlightedMonth.next());
2693 else if (key == "Down")
2694 eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns));
2695 else if (key == "PageUp")
2696 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear));
2697 else if (key == "PageDown")
2698 eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear));
2699 else if (key == "Enter") {
2700 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth);
2702 eventHandled = true;
2704 } else if (key == "Up") {
2705 this.scrollView.scrollBy(-YearListCell.Height, true);
2706 eventHandled = true;
2707 } else if (key == "Down") {
2708 this.scrollView.scrollBy(YearListCell.Height, true);
2709 eventHandled = true;
2710 } else if (key == "PageUp") {
2711 this.scrollView.scrollBy(-this.scrollView.height(), true);
2712 eventHandled = true;
2713 } else if (key == "PageDown") {
2714 this.scrollView.scrollBy(this.scrollView.height(), true);
2715 eventHandled = true;
2719 event.stopPropagation();
2720 event.preventDefault();
2727 * @param {!Month} minimumMonth
2728 * @param {!Month} maximumMonth
2730 function MonthPopupView(minimumMonth, maximumMonth) {
2731 View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));
2734 * @type {!YearListView}
2737 this.yearListView = new YearListView(minimumMonth, maximumMonth);
2738 this.yearListView.attachTo(this);
2743 this.isVisible = false;
2745 this.element.addEventListener("click", this.onClick, false);
2748 MonthPopupView.prototype = Object.create(View.prototype);
2750 MonthPopupView.ClassNameMonthPopupView = "month-popup-view";
2752 MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) {
2753 this.isVisible = true;
2754 document.body.appendChild(this.element);
2755 this.yearListView.setWidth(calendarTableRect.width - 2);
2756 this.yearListView.setHeight(YearListView.Height);
2757 if (global.params.isLocaleRTL)
2758 this.yearListView.element.style.right = calendarTableRect.x + "px";
2760 this.yearListView.element.style.left = calendarTableRect.x + "px";
2761 this.yearListView.element.style.top = calendarTableRect.y + "px";
2762 this.yearListView.show(initialMonth);
2763 this.yearListView.element.focus();
2766 MonthPopupView.prototype.hide = function() {
2767 if (!this.isVisible)
2769 this.isVisible = false;
2770 this.element.parentNode.removeChild(this.element);
2771 this.yearListView.hide();
2775 * @param {?Event} event
2777 MonthPopupView.prototype.onClick = function(event) {
2778 if (event.target !== this.element)
2786 * @param {!number} maxWidth Maximum width in pixels.
2788 function MonthPopupButton(maxWidth) {
2789 View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));
2790 this.element.setAttribute("aria-label", global.params.axShowMonthSelector);
2796 this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
2797 this.element.appendChild(this.labelElement);
2803 this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle);
2804 this.disclosureTriangleIcon.innerHTML = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>";
2805 this.element.appendChild(this.disclosureTriangleIcon);
2811 this._useShortMonth = this._shouldUseShortMonth(maxWidth);
2812 this.element.style.maxWidth = maxWidth + "px";
2814 this.element.addEventListener("click", this.onClick, false);
2817 MonthPopupButton.prototype = Object.create(View.prototype);
2819 MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
2820 MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
2821 MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
2822 MonthPopupButton.EventTypeButtonClick = "buttonClick";
2825 * @param {!number} maxWidth Maximum available width in pixels.
2826 * @return {!boolean}
2828 MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) {
2829 document.body.appendChild(this.element);
2830 var month = Month.Maximum;
2831 for (var i = 0; i < MonthsPerYear; ++i) {
2832 this.labelElement.textContent = month.toLocaleString();
2833 if (this.element.offsetWidth > maxWidth)
2835 month = month.previous();
2837 document.body.removeChild(this.element);
2842 * @param {!Month} month
2844 MonthPopupButton.prototype.setCurrentMonth = function(month) {
2845 this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
2849 * @param {?Event} event
2851 MonthPopupButton.prototype.onClick = function(event) {
2852 this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
2859 function CalendarNavigationButton() {
2860 View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
2862 * @type {number} Threshold for starting repeating clicks in milliseconds.
2864 this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
2866 * @type {number} Interval between reapeating clicks in milliseconds.
2868 this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
2870 * @type {?number} The ID for the timeout that triggers the repeating clicks.
2873 this.element.addEventListener("click", this.onClick, false);
2874 this.element.addEventListener("mousedown", this.onMouseDown, false);
2875 this.element.addEventListener("touchstart", this.onTouchStart, false);
2878 CalendarNavigationButton.prototype = Object.create(View.prototype);
2880 CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600;
2881 CalendarNavigationButton.DefaultRepeatingClicksInterval = 300;
2882 CalendarNavigationButton.LeftMargin = 4;
2883 CalendarNavigationButton.Width = 24;
2884 CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button";
2885 CalendarNavigationButton.EventTypeButtonClick = "buttonClick";
2886 CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick";
2889 * @param {!boolean} disabled
2891 CalendarNavigationButton.prototype.setDisabled = function(disabled) {
2892 this.element.disabled = disabled;
2896 * @param {?Event} event
2898 CalendarNavigationButton.prototype.onClick = function(event) {
2899 this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
2903 * @param {?Event} event
2905 CalendarNavigationButton.prototype.onTouchStart = function(event) {
2906 if (this._timer !== null)
2908 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2909 window.addEventListener("touchend", this.onWindowTouchEnd, false);
2913 * @param {?Event} event
2915 CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
2916 if (this._timer === null)
2918 clearTimeout(this._timer);
2920 window.removeEventListener("touchend", this.onWindowMouseUp, false);
2924 * @param {?Event} event
2926 CalendarNavigationButton.prototype.onMouseDown = function(event) {
2927 if (this._timer !== null)
2929 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2930 window.addEventListener("mouseup", this.onWindowMouseUp, false);
2934 * @param {?Event} event
2936 CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
2937 if (this._timer === null)
2939 clearTimeout(this._timer);
2941 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2945 * @param {?Event} event
2947 CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
2948 this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
2949 this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
2955 * @param {!CalendarPicker} calendarPicker
2957 function CalendarHeaderView(calendarPicker) {
2958 View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
2959 this.calendarPicker = calendarPicker;
2960 this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
2962 var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
2963 this.element.appendChild(titleElement);
2966 * @type {!MonthPopupButton}
2968 this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
2969 this.monthPopupButton.attachTo(titleElement);
2972 * @type {!CalendarNavigationButton}
2975 this._previousMonthButton = new CalendarNavigationButton();
2976 this._previousMonthButton.attachTo(this);
2977 this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2978 this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2979 this._previousMonthButton.element.setAttribute("aria-label", global.params.axShowPreviousMonth);
2982 * @type {!CalendarNavigationButton}
2985 this._todayButton = new CalendarNavigationButton();
2986 this._todayButton.attachTo(this);
2987 this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2988 this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton);
2989 var monthContainingToday = Month.createFromToday();
2990 this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2991 this._todayButton.element.setAttribute("aria-label", global.params.todayLabel);
2994 * @type {!CalendarNavigationButton}
2997 this._nextMonthButton = new CalendarNavigationButton();
2998 this._nextMonthButton.attachTo(this);
2999 this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
3000 this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
3001 this._nextMonthButton.element.setAttribute("aria-label", global.params.axShowNextMonth);
3003 if (global.params.isLocaleRTL) {
3004 this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
3005 this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
3007 this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
3008 this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
3012 CalendarHeaderView.prototype = Object.create(View.prototype);
3014 CalendarHeaderView.Height = 24;
3015 CalendarHeaderView.BottomMargin = 10;
3016 CalendarHeaderView._ForwardTriangle = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>";
3017 CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>";
3018 CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view";
3019 CalendarHeaderView.ClassNameCalendarTitle = "calendar-title";
3020 CalendarHeaderView.ClassNameTodayButton = "today-button";
3022 CalendarHeaderView.prototype.onCurrentMonthChanged = function() {
3023 this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth());
3024 this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
3025 this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
3028 CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) {
3029 if (sender === this._previousMonthButton)
3030 this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation);
3031 else if (sender === this._nextMonthButton)
3032 this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation);
3034 this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
3038 * @param {!boolean} disabled
3040 CalendarHeaderView.prototype.setDisabled = function(disabled) {
3041 this.disabled = disabled;
3042 this.monthPopupButton.element.disabled = this.disabled;
3043 this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
3044 this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
3045 var monthContainingToday = Month.createFromToday();
3046 this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
3053 function DayCell() {
3054 ListCell.call(this);
3055 this.element.classList.add(DayCell.ClassNameDayCell);
3056 this.element.style.width = DayCell.Width + "px";
3057 this.element.style.height = DayCell.Height + "px";
3058 this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px";
3059 this.element.setAttribute("role", "gridcell");
3066 DayCell.prototype = Object.create(ListCell.prototype);
3069 DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20;
3070 DayCell.PaddingSize = 1;
3071 DayCell.ClassNameDayCell = "day-cell";
3072 DayCell.ClassNameHighlighted = "highlighted";
3073 DayCell.ClassNameDisabled = "disabled";
3074 DayCell.ClassNameCurrentMonth = "current-month";
3075 DayCell.ClassNameToday = "today";
3077 DayCell._recycleBin = [];
3079 DayCell.recycleOrCreate = function() {
3080 return DayCell._recycleBin.pop() || new DayCell();
3087 DayCell.prototype._recycleBin = function() {
3088 return DayCell._recycleBin;
3094 DayCell.prototype.throwAway = function() {
3095 ListCell.prototype.throwAway.call(this);
3100 * @param {!boolean} highlighted
3102 DayCell.prototype.setHighlighted = function(highlighted) {
3104 this.element.classList.add(DayCell.ClassNameHighlighted);
3105 this.element.setAttribute("aria-selected", "true");
3107 this.element.classList.remove(DayCell.ClassNameHighlighted);
3108 this.element.setAttribute("aria-selected", "false");
3113 * @param {!boolean} disabled
3115 DayCell.prototype.setDisabled = function(disabled) {
3117 this.element.classList.add(DayCell.ClassNameDisabled);
3119 this.element.classList.remove(DayCell.ClassNameDisabled);
3123 * @param {!boolean} selected
3125 DayCell.prototype.setIsInCurrentMonth = function(selected) {
3127 this.element.classList.add(DayCell.ClassNameCurrentMonth);
3129 this.element.classList.remove(DayCell.ClassNameCurrentMonth);
3133 * @param {!boolean} selected
3135 DayCell.prototype.setIsToday = function(selected) {
3137 this.element.classList.add(DayCell.ClassNameToday);
3139 this.element.classList.remove(DayCell.ClassNameToday);
3145 DayCell.prototype.reset = function(day) {
3147 this.element.textContent = localizeNumber(this.day.date.toString());
3148 this.element.setAttribute("aria-label", this.day.format());
3149 this.element.id = this.day.toString();
3157 function WeekNumberCell() {
3158 ListCell.call(this);
3159 this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell);
3160 this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px";
3161 this.element.style.height = WeekNumberCell.Height + "px";
3162 this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px";
3169 WeekNumberCell.prototype = Object.create(ListCell.prototype);
3171 WeekNumberCell.Width = 48;
3172 WeekNumberCell.Height = DayCell.Height;
3173 WeekNumberCell.SeparatorWidth = 1;
3174 WeekNumberCell.PaddingSize = 1;
3175 WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell";
3176 WeekNumberCell.ClassNameHighlighted = "highlighted";
3177 WeekNumberCell.ClassNameDisabled = "disabled";
3179 WeekNumberCell._recycleBin = [];
3185 WeekNumberCell.prototype._recycleBin = function() {
3186 return WeekNumberCell._recycleBin;
3190 * @return {!WeekNumberCell}
3192 WeekNumberCell.recycleOrCreate = function() {
3193 return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
3197 * @param {!Week} week
3199 WeekNumberCell.prototype.reset = function(week) {
3201 this.element.id = week.toString();
3202 this.element.setAttribute("role", "gridcell");
3203 this.element.setAttribute("aria-label", window.pagePopupController.formatWeek(week.year, week.week, week.firstDay().format()));
3204 this.element.textContent = localizeNumber(this.week.week.toString());
3211 WeekNumberCell.prototype.throwAway = function() {
3212 ListCell.prototype.throwAway.call(this);
3216 WeekNumberCell.prototype.setHighlighted = function(highlighted) {
3218 this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
3219 this.element.setAttribute("aria-selected", "true");
3221 this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
3222 this.element.setAttribute("aria-selected", "false");
3226 WeekNumberCell.prototype.setDisabled = function(disabled) {
3228 this.element.classList.add(WeekNumberCell.ClassNameDisabled);
3230 this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
3236 * @param {!boolean} hasWeekNumberColumn
3238 function CalendarTableHeaderView(hasWeekNumberColumn) {
3239 View.call(this, createElement("div", "calendar-table-header-view"));
3240 if (hasWeekNumberColumn) {
3241 var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel);
3242 weekNumberLabelElement.style.width = WeekNumberCell.Width + "px";
3243 this.element.appendChild(weekNumberLabelElement);
3245 for (var i = 0; i < DaysPerWeek; ++i) {
3246 var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek;
3247 var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]);
3248 labelElement.style.width = DayCell.Width + "px";
3249 this.element.appendChild(labelElement);
3250 if (getLanguage() === "ja") {
3251 if (weekDayNumber === 0)
3252 labelElement.style.color = "red";
3253 else if (weekDayNumber === 6)
3254 labelElement.style.color = "blue";
3259 CalendarTableHeaderView.prototype = Object.create(View.prototype);
3261 CalendarTableHeaderView.Height = 25;
3267 function CalendarRowCell() {
3268 ListCell.call(this);
3269 this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
3270 this.element.style.height = CalendarRowCell.Height + "px";
3271 this.element.setAttribute("role", "row");
3277 this._dayCells = [];
3283 * @type {?CalendarTableView}
3285 this.calendarTableView = null;
3288 CalendarRowCell.prototype = Object.create(ListCell.prototype);
3290 CalendarRowCell.Height = DayCell.Height;
3291 CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";
3293 CalendarRowCell._recycleBin = [];
3299 CalendarRowCell.prototype._recycleBin = function() {
3300 return CalendarRowCell._recycleBin;
3304 * @param {!number} row
3305 * @param {!CalendarTableView} calendarTableView
3307 CalendarRowCell.prototype.reset = function(row, calendarTableView) {
3309 this.calendarTableView = calendarTableView;
3310 if (this.calendarTableView.hasWeekNumberColumn) {
3311 var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row);
3312 var week = Week.createFromDay(middleDay);
3313 this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week);
3314 this.weekNumberCell.attachTo(this);
3316 var day = calendarTableView.dayAtColumnAndRow(0, row);
3317 for (var i = 0; i < DaysPerWeek; ++i) {
3318 var dayCell = this.calendarTableView.prepareNewDayCell(day);
3319 dayCell.attachTo(this);
3320 this._dayCells.push(dayCell);
3329 CalendarRowCell.prototype.throwAway = function() {
3330 ListCell.prototype.throwAway.call(this);
3331 if (this.weekNumberCell)
3332 this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell);
3333 this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView);
3334 this._dayCells.length = 0;
3340 * @param {!CalendarPicker} calendarPicker
3342 function CalendarTableView(calendarPicker) {
3343 ListView.call(this);
3344 this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
3345 this.element.tabIndex = 0;
3351 this.hasWeekNumberColumn = calendarPicker.type === "week";
3353 * @type {!CalendarPicker}
3356 this.calendarPicker = calendarPicker;
3361 this._dayCells = {};
3362 var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
3363 headerView.attachTo(this, this.scrollView);
3365 if (this.hasWeekNumberColumn) {
3366 this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
3371 this._weekNumberCells = [];
3373 this.setWidth(DayCell.Width * DaysPerWeek);
3380 this._ignoreMouseOutUntillNextMouseOver = false;
3382 this.element.addEventListener("click", this.onClick, false);
3383 this.element.addEventListener("mouseover", this.onMouseOver, false);
3384 this.element.addEventListener("mouseout", this.onMouseOut, false);
3386 // You shouldn't be able to use the mouse wheel to scroll.
3387 this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false);
3388 // You shouldn't be able to do gesture scroll.
3389 this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false);
3392 CalendarTableView.prototype = Object.create(ListView.prototype);
3394 CalendarTableView.BorderWidth = 1;
3395 CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";
3398 * @param {!number} scrollOffset
3401 CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
3402 return Math.floor(scrollOffset / CalendarRowCell.Height);
3406 * @param {!number} row
3409 CalendarTableView.prototype.scrollOffsetForRow = function(row) {
3410 return row * CalendarRowCell.Height;
3414 * @param {?Event} event
3416 CalendarTableView.prototype.onClick = function(event) {
3417 if (this.hasWeekNumberColumn) {
3418 var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3419 if (weekNumberCellElement) {
3420 var weekNumberCell = weekNumberCellElement.$view;
3421 this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay());
3425 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3426 if (!dayCellElement)
3428 var dayCell = dayCellElement.$view;
3429 this.calendarPicker.selectRangeContainingDay(dayCell.day);
3433 * @param {?Event} event
3435 CalendarTableView.prototype.onMouseOver = function(event) {
3436 if (this.hasWeekNumberColumn) {
3437 var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3438 if (weekNumberCellElement) {
3439 var weekNumberCell = weekNumberCellElement.$view;
3440 this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay());
3441 this._ignoreMouseOutUntillNextMouseOver = false;
3445 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3446 if (!dayCellElement)
3448 var dayCell = dayCellElement.$view;
3449 this.calendarPicker.highlightRangeContainingDay(dayCell.day);
3450 this._ignoreMouseOutUntillNextMouseOver = false;
3454 * @param {?Event} event
3456 CalendarTableView.prototype.onMouseOut = function(event) {
3457 if (this._ignoreMouseOutUntillNextMouseOver)
3459 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3460 if (!dayCellElement) {
3461 this.calendarPicker.highlightRangeContainingDay(null);
3466 * @param {!number} row
3467 * @return {!CalendarRowCell}
3469 CalendarTableView.prototype.prepareNewCell = function(row) {
3470 var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
3471 cell.reset(row, this);
3476 * @return {!number} Height in pixels.
3478 CalendarTableView.prototype.height = function() {
3479 return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
3483 * @param {!number} height Height in pixels.
3485 CalendarTableView.prototype.setHeight = function(height) {
3486 this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
3490 * @param {!Month} month
3491 * @param {!boolean} animate
3493 CalendarTableView.prototype.scrollToMonth = function(month, animate) {
3494 var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
3495 this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
3499 * @param {!number} column
3500 * @param {!number} row
3503 CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) {
3504 var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay;
3505 return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue);
3508 CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
3509 CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();
3513 * @return {!Object} Object with properties column and row.
3515 CalendarTableView.prototype.columnAndRowForDay = function(day) {
3516 var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay;
3517 var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay;
3518 var row = Math.floor(offset / DaysPerWeek);
3519 var column = offset - row * DaysPerWeek;
3526 CalendarTableView.prototype.updateCells = function() {
3527 ListView.prototype.updateCells.call(this);
3529 var selection = this.calendarPicker.selection();
3530 var firstDayInSelection;
3531 var lastDayInSelection;
3533 firstDayInSelection = selection.firstDay().valueOf();
3534 lastDayInSelection = selection.lastDay().valueOf();
3536 firstDayInSelection = Infinity;
3537 lastDayInSelection = Infinity;
3539 var highlight = this.calendarPicker.highlight();
3540 var firstDayInHighlight;
3541 var lastDayInHighlight;
3543 firstDayInHighlight = highlight.firstDay().valueOf();
3544 lastDayInHighlight = highlight.lastDay().valueOf();
3546 firstDayInHighlight = Infinity;
3547 lastDayInHighlight = Infinity;
3549 var currentMonth = this.calendarPicker.currentMonth();
3550 var firstDayInCurrentMonth = currentMonth.firstDay().valueOf();
3551 var lastDayInCurrentMonth = currentMonth.lastDay().valueOf();
3552 var activeCell = null;
3553 for (var dayString in this._dayCells) {
3554 var dayCell = this._dayCells[dayString];
3555 var day = dayCell.day;
3556 dayCell.setIsToday(Day.createFromToday().equals(day));
3557 dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection);
3558 var isHighlighted = day >= firstDayInHighlight && day <= lastDayInHighlight;
3559 dayCell.setHighlighted(isHighlighted);
3560 if (isHighlighted) {
3561 if (firstDayInHighlight == lastDayInHighlight)
3562 activeCell = dayCell;
3563 else if (this.calendarPicker.type == "month" && day == firstDayInHighlight)
3564 activeCell = dayCell;
3566 dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth);
3567 dayCell.setDisabled(!this.calendarPicker.isValidDay(day));
3569 if (this.hasWeekNumberColumn) {
3570 for (var weekString in this._weekNumberCells) {
3571 var weekNumberCell = this._weekNumberCells[weekString];
3572 var week = weekNumberCell.week;
3573 var isWeekHighlighted = highlight && highlight.equals(week);
3574 weekNumberCell.setSelected(selection && selection.equals(week));
3575 weekNumberCell.setHighlighted(isWeekHighlighted);
3576 if (isWeekHighlighted)
3577 activeCell = weekNumberCell;
3578 weekNumberCell.setDisabled(!this.calendarPicker.isValid(week));
3582 // Ensure a renderer because an element with no renderer doesn't post
3583 // activedescendant events. This shouldn't run in the above |for| loop
3584 // to avoid CSS transition.
3585 activeCell.element.offsetLeft;
3586 this.element.setAttribute("aria-activedescendant", activeCell.element.id);
3592 * @return {!DayCell}
3594 CalendarTableView.prototype.prepareNewDayCell = function(day) {
3595 var dayCell = DayCell.recycleOrCreate();
3597 if (this.calendarPicker.type == "month")
3598 dayCell.element.setAttribute("aria-label", Month.createFromDay(day).toLocaleString());
3599 this._dayCells[dayCell.day.toString()] = dayCell;
3604 * @param {!Week} week
3605 * @return {!WeekNumberCell}
3607 CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) {
3608 var weekNumberCell = WeekNumberCell.recycleOrCreate();
3609 weekNumberCell.reset(week);
3610 this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell;
3611 return weekNumberCell;
3615 * @param {!DayCell} dayCell
3617 CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
3618 delete this._dayCells[dayCell.day.toString()];
3619 dayCell.throwAway();
3623 * @param {!WeekNumberCell} weekNumberCell
3625 CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
3626 delete this._weekNumberCells[weekNumberCell.week.toString()];
3627 weekNumberCell.throwAway();
3633 * @param {!Object} config
3635 function CalendarPicker(type, config) {
3636 View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
3637 this.element.classList.add(CalendarPicker.ClassNamePreparing);
3644 if (this.type === "week")
3645 this._dateTypeConstructor = Week;
3646 else if (this.type === "month")
3647 this._dateTypeConstructor = Month;
3649 this._dateTypeConstructor = Day;
3655 this._setConfig(config);
3660 this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
3665 this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
3666 if (global.params.isLocaleRTL)
3667 this.element.classList.add("rtl");
3669 * @type {!CalendarTableView}
3672 this.calendarTableView = new CalendarTableView(this);
3673 this.calendarTableView.hasNumberColumn = this.type === "week";
3675 * @type {!CalendarHeaderView}
3678 this.calendarHeaderView = new CalendarHeaderView(this);
3679 this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
3681 * @type {!MonthPopupView}
3684 this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth);
3685 this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth);
3686 this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide);
3687 this.calendarHeaderView.attachTo(this);
3688 this.calendarTableView.attachTo(this);
3693 this._currentMonth = new Month(NaN, NaN);
3698 this._selection = null;
3703 this._highlight = null;
3704 this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
3705 document.body.addEventListener("keydown", this.onBodyKeyDown, false);
3707 window.addEventListener("resize", this.onWindowResize, false);
3715 var initialSelection = parseDateString(config.currentValue);
3716 if (initialSelection) {
3717 this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
3718 this.setSelection(initialSelection);
3720 this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
3723 CalendarPicker.prototype = Object.create(View.prototype);
3725 CalendarPicker.Padding = 10;
3726 CalendarPicker.BorderWidth = 1;
3727 CalendarPicker.ClassNameCalendarPicker = "calendar-picker";
3728 CalendarPicker.ClassNamePreparing = "preparing";
3729 CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged";
3730 CalendarPicker.commitDelayMs = 100;
3733 * @param {!Event} event
3735 CalendarPicker.prototype.onWindowResize = function(event) {
3736 this.element.classList.remove(CalendarPicker.ClassNamePreparing);
3737 window.removeEventListener("resize", this.onWindowResize, false);
3741 * @param {!YearListView} sender
3743 CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
3744 this.monthPopupView.hide();
3745 this.calendarHeaderView.setDisabled(false);
3746 this.adjustHeight();
3750 * @param {!YearListView} sender
3751 * @param {!Month} month
3753 CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
3754 this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
3758 * @param {!View|Node} parent
3759 * @param {?View|Node=} before
3762 CalendarPicker.prototype.attachTo = function(parent, before) {
3763 View.prototype.attachTo.call(this, parent, before);
3764 this.calendarTableView.element.focus();
3767 CalendarPicker.prototype.cleanup = function() {
3768 window.removeEventListener("resize", this.onWindowResize, false);
3769 this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false);
3770 // Month popup view might be attached to document.body.
3771 this.monthPopupView.hide();
3775 * @param {?MonthPopupButton} sender
3777 CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) {
3778 var clientRect = this.calendarTableView.element.getBoundingClientRect();
3779 var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height);
3780 this.monthPopupView.show(this.currentMonth(), calendarTableRect);
3781 this.calendarHeaderView.setDisabled(true);
3782 this.adjustHeight();
3785 CalendarPicker.prototype._setConfig = function(config) {
3786 this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum;
3787 this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum;
3788 this.config.minimumValue = this.config.minimum.valueOf();
3789 this.config.maximumValue = this.config.maximum.valueOf();
3790 this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep;
3791 this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase;
3797 CalendarPicker.prototype.currentMonth = function() {
3798 return this._currentMonth;
3804 CalendarPicker.NavigationBehavior = {
3810 * @param {!Month} month
3811 * @param {!CalendarPicker.NavigationBehavior} animate
3813 CalendarPicker.prototype.setCurrentMonth = function(month, behavior) {
3814 if (month > this.maximumMonth)
3815 month = this.maximumMonth;
3816 else if (month < this.minimumMonth)
3817 month = this.minimumMonth;
3818 if (this._currentMonth.equals(month))
3820 this._currentMonth = month;
3821 this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation);
3822 this.adjustHeight();
3823 this.calendarTableView.setNeedsUpdateCells(true);
3824 this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, {
3829 CalendarPicker.prototype.adjustHeight = function() {
3830 var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row;
3831 var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row;
3832 var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1;
3833 var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2;
3834 var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2;
3835 this.setHeight(height);
3838 CalendarPicker.prototype.selection = function() {
3839 return this._selection;
3842 CalendarPicker.prototype.highlight = function() {
3843 return this._highlight;
3849 CalendarPicker.prototype.firstVisibleDay = function() {
3850 var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
3851 var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3852 if (!firstVisibleDay)
3853 firstVisibleDay = Day.Minimum;
3854 return firstVisibleDay;
3860 CalendarPicker.prototype.lastVisibleDay = function() {
3861 var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row;
3862 var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3863 if (!lastVisibleDay)
3864 lastVisibleDay = Day.Maximum;
3865 return lastVisibleDay;
3871 CalendarPicker.prototype.selectRangeContainingDay = function(day) {
3872 var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
3873 this.setSelectionAndCommit(selection);
3879 CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
3880 var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
3881 this._setHighlight(highlight);
3885 * Select the specified date.
3886 * @param {?DateType} dayOrWeekOrMonth
3888 CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
3889 if (!this._selection && !dayOrWeekOrMonth)
3891 if (this._selection && this._selection.equals(dayOrWeekOrMonth))
3893 var firstDayInSelection = dayOrWeekOrMonth.firstDay();
3894 var lastDayInSelection = dayOrWeekOrMonth.lastDay();
3895 var candidateCurrentMonth = Month.createFromDay(firstDayInSelection);
3896 if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) {
3897 // Change current month if the selection is not visible at all.
3898 this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3899 } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) {
3900 // If the selection is partly visible, only change the current month if
3901 // doing so will make the whole selection visible.
3902 var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row;
3903 var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3904 var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row;
3905 var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3906 if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay)
3907 this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3909 this._setHighlight(dayOrWeekOrMonth);
3910 if (!this.isValid(dayOrWeekOrMonth))
3912 this._selection = dayOrWeekOrMonth;
3913 this.calendarTableView.setNeedsUpdateCells(true);
3917 * Select the specified date, commit it, and close the popup.
3918 * @param {?DateType} dayOrWeekOrMonth
3920 CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) {
3921 this.setSelection(dayOrWeekOrMonth);
3922 // Redraw the widget immidiately, and wait for some time to give feedback to
3924 this.element.offsetLeft;
3925 var value = this._selection.toString();
3926 if (CalendarPicker.commitDelayMs == 0) {
3928 window.pagePopupController.setValueAndClosePopup(0, value);
3929 } else if (CalendarPicker.commitDelayMs < 0) {
3931 window.pagePopupController.setValue(value);
3933 setTimeout(function() {
3934 window.pagePopupController.setValueAndClosePopup(0, value);
3935 }, CalendarPicker.commitDelayMs);
3940 * @param {?DateType} dayOrWeekOrMonth
3942 CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
3943 if (!this._highlight && !dayOrWeekOrMonth)
3945 if (!dayOrWeekOrMonth && !this._highlight)
3947 if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
3949 this._highlight = dayOrWeekOrMonth;
3950 this.calendarTableView.setNeedsUpdateCells(true);
3954 * @param {!number} value
3955 * @return {!boolean}
3957 CalendarPicker.prototype._stepMismatch = function(value) {
3958 var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase;
3959 return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep;
3963 * @param {!number} value
3964 * @return {!boolean}
3966 CalendarPicker.prototype._outOfRange = function(value) {
3967 return value < this.config.minimumValue || value > this.config.maximumValue;
3971 * @param {!DateType} dayOrWeekOrMonth
3972 * @return {!boolean}
3974 CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
3975 var value = dayOrWeekOrMonth.valueOf();
3976 return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
3981 * @return {!boolean}
3983 CalendarPicker.prototype.isValidDay = function(day) {
3984 return this.isValid(this._dateTypeConstructor.createFromDay(day));
3988 * @param {!DateType} dateRange
3989 * @return {!boolean} Returns true if the highlight was changed.
3991 CalendarPicker.prototype._moveHighlight = function(dateRange) {
3994 if (this._outOfRange(dateRange.valueOf()))
3996 if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
3997 this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
3998 this._setHighlight(dateRange);
4003 * @param {?Event} event
4005 CalendarPicker.prototype.onCalendarTableKeyDown = function(event) {
4006 var key = event.keyIdentifier;
4007 var eventHandled = false;
4008 if (key == "U+0054") { // 't' key.
4009 this.selectRangeContainingDay(Day.createFromToday());
4010 eventHandled = true;
4011 } else if (key == "PageUp") {
4012 var previousMonth = this.currentMonth().previous();
4013 if (previousMonth && previousMonth >= this.config.minimumValue) {
4014 this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation);
4015 eventHandled = true;
4017 } else if (key == "PageDown") {
4018 var nextMonth = this.currentMonth().next();
4019 if (nextMonth && nextMonth >= this.config.minimumValue) {
4020 this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation);
4021 eventHandled = true;
4023 } else if (this._highlight) {
4024 if (global.params.isLocaleRTL ? key == "Right" : key == "Left") {
4025 eventHandled = this._moveHighlight(this._highlight.previous());
4026 } else if (key == "Up") {
4027 eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1));
4028 } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") {
4029 eventHandled = this._moveHighlight(this._highlight.next());
4030 } else if (key == "Down") {
4031 eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1));
4032 } else if (key == "Enter") {
4033 this.setSelectionAndCommit(this._highlight);
4035 } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") {
4036 // Highlight range near the middle.
4037 this.highlightRangeContainingDay(this.currentMonth().middleDay());
4038 eventHandled = true;
4042 event.stopPropagation();
4043 event.preventDefault();
4048 * @return {!number} Width in pixels.
4050 CalendarPicker.prototype.width = function() {
4051 return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
4055 * @return {!number} Height in pixels.
4057 CalendarPicker.prototype.height = function() {
4058 return this._height;
4062 * @param {!number} height Height in pixels.
4064 CalendarPicker.prototype.setHeight = function(height) {
4065 if (this._height === height)
4067 this._height = height;
4068 resizeWindow(this.width(), this._height);
4069 this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2);
4073 * @param {?Event} event
4075 CalendarPicker.prototype.onBodyKeyDown = function(event) {
4076 var key = event.keyIdentifier;
4077 var eventHandled = false;
4080 case "U+001B": // Esc key.
4081 window.pagePopupController.closePopup();
4082 eventHandled = true;
4084 case "U+004D": // 'm' key.
4085 offset = offset || 1; // Fall-through.
4086 case "U+0059": // 'y' key.
4087 offset = offset || MonthsPerYear; // Fall-through.
4088 case "U+0044": // 'd' key.
4089 offset = offset || MonthsPerYear * 10;
4090 var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4091 this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation);
4092 var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4093 if (this._highlight) {
4094 var highlightMiddleDay = this._highlight.middleDay();
4095 this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek));
4101 event.stopPropagation();
4102 event.preventDefault();
4106 if (window.dialogArguments) {
4107 initialize(dialogArguments);
4109 window.addEventListener("message", handleMessage, false);