"use strict"; /* * Copyright (C) 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @enum {number} */ var WeekDay = { Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3, Thursday: 4, Friday: 5, Saturday: 6 }; /** * @type {Object} */ var global = { picker: null, params: { locale: "en_US", weekStartDay: WeekDay.Sunday, dayLabels: ["S", "M", "T", "W", "T", "F", "S"], shortMonthLabels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], isLocaleRTL: false, mode: "date", weekLabel: "Week", anchorRectInScreen: new Rectangle(0, 0, 0, 0), currentValue: null } }; // ---------------------------------------------------------------- // Utility functions /** * @return {!bool} */ function hasInaccuratePointingDevice() { return matchMedia("(pointer: coarse)").matches; } /** * @return {!string} lowercase locale name. e.g. "en-us" */ function getLocale() { return (global.params.locale || "en-us").toLowerCase(); } /** * @return {!string} lowercase language code. e.g. "en" */ function getLanguage() { var locale = getLocale(); var result = locale.match(/^([a-z]+)/); if (!result) return "en"; return result[1]; } /** * @param {!number} number * @return {!string} */ function localizeNumber(number) { return window.pagePopupController.localizeNumberString(number); } /** * @const * @type {number} */ var ImperialEraLimit = 2087; /** * @param {!number} year * @param {!number} month * @return {!string} */ function formatJapaneseImperialEra(year, month) { // We don't show an imperial era if it is greater than 99 becase of space // limitation. if (year > ImperialEraLimit) return ""; if (year > 1989) return "(平成" + localizeNumber(year - 1988) + "年)"; if (year == 1989) return "(平成元年)"; if (year >= 1927) return "(昭和" + localizeNumber(year - 1925) + "年)"; if (year > 1912) return "(大正" + localizeNumber(year - 1911) + "年)"; if (year == 1912 && month >= 7) return "(大正元年)"; if (year > 1868) return "(明治" + localizeNumber(year - 1867) + "年)"; if (year == 1868) return "(明治元年)"; return ""; } function createUTCDate(year, month, date) { var newDate = new Date(0); newDate.setUTCFullYear(year); newDate.setUTCMonth(month); newDate.setUTCDate(date); return newDate; } /** * @param {string} dateString * @return {?Day|Week|Month} */ function parseDateString(dateString) { var month = Month.parse(dateString); if (month) return month; var week = Week.parse(dateString); if (week) return week; return Day.parse(dateString); } /** * @const * @type {number} */ var DaysPerWeek = 7; /** * @const * @type {number} */ var MonthsPerYear = 12; /** * @const * @type {number} */ var MillisecondsPerDay = 24 * 60 * 60 * 1000; /** * @const * @type {number} */ var MillisecondsPerWeek = DaysPerWeek * MillisecondsPerDay; /** * @constructor */ function DateType() { } /** * @constructor * @extends DateType * @param {!number} year * @param {!number} month * @param {!number} date */ function Day(year, month, date) { var dateObject = createUTCDate(year, month, date); if (isNaN(dateObject.valueOf())) throw "Invalid date"; /** * @type {number} * @const */ this.year = dateObject.getUTCFullYear(); /** * @type {number} * @const */ this.month = dateObject.getUTCMonth(); /** * @type {number} * @const */ this.date = dateObject.getUTCDate(); }; Day.prototype = Object.create(DateType.prototype); Day.ISOStringRegExp = /^(\d+)-(\d+)-(\d+)/; /** * @param {!string} str * @return {?Day} */ Day.parse = function(str) { var match = Day.ISOStringRegExp.exec(str); if (!match) return null; var year = parseInt(match[1], 10); var month = parseInt(match[2], 10) - 1; var date = parseInt(match[3], 10); return new Day(year, month, date); }; /** * @param {!number} value * @return {!Day} */ Day.createFromValue = function(millisecondsSinceEpoch) { return Day.createFromDate(new Date(millisecondsSinceEpoch)) }; /** * @param {!Date} date * @return {!Day} */ Day.createFromDate = function(date) { if (isNaN(date.valueOf())) throw "Invalid date"; return new Day(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); }; /** * @param {!Day} day * @return {!Day} */ Day.createFromDay = function(day) { return day; }; /** * @return {!Day} */ Day.createFromToday = function() { var now = new Date(); return new Day(now.getFullYear(), now.getMonth(), now.getDate()); }; /** * @param {!DateType} other * @return {!boolean} */ Day.prototype.equals = function(other) { return other instanceof Day && this.year === other.year && this.month === other.month && this.date === other.date; }; /** * @param {!number=} offset * @return {!Day} */ Day.prototype.previous = function(offset) { if (typeof offset === "undefined") offset = 1; return new Day(this.year, this.month, this.date - offset); }; /** * @param {!number=} offset * @return {!Day} */ Day.prototype.next = function(offset) { if (typeof offset === "undefined") offset = 1; return new Day(this.year, this.month, this.date + offset); }; /** * @return {!Date} */ Day.prototype.startDate = function() { return createUTCDate(this.year, this.month, this.date); }; /** * @return {!Date} */ Day.prototype.endDate = function() { return createUTCDate(this.year, this.month, this.date + 1); }; /** * @return {!Day} */ Day.prototype.firstDay = function() { return this; }; /** * @return {!Day} */ Day.prototype.middleDay = function() { return this; }; /** * @return {!Day} */ Day.prototype.lastDay = function() { return this; }; /** * @return {!number} */ Day.prototype.valueOf = function() { return createUTCDate(this.year, this.month, this.date).getTime(); }; /** * @return {!WeekDay} */ Day.prototype.weekDay = function() { return createUTCDate(this.year, this.month, this.date).getUTCDay(); }; /** * @return {!string} */ Day.prototype.toString = function() { var yearString = String(this.year); if (yearString.length < 4) yearString = ("000" + yearString).substr(-4, 4); return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2) + "-" + ("0" + this.date).substr(-2, 2); }; // See WebCore/platform/DateComponents.h. Day.Minimum = Day.createFromValue(-62135596800000.0); Day.Maximum = Day.createFromValue(8640000000000000.0); // See WebCore/html/DayInputType.cpp. Day.DefaultStep = 86400000; Day.DefaultStepBase = 0; /** * @constructor * @extends DateType * @param {!number} year * @param {!number} week */ function Week(year, week) { /** * @type {number} * @const */ this.year = year; /** * @type {number} * @const */ this.week = week; // Number of years per year is either 52 or 53. if (this.week < 1 || (this.week > 52 && this.week > Week.numberOfWeeksInYear(this.year))) { var normalizedWeek = Week.createFromDay(this.firstDay()); this.year = normalizedWeek.year; this.week = normalizedWeek.week; } } Week.ISOStringRegExp = /^(\d+)-[wW](\d+)$/; // See WebCore/platform/DateComponents.h. Week.Minimum = new Week(1, 1); Week.Maximum = new Week(275760, 37); // See WebCore/html/WeekInputType.cpp. Week.DefaultStep = 604800000; Week.DefaultStepBase = -259200000; Week.EpochWeekDay = createUTCDate(1970, 0, 0).getUTCDay(); /** * @param {!string} str * @return {?Week} */ Week.parse = function(str) { var match = Week.ISOStringRegExp.exec(str); if (!match) return null; var year = parseInt(match[1], 10); var week = parseInt(match[2], 10); return new Week(year, week); }; /** * @param {!number} millisecondsSinceEpoch * @return {!Week} */ Week.createFromValue = function(millisecondsSinceEpoch) { return Week.createFromDate(new Date(millisecondsSinceEpoch)) }; /** * @param {!Date} date * @return {!Week} */ Week.createFromDate = function(date) { if (isNaN(date.valueOf())) throw "Invalid date"; var year = date.getUTCFullYear(); if (year <= Week.Maximum.year && Week.weekOneStartDateForYear(year + 1).getTime() <= date.getTime()) year++; else if (year > 1 && Week.weekOneStartDateForYear(year).getTime() > date.getTime()) year--; var week = 1 + Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), date); return new Week(year, week); }; /** * @param {!Day} day * @return {!Week} */ Week.createFromDay = function(day) { var year = day.year; if (year <= Week.Maximum.year && Week.weekOneStartDayForYear(year + 1) <= day) year++; else if (year > 1 && Week.weekOneStartDayForYear(year) > day) year--; var week = Math.floor(1 + (day.valueOf() - Week.weekOneStartDayForYear(year).valueOf()) / MillisecondsPerWeek); return new Week(year, week); }; /** * @return {!Week} */ Week.createFromToday = function() { var now = new Date(); return Week.createFromDate(createUTCDate(now.getFullYear(), now.getMonth(), now.getDate())); }; /** * @param {!number} year * @return {!Date} */ Week.weekOneStartDateForYear = function(year) { if (year < 1) return createUTCDate(1, 0, 1); // The week containing January 4th is week one. var yearStartDay = createUTCDate(year, 0, 4).getUTCDay(); return createUTCDate(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek); }; /** * @param {!number} year * @return {!Day} */ Week.weekOneStartDayForYear = function(year) { if (year < 1) return Day.Minimum; // The week containing January 4th is week one. var yearStartDay = createUTCDate(year, 0, 4).getUTCDay(); return new Day(year, 0, 4 - (yearStartDay + 6) % DaysPerWeek); }; /** * @param {!number} year * @return {!number} */ Week.numberOfWeeksInYear = function(year) { if (year < 1 || year > Week.Maximum.year) return 0; else if (year === Week.Maximum.year) return Week.Maximum.week; return Week._numberOfWeeksSinceDate(Week.weekOneStartDateForYear(year), Week.weekOneStartDateForYear(year + 1)); }; /** * @param {!Date} baseDate * @param {!Date} date * @return {!number} */ Week._numberOfWeeksSinceDate = function(baseDate, date) { return Math.floor((date.getTime() - baseDate.getTime()) / MillisecondsPerWeek); }; /** * @param {!DateType} other * @return {!boolean} */ Week.prototype.equals = function(other) { return other instanceof Week && this.year === other.year && this.week === other.week; }; /** * @param {!number=} offset * @return {!Week} */ Week.prototype.previous = function(offset) { if (typeof offset === "undefined") offset = 1; return new Week(this.year, this.week - offset); }; /** * @param {!number=} offset * @return {!Week} */ Week.prototype.next = function(offset) { if (typeof offset === "undefined") offset = 1; return new Week(this.year, this.week + offset); }; /** * @return {!Date} */ Week.prototype.startDate = function() { var weekStartDate = Week.weekOneStartDateForYear(this.year); weekStartDate.setUTCDate(weekStartDate.getUTCDate() + (this.week - 1) * 7); return weekStartDate; }; /** * @return {!Date} */ Week.prototype.endDate = function() { if (this.equals(Week.Maximum)) return Day.Maximum.startDate(); return this.next().startDate(); }; /** * @return {!Day} */ Week.prototype.firstDay = function() { var weekOneStartDay = Week.weekOneStartDayForYear(this.year); return weekOneStartDay.next((this.week - 1) * DaysPerWeek); }; /** * @return {!Day} */ Week.prototype.middleDay = function() { return this.firstDay().next(3); }; /** * @return {!Day} */ Week.prototype.lastDay = function() { if (this.equals(Week.Maximum)) return Day.Maximum; return this.next().firstDay().previous(); }; /** * @return {!number} */ Week.prototype.valueOf = function() { return this.firstDay().valueOf() - createUTCDate(1970, 0, 1).getTime(); }; /** * @return {!string} */ Week.prototype.toString = function() { var yearString = String(this.year); if (yearString.length < 4) yearString = ("000" + yearString).substr(-4, 4); return yearString + "-W" + ("0" + this.week).substr(-2, 2); }; /** * @constructor * @extends DateType * @param {!number} year * @param {!number} month */ function Month(year, month) { /** * @type {number} * @const */ this.year = year + Math.floor(month / MonthsPerYear); /** * @type {number} * @const */ this.month = month % MonthsPerYear < 0 ? month % MonthsPerYear + MonthsPerYear : month % MonthsPerYear; }; Month.ISOStringRegExp = /^(\d+)-(\d+)$/; // See WebCore/platform/DateComponents.h. Month.Minimum = new Month(1, 0); Month.Maximum = new Month(275760, 8); // See WebCore/html/MonthInputType.cpp. Month.DefaultStep = 1; Month.DefaultStepBase = 0; /** * @param {!string} str * @return {?Month} */ Month.parse = function(str) { var match = Month.ISOStringRegExp.exec(str); if (!match) return null; var year = parseInt(match[1], 10); var month = parseInt(match[2], 10) - 1; return new Month(year, month); }; /** * @param {!number} value * @return {!Month} */ Month.createFromValue = function(monthsSinceEpoch) { return new Month(1970, monthsSinceEpoch) }; /** * @param {!Date} date * @return {!Month} */ Month.createFromDate = function(date) { if (isNaN(date.valueOf())) throw "Invalid date"; return new Month(date.getUTCFullYear(), date.getUTCMonth()); }; /** * @param {!Day} day * @return {!Month} */ Month.createFromDay = function(day) { return new Month(day.year, day.month); }; /** * @return {!Month} */ Month.createFromToday = function() { var now = new Date(); return new Month(now.getFullYear(), now.getMonth()); }; /** * @return {!boolean} */ Month.prototype.containsDay = function(day) { return this.year === day.year && this.month === day.month; }; /** * @param {!Month} other * @return {!boolean} */ Month.prototype.equals = function(other) { return other instanceof Month && this.year === other.year && this.month === other.month; }; /** * @param {!number=} offset * @return {!Month} */ Month.prototype.previous = function(offset) { if (typeof offset === "undefined") offset = 1; return new Month(this.year, this.month - offset); }; /** * @param {!number=} offset * @return {!Month} */ Month.prototype.next = function(offset) { if (typeof offset === "undefined") offset = 1; return new Month(this.year, this.month + offset); }; /** * @return {!Date} */ Month.prototype.startDate = function() { return createUTCDate(this.year, this.month, 1); }; /** * @return {!Date} */ Month.prototype.endDate = function() { if (this.equals(Month.Maximum)) return Day.Maximum.startDate(); return this.next().startDate(); }; /** * @return {!Day} */ Month.prototype.firstDay = function() { return new Day(this.year, this.month, 1); }; /** * @return {!Day} */ Month.prototype.middleDay = function() { return new Day(this.year, this.month, this.month === 2 ? 14 : 15); }; /** * @return {!Day} */ Month.prototype.lastDay = function() { if (this.equals(Month.Maximum)) return Day.Maximum; return this.next().firstDay().previous(); }; /** * @return {!number} */ Month.prototype.valueOf = function() { return (this.year - 1970) * MonthsPerYear + this.month; }; /** * @return {!string} */ Month.prototype.toString = function() { var yearString = String(this.year); if (yearString.length < 4) yearString = ("000" + yearString).substr(-4, 4); return yearString + "-" + ("0" + (this.month + 1)).substr(-2, 2); }; /** * @return {!string} */ Month.prototype.toLocaleString = function() { if (global.params.locale === "ja") return "" + this.year + "年" + formatJapaneseImperialEra(this.year, this.month) + " " + (this.month + 1) + "月"; return window.pagePopupController.formatMonth(this.year, this.month); }; /** * @return {!string} */ Month.prototype.toShortLocaleString = function() { return window.pagePopupController.formatShortMonth(this.year, this.month); }; // ---------------------------------------------------------------- // Initialization /** * @param {Event} event */ function handleMessage(event) { if (global.argumentsReceived) return; global.argumentsReceived = true; initialize(JSON.parse(event.data)); } /** * @param {!Object} params */ function setGlobalParams(params) { var name; for (name in global.params) { if (typeof params[name] === "undefined") console.warn("Missing argument: " + name); } for (name in params) { global.params[name] = params[name]; } }; /** * @param {!Object} args */ function initialize(args) { setGlobalParams(args); if (global.params.suggestionValues && global.params.suggestionValues.length) openSuggestionPicker(); else openCalendarPicker(); } function closePicker() { if (global.picker) global.picker.cleanup(); var main = $("main"); main.innerHTML = ""; main.className = ""; }; function openSuggestionPicker() { closePicker(); global.picker = new SuggestionPicker($("main"), global.params); }; function openCalendarPicker() { closePicker(); global.picker = new CalendarPicker(global.params.mode, global.params); global.picker.attachTo($("main")); }; /** * @constructor */ function EventEmitter() { }; /** * @param {!string} type * @param {!function({...*})} callback */ EventEmitter.prototype.on = function(type, callback) { console.assert(callback instanceof Function); if (!this._callbacks) this._callbacks = {}; if (!this._callbacks[type]) this._callbacks[type] = []; this._callbacks[type].push(callback); }; EventEmitter.prototype.hasListener = function(type) { if (!this._callbacks) return false; var callbacksForType = this._callbacks[type]; if (!callbacksForType) return false; return callbacksForType.length > 0; }; /** * @param {!string} type * @param {!function(Object)} callback */ EventEmitter.prototype.removeListener = function(type, callback) { if (!this._callbacks) return; var callbacksForType = this._callbacks[type]; if (!callbacksForType) return; callbacksForType.splice(callbacksForType.indexOf(callback), 1); if (callbacksForType.length === 0) delete this._callbacks[type]; }; /** * @param {!string} type * @param {...*} var_args */ EventEmitter.prototype.dispatchEvent = function(type) { if (!this._callbacks) return; var callbacksForType = this._callbacks[type]; if (!callbacksForType) return; callbacksForType = callbacksForType.slice(0); for (var i = 0; i < callbacksForType.length; ++i) { callbacksForType[i].apply(this, Array.prototype.slice.call(arguments, 1)); } }; // Parameter t should be a number between 0 and 1. var AnimationTimingFunction = { Linear: function(t){ return t; }, EaseInOut: function(t){ t *= 2; if (t < 1) return Math.pow(t, 3) / 2; t -= 2; return Math.pow(t, 3) / 2 + 1; } }; /** * @constructor * @extends EventEmitter */ function AnimationManager() { EventEmitter.call(this); this._isRunning = false; this._runningAnimatorCount = 0; this._runningAnimators = {}; this._animationFrameCallbackBound = this._animationFrameCallback.bind(this); } AnimationManager.prototype = Object.create(EventEmitter.prototype); AnimationManager.EventTypeAnimationFrameWillFinish = "animationFrameWillFinish"; AnimationManager.prototype._startAnimation = function() { if (this._isRunning) return; this._isRunning = true; window.webkitRequestAnimationFrame(this._animationFrameCallbackBound); }; AnimationManager.prototype._stopAnimation = function() { if (!this._isRunning) return; this._isRunning = false; }; /** * @param {!Animator} animator */ AnimationManager.prototype.add = function(animator) { if (this._runningAnimators[animator.id]) return; this._runningAnimators[animator.id] = animator; this._runningAnimatorCount++; if (this._needsTimer()) this._startAnimation(); }; /** * @param {!Animator} animator */ AnimationManager.prototype.remove = function(animator) { if (!this._runningAnimators[animator.id]) return; delete this._runningAnimators[animator.id]; this._runningAnimatorCount--; if (!this._needsTimer()) this._stopAnimation(); }; AnimationManager.prototype._animationFrameCallback = function(now) { if (this._runningAnimatorCount > 0) { for (var id in this._runningAnimators) { this._runningAnimators[id].onAnimationFrame(now); } } this.dispatchEvent(AnimationManager.EventTypeAnimationFrameWillFinish); if (this._isRunning) window.webkitRequestAnimationFrame(this._animationFrameCallbackBound); }; /** * @return {!boolean} */ AnimationManager.prototype._needsTimer = function() { return this._runningAnimatorCount > 0 || this.hasListener(AnimationManager.EventTypeAnimationFrameWillFinish); }; /** * @param {!string} type * @param {!Function} callback * @override */ AnimationManager.prototype.on = function(type, callback) { EventEmitter.prototype.on.call(this, type, callback); if (this._needsTimer()) this._startAnimation(); }; /** * @param {!string} type * @param {!Function} callback * @override */ AnimationManager.prototype.removeListener = function(type, callback) { EventEmitter.prototype.removeListener.call(this, type, callback); if (!this._needsTimer()) this._stopAnimation(); }; AnimationManager.shared = new AnimationManager(); /** * @constructor * @extends EventEmitter */ function Animator() { EventEmitter.call(this); /** * @type {!number} * @const */ this.id = Animator._lastId++; /** * @type {!number} */ this.duration = 100; /** * @type {?function} */ this.step = null; /** * @type {!boolean} * @protected */ this._isRunning = false; /** * @type {!number} */ this.currentValue = 0; /** * @type {!number} * @protected */ this._lastStepTime = 0; } Animator.prototype = Object.create(EventEmitter.prototype); Animator._lastId = 0; Animator.EventTypeDidAnimationStop = "didAnimationStop"; /** * @return {!boolean} */ Animator.prototype.isRunning = function() { return this._isRunning; }; Animator.prototype.start = function() { this._lastStepTime = Date.now(); this._isRunning = true; AnimationManager.shared.add(this); }; Animator.prototype.stop = function() { if (!this._isRunning) return; this._isRunning = false; AnimationManager.shared.remove(this); this.dispatchEvent(Animator.EventTypeDidAnimationStop, this); }; /** * @param {!number} now */ Animator.prototype.onAnimationFrame = function(now) { this._lastStepTime = now; this.step(this); }; /** * @constructor * @extends Animator */ function TransitionAnimator() { Animator.call(this); /** * @type {!number} * @protected */ this._from = 0; /** * @type {!number} * @protected */ this._to = 0; /** * @type {!number} * @protected */ this._delta = 0; /** * @type {!number} */ this.progress = 0.0; /** * @type {!function} */ this.timingFunction = AnimationTimingFunction.Linear; } TransitionAnimator.prototype = Object.create(Animator.prototype); /** * @param {!number} value */ TransitionAnimator.prototype.setFrom = function(value) { this._from = value; this._delta = this._to - this._from; }; TransitionAnimator.prototype.start = function() { console.assert(isFinite(this.duration)); this.progress = 0.0; this.currentValue = this._from; Animator.prototype.start.call(this); }; /** * @param {!number} value */ TransitionAnimator.prototype.setTo = function(value) { this._to = value; this._delta = this._to - this._from; }; /** * @param {!number} now */ TransitionAnimator.prototype.onAnimationFrame = function(now) { this.progress += (now - this._lastStepTime) / this.duration; this.progress = Math.min(1.0, this.progress); this._lastStepTime = now; this.currentValue = this.timingFunction(this.progress) * this._delta + this._from; this.step(this); if (this.progress === 1.0) { this.stop(); return; } }; /** * @constructor * @extends Animator * @param {!number} initialVelocity * @param {!number} initialValue */ function FlingGestureAnimator(initialVelocity, initialValue) { Animator.call(this); /** * @type {!number} */ this.initialVelocity = initialVelocity; /** * @type {!number} */ this.initialValue = initialValue; /** * @type {!number} * @protected */ this._elapsedTime = 0; var startVelocity = Math.abs(this.initialVelocity); if (startVelocity > this._velocityAtTime(0)) startVelocity = this._velocityAtTime(0); if (startVelocity < 0) startVelocity = 0; /** * @type {!number} * @protected */ this._timeOffset = this._timeAtVelocity(startVelocity); /** * @type {!number} * @protected */ this._positionOffset = this._valueAtTime(this._timeOffset); /** * @type {!number} */ this.duration = this._timeAtVelocity(0); } FlingGestureAnimator.prototype = Object.create(Animator.prototype); // Velocity is subject to exponential decay. These parameters are coefficients // that determine the curve. FlingGestureAnimator._P0 = -5707.62; FlingGestureAnimator._P1 = 0.172; FlingGestureAnimator._P2 = 0.0037; /** * @param {!number} t */ FlingGestureAnimator.prototype._valueAtTime = function(t) { return FlingGestureAnimator._P0 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1 * t - FlingGestureAnimator._P0; }; /** * @param {!number} t */ FlingGestureAnimator.prototype._velocityAtTime = function(t) { return -FlingGestureAnimator._P0 * FlingGestureAnimator._P2 * Math.exp(-FlingGestureAnimator._P2 * t) - FlingGestureAnimator._P1; }; /** * @param {!number} v */ FlingGestureAnimator.prototype._timeAtVelocity = function(v) { return -Math.log((v + FlingGestureAnimator._P1) / (-FlingGestureAnimator._P0 * FlingGestureAnimator._P2)) / FlingGestureAnimator._P2; }; FlingGestureAnimator.prototype.start = function() { this._lastStepTime = Date.now(); Animator.prototype.start.call(this); }; /** * @param {!number} now */ FlingGestureAnimator.prototype.onAnimationFrame = function(now) { this._elapsedTime += now - this._lastStepTime; this._lastStepTime = now; if (this._elapsedTime + this._timeOffset >= this.duration) { this.stop(); return; } var position = this._valueAtTime(this._elapsedTime + this._timeOffset) - this._positionOffset; if (this.initialVelocity < 0) position = -position; this.currentValue = position + this.initialValue; this.step(this); }; /** * @constructor * @extends EventEmitter * @param {?Element} element * View adds itself as a property on the element so we can access it from Event.target. */ function View(element) { EventEmitter.call(this); /** * @type {Element} * @const */ this.element = element || createElement("div"); this.element.$view = this; this.bindCallbackMethods(); } View.prototype = Object.create(EventEmitter.prototype); /** * @param {!Element} ancestorElement * @return {?Object} */ View.prototype.offsetRelativeTo = function(ancestorElement) { var x = 0; var y = 0; var element = this.element; while (element) { x += element.offsetLeft || 0; y += element.offsetTop || 0; element = element.offsetParent; if (element === ancestorElement) return {x: x, y: y}; } return null; }; /** * @param {!View|Node} parent * @param {?View|Node=} before */ View.prototype.attachTo = function(parent, before) { if (parent instanceof View) return this.attachTo(parent.element, before); if (typeof before === "undefined") before = null; if (before instanceof View) before = before.element; parent.insertBefore(this.element, before); }; View.prototype.bindCallbackMethods = function() { for (var methodName in this) { if (!/^on[A-Z]/.test(methodName)) continue; if (this.hasOwnProperty(methodName)) continue; var method = this[methodName]; if (!(method instanceof Function)) continue; this[methodName] = method.bind(this); } }; /** * @constructor * @extends View */ function ScrollView() { View.call(this, createElement("div", ScrollView.ClassNameScrollView)); /** * @type {Element} * @const */ this.contentElement = createElement("div", ScrollView.ClassNameScrollViewContent); this.element.appendChild(this.contentElement); /** * @type {number} */ this.minimumContentOffset = -Infinity; /** * @type {number} */ this.maximumContentOffset = Infinity; /** * @type {number} * @protected */ this._contentOffset = 0; /** * @type {number} * @protected */ this._width = 0; /** * @type {number} * @protected */ this._height = 0; /** * @type {Animator} * @protected */ this._scrollAnimator = null; /** * @type {?Object} */ this.delegate = null; /** * @type {!number} */ this._lastTouchPosition = 0; /** * @type {!number} */ this._lastTouchVelocity = 0; /** * @type {!number} */ this._lastTouchTimeStamp = 0; this.element.addEventListener("mousewheel", this.onMouseWheel, false); this.element.addEventListener("touchstart", this.onTouchStart, false); /** * The content offset is partitioned so the it can go beyond the CSS limit * of 33554433px. * @type {number} * @protected */ this._partitionNumber = 0; } ScrollView.prototype = Object.create(View.prototype); ScrollView.PartitionHeight = 100000; ScrollView.ClassNameScrollView = "scroll-view"; ScrollView.ClassNameScrollViewContent = "scroll-view-content"; /** * @param {!Event} event */ ScrollView.prototype.onTouchStart = function(event) { var touch = event.touches[0]; this._lastTouchPosition = touch.clientY; this._lastTouchVelocity = 0; this._lastTouchTimeStamp = event.timeStamp; if (this._scrollAnimator) this._scrollAnimator.stop(); window.addEventListener("touchmove", this.onWindowTouchMove, false); window.addEventListener("touchend", this.onWindowTouchEnd, false); }; /** * @param {!Event} event */ ScrollView.prototype.onWindowTouchMove = function(event) { var touch = event.touches[0]; var deltaTime = event.timeStamp - this._lastTouchTimeStamp; var deltaY = this._lastTouchPosition - touch.clientY; this.scrollBy(deltaY, false); this._lastTouchVelocity = deltaY / deltaTime; this._lastTouchPosition = touch.clientY; this._lastTouchTimeStamp = event.timeStamp; event.stopPropagation(); event.preventDefault(); }; /** * @param {!Event} event */ ScrollView.prototype.onWindowTouchEnd = function(event) { if (Math.abs(this._lastTouchVelocity) > 0.01) { this._scrollAnimator = new FlingGestureAnimator(this._lastTouchVelocity, this._contentOffset); this._scrollAnimator.step = this.onFlingGestureAnimatorStep; this._scrollAnimator.start(); } window.removeEventListener("touchmove", this.onWindowTouchMove, false); window.removeEventListener("touchend", this.onWindowTouchEnd, false); }; /** * @param {!Animator} animator */ ScrollView.prototype.onFlingGestureAnimatorStep = function(animator) { this.scrollTo(animator.currentValue, false); }; /** * @return {!Animator} */ ScrollView.prototype.scrollAnimator = function() { return this._scrollAnimator; }; /** * @param {!number} width */ ScrollView.prototype.setWidth = function(width) { console.assert(isFinite(width)); if (this._width === width) return; this._width = width; this.element.style.width = this._width + "px"; }; /** * @return {!number} */ ScrollView.prototype.width = function() { return this._width; }; /** * @param {!number} height */ ScrollView.prototype.setHeight = function(height) { console.assert(isFinite(height)); if (this._height === height) return; this._height = height; this.element.style.height = height + "px"; if (this.delegate) this.delegate.scrollViewDidChangeHeight(this); }; /** * @return {!number} */ ScrollView.prototype.height = function() { return this._height; }; /** * @param {!Animator} animator */ ScrollView.prototype.onScrollAnimatorStep = function(animator) { this.setContentOffset(animator.currentValue); }; /** * @param {!number} offset * @param {?boolean} animate */ ScrollView.prototype.scrollTo = function(offset, animate) { console.assert(isFinite(offset)); if (!animate) { this.setContentOffset(offset); return; } if (this._scrollAnimator) this._scrollAnimator.stop(); this._scrollAnimator = new TransitionAnimator(); this._scrollAnimator.step = this.onScrollAnimatorStep; this._scrollAnimator.setFrom(this._contentOffset); this._scrollAnimator.setTo(offset); this._scrollAnimator.duration = 300; this._scrollAnimator.start(); }; /** * @param {!number} offset * @param {?boolean} animate */ ScrollView.prototype.scrollBy = function(offset, animate) { this.scrollTo(this._contentOffset + offset, animate); }; /** * @return {!number} */ ScrollView.prototype.contentOffset = function() { return this._contentOffset; }; /** * @param {?Event} event */ ScrollView.prototype.onMouseWheel = function(event) { this.setContentOffset(this._contentOffset - event.wheelDelta / 30); event.stopPropagation(); event.preventDefault(); }; /** * @param {!number} value */ ScrollView.prototype.setContentOffset = function(value) { console.assert(isFinite(value)); value = Math.min(this.maximumContentOffset - this._height, Math.max(this.minimumContentOffset, Math.floor(value))); if (this._contentOffset === value) return; var newPartitionNumber = Math.floor(value / ScrollView.PartitionHeight); var partitionChanged = this._partitionNumber !== newPartitionNumber; this._partitionNumber = newPartitionNumber; this._contentOffset = value; this.contentElement.style.webkitTransform = "translate(0, " + (-this.contentPositionForContentOffset(this._contentOffset)) + "px)"; if (this.delegate) { this.delegate.scrollViewDidChangeContentOffset(this); if (partitionChanged) this.delegate.scrollViewDidChangePartition(this); } }; /** * @param {!number} offset */ ScrollView.prototype.contentPositionForContentOffset = function(offset) { return offset - this._partitionNumber * ScrollView.PartitionHeight; }; /** * @constructor * @extends View */ function ListCell() { View.call(this, createElement("div", ListCell.ClassNameListCell)); /** * @type {!number} */ this.row = NaN; /** * @type {!number} */ this._width = 0; /** * @type {!number} */ this._position = 0; } ListCell.prototype = Object.create(View.prototype); ListCell.DefaultRecycleBinLimit = 64; ListCell.ClassNameListCell = "list-cell"; ListCell.ClassNameHidden = "hidden"; /** * @return {!Array} An array to keep thrown away cells. */ ListCell.prototype._recycleBin = function() { console.assert(false, "NOT REACHED: ListCell.prototype._recycleBin needs to be overridden."); return []; }; ListCell.prototype.throwAway = function() { this.hide(); var limit = typeof this.constructor.RecycleBinLimit === "undefined" ? ListCell.DefaultRecycleBinLimit : this.constructor.RecycleBinLimit; var recycleBin = this._recycleBin(); if (recycleBin.length < limit) recycleBin.push(this); }; ListCell.prototype.show = function() { this.element.classList.remove(ListCell.ClassNameHidden); }; ListCell.prototype.hide = function() { this.element.classList.add(ListCell.ClassNameHidden); }; /** * @return {!number} Width in pixels. */ ListCell.prototype.width = function(){ return this._width; }; /** * @param {!number} width Width in pixels. */ ListCell.prototype.setWidth = function(width){ if (this._width === width) return; this._width = width; this.element.style.width = this._width + "px"; }; /** * @return {!number} Position in pixels. */ ListCell.prototype.position = function(){ return this._position; }; /** * @param {!number} y Position in pixels. */ ListCell.prototype.setPosition = function(y) { if (this._position === y) return; this._position = y; this.element.style.webkitTransform = "translate(0, " + this._position + "px)"; }; /** * @param {!boolean} selected */ ListCell.prototype.setSelected = function(selected) { if (this._selected === selected) return; this._selected = selected; if (this._selected) this.element.classList.add("selected"); else this.element.classList.remove("selected"); }; /** * @constructor * @extends View */ function ListView() { View.call(this, createElement("div", ListView.ClassNameListView)); this.element.tabIndex = 0; /** * @type {!number} * @private */ this._width = 0; /** * @type {!Object} * @private */ this._cells = {}; /** * @type {!number} */ this.selectedRow = ListView.NoSelection; /** * @type {!ScrollView} */ this.scrollView = new ScrollView(); this.scrollView.delegate = this; this.scrollView.minimumContentOffset = 0; this.scrollView.setWidth(0); this.scrollView.setHeight(0); this.scrollView.attachTo(this); this.element.addEventListener("click", this.onClick, false); /** * @type {!boolean} * @private */ this._needsUpdateCells = false; } ListView.prototype = Object.create(View.prototype); ListView.NoSelection = -1; ListView.ClassNameListView = "list-view"; ListView.prototype.onAnimationFrameWillFinish = function() { if (this._needsUpdateCells) this.updateCells(); }; /** * @param {!boolean} needsUpdateCells */ ListView.prototype.setNeedsUpdateCells = function(needsUpdateCells) { if (this._needsUpdateCells === needsUpdateCells) return; this._needsUpdateCells = needsUpdateCells; if (this._needsUpdateCells) AnimationManager.shared.on(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish); else AnimationManager.shared.removeListener(AnimationManager.EventTypeAnimationFrameWillFinish, this.onAnimationFrameWillFinish); }; /** * @param {!number} row * @return {?ListCell} */ ListView.prototype.cellAtRow = function(row) { return this._cells[row]; }; /** * @param {!number} offset Scroll offset in pixels. * @return {!number} */ ListView.prototype.rowAtScrollOffset = function(offset) { console.assert(false, "NOT REACHED: ListView.prototype.rowAtScrollOffset needs to be overridden."); return 0; }; /** * @param {!number} row * @return {!number} Scroll offset in pixels. */ ListView.prototype.scrollOffsetForRow = function(row) { console.assert(false, "NOT REACHED: ListView.prototype.scrollOffsetForRow needs to be overridden."); return 0; }; /** * @param {!number} row * @return {!ListCell} */ ListView.prototype.addCellIfNecessary = function(row) { var cell = this._cells[row]; if (cell) return cell; cell = this.prepareNewCell(row); cell.attachTo(this.scrollView.contentElement); cell.setWidth(this._width); cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(row))); this._cells[row] = cell; return cell; }; /** * @param {!number} row * @return {!ListCell} */ ListView.prototype.prepareNewCell = function(row) { console.assert(false, "NOT REACHED: ListView.prototype.prepareNewCell should be overridden."); return new ListCell(); }; /** * @param {!ListCell} cell */ ListView.prototype.throwAwayCell = function(cell) { delete this._cells[cell.row]; cell.throwAway(); }; /** * @return {!number} */ ListView.prototype.firstVisibleRow = function() { return this.rowAtScrollOffset(this.scrollView.contentOffset()); }; /** * @return {!number} */ ListView.prototype.lastVisibleRow = function() { return this.rowAtScrollOffset(this.scrollView.contentOffset() + this.scrollView.height() - 1); }; /** * @param {!ScrollView} scrollView */ ListView.prototype.scrollViewDidChangeContentOffset = function(scrollView) { this.setNeedsUpdateCells(true); }; /** * @param {!ScrollView} scrollView */ ListView.prototype.scrollViewDidChangeHeight = function(scrollView) { this.setNeedsUpdateCells(true); }; /** * @param {!ScrollView} scrollView */ ListView.prototype.scrollViewDidChangePartition = function(scrollView) { this.setNeedsUpdateCells(true); }; ListView.prototype.updateCells = function() { var firstVisibleRow = this.firstVisibleRow(); var lastVisibleRow = this.lastVisibleRow(); console.assert(firstVisibleRow <= lastVisibleRow); for (var c in this._cells) { var cell = this._cells[c]; if (cell.row < firstVisibleRow || cell.row > lastVisibleRow) this.throwAwayCell(cell); } for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) { var cell = this._cells[i]; if (cell) cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row))); else this.addCellIfNecessary(i); } this.setNeedsUpdateCells(false); }; /** * @return {!number} Width in pixels. */ ListView.prototype.width = function() { return this._width; }; /** * @param {!number} width Width in pixels. */ ListView.prototype.setWidth = function(width) { if (this._width === width) return; this._width = width; this.scrollView.setWidth(this._width); for (var c in this._cells) { this._cells[c].setWidth(this._width); } this.element.style.width = this._width + "px"; this.setNeedsUpdateCells(true); }; /** * @return {!number} Height in pixels. */ ListView.prototype.height = function() { return this.scrollView.height(); }; /** * @param {!number} height Height in pixels. */ ListView.prototype.setHeight = function(height) { this.scrollView.setHeight(height); }; /** * @param {?Event} event */ ListView.prototype.onClick = function(event) { var clickedCellElement = enclosingNodeOrSelfWithClass(event.target, ListCell.ClassNameListCell); if (!clickedCellElement) return; var clickedCell = clickedCellElement.$view; if (clickedCell.row !== this.selectedRow) this.select(clickedCell.row); }; /** * @param {!number} row */ ListView.prototype.select = function(row) { if (this.selectedRow === row) return; this.deselect(); if (row === ListView.NoSelection) return; this.selectedRow = row; var selectedCell = this._cells[this.selectedRow]; if (selectedCell) selectedCell.setSelected(true); }; ListView.prototype.deselect = function() { if (this.selectedRow === ListView.NoSelection) return; var selectedCell = this._cells[this.selectedRow]; if (selectedCell) selectedCell.setSelected(false); this.selectedRow = ListView.NoSelection; }; /** * @param {!number} row * @param {!boolean} animate */ ListView.prototype.scrollToRow = function(row, animate) { this.scrollView.scrollTo(this.scrollOffsetForRow(row), animate); }; /** * @constructor * @extends View * @param {!ScrollView} scrollView */ function ScrubbyScrollBar(scrollView) { View.call(this, createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollBar)); /** * @type {!Element} * @const */ this.thumb = createElement("div", ScrubbyScrollBar.ClassNameScrubbyScrollThumb); this.element.appendChild(this.thumb); /** * @type {!ScrollView} * @const */ this.scrollView = scrollView; /** * @type {!number} * @protected */ this._height = 0; /** * @type {!number} * @protected */ this._thumbHeight = 0; /** * @type {!number} * @protected */ this._thumbPosition = 0; this.setHeight(0); this.setThumbHeight(ScrubbyScrollBar.ThumbHeight); /** * @type {?Animator} * @protected */ this._thumbStyleTopAnimator = null; /** * @type {?number} * @protected */ this._timer = null; this.element.addEventListener("mousedown", this.onMouseDown, false); this.element.addEventListener("touchstart", this.onTouchStart, false); } ScrubbyScrollBar.prototype = Object.create(View.prototype); ScrubbyScrollBar.ScrollInterval = 16; ScrubbyScrollBar.ThumbMargin = 2; ScrubbyScrollBar.ThumbHeight = 30; ScrubbyScrollBar.ClassNameScrubbyScrollBar = "scrubby-scroll-bar"; ScrubbyScrollBar.ClassNameScrubbyScrollThumb = "scrubby-scroll-thumb"; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onTouchStart = function(event) { var touch = event.touches[0]; this._setThumbPositionFromEventPosition(touch.clientY); if (this._thumbStyleTopAnimator) this._thumbStyleTopAnimator.stop(); this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval); window.addEventListener("touchmove", this.onWindowTouchMove, false); window.addEventListener("touchend", this.onWindowTouchEnd, false); event.stopPropagation(); event.preventDefault(); }; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onWindowTouchMove = function(event) { var touch = event.touches[0]; this._setThumbPositionFromEventPosition(touch.clientY); event.stopPropagation(); event.preventDefault(); }; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onWindowTouchEnd = function(event) { this._thumbStyleTopAnimator = new TransitionAnimator(); this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep; this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop); this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2); this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut; this._thumbStyleTopAnimator.duration = 100; this._thumbStyleTopAnimator.start(); window.removeEventListener("touchmove", this.onWindowTouchMove, false); window.removeEventListener("touchend", this.onWindowTouchEnd, false); clearInterval(this._timer); }; /** * @return {!number} Height of the view in pixels. */ ScrubbyScrollBar.prototype.height = function() { return this._height; }; /** * @param {!number} height Height of the view in pixels. */ ScrubbyScrollBar.prototype.setHeight = function(height) { if (this._height === height) return; this._height = height; this.element.style.height = this._height + "px"; this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px"; this._thumbPosition = 0; }; /** * @param {!number} height Height of the scroll bar thumb in pixels. */ ScrubbyScrollBar.prototype.setThumbHeight = function(height) { if (this._thumbHeight === height) return; this._thumbHeight = height; this.thumb.style.height = this._thumbHeight + "px"; this.thumb.style.top = ((this._height - this._thumbHeight) / 2) + "px"; this._thumbPosition = 0; }; /** * @param {number} position */ ScrubbyScrollBar.prototype._setThumbPositionFromEventPosition = function(position) { var thumbMin = ScrubbyScrollBar.ThumbMargin; var thumbMax = this._height - this._thumbHeight - ScrubbyScrollBar.ThumbMargin * 2; var y = position - this.element.getBoundingClientRect().top - this.element.clientTop + this.element.scrollTop; var thumbPosition = y - this._thumbHeight / 2; thumbPosition = Math.max(thumbPosition, thumbMin); thumbPosition = Math.min(thumbPosition, thumbMax); this.thumb.style.top = thumbPosition + "px"; this._thumbPosition = 1.0 - (thumbPosition - thumbMin) / (thumbMax - thumbMin) * 2; }; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onMouseDown = function(event) { this._setThumbPositionFromEventPosition(event.clientY); window.addEventListener("mousemove", this.onWindowMouseMove, false); window.addEventListener("mouseup", this.onWindowMouseUp, false); if (this._thumbStyleTopAnimator) this._thumbStyleTopAnimator.stop(); this._timer = setInterval(this.onScrollTimer, ScrubbyScrollBar.ScrollInterval); event.stopPropagation(); event.preventDefault(); }; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onWindowMouseMove = function(event) { this._setThumbPositionFromEventPosition(event.clientY); }; /** * @param {?Event} event */ ScrubbyScrollBar.prototype.onWindowMouseUp = function(event) { this._thumbStyleTopAnimator = new TransitionAnimator(); this._thumbStyleTopAnimator.step = this.onThumbStyleTopAnimationStep; this._thumbStyleTopAnimator.setFrom(this.thumb.offsetTop); this._thumbStyleTopAnimator.setTo((this._height - this._thumbHeight) / 2); this._thumbStyleTopAnimator.timingFunction = AnimationTimingFunction.EaseInOut; this._thumbStyleTopAnimator.duration = 100; this._thumbStyleTopAnimator.start(); window.removeEventListener("mousemove", this.onWindowMouseMove, false); window.removeEventListener("mouseup", this.onWindowMouseUp, false); clearInterval(this._timer); }; /** * @param {!Animator} animator */ ScrubbyScrollBar.prototype.onThumbStyleTopAnimationStep = function(animator) { this.thumb.style.top = animator.currentValue + "px"; }; ScrubbyScrollBar.prototype.onScrollTimer = function() { var scrollAmount = Math.pow(this._thumbPosition, 2) * 10; if (this._thumbPosition > 0) scrollAmount = -scrollAmount; this.scrollView.scrollBy(scrollAmount, false); }; /** * @constructor * @extends ListCell * @param {!Array} shortMonthLabels */ function YearListCell(shortMonthLabels) { ListCell.call(this); this.element.classList.add(YearListCell.ClassNameYearListCell); this.element.style.height = YearListCell.Height + "px"; /** * @type {!Element} * @const */ this.label = createElement("div", YearListCell.ClassNameLabel, "----"); this.element.appendChild(this.label); this.label.style.height = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px"; this.label.style.lineHeight = (YearListCell.Height - YearListCell.BorderBottomWidth) + "px"; /** * @type {!Array} Array of the 12 month button elements. * @const */ this.monthButtons = []; var monthChooserElement = createElement("div", YearListCell.ClassNameMonthChooser); for (var r = 0; r < YearListCell.ButtonRows; ++r) { var buttonsRow = createElement("div", YearListCell.ClassNameMonthButtonsRow); for (var c = 0; c < YearListCell.ButtonColumns; ++c) { var month = c + r * YearListCell.ButtonColumns; var button = createElement("button", YearListCell.ClassNameMonthButton, shortMonthLabels[month]); button.dataset.month = month; buttonsRow.appendChild(button); this.monthButtons.push(button); } monthChooserElement.appendChild(buttonsRow); } this.element.appendChild(monthChooserElement); /** * @type {!boolean} * @private */ this._selected = false; /** * @type {!number} * @private */ this._height = 0; } YearListCell.prototype = Object.create(ListCell.prototype); YearListCell.Height = hasInaccuratePointingDevice() ? 31 : 25; YearListCell.BorderBottomWidth = 1; YearListCell.ButtonRows = 3; YearListCell.ButtonColumns = 4; YearListCell.SelectedHeight = hasInaccuratePointingDevice() ? 127 : 121; YearListCell.ClassNameYearListCell = "year-list-cell"; YearListCell.ClassNameLabel = "label"; YearListCell.ClassNameMonthChooser = "month-chooser"; YearListCell.ClassNameMonthButtonsRow = "month-buttons-row"; YearListCell.ClassNameMonthButton = "month-button"; YearListCell.ClassNameHighlighted = "highlighted"; YearListCell._recycleBin = []; /** * @return {!Array} * @override */ YearListCell.prototype._recycleBin = function() { return YearListCell._recycleBin; }; /** * @param {!number} row */ YearListCell.prototype.reset = function(row) { this.row = row; this.label.textContent = row + 1; for (var i = 0; i < this.monthButtons.length; ++i) { this.monthButtons[i].classList.remove(YearListCell.ClassNameHighlighted); } this.show(); }; /** * @return {!number} The height in pixels. */ YearListCell.prototype.height = function() { return this._height; }; /** * @param {!number} height Height in pixels. */ YearListCell.prototype.setHeight = function(height) { if (this._height === height) return; this._height = height; this.element.style.height = this._height + "px"; }; /** * @constructor * @extends ListView * @param {!Month} minimumMonth * @param {!Month} maximumMonth */ function YearListView(minimumMonth, maximumMonth) { ListView.call(this); this.element.classList.add("year-list-view"); /** * @type {?Month} */ this.highlightedMonth = null; /** * @type {!Month} * @const * @protected */ this._minimumMonth = minimumMonth; /** * @type {!Month} * @const * @protected */ this._maximumMonth = maximumMonth; this.scrollView.minimumContentOffset = (this._minimumMonth.year - 1) * YearListCell.Height; this.scrollView.maximumContentOffset = (this._maximumMonth.year - 1) * YearListCell.Height + YearListCell.SelectedHeight; /** * @type {!Object} * @const * @protected */ this._runningAnimators = {}; /** * @type {!Array} * @const * @protected */ this._animatingRows = []; /** * @type {!boolean} * @protected */ this._ignoreMouseOutUntillNextMouseOver = false; /** * @type {!ScrubbyScrollBar} * @const */ this.scrubbyScrollBar = new ScrubbyScrollBar(this.scrollView); this.scrubbyScrollBar.attachTo(this); this.element.addEventListener("mouseover", this.onMouseOver, false); this.element.addEventListener("mouseout", this.onMouseOut, false); this.element.addEventListener("keydown", this.onKeyDown, false); this.element.addEventListener("touchstart", this.onTouchStart, false); } YearListView.prototype = Object.create(ListView.prototype); YearListView.Height = YearListCell.SelectedHeight - 1; YearListView.EventTypeYearListViewDidHide = "yearListViewDidHide"; YearListView.EventTypeYearListViewDidSelectMonth = "yearListViewDidSelectMonth"; /** * @param {?Event} event */ YearListView.prototype.onTouchStart = function(event) { var touch = event.touches[0]; var monthButtonElement = enclosingNodeOrSelfWithClass(touch.target, YearListCell.ClassNameMonthButton); if (!monthButtonElement) return; var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell); var cell = cellElement.$view; this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10))); }; /** * @param {?Event} event */ YearListView.prototype.onMouseOver = function(event) { var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton); if (!monthButtonElement) return; var cellElement = enclosingNodeOrSelfWithClass(monthButtonElement, YearListCell.ClassNameYearListCell); var cell = cellElement.$view; this.highlightMonth(new Month(cell.row + 1, parseInt(monthButtonElement.dataset.month, 10))); this._ignoreMouseOutUntillNextMouseOver = false; }; /** * @param {?Event} event */ YearListView.prototype.onMouseOut = function(event) { if (this._ignoreMouseOutUntillNextMouseOver) return; var monthButtonElement = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton); if (!monthButtonElement) { this.dehighlightMonth(); } }; /** * @param {!number} width Width in pixels. * @override */ YearListView.prototype.setWidth = function(width) { ListView.prototype.setWidth.call(this, width - this.scrubbyScrollBar.element.offsetWidth); this.element.style.width = width + "px"; }; /** * @param {!number} height Height in pixels. * @override */ YearListView.prototype.setHeight = function(height) { ListView.prototype.setHeight.call(this, height); this.scrubbyScrollBar.setHeight(height); }; /** * @enum {number} */ YearListView.RowAnimationDirection = { Opening: 0, Closing: 1 }; /** * @param {!number} row * @param {!YearListView.RowAnimationDirection} direction */ YearListView.prototype._animateRow = function(row, direction) { var fromValue = direction === YearListView.RowAnimationDirection.Closing ? YearListCell.SelectedHeight : YearListCell.Height; var oldAnimator = this._runningAnimators[row]; if (oldAnimator) { oldAnimator.stop(); fromValue = oldAnimator.currentValue; } var cell = this.cellAtRow(row); var animator = new TransitionAnimator(); animator.step = this.onCellHeightAnimatorStep; animator.setFrom(fromValue); animator.setTo(direction === YearListView.RowAnimationDirection.Opening ? YearListCell.SelectedHeight : YearListCell.Height); animator.timingFunction = AnimationTimingFunction.EaseInOut; animator.duration = 300; animator.row = row; animator.on(Animator.EventTypeDidAnimationStop, this.onCellHeightAnimatorDidStop); this._runningAnimators[row] = animator; this._animatingRows.push(row); this._animatingRows.sort(); animator.start(); }; /** * @param {?Animator} animator */ YearListView.prototype.onCellHeightAnimatorDidStop = function(animator) { delete this._runningAnimators[animator.row]; var index = this._animatingRows.indexOf(animator.row); this._animatingRows.splice(index, 1); }; /** * @param {!Animator} animator */ YearListView.prototype.onCellHeightAnimatorStep = function(animator) { var cell = this.cellAtRow(animator.row); if (cell) cell.setHeight(animator.currentValue); this.updateCells(); }; /** * @param {?Event} event */ YearListView.prototype.onClick = function(event) { var oldSelectedRow = this.selectedRow; ListView.prototype.onClick.call(this, event); var year = this.selectedRow + 1; if (this.selectedRow !== oldSelectedRow) { var month = this.highlightedMonth ? this.highlightedMonth.month : 0; this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month)); this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true); } else { var monthButton = enclosingNodeOrSelfWithClass(event.target, YearListCell.ClassNameMonthButton); if (!monthButton) return; var month = parseInt(monthButton.dataset.month, 10); this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, new Month(year, month)); this.hide(); } }; /** * @param {!number} scrollOffset * @return {!number} * @override */ YearListView.prototype.rowAtScrollOffset = function(scrollOffset) { var remainingOffset = scrollOffset; var lastAnimatingRow = 0; var rowsWithIrregularHeight = this._animatingRows.slice(); if (this.selectedRow > -1 && !this._runningAnimators[this.selectedRow]) { rowsWithIrregularHeight.push(this.selectedRow); rowsWithIrregularHeight.sort(); } for (var i = 0; i < rowsWithIrregularHeight.length; ++i) { var row = rowsWithIrregularHeight[i]; var animator = this._runningAnimators[row]; var rowHeight = animator ? animator.currentValue : YearListCell.SelectedHeight; if (remainingOffset <= (row - lastAnimatingRow) * YearListCell.Height) { return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height); } remainingOffset -= (row - lastAnimatingRow) * YearListCell.Height; if (remainingOffset <= (rowHeight - YearListCell.Height)) return row; remainingOffset -= rowHeight - YearListCell.Height; lastAnimatingRow = row; } return lastAnimatingRow + Math.floor(remainingOffset / YearListCell.Height); }; /** * @param {!number} row * @return {!number} * @override */ YearListView.prototype.scrollOffsetForRow = function(row) { var scrollOffset = row * YearListCell.Height; for (var i = 0; i < this._animatingRows.length; ++i) { var animatingRow = this._animatingRows[i]; if (animatingRow >= row) break; var animator = this._runningAnimators[animatingRow]; scrollOffset += animator.currentValue - YearListCell.Height; } if (this.selectedRow > -1 && this.selectedRow < row && !this._runningAnimators[this.selectedRow]) { scrollOffset += YearListCell.SelectedHeight - YearListCell.Height; } return scrollOffset; }; /** * @param {!number} row * @return {!YearListCell} * @override */ YearListView.prototype.prepareNewCell = function(row) { var cell = YearListCell._recycleBin.pop() || new YearListCell(global.params.shortMonthLabels); cell.reset(row); cell.setSelected(this.selectedRow === row); if (this.highlightedMonth && row === this.highlightedMonth.year - 1) { cell.monthButtons[this.highlightedMonth.month].classList.add(YearListCell.ClassNameHighlighted); } for (var i = 0; i < cell.monthButtons.length; ++i) { var month = new Month(row + 1, i); cell.monthButtons[i].disabled = this._minimumMonth > month || this._maximumMonth < month; } var animator = this._runningAnimators[row]; if (animator) cell.setHeight(animator.currentValue); else if (row === this.selectedRow) cell.setHeight(YearListCell.SelectedHeight); else cell.setHeight(YearListCell.Height); return cell; }; /** * @override */ YearListView.prototype.updateCells = function() { var firstVisibleRow = this.firstVisibleRow(); var lastVisibleRow = this.lastVisibleRow(); console.assert(firstVisibleRow <= lastVisibleRow); for (var c in this._cells) { var cell = this._cells[c]; if (cell.row < firstVisibleRow || cell.row > lastVisibleRow) this.throwAwayCell(cell); } for (var i = firstVisibleRow; i <= lastVisibleRow; ++i) { var cell = this._cells[i]; if (cell) cell.setPosition(this.scrollView.contentPositionForContentOffset(this.scrollOffsetForRow(cell.row))); else this.addCellIfNecessary(i); } this.setNeedsUpdateCells(false); }; /** * @override */ YearListView.prototype.deselect = function() { if (this.selectedRow === ListView.NoSelection) return; var selectedCell = this._cells[this.selectedRow]; if (selectedCell) selectedCell.setSelected(false); this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Closing); this.selectedRow = ListView.NoSelection; this.setNeedsUpdateCells(true); }; YearListView.prototype.deselectWithoutAnimating = function() { if (this.selectedRow === ListView.NoSelection) return; var selectedCell = this._cells[this.selectedRow]; if (selectedCell) { selectedCell.setSelected(false); selectedCell.setHeight(YearListCell.Height); } this.selectedRow = ListView.NoSelection; this.setNeedsUpdateCells(true); }; /** * @param {!number} row * @override */ YearListView.prototype.select = function(row) { if (this.selectedRow === row) return; this.deselect(); if (row === ListView.NoSelection) return; this.selectedRow = row; if (this.selectedRow !== ListView.NoSelection) { var selectedCell = this._cells[this.selectedRow]; this._animateRow(this.selectedRow, YearListView.RowAnimationDirection.Opening); if (selectedCell) selectedCell.setSelected(true); var month = this.highlightedMonth ? this.highlightedMonth.month : 0; this.highlightMonth(new Month(this.selectedRow + 1, month)); } this.setNeedsUpdateCells(true); }; /** * @param {!number} row */ YearListView.prototype.selectWithoutAnimating = function(row) { if (this.selectedRow === row) return; this.deselectWithoutAnimating(); if (row === ListView.NoSelection) return; this.selectedRow = row; if (this.selectedRow !== ListView.NoSelection) { var selectedCell = this._cells[this.selectedRow]; if (selectedCell) { selectedCell.setSelected(true); selectedCell.setHeight(YearListCell.SelectedHeight); } var month = this.highlightedMonth ? this.highlightedMonth.month : 0; this.highlightMonth(new Month(this.selectedRow + 1, month)); } this.setNeedsUpdateCells(true); }; /** * @param {!Month} month * @return {?HTMLButtonElement} */ YearListView.prototype.buttonForMonth = function(month) { if (!month) return null; var row = month.year - 1; var cell = this.cellAtRow(row); if (!cell) return null; return cell.monthButtons[month.month]; }; YearListView.prototype.dehighlightMonth = function() { if (!this.highlightedMonth) return; var monthButton = this.buttonForMonth(this.highlightedMonth); if (monthButton) { monthButton.classList.remove(YearListCell.ClassNameHighlighted); } this.highlightedMonth = null; }; /** * @param {!Month} month */ YearListView.prototype.highlightMonth = function(month) { if (this.highlightedMonth && this.highlightedMonth.equals(month)) return; this.dehighlightMonth(); this.highlightedMonth = month; if (!this.highlightedMonth) return; var monthButton = this.buttonForMonth(this.highlightedMonth); if (monthButton) { monthButton.classList.add(YearListCell.ClassNameHighlighted); } }; /** * @param {!Month} month */ YearListView.prototype.show = function(month) { this._ignoreMouseOutUntillNextMouseOver = true; this.scrollToRow(month.year - 1, false); this.selectWithoutAnimating(month.year - 1); this.highlightMonth(month); }; YearListView.prototype.hide = function() { this.dispatchEvent(YearListView.EventTypeYearListViewDidHide, this); }; /** * @param {!Month} month */ YearListView.prototype._moveHighlightTo = function(month) { this.highlightMonth(month); this.select(this.highlightedMonth.year - 1); this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, month); this.scrollView.scrollTo(this.selectedRow * YearListCell.Height, true); return true; }; /** * @param {?Event} event */ YearListView.prototype.onKeyDown = function(event) { var key = event.keyIdentifier; var eventHandled = false; if (key == "U+0054") // 't' key. eventHandled = this._moveHighlightTo(Month.createFromToday()); else if (this.highlightedMonth) { if (global.params.isLocaleRTL ? key == "Right" : key == "Left") eventHandled = this._moveHighlightTo(this.highlightedMonth.previous()); else if (key == "Up") eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(YearListCell.ButtonColumns)); else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") eventHandled = this._moveHighlightTo(this.highlightedMonth.next()); else if (key == "Down") eventHandled = this._moveHighlightTo(this.highlightedMonth.next(YearListCell.ButtonColumns)); else if (key == "PageUp") eventHandled = this._moveHighlightTo(this.highlightedMonth.previous(MonthsPerYear)); else if (key == "PageDown") eventHandled = this._moveHighlightTo(this.highlightedMonth.next(MonthsPerYear)); else if (key == "Enter") { this.dispatchEvent(YearListView.EventTypeYearListViewDidSelectMonth, this, this.highlightedMonth); this.hide(); eventHandled = true; } } else if (key == "Up") { this.scrollView.scrollBy(-YearListCell.Height, true); eventHandled = true; } else if (key == "Down") { this.scrollView.scrollBy(YearListCell.Height, true); eventHandled = true; } else if (key == "PageUp") { this.scrollView.scrollBy(-this.scrollView.height(), true); eventHandled = true; } else if (key == "PageDown") { this.scrollView.scrollBy(this.scrollView.height(), true); eventHandled = true; } if (eventHandled) { event.stopPropagation(); event.preventDefault(); } }; /** * @constructor * @extends View * @param {!Month} minimumMonth * @param {!Month} maximumMonth */ function MonthPopupView(minimumMonth, maximumMonth) { View.call(this, createElement("div", MonthPopupView.ClassNameMonthPopupView)); /** * @type {!YearListView} * @const */ this.yearListView = new YearListView(minimumMonth, maximumMonth); this.yearListView.attachTo(this); /** * @type {!boolean} */ this.isVisible = false; this.element.addEventListener("click", this.onClick, false); } MonthPopupView.prototype = Object.create(View.prototype); MonthPopupView.ClassNameMonthPopupView = "month-popup-view"; MonthPopupView.prototype.show = function(initialMonth, calendarTableRect) { this.isVisible = true; document.body.appendChild(this.element); this.yearListView.setWidth(calendarTableRect.width - 2); this.yearListView.setHeight(YearListView.Height); if (global.params.isLocaleRTL) this.yearListView.element.style.right = calendarTableRect.x + "px"; else this.yearListView.element.style.left = calendarTableRect.x + "px"; this.yearListView.element.style.top = calendarTableRect.y + "px"; this.yearListView.show(initialMonth); this.yearListView.element.focus(); }; MonthPopupView.prototype.hide = function() { if (!this.isVisible) return; this.isVisible = false; this.element.parentNode.removeChild(this.element); this.yearListView.hide(); }; /** * @param {?Event} event */ MonthPopupView.prototype.onClick = function(event) { if (event.target !== this.element) return; this.hide(); }; /** * @constructor * @extends View * @param {!number} maxWidth Maximum width in pixels. */ function MonthPopupButton(maxWidth) { View.call(this, createElement("button", MonthPopupButton.ClassNameMonthPopupButton)); /** * @type {!Element} * @const */ this.labelElement = createElement("span", MonthPopupButton.ClassNameMonthPopupButtonLabel, "-----"); this.element.appendChild(this.labelElement); /** * @type {!Element} * @const */ this.disclosureTriangleIcon = createElement("span", MonthPopupButton.ClassNameDisclosureTriangle); this.disclosureTriangleIcon.innerHTML = ""; this.element.appendChild(this.disclosureTriangleIcon); /** * @type {!boolean} * @protected */ this._useShortMonth = this._shouldUseShortMonth(maxWidth); this.element.style.maxWidth = maxWidth + "px"; this.element.addEventListener("click", this.onClick, false); } MonthPopupButton.prototype = Object.create(View.prototype); MonthPopupButton.ClassNameMonthPopupButton = "month-popup-button"; MonthPopupButton.ClassNameMonthPopupButtonLabel = "month-popup-button-label"; MonthPopupButton.ClassNameDisclosureTriangle = "disclosure-triangle"; MonthPopupButton.EventTypeButtonClick = "buttonClick"; /** * @param {!number} maxWidth Maximum available width in pixels. * @return {!boolean} */ MonthPopupButton.prototype._shouldUseShortMonth = function(maxWidth) { document.body.appendChild(this.element); var month = Month.Maximum; for (var i = 0; i < MonthsPerYear; ++i) { this.labelElement.textContent = month.toLocaleString(); if (this.element.offsetWidth > maxWidth) return true; month = month.previous(); } document.body.removeChild(this.element); return false; }; /** * @param {!Month} month */ MonthPopupButton.prototype.setCurrentMonth = function(month) { this.labelElement.textContent = this._useShortMonth ? month.toShortLocaleString() : month.toLocaleString(); }; /** * @param {?Event} event */ MonthPopupButton.prototype.onClick = function(event) { this.dispatchEvent(MonthPopupButton.EventTypeButtonClick, this); }; /** * @constructor * @extends View */ function CalendarNavigationButton() { View.call(this, createElement("button", CalendarNavigationButton.ClassNameCalendarNavigationButton)); /** * @type {number} Threshold for starting repeating clicks in milliseconds. */ this.repeatingClicksStartingThreshold = CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold; /** * @type {number} Interval between reapeating clicks in milliseconds. */ this.reapeatingClicksInterval = CalendarNavigationButton.DefaultRepeatingClicksInterval; /** * @type {?number} The ID for the timeout that triggers the repeating clicks. */ this._timer = null; this.element.addEventListener("click", this.onClick, false); this.element.addEventListener("mousedown", this.onMouseDown, false); this.element.addEventListener("touchstart", this.onTouchStart, false); }; CalendarNavigationButton.prototype = Object.create(View.prototype); CalendarNavigationButton.DefaultRepeatingClicksStartingThreshold = 600; CalendarNavigationButton.DefaultRepeatingClicksInterval = 300; CalendarNavigationButton.LeftMargin = 4; CalendarNavigationButton.Width = 24; CalendarNavigationButton.ClassNameCalendarNavigationButton = "calendar-navigation-button"; CalendarNavigationButton.EventTypeButtonClick = "buttonClick"; CalendarNavigationButton.EventTypeRepeatingButtonClick = "repeatingButtonClick"; /** * @param {!boolean} disabled */ CalendarNavigationButton.prototype.setDisabled = function(disabled) { this.element.disabled = disabled; }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onClick = function(event) { this.dispatchEvent(CalendarNavigationButton.EventTypeButtonClick, this); }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onTouchStart = function(event) { if (this._timer !== null) return; this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold); window.addEventListener("touchend", this.onWindowTouchEnd, false); }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onWindowTouchEnd = function(event) { if (this._timer === null) return; clearTimeout(this._timer); this._timer = null; window.removeEventListener("touchend", this.onWindowMouseUp, false); }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onMouseDown = function(event) { if (this._timer !== null) return; this._timer = setTimeout(this.onRepeatingClick, this.repeatingClicksStartingThreshold); window.addEventListener("mouseup", this.onWindowMouseUp, false); }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onWindowMouseUp = function(event) { if (this._timer === null) return; clearTimeout(this._timer); this._timer = null; window.removeEventListener("mouseup", this.onWindowMouseUp, false); }; /** * @param {?Event} event */ CalendarNavigationButton.prototype.onRepeatingClick = function(event) { this.dispatchEvent(CalendarNavigationButton.EventTypeRepeatingButtonClick, this); this._timer = setTimeout(this.onRepeatingClick, this.reapeatingClicksInterval); }; /** * @constructor * @extends View * @param {!CalendarPicker} calendarPicker */ function CalendarHeaderView(calendarPicker) { View.call(this, createElement("div", CalendarHeaderView.ClassNameCalendarHeaderView)); this.calendarPicker = calendarPicker; this.calendarPicker.on(CalendarPicker.EventTypeCurrentMonthChanged, this.onCurrentMonthChanged); var titleElement = createElement("div", CalendarHeaderView.ClassNameCalendarTitle); this.element.appendChild(titleElement); /** * @type {!MonthPopupButton} */ this.monthPopupButton = new MonthPopupButton(this.calendarPicker.calendarTableView.width() - CalendarTableView.BorderWidth * 2 - CalendarNavigationButton.Width * 3 - CalendarNavigationButton.LeftMargin * 2); this.monthPopupButton.attachTo(titleElement); /** * @type {!CalendarNavigationButton} * @const */ this._previousMonthButton = new CalendarNavigationButton(); this._previousMonthButton.attachTo(this); this._previousMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick); this._previousMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick); /** * @type {!CalendarNavigationButton} * @const */ this._todayButton = new CalendarNavigationButton(); this._todayButton.attachTo(this); this._todayButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick); this._todayButton.element.classList.add(CalendarHeaderView.ClassNameTodayButton); var monthContainingToday = Month.createFromToday(); this._todayButton.setDisabled(monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth); /** * @type {!CalendarNavigationButton} * @const */ this._nextMonthButton = new CalendarNavigationButton(); this._nextMonthButton.attachTo(this); this._nextMonthButton.on(CalendarNavigationButton.EventTypeButtonClick, this.onNavigationButtonClick); this._nextMonthButton.on(CalendarNavigationButton.EventTypeRepeatingButtonClick, this.onNavigationButtonClick); if (global.params.isLocaleRTL) { this._nextMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle; this._previousMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle; } else { this._nextMonthButton.element.innerHTML = CalendarHeaderView._ForwardTriangle; this._previousMonthButton.element.innerHTML = CalendarHeaderView._BackwardTriangle; } } CalendarHeaderView.prototype = Object.create(View.prototype); CalendarHeaderView.Height = 24; CalendarHeaderView.BottomMargin = 10; CalendarHeaderView._ForwardTriangle = ""; CalendarHeaderView._BackwardTriangle = ""; CalendarHeaderView.ClassNameCalendarHeaderView = "calendar-header-view"; CalendarHeaderView.ClassNameCalendarTitle = "calendar-title"; CalendarHeaderView.ClassNameTodayButton = "today-button"; CalendarHeaderView.prototype.onCurrentMonthChanged = function() { this.monthPopupButton.setCurrentMonth(this.calendarPicker.currentMonth()); this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth); this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth); }; CalendarHeaderView.prototype.onNavigationButtonClick = function(sender) { if (sender === this._previousMonthButton) this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().previous(), CalendarPicker.NavigationBehavior.WithAnimation); else if (sender === this._nextMonthButton) this.calendarPicker.setCurrentMonth(this.calendarPicker.currentMonth().next(), CalendarPicker.NavigationBehavior.WithAnimation); else this.calendarPicker.selectRangeContainingDay(Day.createFromToday()); }; /** * @param {!boolean} disabled */ CalendarHeaderView.prototype.setDisabled = function(disabled) { this.disabled = disabled; this.monthPopupButton.element.disabled = this.disabled; this._previousMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() <= this.calendarPicker.minimumMonth); this._nextMonthButton.setDisabled(this.disabled || this.calendarPicker.currentMonth() >= this.calendarPicker.maximumMonth); var monthContainingToday = Month.createFromToday(); this._todayButton.setDisabled(this.disabled || monthContainingToday < this.calendarPicker.minimumMonth || monthContainingToday > this.calendarPicker.maximumMonth); }; /** * @constructor * @extends ListCell */ function DayCell() { ListCell.call(this); this.element.classList.add(DayCell.ClassNameDayCell); this.element.style.width = DayCell.Width + "px"; this.element.style.height = DayCell.Height + "px"; this.element.style.lineHeight = (DayCell.Height - DayCell.PaddingSize * 2) + "px"; /** * @type {?Day} */ this.day = null; }; DayCell.prototype = Object.create(ListCell.prototype); DayCell.Width = 34; DayCell.Height = hasInaccuratePointingDevice() ? 34 : 20; DayCell.PaddingSize = 1; DayCell.ClassNameDayCell = "day-cell"; DayCell.ClassNameHighlighted = "highlighted"; DayCell.ClassNameDisabled = "disabled"; DayCell.ClassNameCurrentMonth = "current-month"; DayCell.ClassNameToday = "today"; DayCell._recycleBin = []; DayCell.recycleOrCreate = function() { return DayCell._recycleBin.pop() || new DayCell(); }; /** * @return {!Array} * @override */ DayCell.prototype._recycleBin = function() { return DayCell._recycleBin; }; /** * @override */ DayCell.prototype.throwAway = function() { ListCell.prototype.throwAway.call(this); this.day = null; }; /** * @param {!boolean} highlighted */ DayCell.prototype.setHighlighted = function(highlighted) { if (highlighted) this.element.classList.add(DayCell.ClassNameHighlighted); else this.element.classList.remove(DayCell.ClassNameHighlighted); }; /** * @param {!boolean} disabled */ DayCell.prototype.setDisabled = function(disabled) { if (disabled) this.element.classList.add(DayCell.ClassNameDisabled); else this.element.classList.remove(DayCell.ClassNameDisabled); }; /** * @param {!boolean} selected */ DayCell.prototype.setIsInCurrentMonth = function(selected) { if (selected) this.element.classList.add(DayCell.ClassNameCurrentMonth); else this.element.classList.remove(DayCell.ClassNameCurrentMonth); }; /** * @param {!boolean} selected */ DayCell.prototype.setIsToday = function(selected) { if (selected) this.element.classList.add(DayCell.ClassNameToday); else this.element.classList.remove(DayCell.ClassNameToday); }; /** * @param {!Day} day */ DayCell.prototype.reset = function(day) { this.day = day; this.element.textContent = localizeNumber(this.day.date.toString()); this.show(); }; /** * @constructor * @extends ListCell */ function WeekNumberCell() { ListCell.call(this); this.element.classList.add(WeekNumberCell.ClassNameWeekNumberCell); this.element.style.width = (WeekNumberCell.Width - WeekNumberCell.SeparatorWidth) + "px"; this.element.style.height = WeekNumberCell.Height + "px"; this.element.style.lineHeight = (WeekNumberCell.Height - WeekNumberCell.PaddingSize * 2) + "px"; /** * @type {?Week} */ this.week = null; }; WeekNumberCell.prototype = Object.create(ListCell.prototype); WeekNumberCell.Width = 48; WeekNumberCell.Height = DayCell.Height; WeekNumberCell.SeparatorWidth = 1; WeekNumberCell.PaddingSize = 1; WeekNumberCell.ClassNameWeekNumberCell = "week-number-cell"; WeekNumberCell.ClassNameHighlighted = "highlighted"; WeekNumberCell.ClassNameDisabled = "disabled"; WeekNumberCell._recycleBin = []; /** * @return {!Array} * @override */ WeekNumberCell.prototype._recycleBin = function() { return WeekNumberCell._recycleBin; }; /** * @return {!WeekNumberCell} */ WeekNumberCell.recycleOrCreate = function() { return WeekNumberCell._recycleBin.pop() || new WeekNumberCell(); }; /** * @param {!Week} week */ WeekNumberCell.prototype.reset = function(week) { this.week = week; this.element.textContent = localizeNumber(this.week.week.toString()); this.show(); }; /** * @override */ WeekNumberCell.prototype.throwAway = function() { ListCell.prototype.throwAway.call(this); this.week = null; }; WeekNumberCell.prototype.setHighlighted = function(highlighted) { if (highlighted) this.element.classList.add(WeekNumberCell.ClassNameHighlighted); else this.element.classList.remove(WeekNumberCell.ClassNameHighlighted); }; WeekNumberCell.prototype.setDisabled = function(disabled) { if (disabled) this.element.classList.add(WeekNumberCell.ClassNameDisabled); else this.element.classList.remove(WeekNumberCell.ClassNameDisabled); }; /** * @constructor * @extends View * @param {!boolean} hasWeekNumberColumn */ function CalendarTableHeaderView(hasWeekNumberColumn) { View.call(this, createElement("div", "calendar-table-header-view")); if (hasWeekNumberColumn) { var weekNumberLabelElement = createElement("div", "week-number-label", global.params.weekLabel); weekNumberLabelElement.style.width = WeekNumberCell.Width + "px"; this.element.appendChild(weekNumberLabelElement); } for (var i = 0; i < DaysPerWeek; ++i) { var weekDayNumber = (global.params.weekStartDay + i) % DaysPerWeek; var labelElement = createElement("div", "week-day-label", global.params.dayLabels[weekDayNumber]); labelElement.style.width = DayCell.Width + "px"; this.element.appendChild(labelElement); if (getLanguage() === "ja") { if (weekDayNumber === 0) labelElement.style.color = "red"; else if (weekDayNumber === 6) labelElement.style.color = "blue"; } } } CalendarTableHeaderView.prototype = Object.create(View.prototype); CalendarTableHeaderView.Height = 25; /** * @constructor * @extends ListCell */ function CalendarRowCell() { ListCell.call(this); this.element.classList.add(CalendarRowCell.ClassNameCalendarRowCell); this.element.style.height = CalendarRowCell.Height + "px"; /** * @type {!Array} * @protected */ this._dayCells = []; /** * @type {!number} */ this.row = 0; /** * @type {?CalendarTableView} */ this.calendarTableView = null; } CalendarRowCell.prototype = Object.create(ListCell.prototype); CalendarRowCell.Height = DayCell.Height; CalendarRowCell.ClassNameCalendarRowCell = "calendar-row-cell"; CalendarRowCell._recycleBin = []; /** * @return {!Array} * @override */ CalendarRowCell.prototype._recycleBin = function() { return CalendarRowCell._recycleBin; }; /** * @param {!number} row * @param {!CalendarTableView} calendarTableView */ CalendarRowCell.prototype.reset = function(row, calendarTableView) { this.row = row; this.calendarTableView = calendarTableView; if (this.calendarTableView.hasWeekNumberColumn) { var middleDay = this.calendarTableView.dayAtColumnAndRow(3, row); var week = Week.createFromDay(middleDay); this.weekNumberCell = this.calendarTableView.prepareNewWeekNumberCell(week); this.weekNumberCell.attachTo(this); } var day = calendarTableView.dayAtColumnAndRow(0, row); for (var i = 0; i < DaysPerWeek; ++i) { var dayCell = this.calendarTableView.prepareNewDayCell(day); dayCell.attachTo(this); this._dayCells.push(dayCell); day = day.next(); } this.show(); }; /** * @override */ CalendarRowCell.prototype.throwAway = function() { ListCell.prototype.throwAway.call(this); if (this.weekNumberCell) this.calendarTableView.throwAwayWeekNumberCell(this.weekNumberCell); this._dayCells.forEach(this.calendarTableView.throwAwayDayCell, this.calendarTableView); this._dayCells.length = 0; }; /** * @constructor * @extends ListView * @param {!CalendarPicker} calendarPicker */ function CalendarTableView(calendarPicker) { ListView.call(this); this.element.classList.add(CalendarTableView.ClassNameCalendarTableView); this.element.tabIndex = 0; /** * @type {!boolean} * @const */ this.hasWeekNumberColumn = calendarPicker.type === "week"; /** * @type {!CalendarPicker} * @const */ this.calendarPicker = calendarPicker; /** * @type {!Object} * @const */ this._dayCells = {}; var headerView = new CalendarTableHeaderView(this.hasWeekNumberColumn); headerView.attachTo(this, this.scrollView); if (this.hasWeekNumberColumn) { this.setWidth(DayCell.Width * DaysPerWeek + WeekNumberCell.Width); /** * @type {?Array} * @const */ this._weekNumberCells = []; } else { this.setWidth(DayCell.Width * DaysPerWeek); } /** * @type {!boolean} * @protected */ this._ignoreMouseOutUntillNextMouseOver = false; this.element.addEventListener("click", this.onClick, false); this.element.addEventListener("mouseover", this.onMouseOver, false); this.element.addEventListener("mouseout", this.onMouseOut, false); // You shouldn't be able to use the mouse wheel to scroll. this.scrollView.element.removeEventListener("mousewheel", this.scrollView.onMouseWheel, false); // You shouldn't be able to do gesture scroll. this.scrollView.element.removeEventListener("touchstart", this.scrollView.onTouchStart, false); } CalendarTableView.prototype = Object.create(ListView.prototype); CalendarTableView.BorderWidth = 1; CalendarTableView.ClassNameCalendarTableView = "calendar-table-view"; /** * @param {!number} scrollOffset * @return {!number} */ CalendarTableView.prototype.rowAtScrollOffset = function(scrollOffset) { return Math.floor(scrollOffset / CalendarRowCell.Height); }; /** * @param {!number} row * @return {!number} */ CalendarTableView.prototype.scrollOffsetForRow = function(row) { return row * CalendarRowCell.Height; }; /** * @param {?Event} event */ CalendarTableView.prototype.onClick = function(event) { if (this.hasWeekNumberColumn) { var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell); if (weekNumberCellElement) { var weekNumberCell = weekNumberCellElement.$view; this.calendarPicker.selectRangeContainingDay(weekNumberCell.week.firstDay()); return; } } var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell); if (!dayCellElement) return; var dayCell = dayCellElement.$view; this.calendarPicker.selectRangeContainingDay(dayCell.day); }; /** * @param {?Event} event */ CalendarTableView.prototype.onMouseOver = function(event) { if (this.hasWeekNumberColumn) { var weekNumberCellElement = enclosingNodeOrSelfWithClass(event.target, WeekNumberCell.ClassNameWeekNumberCell); if (weekNumberCellElement) { var weekNumberCell = weekNumberCellElement.$view; this.calendarPicker.highlightRangeContainingDay(weekNumberCell.week.firstDay()); this._ignoreMouseOutUntillNextMouseOver = false; return; } } var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell); if (!dayCellElement) return; var dayCell = dayCellElement.$view; this.calendarPicker.highlightRangeContainingDay(dayCell.day); this._ignoreMouseOutUntillNextMouseOver = false; }; /** * @param {?Event} event */ CalendarTableView.prototype.onMouseOut = function(event) { if (this._ignoreMouseOutUntillNextMouseOver) return; var dayCellElement = enclosingNodeOrSelfWithClass(event.target, DayCell.ClassNameDayCell); if (!dayCellElement) { this.calendarPicker.highlightRangeContainingDay(null); } }; /** * @param {!number} row * @return {!CalendarRowCell} */ CalendarTableView.prototype.prepareNewCell = function(row) { var cell = CalendarRowCell._recycleBin.pop() || new CalendarRowCell(); cell.reset(row, this); return cell; }; /** * @return {!number} Height in pixels. */ CalendarTableView.prototype.height = function() { return this.scrollView.height() + CalendarTableHeaderView.Height + CalendarTableView.BorderWidth * 2; }; /** * @param {!number} height Height in pixels. */ CalendarTableView.prototype.setHeight = function(height) { this.scrollView.setHeight(height - CalendarTableHeaderView.Height - CalendarTableView.BorderWidth * 2); }; /** * @param {!Month} month * @param {!boolean} animate */ CalendarTableView.prototype.scrollToMonth = function(month, animate) { var rowForFirstDayInMonth = this.columnAndRowForDay(month.firstDay()).row; this.scrollView.scrollTo(this.scrollOffsetForRow(rowForFirstDayInMonth), animate); }; /** * @param {!number} column * @param {!number} row * @return {!Day} */ CalendarTableView.prototype.dayAtColumnAndRow = function(column, row) { var daysSinceMinimum = row * DaysPerWeek + column + global.params.weekStartDay - CalendarTableView._MinimumDayWeekDay; return Day.createFromValue(daysSinceMinimum * MillisecondsPerDay + CalendarTableView._MinimumDayValue); }; CalendarTableView._MinimumDayValue = Day.Minimum.valueOf(); CalendarTableView._MinimumDayWeekDay = Day.Minimum.weekDay(); /** * @param {!Day} day * @return {!Object} Object with properties column and row. */ CalendarTableView.prototype.columnAndRowForDay = function(day) { var daysSinceMinimum = (day.valueOf() - CalendarTableView._MinimumDayValue) / MillisecondsPerDay; var offset = daysSinceMinimum + CalendarTableView._MinimumDayWeekDay - global.params.weekStartDay; var row = Math.floor(offset / DaysPerWeek); var column = offset - row * DaysPerWeek; return { column: column, row: row }; }; CalendarTableView.prototype.updateCells = function() { ListView.prototype.updateCells.call(this); var selection = this.calendarPicker.selection(); var firstDayInSelection; var lastDayInSelection; if (selection) { firstDayInSelection = selection.firstDay().valueOf(); lastDayInSelection = selection.lastDay().valueOf(); } else { firstDayInSelection = Infinity; lastDayInSelection = Infinity; } var highlight = this.calendarPicker.highlight(); var firstDayInHighlight; var lastDayInHighlight; if (highlight) { firstDayInHighlight = highlight.firstDay().valueOf(); lastDayInHighlight = highlight.lastDay().valueOf(); } else { firstDayInHighlight = Infinity; lastDayInHighlight = Infinity; } var currentMonth = this.calendarPicker.currentMonth(); var firstDayInCurrentMonth = currentMonth.firstDay().valueOf(); var lastDayInCurrentMonth = currentMonth.lastDay().valueOf(); for (var dayString in this._dayCells) { var dayCell = this._dayCells[dayString]; var day = dayCell.day; dayCell.setIsToday(Day.createFromToday().equals(day)); dayCell.setSelected(day >= firstDayInSelection && day <= lastDayInSelection); dayCell.setHighlighted(day >= firstDayInHighlight && day <= lastDayInHighlight); dayCell.setIsInCurrentMonth(day >= firstDayInCurrentMonth && day <= lastDayInCurrentMonth); dayCell.setDisabled(!this.calendarPicker.isValidDay(day)); } if (this.hasWeekNumberColumn) { for (var weekString in this._weekNumberCells) { var weekNumberCell = this._weekNumberCells[weekString]; var week = weekNumberCell.week; weekNumberCell.setSelected(selection && selection.equals(week)); weekNumberCell.setHighlighted(highlight && highlight.equals(week)); weekNumberCell.setDisabled(!this.calendarPicker.isValid(week)); } } }; /** * @param {!Day} day * @return {!DayCell} */ CalendarTableView.prototype.prepareNewDayCell = function(day) { var dayCell = DayCell.recycleOrCreate(); dayCell.reset(day); this._dayCells[dayCell.day.toString()] = dayCell; return dayCell; }; /** * @param {!Week} week * @return {!WeekNumberCell} */ CalendarTableView.prototype.prepareNewWeekNumberCell = function(week) { var weekNumberCell = WeekNumberCell.recycleOrCreate(); weekNumberCell.reset(week); this._weekNumberCells[weekNumberCell.week.toString()] = weekNumberCell; return weekNumberCell; }; /** * @param {!DayCell} dayCell */ CalendarTableView.prototype.throwAwayDayCell = function(dayCell) { delete this._dayCells[dayCell.day.toString()]; dayCell.throwAway(); }; /** * @param {!WeekNumberCell} weekNumberCell */ CalendarTableView.prototype.throwAwayWeekNumberCell = function(weekNumberCell) { delete this._weekNumberCells[weekNumberCell.week.toString()]; weekNumberCell.throwAway(); }; /** * @constructor * @extends View * @param {!Object} config */ function CalendarPicker(type, config) { View.call(this, createElement("div", CalendarPicker.ClassNameCalendarPicker)); this.element.classList.add(CalendarPicker.ClassNamePreparing); /** * @type {!string} * @const */ this.type = type; if (this.type === "week") this._dateTypeConstructor = Week; else if (this.type === "month") this._dateTypeConstructor = Month; else this._dateTypeConstructor = Day; /** * @type {!Object} * @const */ this.config = {}; this._setConfig(config); /** * @type {!Month} * @const */ this.minimumMonth = Month.createFromDay(this.config.minimum.firstDay()); /** * @type {!Month} * @const */ this.maximumMonth = Month.createFromDay(this.config.maximum.lastDay()); if (global.params.isLocaleRTL) this.element.classList.add("rtl"); /** * @type {!CalendarTableView} * @const */ this.calendarTableView = new CalendarTableView(this); this.calendarTableView.hasNumberColumn = this.type === "week"; /** * @type {!CalendarHeaderView} * @const */ this.calendarHeaderView = new CalendarHeaderView(this); this.calendarHeaderView.monthPopupButton.on(MonthPopupButton.EventTypeButtonClick, this.onMonthPopupButtonClick); /** * @type {!MonthPopupView} * @const */ this.monthPopupView = new MonthPopupView(this.minimumMonth, this.maximumMonth); this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidSelectMonth, this.onYearListViewDidSelectMonth); this.monthPopupView.yearListView.on(YearListView.EventTypeYearListViewDidHide, this.onYearListViewDidHide); this.calendarHeaderView.attachTo(this); this.calendarTableView.attachTo(this); /** * @type {!Month} * @protected */ this._currentMonth = new Month(NaN, NaN); /** * @type {?DateType} * @protected */ this._selection = null; /** * @type {?DateType} * @protected */ this._highlight = null; this.calendarTableView.element.addEventListener("keydown", this.onCalendarTableKeyDown, false); document.body.addEventListener("keydown", this.onBodyKeyDown, false); window.addEventListener("resize", this.onWindowResize, false); /** * @type {!number} * @protected */ this._height = -1; var initialSelection = parseDateString(config.currentValue); if (initialSelection) { this.setCurrentMonth(Month.createFromDay(initialSelection.middleDay()), CalendarPicker.NavigationBehavior.None); this.setSelection(initialSelection); } else this.setCurrentMonth(Month.createFromToday(), CalendarPicker.NavigationBehavior.None); } CalendarPicker.prototype = Object.create(View.prototype); CalendarPicker.Padding = 10; CalendarPicker.BorderWidth = 1; CalendarPicker.ClassNameCalendarPicker = "calendar-picker"; CalendarPicker.ClassNamePreparing = "preparing"; CalendarPicker.EventTypeCurrentMonthChanged = "currentMonthChanged"; CalendarPicker.commitDelayMs = 100; /** * @param {!Event} event */ CalendarPicker.prototype.onWindowResize = function(event) { this.element.classList.remove(CalendarPicker.ClassNamePreparing); window.removeEventListener("resize", this.onWindowResize, false); }; /** * @param {!YearListView} sender */ CalendarPicker.prototype.onYearListViewDidHide = function(sender) { this.monthPopupView.hide(); this.calendarHeaderView.setDisabled(false); this.adjustHeight(); }; /** * @param {!YearListView} sender * @param {!Month} month */ CalendarPicker.prototype.onYearListViewDidSelectMonth = function(sender, month) { this.setCurrentMonth(month, CalendarPicker.NavigationBehavior.None); }; /** * @param {!View|Node} parent * @param {?View|Node=} before * @override */ CalendarPicker.prototype.attachTo = function(parent, before) { View.prototype.attachTo.call(this, parent, before); this.calendarTableView.element.focus(); }; CalendarPicker.prototype.cleanup = function() { window.removeEventListener("resize", this.onWindowResize, false); this.calendarTableView.element.removeEventListener("keydown", this.onBodyKeyDown, false); // Month popup view might be attached to document.body. this.monthPopupView.hide(); }; /** * @param {?MonthPopupButton} sender */ CalendarPicker.prototype.onMonthPopupButtonClick = function(sender) { var clientRect = this.calendarTableView.element.getBoundingClientRect(); var calendarTableRect = new Rectangle(clientRect.left + document.body.scrollLeft, clientRect.top + document.body.scrollTop, clientRect.width, clientRect.height); this.monthPopupView.show(this.currentMonth(), calendarTableRect); this.calendarHeaderView.setDisabled(true); this.adjustHeight(); }; CalendarPicker.prototype._setConfig = function(config) { this.config.minimum = (typeof config.min !== "undefined" && config.min) ? parseDateString(config.min) : this._dateTypeConstructor.Minimum; this.config.maximum = (typeof config.max !== "undefined" && config.max) ? parseDateString(config.max) : this._dateTypeConstructor.Maximum; this.config.minimumValue = this.config.minimum.valueOf(); this.config.maximumValue = this.config.maximum.valueOf(); this.config.step = (typeof config.step !== undefined) ? Number(config.step) : this._dateTypeConstructor.DefaultStep; this.config.stepBase = (typeof config.stepBase !== "undefined") ? Number(config.stepBase) : this._dateTypeConstructor.DefaultStepBase; }; /** * @return {!Month} */ CalendarPicker.prototype.currentMonth = function() { return this._currentMonth; }; /** * @enum {number} */ CalendarPicker.NavigationBehavior = { None: 0, WithAnimation: 1 }; /** * @param {!Month} month * @param {!CalendarPicker.NavigationBehavior} animate */ CalendarPicker.prototype.setCurrentMonth = function(month, behavior) { if (month > this.maximumMonth) month = this.maximumMonth; else if (month < this.minimumMonth) month = this.minimumMonth; if (this._currentMonth.equals(month)) return; this._currentMonth = month; this.calendarTableView.scrollToMonth(this._currentMonth, behavior === CalendarPicker.NavigationBehavior.WithAnimation); this.adjustHeight(); this.calendarTableView.setNeedsUpdateCells(true); this.dispatchEvent(CalendarPicker.EventTypeCurrentMonthChanged, { target: this }); }; CalendarPicker.prototype.adjustHeight = function() { var rowForFirstDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.firstDay()).row; var rowForLastDayInMonth = this.calendarTableView.columnAndRowForDay(this._currentMonth.lastDay()).row; var numberOfRows = rowForLastDayInMonth - rowForFirstDayInMonth + 1; var calendarTableViewHeight = CalendarTableHeaderView.Height + numberOfRows * DayCell.Height + CalendarTableView.BorderWidth * 2; var height = (this.monthPopupView.isVisible ? YearListView.Height : calendarTableViewHeight) + CalendarHeaderView.Height + CalendarHeaderView.BottomMargin + CalendarPicker.Padding * 2 + CalendarPicker.BorderWidth * 2; this.setHeight(height); }; CalendarPicker.prototype.selection = function() { return this._selection; }; CalendarPicker.prototype.highlight = function() { return this._highlight; }; /** * @return {!Day} */ CalendarPicker.prototype.firstVisibleDay = function() { var firstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row; var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow); if (!firstVisibleDay) firstVisibleDay = Day.Minimum; return firstVisibleDay; }; /** * @return {!Day} */ CalendarPicker.prototype.lastVisibleDay = function() { var lastVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().lastDay()).row; var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow); if (!lastVisibleDay) lastVisibleDay = Day.Maximum; return lastVisibleDay; }; /** * @param {?Day} day */ CalendarPicker.prototype.selectRangeContainingDay = function(day) { var selection = day ? this._dateTypeConstructor.createFromDay(day) : null; this.setSelectionAndCommit(selection); }; /** * @param {?Day} day */ CalendarPicker.prototype.highlightRangeContainingDay = function(day) { var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null; this._setHighlight(highlight); }; /** * Select the specified date. * @param {?DateType} dayOrWeekOrMonth */ CalendarPicker.prototype.setSelection = function(dayOrWeekOrMonth) { if (!this._selection && !dayOrWeekOrMonth) return; if (this._selection && this._selection.equals(dayOrWeekOrMonth)) return; var firstDayInSelection = dayOrWeekOrMonth.firstDay(); var lastDayInSelection = dayOrWeekOrMonth.lastDay(); var candidateCurrentMonth = Month.createFromDay(firstDayInSelection); if (this.firstVisibleDay() > lastDayInSelection || this.lastVisibleDay() < firstDayInSelection) { // Change current month if the selection is not visible at all. this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation); } else if (this.firstVisibleDay() < firstDayInSelection || this.lastVisibleDay() > lastDayInSelection) { // If the selection is partly visible, only change the current month if // doing so will make the whole selection visible. var firstVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.firstDay()).row; var firstVisibleDay = this.calendarTableView.dayAtColumnAndRow(0, firstVisibleRow); var lastVisibleRow = this.calendarTableView.columnAndRowForDay(candidateCurrentMonth.lastDay()).row; var lastVisibleDay = this.calendarTableView.dayAtColumnAndRow(DaysPerWeek - 1, lastVisibleRow); if (firstDayInSelection >= firstVisibleDay && lastDayInSelection <= lastVisibleDay) this.setCurrentMonth(candidateCurrentMonth, CalendarPicker.NavigationBehavior.WithAnimation); } this._setHighlight(dayOrWeekOrMonth); if (!this.isValid(dayOrWeekOrMonth)) return; this._selection = dayOrWeekOrMonth; this.calendarTableView.setNeedsUpdateCells(true); }; /** * Select the specified date, commit it, and close the popup. * @param {?DateType} dayOrWeekOrMonth */ CalendarPicker.prototype.setSelectionAndCommit = function(dayOrWeekOrMonth) { this.setSelection(dayOrWeekOrMonth); // Redraw the widget immidiately, and wait for some time to give feedback to // a user. this.element.offsetLeft; var value = this._selection.toString(); if (CalendarPicker.commitDelayMs == 0) { // For testing. window.pagePopupController.setValueAndClosePopup(0, value); } else if (CalendarPicker.commitDelayMs < 0) { // For testing. window.pagePopupController.setValue(value); } else { setTimeout(function() { window.pagePopupController.setValueAndClosePopup(0, value); }, CalendarPicker.commitDelayMs); } }; /** * @param {?DateType} dayOrWeekOrMonth */ CalendarPicker.prototype._setHighlight = function(dayOrWeekOrMonth) { if (!this._highlight && !dayOrWeekOrMonth) return; if (!dayOrWeekOrMonth && !this._highlight) return; if (this._highlight && this._highlight.equals(dayOrWeekOrMonth)) return; this._highlight = dayOrWeekOrMonth; this.calendarTableView.setNeedsUpdateCells(true); }; /** * @param {!number} value * @return {!boolean} */ CalendarPicker.prototype._stepMismatch = function(value) { var nextAllowedValue = Math.ceil((value - this.config.stepBase) / this.config.step) * this.config.step + this.config.stepBase; return nextAllowedValue >= value + this._dateTypeConstructor.DefaultStep; }; /** * @param {!number} value * @return {!boolean} */ CalendarPicker.prototype._outOfRange = function(value) { return value < this.config.minimumValue || value > this.config.maximumValue; }; /** * @param {!DateType} dayOrWeekOrMonth * @return {!boolean} */ CalendarPicker.prototype.isValid = function(dayOrWeekOrMonth) { var value = dayOrWeekOrMonth.valueOf(); return dayOrWeekOrMonth instanceof this._dateTypeConstructor && !this._outOfRange(value) && !this._stepMismatch(value); }; /** * @param {!Day} day * @return {!boolean} */ CalendarPicker.prototype.isValidDay = function(day) { return this.isValid(this._dateTypeConstructor.createFromDay(day)); }; /** * @param {!DateType} dateRange * @return {!boolean} Returns true if the highlight was changed. */ CalendarPicker.prototype._moveHighlight = function(dateRange) { if (!dateRange) return false; if (this._outOfRange(dateRange.valueOf())) return false; if (this.firstVisibleDay() > dateRange.middleDay() || this.lastVisibleDay() < dateRange.middleDay()) this.setCurrentMonth(Month.createFromDay(dateRange.middleDay()), CalendarPicker.NavigationBehavior.WithAnimation); this._setHighlight(dateRange); return true; }; /** * @param {?Event} event */ CalendarPicker.prototype.onCalendarTableKeyDown = function(event) { var key = event.keyIdentifier; var eventHandled = false; if (key == "U+0054") { // 't' key. this.selectRangeContainingDay(Day.createFromToday()); eventHandled = true; } else if (key == "PageUp") { var previousMonth = this.currentMonth().previous(); if (previousMonth && previousMonth >= this.config.minimumValue) { this.setCurrentMonth(previousMonth, CalendarPicker.NavigationBehavior.WithAnimation); eventHandled = true; } } else if (key == "PageDown") { var nextMonth = this.currentMonth().next(); if (nextMonth && nextMonth >= this.config.minimumValue) { this.setCurrentMonth(nextMonth, CalendarPicker.NavigationBehavior.WithAnimation); eventHandled = true; } } else if (this._highlight) { if (global.params.isLocaleRTL ? key == "Right" : key == "Left") { eventHandled = this._moveHighlight(this._highlight.previous()); } else if (key == "Up") { eventHandled = this._moveHighlight(this._highlight.previous(this.type === "date" ? DaysPerWeek : 1)); } else if (global.params.isLocaleRTL ? key == "Left" : key == "Right") { eventHandled = this._moveHighlight(this._highlight.next()); } else if (key == "Down") { eventHandled = this._moveHighlight(this._highlight.next(this.type === "date" ? DaysPerWeek : 1)); } else if (key == "Enter") { this.setSelectionAndCommit(this._highlight); } } else if (key == "Left" || key == "Up" || key == "Right" || key == "Down") { // Highlight range near the middle. this.highlightRangeContainingDay(this.currentMonth().middleDay()); eventHandled = true; } if (eventHandled) { event.stopPropagation(); event.preventDefault(); } }; /** * @return {!number} Width in pixels. */ CalendarPicker.prototype.width = function() { return this.calendarTableView.width() + (CalendarTableView.BorderWidth + CalendarPicker.BorderWidth + CalendarPicker.Padding) * 2; }; /** * @return {!number} Height in pixels. */ CalendarPicker.prototype.height = function() { return this._height; }; /** * @param {!number} height Height in pixels. */ CalendarPicker.prototype.setHeight = function(height) { if (this._height === height) return; this._height = height; resizeWindow(this.width(), this._height); this.calendarTableView.setHeight(this._height - CalendarHeaderView.Height - CalendarHeaderView.BottomMargin - CalendarPicker.Padding * 2 - CalendarTableView.BorderWidth * 2); }; /** * @param {?Event} event */ CalendarPicker.prototype.onBodyKeyDown = function(event) { var key = event.keyIdentifier; var eventHandled = false; var offset = 0; switch (key) { case "U+001B": // Esc key. window.pagePopupController.closePopup(); eventHandled = true; break; case "U+004D": // 'm' key. offset = offset || 1; // Fall-through. case "U+0059": // 'y' key. offset = offset || MonthsPerYear; // Fall-through. case "U+0044": // 'd' key. offset = offset || MonthsPerYear * 10; var oldFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row; this.setCurrentMonth(event.shiftKey ? this.currentMonth().previous(offset) : this.currentMonth().next(offset), CalendarPicker.NavigationBehavior.WithAnimation); var newFirstVisibleRow = this.calendarTableView.columnAndRowForDay(this.currentMonth().firstDay()).row; if (this._highlight) { var highlightMiddleDay = this._highlight.middleDay(); this.highlightRangeContainingDay(highlightMiddleDay.next((newFirstVisibleRow - oldFirstVisibleRow) * DaysPerWeek)); } eventHandled =true; break; } if (eventHandled) { event.stopPropagation(); event.preventDefault(); } } if (window.dialogArguments) { initialize(dialogArguments); } else { window.addEventListener("message", handleMessage, false); }