| "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; |
| 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 = "<svg width='7' height='5'><polygon points='0,1 7,1 3.5,5' style='fill:#000000;' /></svg>"; |
| 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 = "<svg width='4' height='7'><polygon points='0,7 0,0, 4,3.5' style='fill:#6e6e6e;' /></svg>"; |
| CalendarHeaderView._BackwardTriangle = "<svg width='4' height='7'><polygon points='0,3.5 4,7 4,0' style='fill:#6e6e6e;' /></svg>"; |
| 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"; |
| |
| /** |
| * @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.setSelection(selection); |
| }; |
| |
| /** |
| * @param {?Day} day |
| */ |
| CalendarPicker.prototype.highlightRangeContainingDay = function(day) { |
| var highlight = day ? this._dateTypeConstructor.createFromDay(day) : null; |
| this._setHighlight(highlight); |
| }; |
| |
| /** |
| * @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); |
| window.pagePopupController.setValue(this._selection.toString()); |
| }; |
| |
| /** |
| * @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.setSelection(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); |
| } |