3 * Copyright (C) 2012 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
9 * * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
15 * * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
53 weekStartDay: WeekDay.Sunday,
54 dayLabels: ["S", "M", "T", "W", "T", "F", "S"],
55 shortMonthLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"],
59 anchorRectInScreen: new Rectangle(0, 0, 0, 0),
64 // ----------------------------------------------------------------
70 function hasInaccuratePointingDevice() {
71 return matchMedia("(pointer: coarse)").matches;
75 * @return {!string} lowercase locale name. e.g. "en-us"
77 function getLocale() {
78 return (global.params.locale || "en-us").toLowerCase();
82 * @return {!string} lowercase language code. e.g. "en"
84 function getLanguage() {
85 var locale = getLocale();
86 var result = locale.match(/^([a-z]+)/);
93 * @param {!number} number
96 function localizeNumber(number) {
97 return window.pagePopupController.localizeNumberString(number);
104 var ImperialEraLimit = 2087;
107 * @param {!number} year
108 * @param {!number} month
111 function formatJapaneseImperialEra(year, month) {
112 // We don't show an imperial era if it is greater than 99 becase of space
114 if (year > ImperialEraLimit)
117 return "(\u5e73\u6210" + localizeNumber(year - 1988) + "\u5e74)";
119 return "(\u5e73\u6210\u5143\u5e74)";
121 return "(\u662d\u548c" + localizeNumber(year - 1925) + "\u5e74)";
123 return "(\u5927\u6b63" + localizeNumber(year - 1911) + "\u5e74)";
124 if (year == 1912 && month >= 7)
125 return "(\u5927\u6b63\u5143\u5e74)";
127 return "(\u660e\u6cbb" + localizeNumber(year - 1867) + "\u5e74)";
129 return "(\u660e\u6cbb\u5143\u5e74)";
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 + "\u5e74" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "\u6708";
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"));
840 // Parameter t should be a number between 0 and 1.
841 var AnimationTimingFunction = {
845 EaseInOut: function(t){
848 return Math.pow(t, 3) / 2;
850 return Math.pow(t, 3) / 2 + 1;
856 * @extends EventEmitter
858 function AnimationManager() {
859 EventEmitter.call(this);
861 this._isRunning = false;
862 this._runningAnimatorCount = 0;
863 this._runningAnimators = {};
864 this._animationFrameCallbackBound = this._animationFrameCallback.bind(this);
867 AnimationManager.prototype = Object.create(EventEmitter.prototype);
869 AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish";
871 AnimationManager.prototype._startAnimation = function() {
874 this._isRunning = true;
875 window.requestAnimationFrame(this._animationFrameCallbackBound);
878 AnimationManager.prototype._stopAnimation = function() {
879 if (!this._isRunning)
881 this._isRunning = false;
885 * @param {!Animator} animator
887 AnimationManager.prototype.add = function(animator) {
888 if (this._runningAnimators[animator.id])
890 this._runningAnimators[animator.id] = animator;
891 this._runningAnimatorCount++;
892 if (this._needsTimer())
893 this._startAnimation();
897 * @param {!Animator} animator
899 AnimationManager.prototype.remove = function(animator) {
900 if (!this._runningAnimators[animator.id])
902 delete this._runningAnimators[animator.id];
903 this._runningAnimatorCount--;
904 if (!this._needsTimer())
905 this._stopAnimation();
908 AnimationManager.prototype._animationFrameCallback = function(now) {
909 if (this._runningAnimatorCount > 0) {
910 for (var id in this._runningAnimators) {
911 this._runningAnimators[id].onAnimationFrame(now);
914 this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish);
916 window.requestAnimationFrame(this._animationFrameCallbackBound);
922 AnimationManager.prototype._needsTimer = function() {
923 return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish);
927 * @param {!string} type
928 * @param {!Function} callback
931 AnimationManager.prototype.on = function(type, callback) {
932 EventEmitter.prototype.on.call(this, type, callback);
933 if (this._needsTimer())
934 this._startAnimation();
938 * @param {!string} type
939 * @param {!Function} callback
942 AnimationManager.prototype.removeListener = function(type, callback) {
943 EventEmitter.prototype.removeListener.call(this, type, callback);
944 if (!this._needsTimer())
945 this._stopAnimation();
948 AnimationManager.shared = new AnimationManager();
952 * @extends EventEmitter
954 function Animator() {
955 EventEmitter.call(this);
961 this.id = Animator._lastId++;
974 this._isRunning = false;
978 this.currentValue = 0;
983 this._lastStepTime = 0;
986 Animator.prototype = Object.create(EventEmitter.prototype);
988 Animator._lastId = 0;
990 Animator.EventTypeDidAnimationStop = "didAnimationStop";
995 Animator.prototype.isRunning = function() {
996 return this._isRunning;
999 Animator.prototype.start = function() {
1000 this._lastStepTime = performance.now();
1001 this._isRunning = true;
1002 AnimationManager.shared.add(this);
1005 Animator.prototype.stop = function() {
1006 if (!this._isRunning)
1008 this._isRunning = false;
1009 AnimationManager.shared.remove(this);
1010 this.dispatchEvent(Animator.EventTypeDidAnimationStop, this);
1014 * @param {!number} now
1016 Animator.prototype.onAnimationFrame = function(now) {
1017 this._lastStepTime = now;
1025 function TransitionAnimator() {
1026 Animator.call(this);
1045 this.progress = 0.0;
1049 this.timingFunction = AnimationTimingFunction.Linear;
1052 TransitionAnimator.prototype = Object.create(Animator.prototype);
1055 * @param {!number} value
1057 TransitionAnimator.prototype.setFrom = function(value) {
1059 this._delta = this._to - this._from;
1062 TransitionAnimator.prototype.start = function() {
1063 console.assert(isFinite(this.duration));
1064 this.progress = 0.0;
1065 this.currentValue = this._from;
1066 Animator.prototype.start.call(this);
1070 * @param {!number} value
1072 TransitionAnimator.prototype.setTo = function(value) {
1074 this._delta = this._to - this._from;
1078 * @param {!number} now
1080 TransitionAnimator.prototype.onAnimationFrame = function(now) {
1081 this.progress += (now - this._lastStepTime) / this.duration;
1082 this.progress = Math.min(1.0, this.progress);
1083 this._lastStepTime = now;
1084 this.currentValue = this.timingFunction(this.progress) * this._delta + this._from;
1086 if (this.progress === 1.0) {
1095 * @param {!number} initialVelocity
1096 * @param {!number} initialValue
1098 function FlingGestureAnimator(initialVelocity, initialValue) {
1099 Animator.call(this);
1103 this.initialVelocity = initialVelocity;
1107 this.initialValue = initialValue;
1112 this._elapsedTime = 0;
1113 var startVelocity = Math.abs(this.initialVelocity);
1114 if (startVelocity > this._velocityAtTime(0))
1115 startVelocity = this._velocityAtTime(0);
1116 if (startVelocity < 0)
1122 this._timeOffset = this._timeAtVelocity(startVelocity);
1127 this._positionOffset = this._valueAtTime(this._timeOffset);
1131 this.duration = this._timeAtVelocity(0);
1134 FlingGestureAnimator.prototype = Object.create(Animator.prototype);
1136 // Velocity is subject to exponential decay. These parameters are coefficients
1137 // that determine the curve.
1138 FlingGestureAnimator._P0 = -5707.62;
1139 FlingGestureAnimator._P1 = 0.172;
1140 FlingGestureAnimator._P2 = 0.0037;
1143 * @param {!number} t
1145 FlingGestureAnimator.prototype._valueAtTime = function(t) {
1146 return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0;
1150 * @param {!number} t
1152 FlingGestureAnimator.prototype._velocityAtTime = function(t) {
1153 return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1;
1157 * @param {!number} v
1159 FlingGestureAnimator.prototype._timeAtVelocity = function(v) {
1160 return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2;
1163 FlingGestureAnimator.prototype.start = function() {
1164 this._lastStepTime = performance.now();
1165 Animator.prototype.start.call(this);
1169 * @param {!number} now
1171 FlingGestureAnimator.prototype.onAnimationFrame = function(now) {
1172 this._elapsedTime += now - this._lastStepTime;
1173 this._lastStepTime = now;
1174 if (this._elapsedTime + this._timeOffset >= this.duration) {
1178 var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset;
1179 if (this.initialVelocity < 0)
1180 position = -position;
1181 this.currentValue = position + this.initialValue;
1187 * @extends EventEmitter
1188 * @param {?Element} element
1189 * View adds itself as a property on the element so we can access it from Event.target.
1191 function View(element) {
1192 EventEmitter.call(this);
1197 this.element = element || createElement("div");
1198 this.element.$view = this;
1199 this.bindCallbackMethods();
1202 View.prototype = Object.create(EventEmitter.prototype);
1205 * @param {!Element} ancestorElement
1208 View.prototype.offsetRelativeTo = function(ancestorElement) {
1211 var element = this.element;
1213 x += element.offsetLeft || 0;
1214 y += element.offsetTop || 0;
1215 element = element.offsetParent;
1216 if (element === ancestorElement)
1217 return {x: x, y: y};
1223 * @param {!View|Node} parent
1224 * @param {?View|Node=} before
1226 View.prototype.attachTo = function(parent, before) {
1227 if (parent instanceof View)
1228 return this.attachTo(parent.element, before);
1229 if (typeof before === "undefined")
1231 if (before instanceof View)
1232 before = before.element;
1233 parent.insertBefore(this.element, before);
1236 View.prototype.bindCallbackMethods = function() {
1237 for (var methodName in this) {
1238 if (!/^on[A-Z]/.test(methodName))
1240 if (this.hasOwnProperty(methodName))
1242 var method = this[methodName];
1243 if (!(method instanceof Function))
1245 this[methodName] = method.bind(this);
1253 function ScrollView() {
1254 View.call(this, createElement("div", ScrollView.ClassNameScrollView));
1259 this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent);
1260 this.element.appendChild(this.contentElement);
1264 this.minimumContentOffset = -Infinity;
1268 this.maximumContentOffset = Infinity;
1273 this._contentOffset = 0;
1288 this._scrollAnimator = null;
1292 this.delegate = null;
1296 this._lastTouchPosition = 0;
1300 this._lastTouchVelocity = 0;
1304 this._lastTouchTimeStamp = 0;
1306 this.element.addEventListener("mousewheel", this.onMouseWheel, false);
1307 this.element.addEventListener("touchstart", this.onTouchStart, false);
1310 * The content offset is partitioned so the it can go beyond the CSS limit
1315 this._partitionNumber = 0;
1318 ScrollView.prototype = Object.create(View.prototype);
1320 ScrollView.PartitionHeight = 100000;
1321 ScrollView.ClassNameScrollView = "scroll-view";
1322 ScrollView.ClassNameScrollViewContent = "scroll-view-content";
1325 * @param {!Event} event
1327 ScrollView.prototype.onTouchStart = function(event) {
1328 var touch = event.touches[0];
1329 this._lastTouchPosition = touch.clientY;
1330 this._lastTouchVelocity = 0;
1331 this._lastTouchTimeStamp = event.timeStamp;
1332 if (this._scrollAnimator)
1333 this._scrollAnimator.stop();
1334 window.addEventListener("touchmove", this.onWindowTouchMove, false);
1335 window.addEventListener("touchend", this.onWindowTouchEnd, false);
1339 * @param {!Event} event
1341 ScrollView.prototype.onWindowTouchMove = function(event) {
1342 var touch = event.touches[0];
1343 var deltaTime = event.timeStamp - this._lastTouchTimeStamp;
1344 var deltaY = this._lastTouchPosition - touch.clientY;
1345 this.scrollBy(deltaY, false);
1346 this._lastTouchVelocity = deltaY / deltaTime;
1347 this._lastTouchPosition = touch.clientY;
1348 this._lastTouchTimeStamp = event.timeStamp;
1349 event.stopPropagation();
1350 event.preventDefault();
1354 * @param {!Event} event
1356 ScrollView.prototype.onWindowTouchEnd = function(event) {
1357 if (Math.abs(this._lastTouchVelocity) > 0.01) {
1358 this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset);
1359 this._scrollAnimator.step = this.onFlingGestureAnimatorStep;
1360 this._scrollAnimator.start();
1362 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1363 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1367 * @param {!Animator} animator
1369 ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) {
1370 this.scrollTo(animator.currentValue, false);
1374 * @return {!Animator}
1376 ScrollView.prototype.scrollAnimator = function() {
1377 return this._scrollAnimator;
1381 * @param {!number} width
1383 ScrollView.prototype.setWidth = function(width) {
1384 console.assert(isFinite(width));
1385 if (this._width === width)
1387 this._width = width;
1388 this.element.style.width = this._width + "px";
1394 ScrollView.prototype.width = function() {
1399 * @param {!number} height
1401 ScrollView.prototype.setHeight = function(height) {
1402 console.assert(isFinite(height));
1403 if (this._height === height)
1405 this._height = height;
1406 this.element.style.height = height + "px";
1408 this.delegate.scrollViewDidChangeHeight(this);
1414 ScrollView.prototype.height = function() {
1415 return this._height;
1419 * @param {!Animator} animator
1421 ScrollView.prototype.onScrollAnimatorStep = function(animator) {
1422 this.setContentOffset(animator.currentValue);
1426 * @param {!number} offset
1427 * @param {?boolean} animate
1429 ScrollView.prototype.scrollTo = function(offset, animate) {
1430 console.assert(isFinite(offset));
1432 this.setContentOffset(offset);
1435 if (this._scrollAnimator)
1436 this._scrollAnimator.stop();
1437 this._scrollAnimator = new TransitionAnimator();
1438 this._scrollAnimator.step = this.onScrollAnimatorStep;
1439 this._scrollAnimator.setFrom(this._contentOffset);
1440 this._scrollAnimator.setTo(offset);
1441 this._scrollAnimator.duration = 300;
1442 this._scrollAnimator.start();
1446 * @param {!number} offset
1447 * @param {?boolean} animate
1449 ScrollView.prototype.scrollBy = function(offset, animate) {
1450 this.scrollTo(this._contentOffset + offset, animate);
1456 ScrollView.prototype.contentOffset = function() {
1457 return this._contentOffset;
1461 * @param {?Event} event
1463 ScrollView.prototype.onMouseWheel = function(event) {
1464 this.setContentOffset(this._contentOffset - event.wheelDelta / 30);
1465 event.stopPropagation();
1466 event.preventDefault();
1471 * @param {!number} value
1473 ScrollView.prototype.setContentOffset = function(value) {
1474 console.assert(isFinite(value));
1475 value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value)));
1476 if (this._contentOffset === value)
1478 this._contentOffset = value;
1479 this._updateScrollContent();
1481 this.delegate.scrollViewDidChangeContentOffset(this);
1484 ScrollView.prototype._updateScrollContent = function() {
1485 var newPartitionNumber = Math.floor(this._contentOffset / ScrollView.PartitionHeight);
1486 var partitionChanged = this._partitionNumber !== newPartitionNumber;
1487 this._partitionNumber = newPartitionNumber;
1488 this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)";
1489 if (this.delegate && partitionChanged)
1490 this.delegate.scrollViewDidChangePartition(this);
1494 * @param {!View|Node} parent
1495 * @param {?View|Node=} before
1498 ScrollView.prototype.attachTo = function(parent, before) {
1499 View.prototype.attachTo.call(this, parent, before);
1500 this._updateScrollContent();
1504 * @param {!number} offset
1506 ScrollView.prototype.contentPositionForContentOffset = function(offset) {
1507 return offset - this._partitionNumber * ScrollView.PartitionHeight;
1514 function ListCell() {
1515 View.call(this, createElement("div", ListCell.ClassNameListCell));
1531 ListCell.prototype = Object.create(View.prototype);
1533 ListCell.DefaultRecycleBinLimit = 64;
1534 ListCell.ClassNameListCell = "list-cell";
1535 ListCell.ClassNameHidden = "hidden";
1538 * @return {!Array} An array to keep thrown away cells.
1540 ListCell.prototype._recycleBin = function() {
1541 console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden.");
1545 ListCell.prototype.throwAway = function() {
1547 var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit;
1548 var recycleBin = this._recycleBin();
1549 if (recycleBin.length < limit)
1550 recycleBin.push(this);
1553 ListCell.prototype.show = function() {
1554 this.element.classList.remove(ListCell.ClassNameHidden);
1557 ListCell.prototype.hide = function() {
1558 this.element.classList.add(ListCell.ClassNameHidden);
1562 * @return {!number} Width in pixels.
1564 ListCell.prototype.width = function(){
1569 * @param {!number} width Width in pixels.
1571 ListCell.prototype.setWidth = function(width){
1572 if (this._width === width)
1574 this._width = width;
1575 this.element.style.width = this._width + "px";
1579 * @return {!number} Position in pixels.
1581 ListCell.prototype.position = function(){
1582 return this._position;
1586 * @param {!number} y Position in pixels.
1588 ListCell.prototype.setPosition = function(y) {
1589 if (this._position === y)
1592 this.element.style.webkitTransform = "translate(0, " + this._position + "px)";
1596 * @param {!boolean} selected
1598 ListCell.prototype.setSelected = function(selected) {
1599 if (this._selected === selected)
1601 this._selected = selected;
1603 this.element.classList.add("selected");
1605 this.element.classList.remove("selected");
1612 function ListView() {
1613 View.call(this, createElement("div", ListView.ClassNameListView));
1614 this.element.tabIndex = 0;
1615 this.element.setAttribute("role", "grid");
1631 this.selectedRow = ListView.NoSelection;
1634 * @type {!ScrollView}
1636 this.scrollView = new ScrollView();
1637 this.scrollView.delegate = this;
1638 this.scrollView.minimumContentOffset = 0;
1639 this.scrollView.setWidth(0);
1640 this.scrollView.setHeight(0);
1641 this.scrollView.attachTo(this);
1643 this.element.addEventListener("click", this.onClick, false);
1649 this._needsUpdateCells = false;
1652 ListView.prototype = Object.create(View.prototype);
1654 ListView.NoSelection = -1;
1655 ListView.ClassNameListView = "list-view";
1657 ListView.prototype.onAnimationFrameWillFinish = function() {
1658 if (this._needsUpdateCells)
1663 * @param {!boolean} needsUpdateCells
1665 ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) {
1666 if (this._needsUpdateCells === needsUpdateCells)
1668 this._needsUpdateCells = needsUpdateCells;
1669 if (this._needsUpdateCells)
1670 AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1672 AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish);
1676 * @param {!number} row
1677 * @return {?ListCell}
1679 ListView.prototype.cellAtRow = function(row) {
1680 return this._cells[row];
1684 * @param {!number} offset Scroll offset in pixels.
1687 ListView.prototype.rowAtScrollOffset = function(offset) {
1688 console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden.");
1693 * @param {!number} row
1694 * @return {!number} Scroll offset in pixels.
1696 ListView.prototype.scrollOffsetForRow = function(row) {
1697 console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden.");
1702 * @param {!number} row
1703 * @return {!ListCell}
1705 ListView.prototype.addCellIfNecessary = function(row) {
1706 var cell = this._cells[row];
1709 cell = this.prepareNewCell(row);
1710 cell.attachTo(this.scrollView.contentElement);
1711 cell.setWidth(this._width);
1712 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row)));
1713 this._cells[row] = cell;
1718 * @param {!number} row
1719 * @return {!ListCell}
1721 ListView.prototype.prepareNewCell = function(row) {
1722 console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden.");
1723 return new ListCell();
1727 * @param {!ListCell} cell
1729 ListView.prototype.throwAwayCell = function(cell) {
1730 delete this._cells[cell.row];
1737 ListView.prototype.firstVisibleRow = function() {
1738 return this.rowAtScrollOffset(this.scrollView.contentOffset());
1744 ListView.prototype.lastVisibleRow = function() {
1745 return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1);
1749 * @param {!ScrollView} scrollView
1751 ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) {
1752 this.setNeedsUpdateCells(true);
1756 * @param {!ScrollView} scrollView
1758 ListView.prototype.scrollViewDidChangeHeight = function(scrollView) {
1759 this.setNeedsUpdateCells(true);
1763 * @param {!ScrollView} scrollView
1765 ListView.prototype.scrollViewDidChangePartition = function(scrollView) {
1766 this.setNeedsUpdateCells(true);
1769 ListView.prototype.updateCells = function() {
1770 var firstVisibleRow = this.firstVisibleRow();
1771 var lastVisibleRow = this.lastVisibleRow();
1772 console.assert(firstVisibleRow <= lastVisibleRow);
1773 for (var c in this._cells) {
1774 var cell = this._cells[c];
1775 if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
1776 this.throwAwayCell(cell);
1778 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
1779 var cell = this._cells[i];
1781 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
1783 this.addCellIfNecessary(i);
1785 this.setNeedsUpdateCells(false);
1789 * @return {!number} Width in pixels.
1791 ListView.prototype.width = function() {
1796 * @param {!number} width Width in pixels.
1798 ListView.prototype.setWidth = function(width) {
1799 if (this._width === width)
1801 this._width = width;
1802 this.scrollView.setWidth(this._width);
1803 for (var c in this._cells) {
1804 this._cells[c].setWidth(this._width);
1806 this.element.style.width = this._width + "px";
1807 this.setNeedsUpdateCells(true);
1811 * @return {!number} Height in pixels.
1813 ListView.prototype.height = function() {
1814 return this.scrollView.height();
1818 * @param {!number} height Height in pixels.
1820 ListView.prototype.setHeight = function(height) {
1821 this.scrollView.setHeight(height);
1825 * @param {?Event} event
1827 ListView.prototype.onClick = function(event) {
1828 var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell);
1829 if (!clickedCellElement)
1831 var clickedCell = clickedCellElement.$view;
1832 if (clickedCell.row !== this.selectedRow)
1833 this.select(clickedCell.row);
1837 * @param {!number} row
1839 ListView.prototype.select = function(row) {
1840 if (this.selectedRow === row)
1843 if (row === ListView.NoSelection)
1845 this.selectedRow = row;
1846 var selectedCell = this._cells[this.selectedRow];
1848 selectedCell.setSelected(true);
1851 ListView.prototype.deselect = function() {
1852 if (this.selectedRow === ListView.NoSelection)
1854 var selectedCell = this._cells[this.selectedRow];
1856 selectedCell.setSelected(false);
1857 this.selectedRow = ListView.NoSelection;
1861 * @param {!number} row
1862 * @param {!boolean} animate
1864 ListView.prototype.scrollToRow = function(row, animate) {
1865 this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate);
1871 * @param {!ScrollView} scrollView
1873 function ScrubbyScrollBar(scrollView) {
1874 View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar));
1880 this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb);
1881 this.element.appendChild(this.thumb);
1884 * @type {!ScrollView}
1887 this.scrollView = scrollView;
1898 this._thumbHeight = 0;
1903 this._thumbPosition = 0;
1906 this.setThumbHeight(ScrubbyScrollBar.ThumbHeight);
1912 this._thumbStyleTopAnimator = null;
1920 this.element.addEventListener("mousedown", this.onMouseDown, false);
1921 this.element.addEventListener("touchstart", this.onTouchStart, false);
1924 ScrubbyScrollBar.prototype = Object.create(View.prototype);
1926 ScrubbyScrollBar.ScrollInterval = 16;
1927 ScrubbyScrollBar.ThumbMargin = 2;
1928 ScrubbyScrollBar.ThumbHeight = 30;
1929 ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar";
1930 ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb";
1933 * @param {?Event} event
1935 ScrubbyScrollBar.prototype.onTouchStart = function(event) {
1936 var touch = event.touches[0];
1937 this._setThumbPositionFromEventPosition(touch.clientY);
1938 if (this._thumbStyleTopAnimator)
1939 this._thumbStyleTopAnimator.stop();
1940 this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
1941 window.addEventListener("touchmove", this.onWindowTouchMove, false);
1942 window.addEventListener("touchend", this.onWindowTouchEnd, false);
1943 event.stopPropagation();
1944 event.preventDefault();
1948 * @param {?Event} event
1950 ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) {
1951 var touch = event.touches[0];
1952 this._setThumbPositionFromEventPosition(touch.clientY);
1953 event.stopPropagation();
1954 event.preventDefault();
1958 * @param {?Event} event
1960 ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) {
1961 this._thumbStyleTopAnimator = new TransitionAnimator();
1962 this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
1963 this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
1964 this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
1965 this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
1966 this._thumbStyleTopAnimator.duration = 100;
1967 this._thumbStyleTopAnimator.start();
1969 window.removeEventListener("touchmove", this.onWindowTouchMove, false);
1970 window.removeEventListener("touchend", this.onWindowTouchEnd, false);
1971 clearInterval(this._timer);
1975 * @return {!number} Height of the view in pixels.
1977 ScrubbyScrollBar.prototype.height = function() {
1978 return this._height;
1982 * @param {!number} height Height of the view in pixels.
1984 ScrubbyScrollBar.prototype.setHeight = function(height) {
1985 if (this._height === height)
1987 this._height = height;
1988 this.element.style.height = this._height + "px";
1989 this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
1990 this._thumbPosition = 0;
1994 * @param {!number} height Height of the scroll bar thumb in pixels.
1996 ScrubbyScrollBar.prototype.setThumbHeight = function(height) {
1997 if (this._thumbHeight === height)
1999 this._thumbHeight = height;
2000 this.thumb.style.height = this._thumbHeight + "px";
2001 this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px";
2002 this._thumbPosition = 0;
2006 * @param {number} position
2008 ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) {
2009 var thumbMin = ScrubbyScrollBar.ThumbMargin;
2010 var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2;
2011 var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop;
2012 var thumbPosition = y - this._thumbHeight / 2;
2013 thumbPosition = Math.max(thumbPosition, thumbMin);
2014 thumbPosition = Math.min(thumbPosition, thumbMax);
2015 this.thumb.style.top = thumbPosition + "px";
2016 this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2;
2020 * @param {?Event} event
2022 ScrubbyScrollBar.prototype.onMouseDown = function(event) {
2023 this._setThumbPositionFromEventPosition(event.clientY);
2025 window.addEventListener("mousemove", this.onWindowMouseMove, false);
2026 window.addEventListener("mouseup", this.onWindowMouseUp, false);
2027 if (this._thumbStyleTopAnimator)
2028 this._thumbStyleTopAnimator.stop();
2029 this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval);
2030 event.stopPropagation();
2031 event.preventDefault();
2035 * @param {?Event} event
2037 ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) {
2038 this._setThumbPositionFromEventPosition(event.clientY);
2042 * @param {?Event} event
2044 ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) {
2045 this._thumbStyleTopAnimator = new TransitionAnimator();
2046 this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep;
2047 this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop);
2048 this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2);
2049 this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut;
2050 this._thumbStyleTopAnimator.duration = 100;
2051 this._thumbStyleTopAnimator.start();
2053 window.removeEventListener("mousemove", this.onWindowMouseMove, false);
2054 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2055 clearInterval(this._timer);
2059 * @param {!Animator} animator
2061 ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) {
2062 this.thumb.style.top = animator.currentValue + "px";
2065 ScrubbyScrollBar.prototype.onScrollTimer = function() {
2066 var scrollAmount = Math.pow(this._thumbPosition, 2) * 10;
2067 if (this._thumbPosition > 0)
2068 scrollAmount = -scrollAmount;
2069 this.scrollView.scrollBy(scrollAmount, false);
2075 * @param {!Array} shortMonthLabels
2077 function YearListCell(shortMonthLabels) {
2078 ListCell.call(this);
2079 this.element.classList.add(YearListCell.ClassNameYearListCell);
2080 this.element.style.height = YearListCell.Height + "px";
2086 this.label = createElement("div", YearListCell.ClassNameLabel, "----");
2087 this.element.appendChild(this.label);
2088 this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2089 this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px";
2092 * @type {!Array} Array of the 12 month button elements.
2095 this.monthButtons = [];
2096 var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser);
2097 for (var r = 0; r < YearListCell.ButtonRows; ++r) {
2098 var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow);
2099 buttonsRow.setAttribute("role", "row");
2100 for (var c = 0; c < YearListCell.ButtonColumns; ++c) {
2101 var month = c + r * YearListCell.ButtonColumns;
2102 var button = createElement("div", YearListCell.ClassNameMonthButton, shortMonthLabels[month]);
2103 button.setAttribute("role", "gridcell");
2104 button.dataset.month = month;
2105 buttonsRow.appendChild(button);
2106 this.monthButtons.push(button);
2108 monthChooserElement.appendChild(buttonsRow);
2110 this.element.appendChild(monthChooserElement);
2116 this._selected = false;
2124 YearListCell.prototype = Object.create(ListCell.prototype);
2126 YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25;
2127 YearListCell.BorderBottomWidth = 1;
2128 YearListCell.ButtonRows = 3;
2129 YearListCell.ButtonColumns = 4;
2130 YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121;
2131 YearListCell.ClassNameYearListCell = "year-list-cell";
2132 YearListCell.ClassNameLabel = "label";
2133 YearListCell.ClassNameMonthChooser = "month-chooser";
2134 YearListCell.ClassNameMonthButtonsRow = "month-buttons-row";
2135 YearListCell.ClassNameMonthButton = "month-button";
2136 YearListCell.ClassNameHighlighted = "highlighted";
2138 YearListCell._recycleBin = [];
2144 YearListCell.prototype._recycleBin = function() {
2145 return YearListCell._recycleBin;
2149 * @param {!number} row
2151 YearListCell.prototype.reset = function(row) {
2153 this.label.textContent = row + 1;
2154 for (var i = 0; i < this.monthButtons.length; ++i) {
2155 this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted);
2161 * @return {!number} The height in pixels.
2163 YearListCell.prototype.height = function() {
2164 return this._height;
2168 * @param {!number} height Height in pixels.
2170 YearListCell.prototype.setHeight = function(height) {
2171 if (this._height === height)
2173 this._height = height;
2174 this.element.style.height = this._height + "px";
2180 * @param {!Month} minimumMonth
2181 * @param {!Month} maximumMonth
2183 function YearListView(minimumMonth, maximumMonth) {
2184 ListView.call(this);
2185 this.element.classList.add("year-list-view");
2190 this.highlightedMonth = null;
2196 this._minimumMonth = minimumMonth;
2202 this._maximumMonth = maximumMonth;
2204 this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height;
2205 this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight;
2212 this._runningAnimators = {};
2218 this._animatingRows = [];
2223 this._ignoreMouseOutUntillNextMouseOver = false;
2226 * @type {!ScrubbyScrollBar}
2229 this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView);
2230 this.scrubbyScrollBar.attachTo(this);
2232 this.element.addEventListener("mouseover", this.onMouseOver, false);
2233 this.element.addEventListener("mouseout", this.onMouseOut, false);
2234 this.element.addEventListener("keydown", this.onKeyDown, false);
2235 this.element.addEventListener("touchstart", this.onTouchStart, false);
2238 YearListView.prototype = Object.create(ListView.prototype);
2240 YearListView.Height = YearListCell.SelectedHeight - 1;
2241 YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide";
2242 YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth";
2245 * @param {?Event} event
2247 YearListView.prototype.onTouchStart = function(event) {
2248 var touch = event.touches[0];
2249 var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton);
2250 if (!monthButtonElement)
2252 var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2253 var cell = cellElement.$view;
2254 this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2258 * @param {?Event} event
2260 YearListView.prototype.onMouseOver = function(event) {
2261 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2262 if (!monthButtonElement)
2264 var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell);
2265 var cell = cellElement.$view;
2266 this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10)));
2267 this._ignoreMouseOutUntillNextMouseOver = false;
2271 * @param {?Event} event
2273 YearListView.prototype.onMouseOut = function(event) {
2274 if (this._ignoreMouseOutUntillNextMouseOver)
2276 var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2277 if (!monthButtonElement) {
2278 this.dehighlightMonth();
2283 * @param {!number} width Width in pixels.
2286 YearListView.prototype.setWidth = function(width) {
2287 ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth);
2288 this.element.style.width = width + "px";
2292 * @param {!number} height Height in pixels.
2295 YearListView.prototype.setHeight = function(height) {
2296 ListView.prototype.setHeight.call(this, height);
2297 this.scrubbyScrollBar.setHeight(height);
2303 YearListView.RowAnimationDirection = {
2309 * @param {!number} row
2310 * @param {!YearListView.RowAnimationDirection} direction
2312 YearListView.prototype._animateRow = function(row, direction) {
2313 var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height;
2314 var oldAnimator = this._runningAnimators[row];
2317 fromValue = oldAnimator.currentValue;
2319 var cell = this.cellAtRow(row);
2320 var animator = new TransitionAnimator();
2321 animator.step = this.onCellHeightAnimatorStep;
2322 animator.setFrom(fromValue);
2323 animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height);
2324 animator.timingFunction = AnimationTimingFunction.EaseInOut;
2325 animator.duration = 300;
2327 animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop);
2328 this._runningAnimators[row] = animator;
2329 this._animatingRows.push(row);
2330 this._animatingRows.sort();
2335 * @param {?Animator} animator
2337 YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) {
2338 delete this._runningAnimators[animator.row];
2339 var index = this._animatingRows.indexOf(animator.row);
2340 this._animatingRows.splice(index, 1);
2344 * @param {!Animator} animator
2346 YearListView.prototype.onCellHeightAnimatorStep = function(animator) {
2347 var cell = this.cellAtRow(animator.row);
2349 cell.setHeight(animator.currentValue);
2354 * @param {?Event} event
2356 YearListView.prototype.onClick = function(event) {
2357 var oldSelectedRow = this.selectedRow;
2358 ListView.prototype.onClick.call(this, event);
2359 var year = this.selectedRow + 1;
2360 if (this.selectedRow !== oldSelectedRow) {
2361 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2362 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2363 this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2365 var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton);
2366 if (!monthButton || monthButton.getAttribute("aria-disabled") == "true")
2368 var month = parseInt(monthButton.dataset.month, 10);
2369 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month));
2375 * @param {!number} scrollOffset
2379 YearListView.prototype.rowAtScrollOffset = function(scrollOffset) {
2380 var remainingOffset = scrollOffset;
2381 var lastAnimatingRow = 0;
2382 var rowsWithIrregularHeight = this._animatingRows.slice();
2383 if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) {
2384 rowsWithIrregularHeight.push(this.selectedRow);
2385 rowsWithIrregularHeight.sort();
2387 for (var i = 0; i < rowsWithIrregularHeight.length; ++i) {
2388 var row = rowsWithIrregularHeight[i];
2389 var animator = this._runningAnimators[row];
2390 var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight;
2391 if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) {
2392 return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2394 remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height;
2395 if (remainingOffset <= (rowHeight - YearListCell.Height))
2397 remainingOffset -= rowHeight - YearListCell.Height;
2398 lastAnimatingRow = row;
2400 return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height);
2404 * @param {!number} row
2408 YearListView.prototype.scrollOffsetForRow = function(row) {
2409 var scrollOffset = row * YearListCell.Height;
2410 for (var i = 0; i < this._animatingRows.length; ++i) {
2411 var animatingRow = this._animatingRows[i];
2412 if (animatingRow >= row)
2414 var animator = this._runningAnimators[animatingRow];
2415 scrollOffset += animator.currentValue - YearListCell.Height;
2417 if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) {
2418 scrollOffset += YearListCell.SelectedHeight - YearListCell.Height;
2420 return scrollOffset;
2424 * @param {!number} row
2425 * @return {!YearListCell}
2428 YearListView.prototype.prepareNewCell = function(row) {
2429 var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels);
2431 cell.setSelected(this.selectedRow === row);
2432 for (var i = 0; i < cell.monthButtons.length; ++i) {
2433 var month = new Month(row + 1, i);
2434 cell.monthButtons[i].id = month.toString();
2435 cell.monthButtons[i].setAttribute("aria-disabled", this._minimumMonth > month || this._maximumMonth < month ? "true" : "false");
2436 cell.monthButtons[i].setAttribute("aria-label", month.toLocaleString());
2438 if (this.highlightedMonth && row === this.highlightedMonth.year - 1) {
2439 var monthButton = cell.monthButtons[this.highlightedMonth.month];
2440 monthButton.classList.add(YearListCell.ClassNameHighlighted);
2441 // aira-activedescendant assumes both elements have layoutObjects, and
2442 // |monthButton| might have no layoutObject yet.
2443 var element = this.element;
2444 setTimeout(function() {
2445 element.setAttribute("aria-activedescendant", monthButton.id);
2448 var animator = this._runningAnimators[row];
2450 cell.setHeight(animator.currentValue);
2451 else if (row === this.selectedRow)
2452 cell.setHeight(YearListCell.SelectedHeight);
2454 cell.setHeight(YearListCell.Height);
2461 YearListView.prototype.updateCells = function() {
2462 var firstVisibleRow = this.firstVisibleRow();
2463 var lastVisibleRow = this.lastVisibleRow();
2464 console.assert(firstVisibleRow <= lastVisibleRow);
2465 for (var c in this._cells) {
2466 var cell = this._cells[c];
2467 if (cell.row < firstVisibleRow || cell.row > lastVisibleRow)
2468 this.throwAwayCell(cell);
2470 for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) {
2471 var cell = this._cells[i];
2473 cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row)));
2475 this.addCellIfNecessary(i);
2477 this.setNeedsUpdateCells(false);
2483 YearListView.prototype.deselect = function() {
2484 if (this.selectedRow === ListView.NoSelection)
2486 var selectedCell = this._cells[this.selectedRow];
2488 selectedCell.setSelected(false);
2489 this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing);
2490 this.selectedRow = ListView.NoSelection;
2491 this.setNeedsUpdateCells(true);
2494 YearListView.prototype.deselectWithoutAnimating = function() {
2495 if (this.selectedRow === ListView.NoSelection)
2497 var selectedCell = this._cells[this.selectedRow];
2499 selectedCell.setSelected(false);
2500 selectedCell.setHeight(YearListCell.Height);
2502 this.selectedRow = ListView.NoSelection;
2503 this.setNeedsUpdateCells(true);
2507 * @param {!number} row
2510 YearListView.prototype.select = function(row) {
2511 if (this.selectedRow === row)
2514 if (row === ListView.NoSelection)
2516 this.selectedRow = row;
2517 if (this.selectedRow !== ListView.NoSelection) {
2518 var selectedCell = this._cells[this.selectedRow];
2519 this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening);
2521 selectedCell.setSelected(true);
2522 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2523 this.highlightMonth(new Month(this.selectedRow + 1, month));
2525 this.setNeedsUpdateCells(true);
2529 * @param {!number} row
2531 YearListView.prototype.selectWithoutAnimating = function(row) {
2532 if (this.selectedRow === row)
2534 this.deselectWithoutAnimating();
2535 if (row === ListView.NoSelection)
2537 this.selectedRow = row;
2538 if (this.selectedRow !== ListView.NoSelection) {
2539 var selectedCell = this._cells[this.selectedRow];
2541 selectedCell.setSelected(true);
2542 selectedCell.setHeight(YearListCell.SelectedHeight);
2544 var month = this.highlightedMonth ? this.highlightedMonth.month : 0;
2545 this.highlightMonth(new Month(this.selectedRow + 1, month));
2547 this.setNeedsUpdateCells(true);
2551 * @param {!Month} month
2552 * @return {?HTMLDivElement}
2554 YearListView.prototype.buttonForMonth = function(month) {
2557 var row = month.year - 1;
2558 var cell = this.cellAtRow(row);
2561 return cell.monthButtons[month.month];
2564 YearListView.prototype.dehighlightMonth = function() {
2565 if (!this.highlightedMonth)
2567 var monthButton = this.buttonForMonth(this.highlightedMonth);
2569 monthButton.classList.remove(YearListCell.ClassNameHighlighted);
2571 this.highlightedMonth = null;
2572 this.element.removeAttribute("aria-activedescendant");
2576 * @param {!Month} month
2578 YearListView.prototype.highlightMonth = function(month) {
2579 if (this.highlightedMonth && this.highlightedMonth.equals(month))
2581 this.dehighlightMonth();
2582 this.highlightedMonth = month;
2583 if (!this.highlightedMonth)
2585 var monthButton = this.buttonForMonth(this.highlightedMonth);
2587 monthButton.classList.add(YearListCell.ClassNameHighlighted);
2588 this.element.setAttribute("aria-activedescendant", monthButton.id);
2593 * @param {!Month} month
2595 YearListView.prototype.show = function(month) {
2596 this._ignoreMouseOutUntillNextMouseOver = true;
2598 this.scrollToRow(month.year - 1, false);
2599 this.selectWithoutAnimating(month.year - 1);
2600 this.highlightMonth(month);
2603 YearListView.prototype.hide = function() {
2604 this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this);
2608 * @param {!Month} month
2610 YearListView.prototype._moveHighlightTo = function(month) {
2611 this.highlightMonth(month);
2612 this.select(this.highlightedMonth.year - 1);
2614 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month);
2615 this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true);
2620 * @param {?Event} event
2622 YearListView.prototype.onKeyDown = function(event) {
2623 var key = event.keyIdentifier;
2624 var eventHandled = false;
2625 if (key == "U+0054") // 't' key.
2626 eventHandled = this._moveHighlightTo(Month.createFromToday());
2627 else if (this.highlightedMonth) {
2628 if (global.params.isLocaleRTL ? key == "Right" : key == "Left")
2629 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous());
2630 else if (key == "Up")
2631 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns));
2632 else if (global.params.isLocaleRTL ? key == "Left" : key == "Right")
2633 eventHandled = this._moveHighlightTo(this.highlightedMonth.next());
2634 else if (key == "Down")
2635 eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns));
2636 else if (key == "PageUp")
2637 eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear));
2638 else if (key == "PageDown")
2639 eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear));
2640 else if (key == "Enter") {
2641 this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth);
2643 eventHandled = true;
2645 } else if (key == "Up") {
2646 this.scrollView.scrollBy(-YearListCell.Height, true);
2647 eventHandled = true;
2648 } else if (key == "Down") {
2649 this.scrollView.scrollBy(YearListCell.Height, true);
2650 eventHandled = true;
2651 } else if (key == "PageUp") {
2652 this.scrollView.scrollBy(-this.scrollView.height(), true);
2653 eventHandled = true;
2654 } else if (key == "PageDown") {
2655 this.scrollView.scrollBy(this.scrollView.height(), true);
2656 eventHandled = true;
2660 event.stopPropagation();
2661 event.preventDefault();
2668 * @param {!Month} minimumMonth
2669 * @param {!Month} maximumMonth
2671 function MonthPopupView(minimumMonth, maximumMonth) {
2672 View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView));
2675 * @type {!YearListView}
2678 this.yearListView = new YearListView(minimumMonth, maximumMonth);
2679 this.yearListView.attachTo(this);
2684 this.isVisible = false;
2686 this.element.addEventListener("click", this.onClick, false);
2689 MonthPopupView.prototype = Object.create(View.prototype);
2691 MonthPopupView.ClassNameMonthPopupView = "month-popup-view";
2693 MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) {
2694 this.isVisible = true;
2695 document.body.appendChild(this.element);
2696 this.yearListView.setWidth(calendarTableRect.width - 2);
2697 this.yearListView.setHeight(YearListView.Height);
2698 if (global.params.isLocaleRTL)
2699 this.yearListView.element.style.right = calendarTableRect.x + "px";
2701 this.yearListView.element.style.left = calendarTableRect.x + "px";
2702 this.yearListView.element.style.top = calendarTableRect.y + "px";
2703 this.yearListView.show(initialMonth);
2704 this.yearListView.element.focus();
2707 MonthPopupView.prototype.hide = function() {
2708 if (!this.isVisible)
2710 this.isVisible = false;
2711 this.element.parentNode.removeChild(this.element);
2712 this.yearListView.hide();
2716 * @param {?Event} event
2718 MonthPopupView.prototype.onClick = function(event) {
2719 if (event.target !== this.element)
2727 * @param {!number} maxWidth Maximum width in pixels.
2729 function MonthPopupButton(maxWidth) {
2730 View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton));
2731 this.element.setAttribute("aria-label", global.params.axShowMonthSelector);
2737 this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----");
2738 this.element.appendChild(this.labelElement);
2744 this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle);
2745 this.disclosureTriangleIcon.innerHTML = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>";
2746 this.element.appendChild(this.disclosureTriangleIcon);
2752 this._useShortMonth = this._shouldUseShortMonth(maxWidth);
2753 this.element.style.maxWidth = maxWidth + "px";
2755 this.element.addEventListener("click", this.onClick, false);
2758 MonthPopupButton.prototype = Object.create(View.prototype);
2760 MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button";
2761 MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label";
2762 MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle";
2763 MonthPopupButton.EventTypeButtonClick = "buttonClick";
2766 * @param {!number} maxWidth Maximum available width in pixels.
2767 * @return {!boolean}
2769 MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) {
2770 document.body.appendChild(this.element);
2771 var month = Month.Maximum;
2772 for (var i = 0; i < MonthsPerYear; ++i) {
2773 this.labelElement.textContent = month.toLocaleString();
2774 if (this.element.offsetWidth > maxWidth)
2776 month = month.previous();
2778 document.body.removeChild(this.element);
2783 * @param {!Month} month
2785 MonthPopupButton.prototype.setCurrentMonth = function(month) {
2786 this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString();
2790 * @param {?Event} event
2792 MonthPopupButton.prototype.onClick = function(event) {
2793 this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this);
2800 function CalendarNavigationButton() {
2801 View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton));
2803 * @type {number} Threshold for starting repeating clicks in milliseconds.
2805 this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold;
2807 * @type {number} Interval between reapeating clicks in milliseconds.
2809 this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval;
2811 * @type {?number} The ID for the timeout that triggers the repeating clicks.
2814 this.element.addEventListener("click", this.onClick, false);
2815 this.element.addEventListener("mousedown", this.onMouseDown, false);
2816 this.element.addEventListener("touchstart", this.onTouchStart, false);
2819 CalendarNavigationButton.prototype = Object.create(View.prototype);
2821 CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600;
2822 CalendarNavigationButton.DefaultRepeatingClicksInterval = 300;
2823 CalendarNavigationButton.LeftMargin = 4;
2824 CalendarNavigationButton.Width = 24;
2825 CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button";
2826 CalendarNavigationButton.EventTypeButtonClick = "buttonClick";
2827 CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick";
2830 * @param {!boolean} disabled
2832 CalendarNavigationButton.prototype.setDisabled = function(disabled) {
2833 this.element.disabled = disabled;
2837 * @param {?Event} event
2839 CalendarNavigationButton.prototype.onClick = function(event) {
2840 this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this);
2844 * @param {?Event} event
2846 CalendarNavigationButton.prototype.onTouchStart = function(event) {
2847 if (this._timer !== null)
2849 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2850 window.addEventListener("touchend", this.onWindowTouchEnd, false);
2854 * @param {?Event} event
2856 CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) {
2857 if (this._timer === null)
2859 clearTimeout(this._timer);
2861 window.removeEventListener("touchend", this.onWindowMouseUp, false);
2865 * @param {?Event} event
2867 CalendarNavigationButton.prototype.onMouseDown = function(event) {
2868 if (this._timer !== null)
2870 this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold);
2871 window.addEventListener("mouseup", this.onWindowMouseUp, false);
2875 * @param {?Event} event
2877 CalendarNavigationButton.prototype.onWindowMouseUp = function(event) {
2878 if (this._timer === null)
2880 clearTimeout(this._timer);
2882 window.removeEventListener("mouseup", this.onWindowMouseUp, false);
2886 * @param {?Event} event
2888 CalendarNavigationButton.prototype.onRepeatingClick = function(event) {
2889 this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this);
2890 this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval);
2896 * @param {!CalendarPicker} calendarPicker
2898 function CalendarHeaderView(calendarPicker) {
2899 View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView));
2900 this.calendarPicker = calendarPicker;
2901 this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged);
2903 var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle);
2904 this.element.appendChild(titleElement);
2907 * @type {!MonthPopupButton}
2909 this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2);
2910 this.monthPopupButton.attachTo(titleElement);
2913 * @type {!CalendarNavigationButton}
2916 this._previousMonthButton = new CalendarNavigationButton();
2917 this._previousMonthButton.attachTo(this);
2918 this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2919 this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2920 this._previousMonthButton.element.setAttribute("aria-label", global.params.axShowPreviousMonth);
2923 * @type {!CalendarNavigationButton}
2926 this._todayButton = new CalendarNavigationButton();
2927 this._todayButton.attachTo(this);
2928 this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2929 this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton);
2930 var monthContainingToday = Month.createFromToday();
2931 this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2932 this._todayButton.element.setAttribute("aria-label", global.params.todayLabel);
2935 * @type {!CalendarNavigationButton}
2938 this._nextMonthButton = new CalendarNavigationButton();
2939 this._nextMonthButton.attachTo(this);
2940 this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick);
2941 this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick);
2942 this._nextMonthButton.element.setAttribute("aria-label", global.params.axShowNextMonth);
2944 if (global.params.isLocaleRTL) {
2945 this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2946 this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2948 this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle;
2949 this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle;
2953 CalendarHeaderView.prototype = Object.create(View.prototype);
2955 CalendarHeaderView.Height = 24;
2956 CalendarHeaderView.BottomMargin = 10;
2957 CalendarHeaderView._ForwardTriangle = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>";
2958 CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>";
2959 CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view";
2960 CalendarHeaderView.ClassNameCalendarTitle = "calendar-title";
2961 CalendarHeaderView.ClassNameTodayButton = "today-button";
2963 CalendarHeaderView.prototype.onCurrentMonthChanged = function() {
2964 this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth());
2965 this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
2966 this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
2969 CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) {
2970 if (sender === this._previousMonthButton)
2971 this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation);
2972 else if (sender === this._nextMonthButton)
2973 this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation);
2975 this.calendarPicker.selectRangeContainingDay(Day.createFromToday());
2979 * @param {!boolean} disabled
2981 CalendarHeaderView.prototype.setDisabled = function(disabled) {
2982 this.disabled = disabled;
2983 this.monthPopupButton.element.disabled = this.disabled;
2984 this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth);
2985 this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth);
2986 var monthContainingToday = Month.createFromToday();
2987 this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth);
2994 function DayCell() {
2995 ListCell.call(this);
2996 this.element.classList.add(DayCell.ClassNameDayCell);
2997 this.element.style.width = DayCell.Width + "px";
2998 this.element.style.height = DayCell.Height + "px";
2999 this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px";
3000 this.element.setAttribute("role", "gridcell");
3007 DayCell.prototype = Object.create(ListCell.prototype);
3010 DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20;
3011 DayCell.PaddingSize = 1;
3012 DayCell.ClassNameDayCell = "day-cell";
3013 DayCell.ClassNameHighlighted = "highlighted";
3014 DayCell.ClassNameDisabled = "disabled";
3015 DayCell.ClassNameCurrentMonth = "current-month";
3016 DayCell.ClassNameToday = "today";
3018 DayCell._recycleBin = [];
3020 DayCell.recycleOrCreate = function() {
3021 return DayCell._recycleBin.pop() || new DayCell();
3028 DayCell.prototype._recycleBin = function() {
3029 return DayCell._recycleBin;
3035 DayCell.prototype.throwAway = function() {
3036 ListCell.prototype.throwAway.call(this);
3041 * @param {!boolean} highlighted
3043 DayCell.prototype.setHighlighted = function(highlighted) {
3045 this.element.classList.add(DayCell.ClassNameHighlighted);
3046 this.element.setAttribute("aria-selected", "true");
3048 this.element.classList.remove(DayCell.ClassNameHighlighted);
3049 this.element.setAttribute("aria-selected", "false");
3054 * @param {!boolean} disabled
3056 DayCell.prototype.setDisabled = function(disabled) {
3058 this.element.classList.add(DayCell.ClassNameDisabled);
3060 this.element.classList.remove(DayCell.ClassNameDisabled);
3064 * @param {!boolean} selected
3066 DayCell.prototype.setIsInCurrentMonth = function(selected) {
3068 this.element.classList.add(DayCell.ClassNameCurrentMonth);
3070 this.element.classList.remove(DayCell.ClassNameCurrentMonth);
3074 * @param {!boolean} selected
3076 DayCell.prototype.setIsToday = function(selected) {
3078 this.element.classList.add(DayCell.ClassNameToday);
3080 this.element.classList.remove(DayCell.ClassNameToday);
3086 DayCell.prototype.reset = function(day) {
3088 this.element.textContent = localizeNumber(this.day.date.toString());
3089 this.element.setAttribute("aria-label", this.day.format());
3090 this.element.id = this.day.toString();
3098 function WeekNumberCell() {
3099 ListCell.call(this);
3100 this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell);
3101 this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px";
3102 this.element.style.height = WeekNumberCell.Height + "px";
3103 this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px";
3110 WeekNumberCell.prototype = Object.create(ListCell.prototype);
3112 WeekNumberCell.Width = 48;
3113 WeekNumberCell.Height = DayCell.Height;
3114 WeekNumberCell.SeparatorWidth = 1;
3115 WeekNumberCell.PaddingSize = 1;
3116 WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell";
3117 WeekNumberCell.ClassNameHighlighted = "highlighted";
3118 WeekNumberCell.ClassNameDisabled = "disabled";
3120 WeekNumberCell._recycleBin = [];
3126 WeekNumberCell.prototype._recycleBin = function() {
3127 return WeekNumberCell._recycleBin;
3131 * @return {!WeekNumberCell}
3133 WeekNumberCell.recycleOrCreate = function() {
3134 return WeekNumberCell._recycleBin.pop() || new WeekNumberCell();
3138 * @param {!Week} week
3140 WeekNumberCell.prototype.reset = function(week) {
3142 this.element.id = week.toString();
3143 this.element.setAttribute("role", "gridcell");
3144 this.element.setAttribute("aria-label", window.pagePopupController.formatWeek(week.year, week.week, week.firstDay().format()));
3145 this.element.textContent = localizeNumber(this.week.week.toString());
3152 WeekNumberCell.prototype.throwAway = function() {
3153 ListCell.prototype.throwAway.call(this);
3157 WeekNumberCell.prototype.setHighlighted = function(highlighted) {
3159 this.element.classList.add(WeekNumberCell.ClassNameHighlighted);
3160 this.element.setAttribute("aria-selected", "true");
3162 this.element.classList.remove(WeekNumberCell.ClassNameHighlighted);
3163 this.element.setAttribute("aria-selected", "false");
3167 WeekNumberCell.prototype.setDisabled = function(disabled) {
3169 this.element.classList.add(WeekNumberCell.ClassNameDisabled);
3171 this.element.classList.remove(WeekNumberCell.ClassNameDisabled);
3177 * @param {!boolean} hasWeekNumberColumn
3179 function CalendarTableHeaderView(hasWeekNumberColumn) {
3180 View.call(this, createElement("div", "calendar-table-header-view"));
3181 if (hasWeekNumberColumn) {
3182 var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel);
3183 weekNumberLabelElement.style.width = WeekNumberCell.Width + "px";
3184 this.element.appendChild(weekNumberLabelElement);
3186 for (var i = 0; i < DaysPerWeek; ++i) {
3187 var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek;
3188 var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]);
3189 labelElement.style.width = DayCell.Width + "px";
3190 this.element.appendChild(labelElement);
3191 if (getLanguage() === "ja") {
3192 if (weekDayNumber === 0)
3193 labelElement.style.color = "red";
3194 else if (weekDayNumber === 6)
3195 labelElement.style.color = "blue";
3200 CalendarTableHeaderView.prototype = Object.create(View.prototype);
3202 CalendarTableHeaderView.Height = 25;
3208 function CalendarRowCell() {
3209 ListCell.call(this);
3210 this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell);
3211 this.element.style.height = CalendarRowCell.Height + "px";
3212 this.element.setAttribute("role", "row");
3218 this._dayCells = [];
3224 * @type {?CalendarTableView}
3226 this.calendarTableView = null;
3229 CalendarRowCell.prototype = Object.create(ListCell.prototype);
3231 CalendarRowCell.Height = DayCell.Height;
3232 CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell";
3234 CalendarRowCell._recycleBin = [];
3240 CalendarRowCell.prototype._recycleBin = function() {
3241 return CalendarRowCell._recycleBin;
3245 * @param {!number} row
3246 * @param {!CalendarTableView} calendarTableView
3248 CalendarRowCell.prototype.reset = function(row, calendarTableView) {
3250 this.calendarTableView = calendarTableView;
3251 if (this.calendarTableView.hasWeekNumberColumn) {
3252 var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row);
3253 var week = Week.createFromDay(middleDay);
3254 this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week);
3255 this.weekNumberCell.attachTo(this);
3257 var day = calendarTableView.dayAtColumnAndRow(0, row);
3258 for (var i = 0; i < DaysPerWeek; ++i) {
3259 var dayCell = this.calendarTableView.prepareNewDayCell(day);
3260 dayCell.attachTo(this);
3261 this._dayCells.push(dayCell);
3270 CalendarRowCell.prototype.throwAway = function() {
3271 ListCell.prototype.throwAway.call(this);
3272 if (this.weekNumberCell)
3273 this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell);
3274 this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView);
3275 this._dayCells.length = 0;
3281 * @param {!CalendarPicker} calendarPicker
3283 function CalendarTableView(calendarPicker) {
3284 ListView.call(this);
3285 this.element.classList.add(CalendarTableView.ClassNameCalendarTableView);
3286 this.element.tabIndex = 0;
3292 this.hasWeekNumberColumn = calendarPicker.type === "week";
3294 * @type {!CalendarPicker}
3297 this.calendarPicker = calendarPicker;
3302 this._dayCells = {};
3303 var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn);
3304 headerView.attachTo(this, this.scrollView);
3306 if (this.hasWeekNumberColumn) {
3307 this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width);
3312 this._weekNumberCells = [];
3314 this.setWidth(DayCell.Width * DaysPerWeek);
3321 this._ignoreMouseOutUntillNextMouseOver = false;
3323 this.element.addEventListener("click", this.onClick, false);
3324 this.element.addEventListener("mouseover", this.onMouseOver, false);
3325 this.element.addEventListener("mouseout", this.onMouseOut, false);
3327 // You shouldn't be able to use the mouse wheel to scroll.
3328 this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false);
3329 // You shouldn't be able to do gesture scroll.
3330 this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false);
3333 CalendarTableView.prototype = Object.create(ListView.prototype);
3335 CalendarTableView.BorderWidth = 1;
3336 CalendarTableView.ClassNameCalendarTableView = "calendar-table-view";
3339 * @param {!number} scrollOffset
3342 CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) {
3343 return Math.floor(scrollOffset / CalendarRowCell.Height);
3347 * @param {!number} row
3350 CalendarTableView.prototype.scrollOffsetForRow = function(row) {
3351 return row * CalendarRowCell.Height;
3355 * @param {?Event} event
3357 CalendarTableView.prototype.onClick = function(event) {
3358 if (this.hasWeekNumberColumn) {
3359 var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3360 if (weekNumberCellElement) {
3361 var weekNumberCell = weekNumberCellElement.$view;
3362 this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay());
3366 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3367 if (!dayCellElement)
3369 var dayCell = dayCellElement.$view;
3370 this.calendarPicker.selectRangeContainingDay(dayCell.day);
3374 * @param {?Event} event
3376 CalendarTableView.prototype.onMouseOver = function(event) {
3377 if (this.hasWeekNumberColumn) {
3378 var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell);
3379 if (weekNumberCellElement) {
3380 var weekNumberCell = weekNumberCellElement.$view;
3381 this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay());
3382 this._ignoreMouseOutUntillNextMouseOver = false;
3386 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3387 if (!dayCellElement)
3389 var dayCell = dayCellElement.$view;
3390 this.calendarPicker.highlightRangeContainingDay(dayCell.day);
3391 this._ignoreMouseOutUntillNextMouseOver = false;
3395 * @param {?Event} event
3397 CalendarTableView.prototype.onMouseOut = function(event) {
3398 if (this._ignoreMouseOutUntillNextMouseOver)
3400 var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell);
3401 if (!dayCellElement) {
3402 this.calendarPicker.highlightRangeContainingDay(null);
3407 * @param {!number} row
3408 * @return {!CalendarRowCell}
3410 CalendarTableView.prototype.prepareNewCell = function(row) {
3411 var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell();
3412 cell.reset(row, this);
3417 * @return {!number} Height in pixels.
3419 CalendarTableView.prototype.height = function() {
3420 return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2;
3424 * @param {!number} height Height in pixels.
3426 CalendarTableView.prototype.setHeight = function(height) {
3427 this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2);
3431 * @param {!Month} month
3432 * @param {!boolean} animate
3434 CalendarTableView.prototype.scrollToMonth = function(month, animate) {
3435 var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row;
3436 this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate);
3440 * @param {!number} column
3441 * @param {!number} row
3444 CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) {
3445 var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay;
3446 return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue);
3449 CalendarTableView._MinimumDayValue = Day.Minimum.valueOf();
3450 CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay();
3454 * @return {!Object} Object with properties column and row.
3456 CalendarTableView.prototype.columnAndRowForDay = function(day) {
3457 var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay;
3458 var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay;
3459 var row = Math.floor(offset / DaysPerWeek);
3460 var column = offset - row * DaysPerWeek;
3467 CalendarTableView.prototype.updateCells = function() {
3468 ListView.prototype.updateCells.call(this);
3470 var selection = this.calendarPicker.selection();
3471 var firstDayInSelection;
3472 var lastDayInSelection;
3474 firstDayInSelection = selection.firstDay().valueOf();
3475 lastDayInSelection = selection.lastDay().valueOf();
3477 firstDayInSelection = Infinity;
3478 lastDayInSelection = Infinity;
3480 var highlight = this.calendarPicker.highlight();
3481 var firstDayInHighlight;
3482 var lastDayInHighlight;
3484 firstDayInHighlight = highlight.firstDay().valueOf();
3485 lastDayInHighlight = highlight.lastDay().valueOf();
3487 firstDayInHighlight = Infinity;
3488 lastDayInHighlight = Infinity;
3490 var currentMonth = this.calendarPicker.currentMonth();
3491 var firstDayInCurrentMonth = currentMonth.firstDay().valueOf();
3492 var lastDayInCurrentMonth = currentMonth.lastDay().valueOf();
3493 var activeCell = null;
3494 for (var dayString in this._dayCells) {
3495 var dayCell = this._dayCells[dayString];
3496 var day = dayCell.day;
3497 dayCell.setIsToday(Day.createFromToday().equals(day));
3498 dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection);
3499 var isHighlighted = day >= firstDayInHighlight && day <= lastDayInHighlight;
3500 dayCell.setHighlighted(isHighlighted);
3501 if (isHighlighted) {
3502 if (firstDayInHighlight == lastDayInHighlight)
3503 activeCell = dayCell;
3504 else if (this.calendarPicker.type == "month" && day == firstDayInHighlight)
3505 activeCell = dayCell;
3507 dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth);
3508 dayCell.setDisabled(!this.calendarPicker.isValidDay(day));
3510 if (this.hasWeekNumberColumn) {
3511 for (var weekString in this._weekNumberCells) {
3512 var weekNumberCell = this._weekNumberCells[weekString];
3513 var week = weekNumberCell.week;
3514 var isWeekHighlighted = highlight && highlight.equals(week);
3515 weekNumberCell.setSelected(selection && selection.equals(week));
3516 weekNumberCell.setHighlighted(isWeekHighlighted);
3517 if (isWeekHighlighted)
3518 activeCell = weekNumberCell;
3519 weekNumberCell.setDisabled(!this.calendarPicker.isValid(week));
3523 // Ensure a layoutObject because an element with no layoutObject doesn't post
3524 // activedescendant events. This shouldn't run in the above |for| loop
3525 // to avoid CSS transition.
3526 activeCell.element.offsetLeft;
3527 this.element.setAttribute("aria-activedescendant", activeCell.element.id);
3533 * @return {!DayCell}
3535 CalendarTableView.prototype.prepareNewDayCell = function(day) {
3536 var dayCell = DayCell.recycleOrCreate();
3538 if (this.calendarPicker.type == "month")
3539 dayCell.element.setAttribute("aria-label", Month.createFromDay(day).toLocaleString());
3540 this._dayCells[dayCell.day.toString()] = dayCell;
3545 * @param {!Week} week
3546 * @return {!WeekNumberCell}
3548 CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) {
3549 var weekNumberCell = WeekNumberCell.recycleOrCreate();
3550 weekNumberCell.reset(week);
3551 this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell;
3552 return weekNumberCell;
3556 * @param {!DayCell} dayCell
3558 CalendarTableView.prototype.throwAwayDayCell = function(dayCell) {
3559 delete this._dayCells[dayCell.day.toString()];
3560 dayCell.throwAway();
3564 * @param {!WeekNumberCell} weekNumberCell
3566 CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) {
3567 delete this._weekNumberCells[weekNumberCell.week.toString()];
3568 weekNumberCell.throwAway();
3574 * @param {!Object} config
3576 function CalendarPicker(type, config) {
3577 View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker));
3578 this.element.classList.add(CalendarPicker.ClassNamePreparing);
3585 if (this.type === "week")
3586 this._dateTypeConstructor = Week;
3587 else if (this.type === "month")
3588 this._dateTypeConstructor = Month;
3590 this._dateTypeConstructor = Day;
3596 this._setConfig(config);
3601 this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay());
3606 this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay());
3607 if (global.params.isLocaleRTL)
3608 this.element.classList.add("rtl");
3610 * @type {!CalendarTableView}
3613 this.calendarTableView = new CalendarTableView(this);
3614 this.calendarTableView.hasNumberColumn = this.type === "week";
3616 * @type {!CalendarHeaderView}
3619 this.calendarHeaderView = new CalendarHeaderView(this);
3620 this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick);
3622 * @type {!MonthPopupView}
3625 this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth);
3626 this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth);
3627 this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide);
3628 this.calendarHeaderView.attachTo(this);
3629 this.calendarTableView.attachTo(this);
3634 this._currentMonth = new Month(NaN, NaN);
3639 this._selection = null;
3644 this._highlight = null;
3645 this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false);
3646 document.body.addEventListener("keydown", this.onBodyKeyDown, false);
3648 window.addEventListener("resize", this.onWindowResize, false);
3656 var initialSelection = parseDateString(config.currentValue);
3657 if (initialSelection) {
3658 this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None);
3659 this.setSelection(initialSelection);
3661 this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None);
3664 CalendarPicker.prototype = Object.create(View.prototype);
3666 CalendarPicker.Padding = 10;
3667 CalendarPicker.BorderWidth = 1;
3668 CalendarPicker.ClassNameCalendarPicker = "calendar-picker";
3669 CalendarPicker.ClassNamePreparing = "preparing";
3670 CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged";
3671 CalendarPicker.commitDelayMs = 100;
3674 * @param {!Event} event
3676 CalendarPicker.prototype.onWindowResize = function(event) {
3677 this.element.classList.remove(CalendarPicker.ClassNamePreparing);
3678 window.removeEventListener("resize", this.onWindowResize, false);
3682 * @param {!YearListView} sender
3684 CalendarPicker.prototype.onYearListViewDidHide = function(sender) {
3685 this.monthPopupView.hide();
3686 this.calendarHeaderView.setDisabled(false);
3687 this.adjustHeight();
3691 * @param {!YearListView} sender
3692 * @param {!Month} month
3694 CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) {
3695 this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None);
3699 * @param {!View|Node} parent
3700 * @param {?View|Node=} before
3703 CalendarPicker.prototype.attachTo = function(parent, before) {
3704 View.prototype.attachTo.call(this, parent, before);
3705 this.calendarTableView.element.focus();
3708 CalendarPicker.prototype.cleanup = function() {
3709 window.removeEventListener("resize", this.onWindowResize, false);
3710 this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false);
3711 // Month popup view might be attached to document.body.
3712 this.monthPopupView.hide();
3716 * @param {?MonthPopupButton} sender
3718 CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) {
3719 var clientRect = this.calendarTableView.element.getBoundingClientRect();
3720 var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height);
3721 this.monthPopupView.show(this.currentMonth(), calendarTableRect);
3722 this.calendarHeaderView.setDisabled(true);
3723 this.adjustHeight();
3726 CalendarPicker.prototype._setConfig = function(config) {
3727 this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum;
3728 this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum;
3729 this.config.minimumValue = this.config.minimum.valueOf();
3730 this.config.maximumValue = this.config.maximum.valueOf();
3731 this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep;
3732 this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase;
3738 CalendarPicker.prototype.currentMonth = function() {
3739 return this._currentMonth;
3745 CalendarPicker.NavigationBehavior = {
3751 * @param {!Month} month
3752 * @param {!CalendarPicker.NavigationBehavior} animate
3754 CalendarPicker.prototype.setCurrentMonth = function(month, behavior) {
3755 if (month > this.maximumMonth)
3756 month = this.maximumMonth;
3757 else if (month < this.minimumMonth)
3758 month = this.minimumMonth;
3759 if (this._currentMonth.equals(month))
3761 this._currentMonth = month;
3762 this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation);
3763 this.adjustHeight();
3764 this.calendarTableView.setNeedsUpdateCells(true);
3765 this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, {
3770 CalendarPicker.prototype.adjustHeight = function() {
3771 var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row;
3772 var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row;
3773 var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1;
3774 var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2;
3775 var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2;
3776 this.setHeight(height);
3779 CalendarPicker.prototype.selection = function() {
3780 return this._selection;
3783 CalendarPicker.prototype.highlight = function() {
3784 return this._highlight;
3790 CalendarPicker.prototype.firstVisibleDay = function() {
3791 var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
3792 var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3793 if (!firstVisibleDay)
3794 firstVisibleDay = Day.Minimum;
3795 return firstVisibleDay;
3801 CalendarPicker.prototype.lastVisibleDay = function() {
3802 var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row;
3803 var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3804 if (!lastVisibleDay)
3805 lastVisibleDay = Day.Maximum;
3806 return lastVisibleDay;
3812 CalendarPicker.prototype.selectRangeContainingDay = function(day) {
3813 var selection = day ? this._dateTypeConstructor.createFromDay(day) : null;
3814 this.setSelectionAndCommit(selection);
3820 CalendarPicker.prototype.highlightRangeContainingDay = function(day) {
3821 var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null;
3822 this._setHighlight(highlight);
3826 * Select the specified date.
3827 * @param {?DateType} dayOrWeekOrMonth
3829 CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) {
3830 if (!this._selection && !dayOrWeekOrMonth)
3832 if (this._selection && this._selection.equals(dayOrWeekOrMonth))
3834 var firstDayInSelection = dayOrWeekOrMonth.firstDay();
3835 var lastDayInSelection = dayOrWeekOrMonth.lastDay();
3836 var candidateCurrentMonth = Month.createFromDay(firstDayInSelection);
3837 if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) {
3838 // Change current month if the selection is not visible at all.
3839 this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3840 } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) {
3841 // If the selection is partly visible, only change the current month if
3842 // doing so will make the whole selection visible.
3843 var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row;
3844 var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow);
3845 var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row;
3846 var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow);
3847 if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay)
3848 this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3850 this._setHighlight(dayOrWeekOrMonth);
3851 if (!this.isValid(dayOrWeekOrMonth))
3853 this._selection = dayOrWeekOrMonth;
3854 this.calendarTableView.setNeedsUpdateCells(true);
3858 * Select the specified date, commit it, and close the popup.
3859 * @param {?DateType} dayOrWeekOrMonth
3861 CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) {
3862 this.setSelection(dayOrWeekOrMonth);
3863 // Redraw the widget immidiately, and wait for some time to give feedback to
3865 this.element.offsetLeft;
3866 var value = this._selection.toString();
3867 if (CalendarPicker.commitDelayMs == 0) {
3869 window.pagePopupController.setValueAndClosePopup(0, value);
3870 } else if (CalendarPicker.commitDelayMs < 0) {
3872 window.pagePopupController.setValue(value);
3874 setTimeout(function() {
3875 window.pagePopupController.setValueAndClosePopup(0, value);
3876 }, CalendarPicker.commitDelayMs);
3881 * @param {?DateType} dayOrWeekOrMonth
3883 CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) {
3884 if (!this._highlight && !dayOrWeekOrMonth)
3886 if (!dayOrWeekOrMonth && !this._highlight)
3888 if (this._highlight && this._highlight.equals(dayOrWeekOrMonth))
3890 this._highlight = dayOrWeekOrMonth;
3891 this.calendarTableView.setNeedsUpdateCells(true);
3895 * @param {!number} value
3896 * @return {!boolean}
3898 CalendarPicker.prototype._stepMismatch = function(value) {
3899 var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase;
3900 return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep;
3904 * @param {!number} value
3905 * @return {!boolean}
3907 CalendarPicker.prototype._outOfRange = function(value) {
3908 return value < this.config.minimumValue || value > this.config.maximumValue;
3912 * @param {!DateType} dayOrWeekOrMonth
3913 * @return {!boolean}
3915 CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) {
3916 var value = dayOrWeekOrMonth.valueOf();
3917 return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value);
3922 * @return {!boolean}
3924 CalendarPicker.prototype.isValidDay = function(day) {
3925 return this.isValid(this._dateTypeConstructor.createFromDay(day));
3929 * @param {!DateType} dateRange
3930 * @return {!boolean} Returns true if the highlight was changed.
3932 CalendarPicker.prototype._moveHighlight = function(dateRange) {
3935 if (this._outOfRange(dateRange.valueOf()))
3937 if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay())
3938 this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation);
3939 this._setHighlight(dateRange);
3944 * @param {?Event} event
3946 CalendarPicker.prototype.onCalendarTableKeyDown = function(event) {
3947 var key = event.keyIdentifier;
3948 var eventHandled = false;
3949 if (key == "U+0054") { // 't' key.
3950 this.selectRangeContainingDay(Day.createFromToday());
3951 eventHandled = true;
3952 } else if (key == "PageUp") {
3953 var previousMonth = this.currentMonth().previous();
3954 if (previousMonth && previousMonth >= this.config.minimumValue) {
3955 this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3956 eventHandled = true;
3958 } else if (key == "PageDown") {
3959 var nextMonth = this.currentMonth().next();
3960 if (nextMonth && nextMonth >= this.config.minimumValue) {
3961 this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation);
3962 eventHandled = true;
3964 } else if (this._highlight) {
3965 if (global.params.isLocaleRTL ? key == "Right" : key == "Left") {
3966 eventHandled = this._moveHighlight(this._highlight.previous());
3967 } else if (key == "Up") {
3968 eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1));
3969 } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") {
3970 eventHandled = this._moveHighlight(this._highlight.next());
3971 } else if (key == "Down") {
3972 eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1));
3973 } else if (key == "Enter") {
3974 this.setSelectionAndCommit(this._highlight);
3976 } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") {
3977 // Highlight range near the middle.
3978 this.highlightRangeContainingDay(this.currentMonth().middleDay());
3979 eventHandled = true;
3983 event.stopPropagation();
3984 event.preventDefault();
3989 * @return {!number} Width in pixels.
3991 CalendarPicker.prototype.width = function() {
3992 return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2;
3996 * @return {!number} Height in pixels.
3998 CalendarPicker.prototype.height = function() {
3999 return this._height;
4003 * @param {!number} height Height in pixels.
4005 CalendarPicker.prototype.setHeight = function(height) {
4006 if (this._height === height)
4008 this._height = height;
4009 resizeWindow(this.width(), this._height);
4010 this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2);
4014 * @param {?Event} event
4016 CalendarPicker.prototype.onBodyKeyDown = function(event) {
4017 var key = event.keyIdentifier;
4018 var eventHandled = false;
4021 case "U+001B": // Esc key.
4022 window.pagePopupController.closePopup();
4023 eventHandled = true;
4025 case "U+004D": // 'm' key.
4026 offset = offset || 1; // Fall-through.
4027 case "U+0059": // 'y' key.
4028 offset = offset || MonthsPerYear; // Fall-through.
4029 case "U+0044": // 'd' key.
4030 offset = offset || MonthsPerYear * 10;
4031 var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4032 this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation);
4033 var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row;
4034 if (this._highlight) {
4035 var highlightMiddleDay = this._highlight.middleDay();
4036 this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek));
4042 event.stopPropagation();
4043 event.preventDefault();
4047 if (window.dialogArguments) {
4048 initialize(dialogArguments);
4050 window.addEventListener("message", handleMessage, false);