Mercurial > kallithea
diff pylons_app/public/js/yui/calendar/calendar-debug.js @ 0:564e40829f80
initial commit.
author | Marcin Kuzminski |
---|---|
date | Thu, 18 Feb 2010 13:01:57 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pylons_app/public/js/yui/calendar/calendar-debug.js Thu Feb 18 13:01:57 2010 +0100 @@ -0,0 +1,7324 @@ +/* +Copyright (c) 2009, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.net/yui/license.txt +version: 2.8.0r4 +*/ +(function () { + + /** + * Config is a utility used within an Object to allow the implementer to + * maintain a list of local configuration properties and listen for changes + * to those properties dynamically using CustomEvent. The initial values are + * also maintained so that the configuration can be reset at any given point + * to its initial state. + * @namespace YAHOO.util + * @class Config + * @constructor + * @param {Object} owner The owner Object to which this Config Object belongs + */ + YAHOO.util.Config = function (owner) { + + if (owner) { + this.init(owner); + } + + if (!owner) { YAHOO.log("No owner specified for Config object", "error", "Config"); } + + }; + + + var Lang = YAHOO.lang, + CustomEvent = YAHOO.util.CustomEvent, + Config = YAHOO.util.Config; + + + /** + * Constant representing the CustomEvent type for the config changed event. + * @property YAHOO.util.Config.CONFIG_CHANGED_EVENT + * @private + * @static + * @final + */ + Config.CONFIG_CHANGED_EVENT = "configChanged"; + + /** + * Constant representing the boolean type string + * @property YAHOO.util.Config.BOOLEAN_TYPE + * @private + * @static + * @final + */ + Config.BOOLEAN_TYPE = "boolean"; + + Config.prototype = { + + /** + * Object reference to the owner of this Config Object + * @property owner + * @type Object + */ + owner: null, + + /** + * Boolean flag that specifies whether a queue is currently + * being executed + * @property queueInProgress + * @type Boolean + */ + queueInProgress: false, + + /** + * Maintains the local collection of configuration property objects and + * their specified values + * @property config + * @private + * @type Object + */ + config: null, + + /** + * Maintains the local collection of configuration property objects as + * they were initially applied. + * This object is used when resetting a property. + * @property initialConfig + * @private + * @type Object + */ + initialConfig: null, + + /** + * Maintains the local, normalized CustomEvent queue + * @property eventQueue + * @private + * @type Object + */ + eventQueue: null, + + /** + * Custom Event, notifying subscribers when Config properties are set + * (setProperty is called without the silent flag + * @event configChangedEvent + */ + configChangedEvent: null, + + /** + * Initializes the configuration Object and all of its local members. + * @method init + * @param {Object} owner The owner Object to which this Config + * Object belongs + */ + init: function (owner) { + + this.owner = owner; + + this.configChangedEvent = + this.createEvent(Config.CONFIG_CHANGED_EVENT); + + this.configChangedEvent.signature = CustomEvent.LIST; + this.queueInProgress = false; + this.config = {}; + this.initialConfig = {}; + this.eventQueue = []; + + }, + + /** + * Validates that the value passed in is a Boolean. + * @method checkBoolean + * @param {Object} val The value to validate + * @return {Boolean} true, if the value is valid + */ + checkBoolean: function (val) { + return (typeof val == Config.BOOLEAN_TYPE); + }, + + /** + * Validates that the value passed in is a number. + * @method checkNumber + * @param {Object} val The value to validate + * @return {Boolean} true, if the value is valid + */ + checkNumber: function (val) { + return (!isNaN(val)); + }, + + /** + * Fires a configuration property event using the specified value. + * @method fireEvent + * @private + * @param {String} key The configuration property's name + * @param {value} Object The value of the correct type for the property + */ + fireEvent: function ( key, value ) { + YAHOO.log("Firing Config event: " + key + "=" + value, "info", "Config"); + var property = this.config[key]; + + if (property && property.event) { + property.event.fire(value); + } + }, + + /** + * Adds a property to the Config Object's private config hash. + * @method addProperty + * @param {String} key The configuration property's name + * @param {Object} propertyObject The Object containing all of this + * property's arguments + */ + addProperty: function ( key, propertyObject ) { + key = key.toLowerCase(); + YAHOO.log("Added property: " + key, "info", "Config"); + + this.config[key] = propertyObject; + + propertyObject.event = this.createEvent(key, { scope: this.owner }); + propertyObject.event.signature = CustomEvent.LIST; + + + propertyObject.key = key; + + if (propertyObject.handler) { + propertyObject.event.subscribe(propertyObject.handler, + this.owner); + } + + this.setProperty(key, propertyObject.value, true); + + if (! propertyObject.suppressEvent) { + this.queueProperty(key, propertyObject.value); + } + + }, + + /** + * Returns a key-value configuration map of the values currently set in + * the Config Object. + * @method getConfig + * @return {Object} The current config, represented in a key-value map + */ + getConfig: function () { + + var cfg = {}, + currCfg = this.config, + prop, + property; + + for (prop in currCfg) { + if (Lang.hasOwnProperty(currCfg, prop)) { + property = currCfg[prop]; + if (property && property.event) { + cfg[prop] = property.value; + } + } + } + + return cfg; + }, + + /** + * Returns the value of specified property. + * @method getProperty + * @param {String} key The name of the property + * @return {Object} The value of the specified property + */ + getProperty: function (key) { + var property = this.config[key.toLowerCase()]; + if (property && property.event) { + return property.value; + } else { + return undefined; + } + }, + + /** + * Resets the specified property's value to its initial value. + * @method resetProperty + * @param {String} key The name of the property + * @return {Boolean} True is the property was reset, false if not + */ + resetProperty: function (key) { + + key = key.toLowerCase(); + + var property = this.config[key]; + + if (property && property.event) { + + if (this.initialConfig[key] && + !Lang.isUndefined(this.initialConfig[key])) { + + this.setProperty(key, this.initialConfig[key]); + + return true; + + } + + } else { + + return false; + } + + }, + + /** + * Sets the value of a property. If the silent property is passed as + * true, the property's event will not be fired. + * @method setProperty + * @param {String} key The name of the property + * @param {String} value The value to set the property to + * @param {Boolean} silent Whether the value should be set silently, + * without firing the property event. + * @return {Boolean} True, if the set was successful, false if it failed. + */ + setProperty: function (key, value, silent) { + + var property; + + key = key.toLowerCase(); + YAHOO.log("setProperty: " + key + "=" + value, "info", "Config"); + + if (this.queueInProgress && ! silent) { + // Currently running through a queue... + this.queueProperty(key,value); + return true; + + } else { + property = this.config[key]; + if (property && property.event) { + if (property.validator && !property.validator(value)) { + return false; + } else { + property.value = value; + if (! silent) { + this.fireEvent(key, value); + this.configChangedEvent.fire([key, value]); + } + return true; + } + } else { + return false; + } + } + }, + + /** + * Sets the value of a property and queues its event to execute. If the + * event is already scheduled to execute, it is + * moved from its current position to the end of the queue. + * @method queueProperty + * @param {String} key The name of the property + * @param {String} value The value to set the property to + * @return {Boolean} true, if the set was successful, false if + * it failed. + */ + queueProperty: function (key, value) { + + key = key.toLowerCase(); + YAHOO.log("queueProperty: " + key + "=" + value, "info", "Config"); + + var property = this.config[key], + foundDuplicate = false, + iLen, + queueItem, + queueItemKey, + queueItemValue, + sLen, + supercedesCheck, + qLen, + queueItemCheck, + queueItemCheckKey, + queueItemCheckValue, + i, + s, + q; + + if (property && property.event) { + + if (!Lang.isUndefined(value) && property.validator && + !property.validator(value)) { // validator + return false; + } else { + + if (!Lang.isUndefined(value)) { + property.value = value; + } else { + value = property.value; + } + + foundDuplicate = false; + iLen = this.eventQueue.length; + + for (i = 0; i < iLen; i++) { + queueItem = this.eventQueue[i]; + + if (queueItem) { + queueItemKey = queueItem[0]; + queueItemValue = queueItem[1]; + + if (queueItemKey == key) { + + /* + found a dupe... push to end of queue, null + current item, and break + */ + + this.eventQueue[i] = null; + + this.eventQueue.push( + [key, (!Lang.isUndefined(value) ? + value : queueItemValue)]); + + foundDuplicate = true; + break; + } + } + } + + // this is a refire, or a new property in the queue + + if (! foundDuplicate && !Lang.isUndefined(value)) { + this.eventQueue.push([key, value]); + } + } + + if (property.supercedes) { + + sLen = property.supercedes.length; + + for (s = 0; s < sLen; s++) { + + supercedesCheck = property.supercedes[s]; + qLen = this.eventQueue.length; + + for (q = 0; q < qLen; q++) { + queueItemCheck = this.eventQueue[q]; + + if (queueItemCheck) { + queueItemCheckKey = queueItemCheck[0]; + queueItemCheckValue = queueItemCheck[1]; + + if (queueItemCheckKey == + supercedesCheck.toLowerCase() ) { + + this.eventQueue.push([queueItemCheckKey, + queueItemCheckValue]); + + this.eventQueue[q] = null; + break; + + } + } + } + } + } + + YAHOO.log("Config event queue: " + this.outputEventQueue(), "info", "Config"); + + return true; + } else { + return false; + } + }, + + /** + * Fires the event for a property using the property's current value. + * @method refireEvent + * @param {String} key The name of the property + */ + refireEvent: function (key) { + + key = key.toLowerCase(); + + var property = this.config[key]; + + if (property && property.event && + + !Lang.isUndefined(property.value)) { + + if (this.queueInProgress) { + + this.queueProperty(key); + + } else { + + this.fireEvent(key, property.value); + + } + + } + }, + + /** + * Applies a key-value Object literal to the configuration, replacing + * any existing values, and queueing the property events. + * Although the values will be set, fireQueue() must be called for their + * associated events to execute. + * @method applyConfig + * @param {Object} userConfig The configuration Object literal + * @param {Boolean} init When set to true, the initialConfig will + * be set to the userConfig passed in, so that calling a reset will + * reset the properties to the passed values. + */ + applyConfig: function (userConfig, init) { + + var sKey, + oConfig; + + if (init) { + oConfig = {}; + for (sKey in userConfig) { + if (Lang.hasOwnProperty(userConfig, sKey)) { + oConfig[sKey.toLowerCase()] = userConfig[sKey]; + } + } + this.initialConfig = oConfig; + } + + for (sKey in userConfig) { + if (Lang.hasOwnProperty(userConfig, sKey)) { + this.queueProperty(sKey, userConfig[sKey]); + } + } + }, + + /** + * Refires the events for all configuration properties using their + * current values. + * @method refresh + */ + refresh: function () { + + var prop; + + for (prop in this.config) { + if (Lang.hasOwnProperty(this.config, prop)) { + this.refireEvent(prop); + } + } + }, + + /** + * Fires the normalized list of queued property change events + * @method fireQueue + */ + fireQueue: function () { + + var i, + queueItem, + key, + value, + property; + + this.queueInProgress = true; + for (i = 0;i < this.eventQueue.length; i++) { + queueItem = this.eventQueue[i]; + if (queueItem) { + + key = queueItem[0]; + value = queueItem[1]; + property = this.config[key]; + + property.value = value; + + // Clear out queue entry, to avoid it being + // re-added to the queue by any queueProperty/supercedes + // calls which are invoked during fireEvent + this.eventQueue[i] = null; + + this.fireEvent(key,value); + } + } + + this.queueInProgress = false; + this.eventQueue = []; + }, + + /** + * Subscribes an external handler to the change event for any + * given property. + * @method subscribeToConfigEvent + * @param {String} key The property name + * @param {Function} handler The handler function to use subscribe to + * the property's event + * @param {Object} obj The Object to use for scoping the event handler + * (see CustomEvent documentation) + * @param {Boolean} overrideContext Optional. If true, will override + * "this" within the handler to map to the scope Object passed into the + * method. + * @return {Boolean} True, if the subscription was successful, + * otherwise false. + */ + subscribeToConfigEvent: function (key, handler, obj, overrideContext) { + + var property = this.config[key.toLowerCase()]; + + if (property && property.event) { + if (!Config.alreadySubscribed(property.event, handler, obj)) { + property.event.subscribe(handler, obj, overrideContext); + } + return true; + } else { + return false; + } + + }, + + /** + * Unsubscribes an external handler from the change event for any + * given property. + * @method unsubscribeFromConfigEvent + * @param {String} key The property name + * @param {Function} handler The handler function to use subscribe to + * the property's event + * @param {Object} obj The Object to use for scoping the event + * handler (see CustomEvent documentation) + * @return {Boolean} True, if the unsubscription was successful, + * otherwise false. + */ + unsubscribeFromConfigEvent: function (key, handler, obj) { + var property = this.config[key.toLowerCase()]; + if (property && property.event) { + return property.event.unsubscribe(handler, obj); + } else { + return false; + } + }, + + /** + * Returns a string representation of the Config object + * @method toString + * @return {String} The Config object in string format. + */ + toString: function () { + var output = "Config"; + if (this.owner) { + output += " [" + this.owner.toString() + "]"; + } + return output; + }, + + /** + * Returns a string representation of the Config object's current + * CustomEvent queue + * @method outputEventQueue + * @return {String} The string list of CustomEvents currently queued + * for execution + */ + outputEventQueue: function () { + + var output = "", + queueItem, + q, + nQueue = this.eventQueue.length; + + for (q = 0; q < nQueue; q++) { + queueItem = this.eventQueue[q]; + if (queueItem) { + output += queueItem[0] + "=" + queueItem[1] + ", "; + } + } + return output; + }, + + /** + * Sets all properties to null, unsubscribes all listeners from each + * property's change event and all listeners from the configChangedEvent. + * @method destroy + */ + destroy: function () { + + var oConfig = this.config, + sProperty, + oProperty; + + + for (sProperty in oConfig) { + + if (Lang.hasOwnProperty(oConfig, sProperty)) { + + oProperty = oConfig[sProperty]; + + oProperty.event.unsubscribeAll(); + oProperty.event = null; + + } + + } + + this.configChangedEvent.unsubscribeAll(); + + this.configChangedEvent = null; + this.owner = null; + this.config = null; + this.initialConfig = null; + this.eventQueue = null; + + } + + }; + + + + /** + * Checks to determine if a particular function/Object pair are already + * subscribed to the specified CustomEvent + * @method YAHOO.util.Config.alreadySubscribed + * @static + * @param {YAHOO.util.CustomEvent} evt The CustomEvent for which to check + * the subscriptions + * @param {Function} fn The function to look for in the subscribers list + * @param {Object} obj The execution scope Object for the subscription + * @return {Boolean} true, if the function/Object pair is already subscribed + * to the CustomEvent passed in + */ + Config.alreadySubscribed = function (evt, fn, obj) { + + var nSubscribers = evt.subscribers.length, + subsc, + i; + + if (nSubscribers > 0) { + i = nSubscribers - 1; + do { + subsc = evt.subscribers[i]; + if (subsc && subsc.obj == obj && subsc.fn == fn) { + return true; + } + } + while (i--); + } + + return false; + + }; + + YAHOO.lang.augmentProto(Config, YAHOO.util.EventProvider); + +}()); +/** +* The datemath module provides utility methods for basic JavaScript Date object manipulation and +* comparison. +* +* @module datemath +*/ + +/** +* YAHOO.widget.DateMath is used for simple date manipulation. The class is a static utility +* used for adding, subtracting, and comparing dates. +* @namespace YAHOO.widget +* @class DateMath +*/ +YAHOO.widget.DateMath = { + /** + * Constant field representing Day + * @property DAY + * @static + * @final + * @type String + */ + DAY : "D", + + /** + * Constant field representing Week + * @property WEEK + * @static + * @final + * @type String + */ + WEEK : "W", + + /** + * Constant field representing Year + * @property YEAR + * @static + * @final + * @type String + */ + YEAR : "Y", + + /** + * Constant field representing Month + * @property MONTH + * @static + * @final + * @type String + */ + MONTH : "M", + + /** + * Constant field representing one day, in milliseconds + * @property ONE_DAY_MS + * @static + * @final + * @type Number + */ + ONE_DAY_MS : 1000*60*60*24, + + /** + * Constant field representing the date in first week of January + * which identifies the first week of the year. + * <p> + * In the U.S, Jan 1st is normally used based on a Sunday start of week. + * ISO 8601, used widely throughout Europe, uses Jan 4th, based on a Monday start of week. + * </p> + * @property WEEK_ONE_JAN_DATE + * @static + * @type Number + */ + WEEK_ONE_JAN_DATE : 1, + + /** + * Adds the specified amount of time to the this instance. + * @method add + * @param {Date} date The JavaScript Date object to perform addition on + * @param {String} field The field constant to be used for performing addition. + * @param {Number} amount The number of units (measured in the field constant) to add to the date. + * @return {Date} The resulting Date object + */ + add : function(date, field, amount) { + var d = new Date(date.getTime()); + switch (field) { + case this.MONTH: + var newMonth = date.getMonth() + amount; + var years = 0; + + if (newMonth < 0) { + while (newMonth < 0) { + newMonth += 12; + years -= 1; + } + } else if (newMonth > 11) { + while (newMonth > 11) { + newMonth -= 12; + years += 1; + } + } + + d.setMonth(newMonth); + d.setFullYear(date.getFullYear() + years); + break; + case this.DAY: + this._addDays(d, amount); + // d.setDate(date.getDate() + amount); + break; + case this.YEAR: + d.setFullYear(date.getFullYear() + amount); + break; + case this.WEEK: + this._addDays(d, (amount * 7)); + // d.setDate(date.getDate() + (amount * 7)); + break; + } + return d; + }, + + /** + * Private helper method to account for bug in Safari 2 (webkit < 420) + * when Date.setDate(n) is called with n less than -128 or greater than 127. + * <p> + * Fix approach and original findings are available here: + * http://brianary.blogspot.com/2006/03/safari-date-bug.html + * </p> + * @method _addDays + * @param {Date} d JavaScript date object + * @param {Number} nDays The number of days to add to the date object (can be negative) + * @private + */ + _addDays : function(d, nDays) { + if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420) { + if (nDays < 0) { + // Ensure we don't go below -128 (getDate() is always 1 to 31, so we won't go above 127) + for(var min = -128; nDays < min; nDays -= min) { + d.setDate(d.getDate() + min); + } + } else { + // Ensure we don't go above 96 + 31 = 127 + for(var max = 96; nDays > max; nDays -= max) { + d.setDate(d.getDate() + max); + } + } + // nDays should be remainder between -128 and 96 + } + d.setDate(d.getDate() + nDays); + }, + + /** + * Subtracts the specified amount of time from the this instance. + * @method subtract + * @param {Date} date The JavaScript Date object to perform subtraction on + * @param {Number} field The this field constant to be used for performing subtraction. + * @param {Number} amount The number of units (measured in the field constant) to subtract from the date. + * @return {Date} The resulting Date object + */ + subtract : function(date, field, amount) { + return this.add(date, field, (amount*-1)); + }, + + /** + * Determines whether a given date is before another date on the calendar. + * @method before + * @param {Date} date The Date object to compare with the compare argument + * @param {Date} compareTo The Date object to use for the comparison + * @return {Boolean} true if the date occurs before the compared date; false if not. + */ + before : function(date, compareTo) { + var ms = compareTo.getTime(); + if (date.getTime() < ms) { + return true; + } else { + return false; + } + }, + + /** + * Determines whether a given date is after another date on the calendar. + * @method after + * @param {Date} date The Date object to compare with the compare argument + * @param {Date} compareTo The Date object to use for the comparison + * @return {Boolean} true if the date occurs after the compared date; false if not. + */ + after : function(date, compareTo) { + var ms = compareTo.getTime(); + if (date.getTime() > ms) { + return true; + } else { + return false; + } + }, + + /** + * Determines whether a given date is between two other dates on the calendar. + * @method between + * @param {Date} date The date to check for + * @param {Date} dateBegin The start of the range + * @param {Date} dateEnd The end of the range + * @return {Boolean} true if the date occurs between the compared dates; false if not. + */ + between : function(date, dateBegin, dateEnd) { + if (this.after(date, dateBegin) && this.before(date, dateEnd)) { + return true; + } else { + return false; + } + }, + + /** + * Retrieves a JavaScript Date object representing January 1 of any given year. + * @method getJan1 + * @param {Number} calendarYear The calendar year for which to retrieve January 1 + * @return {Date} January 1 of the calendar year specified. + */ + getJan1 : function(calendarYear) { + return this.getDate(calendarYear,0,1); + }, + + /** + * Calculates the number of days the specified date is from January 1 of the specified calendar year. + * Passing January 1 to this function would return an offset value of zero. + * @method getDayOffset + * @param {Date} date The JavaScript date for which to find the offset + * @param {Number} calendarYear The calendar year to use for determining the offset + * @return {Number} The number of days since January 1 of the given year + */ + getDayOffset : function(date, calendarYear) { + var beginYear = this.getJan1(calendarYear); // Find the start of the year. This will be in week 1. + + // Find the number of days the passed in date is away from the calendar year start + var dayOffset = Math.ceil((date.getTime()-beginYear.getTime()) / this.ONE_DAY_MS); + return dayOffset; + }, + + /** + * Calculates the week number for the given date. Can currently support standard + * U.S. week numbers, based on Jan 1st defining the 1st week of the year, and + * ISO8601 week numbers, based on Jan 4th defining the 1st week of the year. + * + * @method getWeekNumber + * @param {Date} date The JavaScript date for which to find the week number + * @param {Number} firstDayOfWeek The index of the first day of the week (0 = Sun, 1 = Mon ... 6 = Sat). + * Defaults to 0 + * @param {Number} janDate The date in the first week of January which defines week one for the year + * Defaults to the value of YAHOO.widget.DateMath.WEEK_ONE_JAN_DATE, which is 1 (Jan 1st). + * For the U.S, this is normally Jan 1st. ISO8601 uses Jan 4th to define the first week of the year. + * + * @return {Number} The number of the week containing the given date. + */ + getWeekNumber : function(date, firstDayOfWeek, janDate) { + + // Setup Defaults + firstDayOfWeek = firstDayOfWeek || 0; + janDate = janDate || this.WEEK_ONE_JAN_DATE; + + var targetDate = this.clearTime(date), + startOfWeek, + endOfWeek; + + if (targetDate.getDay() === firstDayOfWeek) { + startOfWeek = targetDate; + } else { + startOfWeek = this.getFirstDayOfWeek(targetDate, firstDayOfWeek); + } + + var startYear = startOfWeek.getFullYear(); + + // DST shouldn't be a problem here, math is quicker than setDate(); + endOfWeek = new Date(startOfWeek.getTime() + 6*this.ONE_DAY_MS); + + var weekNum; + if (startYear !== endOfWeek.getFullYear() && endOfWeek.getDate() >= janDate) { + // If years don't match, endOfWeek is in Jan. and if the + // week has WEEK_ONE_JAN_DATE in it, it's week one by definition. + weekNum = 1; + } else { + // Get the 1st day of the 1st week, and + // find how many days away we are from it. + var weekOne = this.clearTime(this.getDate(startYear, 0, janDate)), + weekOneDayOne = this.getFirstDayOfWeek(weekOne, firstDayOfWeek); + + // Round days to smoothen out 1 hr DST diff + var daysDiff = Math.round((targetDate.getTime() - weekOneDayOne.getTime())/this.ONE_DAY_MS); + + // Calc. Full Weeks + var rem = daysDiff % 7; + var weeksDiff = (daysDiff - rem)/7; + weekNum = weeksDiff + 1; + } + return weekNum; + }, + + /** + * Get the first day of the week, for the give date. + * @param {Date} dt The date in the week for which the first day is required. + * @param {Number} startOfWeek The index for the first day of the week, 0 = Sun, 1 = Mon ... 6 = Sat (defaults to 0) + * @return {Date} The first day of the week + */ + getFirstDayOfWeek : function (dt, startOfWeek) { + startOfWeek = startOfWeek || 0; + var dayOfWeekIndex = dt.getDay(), + dayOfWeek = (dayOfWeekIndex - startOfWeek + 7) % 7; + + return this.subtract(dt, this.DAY, dayOfWeek); + }, + + /** + * Determines if a given week overlaps two different years. + * @method isYearOverlapWeek + * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week. + * @return {Boolean} true if the date overlaps two different years. + */ + isYearOverlapWeek : function(weekBeginDate) { + var overlaps = false; + var nextWeek = this.add(weekBeginDate, this.DAY, 6); + if (nextWeek.getFullYear() != weekBeginDate.getFullYear()) { + overlaps = true; + } + return overlaps; + }, + + /** + * Determines if a given week overlaps two different months. + * @method isMonthOverlapWeek + * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week. + * @return {Boolean} true if the date overlaps two different months. + */ + isMonthOverlapWeek : function(weekBeginDate) { + var overlaps = false; + var nextWeek = this.add(weekBeginDate, this.DAY, 6); + if (nextWeek.getMonth() != weekBeginDate.getMonth()) { + overlaps = true; + } + return overlaps; + }, + + /** + * Gets the first day of a month containing a given date. + * @method findMonthStart + * @param {Date} date The JavaScript Date used to calculate the month start + * @return {Date} The JavaScript Date representing the first day of the month + */ + findMonthStart : function(date) { + var start = this.getDate(date.getFullYear(), date.getMonth(), 1); + return start; + }, + + /** + * Gets the last day of a month containing a given date. + * @method findMonthEnd + * @param {Date} date The JavaScript Date used to calculate the month end + * @return {Date} The JavaScript Date representing the last day of the month + */ + findMonthEnd : function(date) { + var start = this.findMonthStart(date); + var nextMonth = this.add(start, this.MONTH, 1); + var end = this.subtract(nextMonth, this.DAY, 1); + return end; + }, + + /** + * Clears the time fields from a given date, effectively setting the time to 12 noon. + * @method clearTime + * @param {Date} date The JavaScript Date for which the time fields will be cleared + * @return {Date} The JavaScript Date cleared of all time fields + */ + clearTime : function(date) { + date.setHours(12,0,0,0); + return date; + }, + + /** + * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object + * are set to 0. The method allows Date instances to be created with the a year less than 100. "new Date(year, month, date)" implementations + * set the year to 19xx if a year (xx) which is less than 100 is provided. + * <p> + * <em>NOTE:</em>Validation on argument values is not performed. It is the caller's responsibility to ensure + * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor. + * </p> + * @method getDate + * @param {Number} y Year. + * @param {Number} m Month index from 0 (Jan) to 11 (Dec). + * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1. + * @return {Date} The JavaScript date object with year, month, date set as provided. + */ + getDate : function(y, m, d) { + var dt = null; + if (YAHOO.lang.isUndefined(d)) { + d = 1; + } + if (y >= 100) { + dt = new Date(y, m, d); + } else { + dt = new Date(); + dt.setFullYear(y); + dt.setMonth(m); + dt.setDate(d); + dt.setHours(0,0,0,0); + } + return dt; + } +}; +/** +* The Calendar component is a UI control that enables users to choose one or more dates from a graphical calendar presented in a one-month or +* multi-month interface. Calendars are generated entirely via script and can be navigated without any page refreshes. +* @module calendar +* @title Calendar +* @namespace YAHOO.widget +* @requires yahoo,dom,event +*/ +(function(){ + + var Dom = YAHOO.util.Dom, + Event = YAHOO.util.Event, + Lang = YAHOO.lang, + DateMath = YAHOO.widget.DateMath; + +/** +* Calendar is the base class for the Calendar widget. In its most basic +* implementation, it has the ability to render a calendar widget on the page +* that can be manipulated to select a single date, move back and forth between +* months and years. +* <p>To construct the placeholder for the calendar widget, the code is as +* follows: +* <xmp> +* <div id="calContainer"></div> +* </xmp> +* </p> +* <p> +* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong> +* The Calendar can be constructed by simply providing a container ID string, +* or a reference to a container DIV HTMLElement (the element needs to exist +* in the document). +* +* E.g.: +* <xmp> +* var c = new YAHOO.widget.Calendar("calContainer", configOptions); +* </xmp> +* or: +* <xmp> +* var containerDiv = YAHOO.util.Dom.get("calContainer"); +* var c = new YAHOO.widget.Calendar(containerDiv, configOptions); +* </xmp> +* </p> +* <p> +* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix. +* For example if an ID is not provided, and the container's ID is "calContainer", the Calendar's ID will be set to "calContainer_t". +* </p> +* +* @namespace YAHOO.widget +* @class Calendar +* @constructor +* @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional. +* @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document. +* @param {Object} config optional The configuration object containing the initial configuration values for the Calendar. +*/ +function Calendar(id, containerId, config) { + this.init.apply(this, arguments); +} + +/** +* The path to be used for images loaded for the Calendar +* @property YAHOO.widget.Calendar.IMG_ROOT +* @static +* @deprecated You can now customize images by overriding the calclose, calnavleft and calnavright default CSS classes for the close icon, left arrow and right arrow respectively +* @type String +*/ +Calendar.IMG_ROOT = null; + +/** +* Type constant used for renderers to represent an individual date (M/D/Y) +* @property YAHOO.widget.Calendar.DATE +* @static +* @final +* @type String +*/ +Calendar.DATE = "D"; + +/** +* Type constant used for renderers to represent an individual date across any year (M/D) +* @property YAHOO.widget.Calendar.MONTH_DAY +* @static +* @final +* @type String +*/ +Calendar.MONTH_DAY = "MD"; + +/** +* Type constant used for renderers to represent a weekday +* @property YAHOO.widget.Calendar.WEEKDAY +* @static +* @final +* @type String +*/ +Calendar.WEEKDAY = "WD"; + +/** +* Type constant used for renderers to represent a range of individual dates (M/D/Y-M/D/Y) +* @property YAHOO.widget.Calendar.RANGE +* @static +* @final +* @type String +*/ +Calendar.RANGE = "R"; + +/** +* Type constant used for renderers to represent a month across any year +* @property YAHOO.widget.Calendar.MONTH +* @static +* @final +* @type String +*/ +Calendar.MONTH = "M"; + +/** +* Constant that represents the total number of date cells that are displayed in a given month +* @property YAHOO.widget.Calendar.DISPLAY_DAYS +* @static +* @final +* @type Number +*/ +Calendar.DISPLAY_DAYS = 42; + +/** +* Constant used for halting the execution of the remainder of the render stack +* @property YAHOO.widget.Calendar.STOP_RENDER +* @static +* @final +* @type String +*/ +Calendar.STOP_RENDER = "S"; + +/** +* Constant used to represent short date field string formats (e.g. Tu or Feb) +* @property YAHOO.widget.Calendar.SHORT +* @static +* @final +* @type String +*/ +Calendar.SHORT = "short"; + +/** +* Constant used to represent long date field string formats (e.g. Monday or February) +* @property YAHOO.widget.Calendar.LONG +* @static +* @final +* @type String +*/ +Calendar.LONG = "long"; + +/** +* Constant used to represent medium date field string formats (e.g. Mon) +* @property YAHOO.widget.Calendar.MEDIUM +* @static +* @final +* @type String +*/ +Calendar.MEDIUM = "medium"; + +/** +* Constant used to represent single character date field string formats (e.g. M, T, W) +* @property YAHOO.widget.Calendar.ONE_CHAR +* @static +* @final +* @type String +*/ +Calendar.ONE_CHAR = "1char"; + +/** +* The set of default Config property keys and values for the Calendar. +* +* <p> +* NOTE: This property is made public in order to allow users to change +* the default values of configuration properties. Users should not +* modify the key string, unless they are overriding the Calendar implementation +* </p> +* +* <p> +* The property is an object with key/value pairs, the key being the +* uppercase configuration property name and the value being an object +* literal with a key string property, and a value property, specifying the +* default value of the property. To override a default value, you can set +* the value property, for example, <code>YAHOO.widget.Calendar.DEFAULT_CONFIG.MULTI_SELECT.value = true;</code> +* +* @property YAHOO.widget.Calendar.DEFAULT_CONFIG +* @static +* @type Object +*/ + +Calendar.DEFAULT_CONFIG = { + YEAR_OFFSET : {key:"year_offset", value:0, supercedes:["pagedate", "selected", "mindate","maxdate"]}, + TODAY : {key:"today", value:new Date(), supercedes:["pagedate"]}, + PAGEDATE : {key:"pagedate", value:null}, + SELECTED : {key:"selected", value:[]}, + TITLE : {key:"title", value:""}, + CLOSE : {key:"close", value:false}, + IFRAME : {key:"iframe", value:(YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) ? true : false}, + MINDATE : {key:"mindate", value:null}, + MAXDATE : {key:"maxdate", value:null}, + MULTI_SELECT : {key:"multi_select", value:false}, + START_WEEKDAY : {key:"start_weekday", value:0}, + SHOW_WEEKDAYS : {key:"show_weekdays", value:true}, + SHOW_WEEK_HEADER : {key:"show_week_header", value:false}, + SHOW_WEEK_FOOTER : {key:"show_week_footer", value:false}, + HIDE_BLANK_WEEKS : {key:"hide_blank_weeks", value:false}, + NAV_ARROW_LEFT: {key:"nav_arrow_left", value:null} , + NAV_ARROW_RIGHT : {key:"nav_arrow_right", value:null} , + MONTHS_SHORT : {key:"months_short", value:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]}, + MONTHS_LONG: {key:"months_long", value:["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]}, + WEEKDAYS_1CHAR: {key:"weekdays_1char", value:["S", "M", "T", "W", "T", "F", "S"]}, + WEEKDAYS_SHORT: {key:"weekdays_short", value:["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]}, + WEEKDAYS_MEDIUM: {key:"weekdays_medium", value:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]}, + WEEKDAYS_LONG: {key:"weekdays_long", value:["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]}, + LOCALE_MONTHS:{key:"locale_months", value:"long"}, + LOCALE_WEEKDAYS:{key:"locale_weekdays", value:"short"}, + DATE_DELIMITER:{key:"date_delimiter", value:","}, + DATE_FIELD_DELIMITER:{key:"date_field_delimiter", value:"/"}, + DATE_RANGE_DELIMITER:{key:"date_range_delimiter", value:"-"}, + MY_MONTH_POSITION:{key:"my_month_position", value:1}, + MY_YEAR_POSITION:{key:"my_year_position", value:2}, + MD_MONTH_POSITION:{key:"md_month_position", value:1}, + MD_DAY_POSITION:{key:"md_day_position", value:2}, + MDY_MONTH_POSITION:{key:"mdy_month_position", value:1}, + MDY_DAY_POSITION:{key:"mdy_day_position", value:2}, + MDY_YEAR_POSITION:{key:"mdy_year_position", value:3}, + MY_LABEL_MONTH_POSITION:{key:"my_label_month_position", value:1}, + MY_LABEL_YEAR_POSITION:{key:"my_label_year_position", value:2}, + MY_LABEL_MONTH_SUFFIX:{key:"my_label_month_suffix", value:" "}, + MY_LABEL_YEAR_SUFFIX:{key:"my_label_year_suffix", value:""}, + NAV: {key:"navigator", value: null}, + STRINGS : { + key:"strings", + value: { + previousMonth : "Previous Month", + nextMonth : "Next Month", + close: "Close" + }, + supercedes : ["close", "title"] + } +}; + +/** +* The set of default Config property keys and values for the Calendar +* @property YAHOO.widget.Calendar._DEFAULT_CONFIG +* @deprecated Made public. See the public DEFAULT_CONFIG property for details +* @final +* @static +* @private +* @type Object +*/ +Calendar._DEFAULT_CONFIG = Calendar.DEFAULT_CONFIG; + +var DEF_CFG = Calendar.DEFAULT_CONFIG; + +/** +* The set of Custom Event types supported by the Calendar +* @property YAHOO.widget.Calendar._EVENT_TYPES +* @final +* @static +* @private +* @type Object +*/ +Calendar._EVENT_TYPES = { + BEFORE_SELECT : "beforeSelect", + SELECT : "select", + BEFORE_DESELECT : "beforeDeselect", + DESELECT : "deselect", + CHANGE_PAGE : "changePage", + BEFORE_RENDER : "beforeRender", + RENDER : "render", + BEFORE_DESTROY : "beforeDestroy", + DESTROY : "destroy", + RESET : "reset", + CLEAR : "clear", + BEFORE_HIDE : "beforeHide", + HIDE : "hide", + BEFORE_SHOW : "beforeShow", + SHOW : "show", + BEFORE_HIDE_NAV : "beforeHideNav", + HIDE_NAV : "hideNav", + BEFORE_SHOW_NAV : "beforeShowNav", + SHOW_NAV : "showNav", + BEFORE_RENDER_NAV : "beforeRenderNav", + RENDER_NAV : "renderNav" +}; + +/** +* The set of default style constants for the Calendar +* @property YAHOO.widget.Calendar.STYLES +* @static +* @type Object An object with name/value pairs for the class name identifier/value. +*/ +Calendar.STYLES = { + CSS_ROW_HEADER: "calrowhead", + CSS_ROW_FOOTER: "calrowfoot", + CSS_CELL : "calcell", + CSS_CELL_SELECTOR : "selector", + CSS_CELL_SELECTED : "selected", + CSS_CELL_SELECTABLE : "selectable", + CSS_CELL_RESTRICTED : "restricted", + CSS_CELL_TODAY : "today", + CSS_CELL_OOM : "oom", + CSS_CELL_OOB : "previous", + CSS_HEADER : "calheader", + CSS_HEADER_TEXT : "calhead", + CSS_BODY : "calbody", + CSS_WEEKDAY_CELL : "calweekdaycell", + CSS_WEEKDAY_ROW : "calweekdayrow", + CSS_FOOTER : "calfoot", + CSS_CALENDAR : "yui-calendar", + CSS_SINGLE : "single", + CSS_CONTAINER : "yui-calcontainer", + CSS_NAV_LEFT : "calnavleft", + CSS_NAV_RIGHT : "calnavright", + CSS_NAV : "calnav", + CSS_CLOSE : "calclose", + CSS_CELL_TOP : "calcelltop", + CSS_CELL_LEFT : "calcellleft", + CSS_CELL_RIGHT : "calcellright", + CSS_CELL_BOTTOM : "calcellbottom", + CSS_CELL_HOVER : "calcellhover", + CSS_CELL_HIGHLIGHT1 : "highlight1", + CSS_CELL_HIGHLIGHT2 : "highlight2", + CSS_CELL_HIGHLIGHT3 : "highlight3", + CSS_CELL_HIGHLIGHT4 : "highlight4", + CSS_WITH_TITLE: "withtitle", + CSS_FIXED_SIZE: "fixedsize", + CSS_LINK_CLOSE: "link-close" +}; + +/** +* The set of default style constants for the Calendar +* @property YAHOO.widget.Calendar._STYLES +* @deprecated Made public. See the public STYLES property for details +* @final +* @static +* @private +* @type Object +*/ +Calendar._STYLES = Calendar.STYLES; + +Calendar.prototype = { + + /** + * The configuration object used to set up the calendars various locale and style options. + * @property Config + * @private + * @deprecated Configuration properties should be set by calling Calendar.cfg.setProperty. + * @type Object + */ + Config : null, + + /** + * The parent CalendarGroup, only to be set explicitly by the parent group + * @property parent + * @type CalendarGroup + */ + parent : null, + + /** + * The index of this item in the parent group + * @property index + * @type Number + */ + index : -1, + + /** + * The collection of calendar table cells + * @property cells + * @type HTMLTableCellElement[] + */ + cells : null, + + /** + * The collection of calendar cell dates that is parallel to the cells collection. The array contains dates field arrays in the format of [YYYY, M, D]. + * @property cellDates + * @type Array[](Number[]) + */ + cellDates : null, + + /** + * The id that uniquely identifies this Calendar. + * @property id + * @type String + */ + id : null, + + /** + * The unique id associated with the Calendar's container + * @property containerId + * @type String + */ + containerId: null, + + /** + * The DOM element reference that points to this calendar's container element. The calendar will be inserted into this element when the shell is rendered. + * @property oDomContainer + * @type HTMLElement + */ + oDomContainer : null, + + /** + * A Date object representing today's date. + * @deprecated Use the "today" configuration property + * @property today + * @type Date + */ + today : null, + + /** + * The list of render functions, along with required parameters, used to render cells. + * @property renderStack + * @type Array[] + */ + renderStack : null, + + /** + * A copy of the initial render functions created before rendering. + * @property _renderStack + * @private + * @type Array + */ + _renderStack : null, + + /** + * A reference to the CalendarNavigator instance created for this Calendar. + * Will be null if the "navigator" configuration property has not been set + * @property oNavigator + * @type CalendarNavigator + */ + oNavigator : null, + + /** + * The private list of initially selected dates. + * @property _selectedDates + * @private + * @type Array + */ + _selectedDates : null, + + /** + * A map of DOM event handlers to attach to cells associated with specific CSS class names + * @property domEventMap + * @type Object + */ + domEventMap : null, + + /** + * Protected helper used to parse Calendar constructor/init arguments. + * + * As of 2.4.0, Calendar supports a simpler constructor + * signature. This method reconciles arguments + * received in the pre 2.4.0 and 2.4.0 formats. + * + * @protected + * @method _parseArgs + * @param {Array} Function "arguments" array + * @return {Object} Object with id, container, config properties containing + * the reconciled argument values. + **/ + _parseArgs : function(args) { + /* + 2.4.0 Constructors signatures + + new Calendar(String) + new Calendar(HTMLElement) + new Calendar(String, ConfigObject) + new Calendar(HTMLElement, ConfigObject) + + Pre 2.4.0 Constructor signatures + + new Calendar(String, String) + new Calendar(String, HTMLElement) + new Calendar(String, String, ConfigObject) + new Calendar(String, HTMLElement, ConfigObject) + */ + var nArgs = {id:null, container:null, config:null}; + + if (args && args.length && args.length > 0) { + switch (args.length) { + case 1: + nArgs.id = null; + nArgs.container = args[0]; + nArgs.config = null; + break; + case 2: + if (Lang.isObject(args[1]) && !args[1].tagName && !(args[1] instanceof String)) { + nArgs.id = null; + nArgs.container = args[0]; + nArgs.config = args[1]; + } else { + nArgs.id = args[0]; + nArgs.container = args[1]; + nArgs.config = null; + } + break; + default: // 3+ + nArgs.id = args[0]; + nArgs.container = args[1]; + nArgs.config = args[2]; + break; + } + } else { + this.logger.log("Invalid constructor/init arguments", "error"); + } + return nArgs; + }, + + /** + * Initializes the Calendar widget. + * @method init + * + * @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional. + * @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document. + * @param {Object} config optional The configuration object containing the initial configuration values for the Calendar. + */ + init : function(id, container, config) { + // Normalize 2.4.0, pre 2.4.0 args + var nArgs = this._parseArgs(arguments); + + id = nArgs.id; + container = nArgs.container; + config = nArgs.config; + + this.oDomContainer = Dom.get(container); + if (!this.oDomContainer) { this.logger.log("Container not found in document.", "error"); } + + if (!this.oDomContainer.id) { + this.oDomContainer.id = Dom.generateId(); + } + if (!id) { + id = this.oDomContainer.id + "_t"; + } + + this.id = id; + this.containerId = this.oDomContainer.id; + + this.logger = new YAHOO.widget.LogWriter("Calendar " + this.id); + this.initEvents(); + + /** + * The Config object used to hold the configuration variables for the Calendar + * @property cfg + * @type YAHOO.util.Config + */ + this.cfg = new YAHOO.util.Config(this); + + /** + * The local object which contains the Calendar's options + * @property Options + * @type Object + */ + this.Options = {}; + + /** + * The local object which contains the Calendar's locale settings + * @property Locale + * @type Object + */ + this.Locale = {}; + + this.initStyles(); + + Dom.addClass(this.oDomContainer, this.Style.CSS_CONTAINER); + Dom.addClass(this.oDomContainer, this.Style.CSS_SINGLE); + + this.cellDates = []; + this.cells = []; + this.renderStack = []; + this._renderStack = []; + + this.setupConfig(); + + if (config) { + this.cfg.applyConfig(config, true); + } + + this.cfg.fireQueue(); + + this.today = this.cfg.getProperty("today"); + }, + + /** + * Default Config listener for the iframe property. If the iframe config property is set to true, + * renders the built-in IFRAME shim if the container is relatively or absolutely positioned. + * + * @method configIframe + */ + configIframe : function(type, args, obj) { + var useIframe = args[0]; + + if (!this.parent) { + if (Dom.inDocument(this.oDomContainer)) { + if (useIframe) { + var pos = Dom.getStyle(this.oDomContainer, "position"); + + if (pos == "absolute" || pos == "relative") { + + if (!Dom.inDocument(this.iframe)) { + this.iframe = document.createElement("iframe"); + this.iframe.src = "javascript:false;"; + + Dom.setStyle(this.iframe, "opacity", "0"); + + if (YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) { + Dom.addClass(this.iframe, this.Style.CSS_FIXED_SIZE); + } + + this.oDomContainer.insertBefore(this.iframe, this.oDomContainer.firstChild); + } + } + } else { + if (this.iframe) { + if (this.iframe.parentNode) { + this.iframe.parentNode.removeChild(this.iframe); + } + this.iframe = null; + } + } + } + } + }, + + /** + * Default handler for the "title" property + * @method configTitle + */ + configTitle : function(type, args, obj) { + var title = args[0]; + + // "" disables title bar + if (title) { + this.createTitleBar(title); + } else { + var close = this.cfg.getProperty(DEF_CFG.CLOSE.key); + if (!close) { + this.removeTitleBar(); + } else { + this.createTitleBar(" "); + } + } + }, + + /** + * Default handler for the "close" property + * @method configClose + */ + configClose : function(type, args, obj) { + var close = args[0], + title = this.cfg.getProperty(DEF_CFG.TITLE.key); + + if (close) { + if (!title) { + this.createTitleBar(" "); + } + this.createCloseButton(); + } else { + this.removeCloseButton(); + if (!title) { + this.removeTitleBar(); + } + } + }, + + /** + * Initializes Calendar's built-in CustomEvents + * @method initEvents + */ + initEvents : function() { + + var defEvents = Calendar._EVENT_TYPES, + CE = YAHOO.util.CustomEvent, + cal = this; // To help with minification + + /** + * Fired before a date selection is made + * @event beforeSelectEvent + */ + cal.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT); + + /** + * Fired when a date selection is made + * @event selectEvent + * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. + */ + cal.selectEvent = new CE(defEvents.SELECT); + + /** + * Fired before a date or set of dates is deselected + * @event beforeDeselectEvent + */ + cal.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT); + + /** + * Fired when a date or set of dates is deselected + * @event deselectEvent + * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. + */ + cal.deselectEvent = new CE(defEvents.DESELECT); + + /** + * Fired when the Calendar page is changed + * @event changePageEvent + * @param {Date} prevDate The date before the page was changed + * @param {Date} newDate The date after the page was changed + */ + cal.changePageEvent = new CE(defEvents.CHANGE_PAGE); + + /** + * Fired before the Calendar is rendered + * @event beforeRenderEvent + */ + cal.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER); + + /** + * Fired when the Calendar is rendered + * @event renderEvent + */ + cal.renderEvent = new CE(defEvents.RENDER); + + /** + * Fired just before the Calendar is to be destroyed + * @event beforeDestroyEvent + */ + cal.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY); + + /** + * Fired after the Calendar is destroyed. This event should be used + * for notification only. When this event is fired, important Calendar instance + * properties, dom references and event listeners have already been + * removed/dereferenced, and hence the Calendar instance is not in a usable + * state. + * + * @event destroyEvent + */ + cal.destroyEvent = new CE(defEvents.DESTROY); + + /** + * Fired when the Calendar is reset + * @event resetEvent + */ + cal.resetEvent = new CE(defEvents.RESET); + + /** + * Fired when the Calendar is cleared + * @event clearEvent + */ + cal.clearEvent = new CE(defEvents.CLEAR); + + /** + * Fired just before the Calendar is to be shown + * @event beforeShowEvent + */ + cal.beforeShowEvent = new CE(defEvents.BEFORE_SHOW); + + /** + * Fired after the Calendar is shown + * @event showEvent + */ + cal.showEvent = new CE(defEvents.SHOW); + + /** + * Fired just before the Calendar is to be hidden + * @event beforeHideEvent + */ + cal.beforeHideEvent = new CE(defEvents.BEFORE_HIDE); + + /** + * Fired after the Calendar is hidden + * @event hideEvent + */ + cal.hideEvent = new CE(defEvents.HIDE); + + /** + * Fired just before the CalendarNavigator is to be shown + * @event beforeShowNavEvent + */ + cal.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV); + + /** + * Fired after the CalendarNavigator is shown + * @event showNavEvent + */ + cal.showNavEvent = new CE(defEvents.SHOW_NAV); + + /** + * Fired just before the CalendarNavigator is to be hidden + * @event beforeHideNavEvent + */ + cal.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV); + + /** + * Fired after the CalendarNavigator is hidden + * @event hideNavEvent + */ + cal.hideNavEvent = new CE(defEvents.HIDE_NAV); + + /** + * Fired just before the CalendarNavigator is to be rendered + * @event beforeRenderNavEvent + */ + cal.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV); + + /** + * Fired after the CalendarNavigator is rendered + * @event renderNavEvent + */ + cal.renderNavEvent = new CE(defEvents.RENDER_NAV); + + cal.beforeSelectEvent.subscribe(cal.onBeforeSelect, this, true); + cal.selectEvent.subscribe(cal.onSelect, this, true); + cal.beforeDeselectEvent.subscribe(cal.onBeforeDeselect, this, true); + cal.deselectEvent.subscribe(cal.onDeselect, this, true); + cal.changePageEvent.subscribe(cal.onChangePage, this, true); + cal.renderEvent.subscribe(cal.onRender, this, true); + cal.resetEvent.subscribe(cal.onReset, this, true); + cal.clearEvent.subscribe(cal.onClear, this, true); + }, + + /** + * The default event handler for clicks on the "Previous Month" navigation UI + * + * @method doPreviousMonthNav + * @param {DOMEvent} e The DOM event + * @param {Calendar} cal A reference to the calendar + */ + doPreviousMonthNav : function(e, cal) { + Event.preventDefault(e); + // previousMonth invoked in a timeout, to allow + // event to bubble up, with correct target. Calling + // previousMonth, will call render which will remove + // HTML which generated the event, resulting in an + // invalid event target in certain browsers. + setTimeout(function() { + cal.previousMonth(); + var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_LEFT, "a", cal.oDomContainer); + if (navs && navs[0]) { + try { + navs[0].focus(); + } catch (ex) { + // ignore + } + } + }, 0); + }, + + /** + * The default event handler for clicks on the "Next Month" navigation UI + * + * @method doNextMonthNav + * @param {DOMEvent} e The DOM event + * @param {Calendar} cal A reference to the calendar + */ + doNextMonthNav : function(e, cal) { + Event.preventDefault(e); + setTimeout(function() { + cal.nextMonth(); + var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_RIGHT, "a", cal.oDomContainer); + if (navs && navs[0]) { + try { + navs[0].focus(); + } catch (ex) { + // ignore + } + } + }, 0); + }, + + /** + * The default event handler for date cell selection. Currently attached to + * the Calendar's bounding box, referenced by it's <a href="#property_oDomContainer">oDomContainer</a> property. + * + * @method doSelectCell + * @param {DOMEvent} e The DOM event + * @param {Calendar} cal A reference to the calendar + */ + doSelectCell : function(e, cal) { + var cell, d, date, index; + + var target = Event.getTarget(e), + tagName = target.tagName.toLowerCase(), + defSelector = false; + + while (tagName != "td" && !Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { + + if (!defSelector && tagName == "a" && Dom.hasClass(target, cal.Style.CSS_CELL_SELECTOR)) { + defSelector = true; + } + + target = target.parentNode; + tagName = target.tagName.toLowerCase(); + + if (target == this.oDomContainer || tagName == "html") { + return; + } + } + + if (defSelector) { + // Stop link href navigation for default renderer + Event.preventDefault(e); + } + + cell = target; + + if (Dom.hasClass(cell, cal.Style.CSS_CELL_SELECTABLE)) { + index = cal.getIndexFromId(cell.id); + if (index > -1) { + d = cal.cellDates[index]; + if (d) { + date = DateMath.getDate(d[0],d[1]-1,d[2]); + + var link; + + cal.logger.log("Selecting cell " + index + " via click", "info"); + if (cal.Options.MULTI_SELECT) { + link = cell.getElementsByTagName("a")[0]; + if (link) { + link.blur(); + } + + var cellDate = cal.cellDates[index]; + var cellDateIndex = cal._indexOfSelectedFieldArray(cellDate); + + if (cellDateIndex > -1) { + cal.deselectCell(index); + } else { + cal.selectCell(index); + } + + } else { + link = cell.getElementsByTagName("a")[0]; + if (link) { + link.blur(); + } + cal.selectCell(index); + } + } + } + } + }, + + /** + * The event that is executed when the user hovers over a cell + * @method doCellMouseOver + * @param {DOMEvent} e The event + * @param {Calendar} cal A reference to the calendar passed by the Event utility + */ + doCellMouseOver : function(e, cal) { + var target; + if (e) { + target = Event.getTarget(e); + } else { + target = this; + } + + while (target.tagName && target.tagName.toLowerCase() != "td") { + target = target.parentNode; + if (!target.tagName || target.tagName.toLowerCase() == "html") { + return; + } + } + + if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { + Dom.addClass(target, cal.Style.CSS_CELL_HOVER); + } + }, + + /** + * The event that is executed when the user moves the mouse out of a cell + * @method doCellMouseOut + * @param {DOMEvent} e The event + * @param {Calendar} cal A reference to the calendar passed by the Event utility + */ + doCellMouseOut : function(e, cal) { + var target; + if (e) { + target = Event.getTarget(e); + } else { + target = this; + } + + while (target.tagName && target.tagName.toLowerCase() != "td") { + target = target.parentNode; + if (!target.tagName || target.tagName.toLowerCase() == "html") { + return; + } + } + + if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { + Dom.removeClass(target, cal.Style.CSS_CELL_HOVER); + } + }, + + setupConfig : function() { + + var cfg = this.cfg; + + /** + * The date to use to represent "Today". + * + * @config today + * @type Date + * @default The client side date (new Date()) when the Calendar is instantiated. + */ + cfg.addProperty(DEF_CFG.TODAY.key, { value: new Date(DEF_CFG.TODAY.value.getTime()), supercedes:DEF_CFG.TODAY.supercedes, handler:this.configToday, suppressEvent:true } ); + + /** + * The month/year representing the current visible Calendar date (mm/yyyy) + * @config pagedate + * @type String | Date + * @default Today's date + */ + cfg.addProperty(DEF_CFG.PAGEDATE.key, { value: DEF_CFG.PAGEDATE.value || new Date(DEF_CFG.TODAY.value.getTime()), handler:this.configPageDate } ); + + /** + * The date or range of dates representing the current Calendar selection + * @config selected + * @type String + * @default [] + */ + cfg.addProperty(DEF_CFG.SELECTED.key, { value:DEF_CFG.SELECTED.value.concat(), handler:this.configSelected } ); + + /** + * The title to display above the Calendar's month header + * @config title + * @type String + * @default "" + */ + cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } ); + + /** + * Whether or not a close button should be displayed for this Calendar + * @config close + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } ); + + /** + * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below. + * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be + * enabled if required. + * + * @config iframe + * @type Boolean + * @default true for IE6 and below, false for all other browsers + */ + cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } ); + + /** + * The minimum selectable date in the current Calendar (mm/dd/yyyy) + * @config mindate + * @type String | Date + * @default null + */ + cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.configMinDate } ); + + /** + * The maximum selectable date in the current Calendar (mm/dd/yyyy) + * @config maxdate + * @type String | Date + * @default null + */ + cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.configMaxDate } ); + + // Options properties + + /** + * True if the Calendar should allow multiple selections. False by default. + * @config MULTI_SELECT + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.MULTI_SELECT.key, { value:DEF_CFG.MULTI_SELECT.value, handler:this.configOptions, validator:cfg.checkBoolean } ); + + /** + * The weekday the week begins on. Default is 0 (Sunday = 0, Monday = 1 ... Saturday = 6). + * @config START_WEEKDAY + * @type number + * @default 0 + */ + cfg.addProperty(DEF_CFG.START_WEEKDAY.key, { value:DEF_CFG.START_WEEKDAY.value, handler:this.configOptions, validator:cfg.checkNumber } ); + + /** + * True if the Calendar should show weekday labels. True by default. + * @config SHOW_WEEKDAYS + * @type Boolean + * @default true + */ + cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key, { value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.configOptions, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should show week row headers. False by default. + * @config SHOW_WEEK_HEADER + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key, { value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.configOptions, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should show week row footers. False by default. + * @config SHOW_WEEK_FOOTER + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.configOptions, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should suppress weeks that are not a part of the current month. False by default. + * @config HIDE_BLANK_WEEKS + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key, { value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.configOptions, validator:cfg.checkBoolean } ); + + /** + * The image that should be used for the left navigation arrow. + * @config NAV_ARROW_LEFT + * @type String + * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft" + * @default null + */ + cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key, { value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.configOptions } ); + + /** + * The image that should be used for the right navigation arrow. + * @config NAV_ARROW_RIGHT + * @type String + * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright" + * @default null + */ + cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key, { value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.configOptions } ); + + // Locale properties + + /** + * The short month labels for the current locale. + * @config MONTHS_SHORT + * @type String[] + * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + */ + cfg.addProperty(DEF_CFG.MONTHS_SHORT.key, { value:DEF_CFG.MONTHS_SHORT.value, handler:this.configLocale } ); + + /** + * The long month labels for the current locale. + * @config MONTHS_LONG + * @type String[] + * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + */ + cfg.addProperty(DEF_CFG.MONTHS_LONG.key, { value:DEF_CFG.MONTHS_LONG.value, handler:this.configLocale } ); + + /** + * The 1-character weekday labels for the current locale. + * @config WEEKDAYS_1CHAR + * @type String[] + * @default ["S", "M", "T", "W", "T", "F", "S"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key, { value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.configLocale } ); + + /** + * The short weekday labels for the current locale. + * @config WEEKDAYS_SHORT + * @type String[] + * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key, { value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.configLocale } ); + + /** + * The medium weekday labels for the current locale. + * @config WEEKDAYS_MEDIUM + * @type String[] + * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key, { value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.configLocale } ); + + /** + * The long weekday labels for the current locale. + * @config WEEKDAYS_LONG + * @type String[] + * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key, { value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.configLocale } ); + + /** + * Refreshes the locale values used to build the Calendar. + * @method refreshLocale + * @private + */ + var refreshLocale = function() { + cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key); + cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key); + }; + + cfg.subscribeToConfigEvent(DEF_CFG.START_WEEKDAY.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_SHORT.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_LONG.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_1CHAR.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_SHORT.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_MEDIUM.key, refreshLocale, this, true); + cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_LONG.key, refreshLocale, this, true); + + /** + * The setting that determines which length of month labels should be used. Possible values are "short" and "long". + * @config LOCALE_MONTHS + * @type String + * @default "long" + */ + cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key, { value:DEF_CFG.LOCALE_MONTHS.value, handler:this.configLocaleValues } ); + + /** + * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long". + * @config LOCALE_WEEKDAYS + * @type String + * @default "short" + */ + cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key, { value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.configLocaleValues } ); + + /** + * The positive or negative year offset from the Gregorian calendar year (assuming a January 1st rollover) to + * be used when displaying and parsing dates. NOTE: All JS Date objects returned by methods, or expected as input by + * methods will always represent the Gregorian year, in order to maintain date/month/week values. + * + * @config YEAR_OFFSET + * @type Number + * @default 0 + */ + cfg.addProperty(DEF_CFG.YEAR_OFFSET.key, { value:DEF_CFG.YEAR_OFFSET.value, supercedes:DEF_CFG.YEAR_OFFSET.supercedes, handler:this.configLocale } ); + + /** + * The value used to delimit individual dates in a date string passed to various Calendar functions. + * @config DATE_DELIMITER + * @type String + * @default "," + */ + cfg.addProperty(DEF_CFG.DATE_DELIMITER.key, { value:DEF_CFG.DATE_DELIMITER.value, handler:this.configLocale } ); + + /** + * The value used to delimit date fields in a date string passed to various Calendar functions. + * @config DATE_FIELD_DELIMITER + * @type String + * @default "/" + */ + cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key, { value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.configLocale } ); + + /** + * The value used to delimit date ranges in a date string passed to various Calendar functions. + * @config DATE_RANGE_DELIMITER + * @type String + * @default "-" + */ + cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key, { value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.configLocale } ); + + /** + * The position of the month in a month/year date string + * @config MY_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key, { value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the year in a month/year date string + * @config MY_YEAR_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key, { value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the month in a month/day date string + * @config MD_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key, { value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the day in a month/year date string + * @config MD_DAY_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key, { value:DEF_CFG.MD_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the month in a month/day/year date string + * @config MDY_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key, { value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the day in a month/day/year date string + * @config MDY_DAY_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key, { value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the year in a month/day/year date string + * @config MDY_YEAR_POSITION + * @type Number + * @default 3 + */ + cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key, { value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the month in the month year label string used as the Calendar header + * @config MY_LABEL_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key, { value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The position of the year in the month year label string used as the Calendar header + * @config MY_LABEL_YEAR_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key, { value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } ); + + /** + * The suffix used after the month when rendering the Calendar header + * @config MY_LABEL_MONTH_SUFFIX + * @type String + * @default " " + */ + cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key, { value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.configLocale } ); + + /** + * The suffix used after the year when rendering the Calendar header + * @config MY_LABEL_YEAR_SUFFIX + * @type String + * @default "" + */ + cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.configLocale } ); + + /** + * Configuration for the Month/Year CalendarNavigator UI which allows the user to jump directly to a + * specific Month/Year without having to scroll sequentially through months. + * <p> + * Setting this property to null (default value) or false, will disable the CalendarNavigator UI. + * </p> + * <p> + * Setting this property to true will enable the CalendarNavigatior UI with the default CalendarNavigator configuration values. + * </p> + * <p> + * This property can also be set to an object literal containing configuration properties for the CalendarNavigator UI. + * The configuration object expects the the following case-sensitive properties, with the "strings" property being a nested object. + * Any properties which are not provided will use the default values (defined in the CalendarNavigator class). + * </p> + * <dl> + * <dt>strings</dt> + * <dd><em>Object</em> : An object with the properties shown below, defining the string labels to use in the Navigator's UI + * <dl> + * <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd> + * <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd> + * <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd> + * <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd> + * <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd> + * </dl> + * </dd> + * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd> + * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd> + * </dl> + * <p>E.g.</p> + * <pre> + * var navConfig = { + * strings: { + * month:"Calendar Month", + * year:"Calendar Year", + * submit: "Submit", + * cancel: "Cancel", + * invalidYear: "Please enter a valid year" + * }, + * monthFormat: YAHOO.widget.Calendar.SHORT, + * initialFocus: "month" + * } + * </pre> + * @config navigator + * @type {Object|Boolean} + * @default null + */ + cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } ); + + /** + * The map of UI strings which the Calendar UI uses. + * + * @config strings + * @type {Object} + * @default An object with the properties shown below: + * <dl> + * <dt>previousMonth</dt><dd><em>String</em> : The string to use for the "Previous Month" navigation UI. Defaults to "Previous Month".</dd> + * <dt>nextMonth</dt><dd><em>String</em> : The string to use for the "Next Month" navigation UI. Defaults to "Next Month".</dd> + * <dt>close</dt><dd><em>String</em> : The string to use for the close button label. Defaults to "Close".</dd> + * </dl> + */ + cfg.addProperty(DEF_CFG.STRINGS.key, { + value:DEF_CFG.STRINGS.value, + handler:this.configStrings, + validator: function(val) { + return Lang.isObject(val); + }, + supercedes:DEF_CFG.STRINGS.supercedes + }); + }, + + /** + * The default handler for the "strings" property + * @method configStrings + */ + configStrings : function(type, args, obj) { + var val = Lang.merge(DEF_CFG.STRINGS.value, args[0]); + this.cfg.setProperty(DEF_CFG.STRINGS.key, val, true); + }, + + /** + * The default handler for the "pagedate" property + * @method configPageDate + */ + configPageDate : function(type, args, obj) { + this.cfg.setProperty(DEF_CFG.PAGEDATE.key, this._parsePageDate(args[0]), true); + }, + + /** + * The default handler for the "mindate" property + * @method configMinDate + */ + configMinDate : function(type, args, obj) { + var val = args[0]; + if (Lang.isString(val)) { + val = this._parseDate(val); + this.cfg.setProperty(DEF_CFG.MINDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2])); + } + }, + + /** + * The default handler for the "maxdate" property + * @method configMaxDate + */ + configMaxDate : function(type, args, obj) { + var val = args[0]; + if (Lang.isString(val)) { + val = this._parseDate(val); + this.cfg.setProperty(DEF_CFG.MAXDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2])); + } + }, + + /** + * The default handler for the "today" property + * @method configToday + */ + configToday : function(type, args, obj) { + // Only do this for initial set. Changing the today property after the initial + // set, doesn't affect pagedate + var val = args[0]; + if (Lang.isString(val)) { + val = this._parseDate(val); + } + var today = DateMath.clearTime(val); + if (!this.cfg.initialConfig[DEF_CFG.PAGEDATE.key]) { + this.cfg.setProperty(DEF_CFG.PAGEDATE.key, today); + } + this.today = today; + this.cfg.setProperty(DEF_CFG.TODAY.key, today, true); + }, + + /** + * The default handler for the "selected" property + * @method configSelected + */ + configSelected : function(type, args, obj) { + var selected = args[0], + cfgSelected = DEF_CFG.SELECTED.key; + + if (selected) { + if (Lang.isString(selected)) { + this.cfg.setProperty(cfgSelected, this._parseDates(selected), true); + } + } + if (! this._selectedDates) { + this._selectedDates = this.cfg.getProperty(cfgSelected); + } + }, + + /** + * The default handler for all configuration options properties + * @method configOptions + */ + configOptions : function(type, args, obj) { + this.Options[type.toUpperCase()] = args[0]; + }, + + /** + * The default handler for all configuration locale properties + * @method configLocale + */ + configLocale : function(type, args, obj) { + this.Locale[type.toUpperCase()] = args[0]; + + this.cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key); + this.cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key); + }, + + /** + * The default handler for all configuration locale field length properties + * @method configLocaleValues + */ + configLocaleValues : function(type, args, obj) { + + type = type.toLowerCase(); + + var val = args[0], + cfg = this.cfg, + Locale = this.Locale; + + switch (type) { + case DEF_CFG.LOCALE_MONTHS.key: + switch (val) { + case Calendar.SHORT: + Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_SHORT.key).concat(); + break; + case Calendar.LONG: + Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_LONG.key).concat(); + break; + } + break; + case DEF_CFG.LOCALE_WEEKDAYS.key: + switch (val) { + case Calendar.ONE_CHAR: + Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_1CHAR.key).concat(); + break; + case Calendar.SHORT: + Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_SHORT.key).concat(); + break; + case Calendar.MEDIUM: + Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_MEDIUM.key).concat(); + break; + case Calendar.LONG: + Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_LONG.key).concat(); + break; + } + + var START_WEEKDAY = cfg.getProperty(DEF_CFG.START_WEEKDAY.key); + + if (START_WEEKDAY > 0) { + for (var w=0; w < START_WEEKDAY; ++w) { + Locale.LOCALE_WEEKDAYS.push(Locale.LOCALE_WEEKDAYS.shift()); + } + } + break; + } + }, + + /** + * The default handler for the "navigator" property + * @method configNavigator + */ + configNavigator : function(type, args, obj) { + var val = args[0]; + if (YAHOO.widget.CalendarNavigator && (val === true || Lang.isObject(val))) { + if (!this.oNavigator) { + this.oNavigator = new YAHOO.widget.CalendarNavigator(this); + // Cleanup DOM Refs/Events before innerHTML is removed. + this.beforeRenderEvent.subscribe(function () { + if (!this.pages) { + this.oNavigator.erase(); + } + }, this, true); + } + } else { + if (this.oNavigator) { + this.oNavigator.destroy(); + this.oNavigator = null; + } + } + }, + + /** + * Defines the style constants for the Calendar + * @method initStyles + */ + initStyles : function() { + + var defStyle = Calendar.STYLES; + + this.Style = { + /** + * @property Style.CSS_ROW_HEADER + */ + CSS_ROW_HEADER: defStyle.CSS_ROW_HEADER, + /** + * @property Style.CSS_ROW_FOOTER + */ + CSS_ROW_FOOTER: defStyle.CSS_ROW_FOOTER, + /** + * @property Style.CSS_CELL + */ + CSS_CELL : defStyle.CSS_CELL, + /** + * @property Style.CSS_CELL_SELECTOR + */ + CSS_CELL_SELECTOR : defStyle.CSS_CELL_SELECTOR, + /** + * @property Style.CSS_CELL_SELECTED + */ + CSS_CELL_SELECTED : defStyle.CSS_CELL_SELECTED, + /** + * @property Style.CSS_CELL_SELECTABLE + */ + CSS_CELL_SELECTABLE : defStyle.CSS_CELL_SELECTABLE, + /** + * @property Style.CSS_CELL_RESTRICTED + */ + CSS_CELL_RESTRICTED : defStyle.CSS_CELL_RESTRICTED, + /** + * @property Style.CSS_CELL_TODAY + */ + CSS_CELL_TODAY : defStyle.CSS_CELL_TODAY, + /** + * @property Style.CSS_CELL_OOM + */ + CSS_CELL_OOM : defStyle.CSS_CELL_OOM, + /** + * @property Style.CSS_CELL_OOB + */ + CSS_CELL_OOB : defStyle.CSS_CELL_OOB, + /** + * @property Style.CSS_HEADER + */ + CSS_HEADER : defStyle.CSS_HEADER, + /** + * @property Style.CSS_HEADER_TEXT + */ + CSS_HEADER_TEXT : defStyle.CSS_HEADER_TEXT, + /** + * @property Style.CSS_BODY + */ + CSS_BODY : defStyle.CSS_BODY, + /** + * @property Style.CSS_WEEKDAY_CELL + */ + CSS_WEEKDAY_CELL : defStyle.CSS_WEEKDAY_CELL, + /** + * @property Style.CSS_WEEKDAY_ROW + */ + CSS_WEEKDAY_ROW : defStyle.CSS_WEEKDAY_ROW, + /** + * @property Style.CSS_FOOTER + */ + CSS_FOOTER : defStyle.CSS_FOOTER, + /** + * @property Style.CSS_CALENDAR + */ + CSS_CALENDAR : defStyle.CSS_CALENDAR, + /** + * @property Style.CSS_SINGLE + */ + CSS_SINGLE : defStyle.CSS_SINGLE, + /** + * @property Style.CSS_CONTAINER + */ + CSS_CONTAINER : defStyle.CSS_CONTAINER, + /** + * @property Style.CSS_NAV_LEFT + */ + CSS_NAV_LEFT : defStyle.CSS_NAV_LEFT, + /** + * @property Style.CSS_NAV_RIGHT + */ + CSS_NAV_RIGHT : defStyle.CSS_NAV_RIGHT, + /** + * @property Style.CSS_NAV + */ + CSS_NAV : defStyle.CSS_NAV, + /** + * @property Style.CSS_CLOSE + */ + CSS_CLOSE : defStyle.CSS_CLOSE, + /** + * @property Style.CSS_CELL_TOP + */ + CSS_CELL_TOP : defStyle.CSS_CELL_TOP, + /** + * @property Style.CSS_CELL_LEFT + */ + CSS_CELL_LEFT : defStyle.CSS_CELL_LEFT, + /** + * @property Style.CSS_CELL_RIGHT + */ + CSS_CELL_RIGHT : defStyle.CSS_CELL_RIGHT, + /** + * @property Style.CSS_CELL_BOTTOM + */ + CSS_CELL_BOTTOM : defStyle.CSS_CELL_BOTTOM, + /** + * @property Style.CSS_CELL_HOVER + */ + CSS_CELL_HOVER : defStyle.CSS_CELL_HOVER, + /** + * @property Style.CSS_CELL_HIGHLIGHT1 + */ + CSS_CELL_HIGHLIGHT1 : defStyle.CSS_CELL_HIGHLIGHT1, + /** + * @property Style.CSS_CELL_HIGHLIGHT2 + */ + CSS_CELL_HIGHLIGHT2 : defStyle.CSS_CELL_HIGHLIGHT2, + /** + * @property Style.CSS_CELL_HIGHLIGHT3 + */ + CSS_CELL_HIGHLIGHT3 : defStyle.CSS_CELL_HIGHLIGHT3, + /** + * @property Style.CSS_CELL_HIGHLIGHT4 + */ + CSS_CELL_HIGHLIGHT4 : defStyle.CSS_CELL_HIGHLIGHT4, + /** + * @property Style.CSS_WITH_TITLE + */ + CSS_WITH_TITLE : defStyle.CSS_WITH_TITLE, + /** + * @property Style.CSS_FIXED_SIZE + */ + CSS_FIXED_SIZE : defStyle.CSS_FIXED_SIZE, + /** + * @property Style.CSS_LINK_CLOSE + */ + CSS_LINK_CLOSE : defStyle.CSS_LINK_CLOSE + }; + }, + + /** + * Builds the date label that will be displayed in the calendar header or + * footer, depending on configuration. + * @method buildMonthLabel + * @return {String} The formatted calendar month label + */ + buildMonthLabel : function() { + return this._buildMonthLabel(this.cfg.getProperty(DEF_CFG.PAGEDATE.key)); + }, + + /** + * Helper method, to format a Month Year string, given a JavaScript Date, based on the + * Calendar localization settings + * + * @method _buildMonthLabel + * @private + * @param {Date} date + * @return {String} Formated month, year string + */ + _buildMonthLabel : function(date) { + var monthLabel = this.Locale.LOCALE_MONTHS[date.getMonth()] + this.Locale.MY_LABEL_MONTH_SUFFIX, + yearLabel = (date.getFullYear() + this.Locale.YEAR_OFFSET) + this.Locale.MY_LABEL_YEAR_SUFFIX; + + if (this.Locale.MY_LABEL_MONTH_POSITION == 2 || this.Locale.MY_LABEL_YEAR_POSITION == 1) { + return yearLabel + monthLabel; + } else { + return monthLabel + yearLabel; + } + }, + + /** + * Builds the date digit that will be displayed in calendar cells + * @method buildDayLabel + * @param {Date} workingDate The current working date + * @return {String} The formatted day label + */ + buildDayLabel : function(workingDate) { + return workingDate.getDate(); + }, + + /** + * Creates the title bar element and adds it to Calendar container DIV + * + * @method createTitleBar + * @param {String} strTitle The title to display in the title bar + * @return The title bar element + */ + createTitleBar : function(strTitle) { + var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || document.createElement("div"); + tDiv.className = YAHOO.widget.CalendarGroup.CSS_2UPTITLE; + tDiv.innerHTML = strTitle; + this.oDomContainer.insertBefore(tDiv, this.oDomContainer.firstChild); + + Dom.addClass(this.oDomContainer, this.Style.CSS_WITH_TITLE); + + return tDiv; + }, + + /** + * Removes the title bar element from the DOM + * + * @method removeTitleBar + */ + removeTitleBar : function() { + var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || null; + if (tDiv) { + Event.purgeElement(tDiv); + this.oDomContainer.removeChild(tDiv); + } + Dom.removeClass(this.oDomContainer, this.Style.CSS_WITH_TITLE); + }, + + /** + * Creates the close button HTML element and adds it to Calendar container DIV + * + * @method createCloseButton + * @return The close HTML element created + */ + createCloseButton : function() { + var cssClose = YAHOO.widget.CalendarGroup.CSS_2UPCLOSE, + cssLinkClose = this.Style.CSS_LINK_CLOSE, + DEPR_CLOSE_PATH = "us/my/bn/x_d.gif", + + lnk = Dom.getElementsByClassName(cssLinkClose, "a", this.oDomContainer)[0], + strings = this.cfg.getProperty(DEF_CFG.STRINGS.key), + closeStr = (strings && strings.close) ? strings.close : ""; + + if (!lnk) { + lnk = document.createElement("a"); + Event.addListener(lnk, "click", function(e, cal) { + cal.hide(); + Event.preventDefault(e); + }, this); + } + + lnk.href = "#"; + lnk.className = cssLinkClose; + + if (Calendar.IMG_ROOT !== null) { + var img = Dom.getElementsByClassName(cssClose, "img", lnk)[0] || document.createElement("img"); + img.src = Calendar.IMG_ROOT + DEPR_CLOSE_PATH; + img.className = cssClose; + lnk.appendChild(img); + } else { + lnk.innerHTML = '<span class="' + cssClose + ' ' + this.Style.CSS_CLOSE + '">' + closeStr + '</span>'; + } + this.oDomContainer.appendChild(lnk); + + return lnk; + }, + + /** + * Removes the close button HTML element from the DOM + * + * @method removeCloseButton + */ + removeCloseButton : function() { + var btn = Dom.getElementsByClassName(this.Style.CSS_LINK_CLOSE, "a", this.oDomContainer)[0] || null; + if (btn) { + Event.purgeElement(btn); + this.oDomContainer.removeChild(btn); + } + }, + + /** + * Renders the calendar header. + * @method renderHeader + * @param {Array} html The current working HTML array + * @return {Array} The current working HTML array + */ + renderHeader : function(html) { + + this.logger.log("Rendering header", "render"); + + var colSpan = 7, + DEPR_NAV_LEFT = "us/tr/callt.gif", + DEPR_NAV_RIGHT = "us/tr/calrt.gif", + cfg = this.cfg, + pageDate = cfg.getProperty(DEF_CFG.PAGEDATE.key), + strings= cfg.getProperty(DEF_CFG.STRINGS.key), + prevStr = (strings && strings.previousMonth) ? strings.previousMonth : "", + nextStr = (strings && strings.nextMonth) ? strings.nextMonth : "", + monthLabel; + + if (cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) { + colSpan += 1; + } + + if (cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) { + colSpan += 1; + } + + html[html.length] = "<thead>"; + html[html.length] = "<tr>"; + html[html.length] = '<th colspan="' + colSpan + '" class="' + this.Style.CSS_HEADER_TEXT + '">'; + html[html.length] = '<div class="' + this.Style.CSS_HEADER + '">'; + + var renderLeft, renderRight = false; + + if (this.parent) { + if (this.index === 0) { + renderLeft = true; + } + if (this.index == (this.parent.cfg.getProperty("pages") -1)) { + renderRight = true; + } + } else { + renderLeft = true; + renderRight = true; + } + + if (renderLeft) { + monthLabel = this._buildMonthLabel(DateMath.subtract(pageDate, DateMath.MONTH, 1)); + + var leftArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_LEFT.key); + // Check for deprecated customization - If someone set IMG_ROOT, but didn't set NAV_ARROW_LEFT, then set NAV_ARROW_LEFT to the old deprecated value + if (leftArrow === null && Calendar.IMG_ROOT !== null) { + leftArrow = Calendar.IMG_ROOT + DEPR_NAV_LEFT; + } + var leftStyle = (leftArrow === null) ? "" : ' style="background-image:url(' + leftArrow + ')"'; + html[html.length] = '<a class="' + this.Style.CSS_NAV_LEFT + '"' + leftStyle + ' href="#">' + prevStr + ' (' + monthLabel + ')' + '</a>'; + } + + var lbl = this.buildMonthLabel(); + var cal = this.parent || this; + if (cal.cfg.getProperty("navigator")) { + lbl = "<a class=\"" + this.Style.CSS_NAV + "\" href=\"#\">" + lbl + "</a>"; + } + html[html.length] = lbl; + + if (renderRight) { + monthLabel = this._buildMonthLabel(DateMath.add(pageDate, DateMath.MONTH, 1)); + + var rightArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_RIGHT.key); + if (rightArrow === null && Calendar.IMG_ROOT !== null) { + rightArrow = Calendar.IMG_ROOT + DEPR_NAV_RIGHT; + } + var rightStyle = (rightArrow === null) ? "" : ' style="background-image:url(' + rightArrow + ')"'; + html[html.length] = '<a class="' + this.Style.CSS_NAV_RIGHT + '"' + rightStyle + ' href="#">' + nextStr + ' (' + monthLabel + ')' + '</a>'; + } + + html[html.length] = '</div>\n</th>\n</tr>'; + + if (cfg.getProperty(DEF_CFG.SHOW_WEEKDAYS.key)) { + html = this.buildWeekdays(html); + } + + html[html.length] = '</thead>'; + + return html; + }, + + /** + * Renders the Calendar's weekday headers. + * @method buildWeekdays + * @param {Array} html The current working HTML array + * @return {Array} The current working HTML array + */ + buildWeekdays : function(html) { + + html[html.length] = '<tr class="' + this.Style.CSS_WEEKDAY_ROW + '">'; + + if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) { + html[html.length] = '<th> </th>'; + } + + for(var i=0;i < this.Locale.LOCALE_WEEKDAYS.length; ++i) { + html[html.length] = '<th class="' + this.Style.CSS_WEEKDAY_CELL + '">' + this.Locale.LOCALE_WEEKDAYS[i] + '</th>'; + } + + if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) { + html[html.length] = '<th> </th>'; + } + + html[html.length] = '</tr>'; + + return html; + }, + + /** + * Renders the calendar body. + * @method renderBody + * @param {Date} workingDate The current working Date being used for the render process + * @param {Array} html The current working HTML array + * @return {Array} The current working HTML array + */ + renderBody : function(workingDate, html) { + this.logger.log("Rendering body", "render"); + + var startDay = this.cfg.getProperty(DEF_CFG.START_WEEKDAY.key); + + this.preMonthDays = workingDate.getDay(); + if (startDay > 0) { + this.preMonthDays -= startDay; + } + if (this.preMonthDays < 0) { + this.preMonthDays += 7; + } + + this.monthDays = DateMath.findMonthEnd(workingDate).getDate(); + this.postMonthDays = Calendar.DISPLAY_DAYS-this.preMonthDays-this.monthDays; + + this.logger.log(this.preMonthDays + " preciding out-of-month days", "render"); + this.logger.log(this.monthDays + " month days", "render"); + this.logger.log(this.postMonthDays + " post-month days", "render"); + + workingDate = DateMath.subtract(workingDate, DateMath.DAY, this.preMonthDays); + this.logger.log("Calendar page starts on " + workingDate, "render"); + + var weekNum, + weekClass, + weekPrefix = "w", + cellPrefix = "_cell", + workingDayPrefix = "wd", + dayPrefix = "d", + cellRenderers, + renderer, + t = this.today, + cfg = this.cfg, + todayYear = t.getFullYear(), + todayMonth = t.getMonth(), + todayDate = t.getDate(), + useDate = cfg.getProperty(DEF_CFG.PAGEDATE.key), + hideBlankWeeks = cfg.getProperty(DEF_CFG.HIDE_BLANK_WEEKS.key), + showWeekFooter = cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key), + showWeekHeader = cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key), + mindate = cfg.getProperty(DEF_CFG.MINDATE.key), + maxdate = cfg.getProperty(DEF_CFG.MAXDATE.key), + yearOffset = this.Locale.YEAR_OFFSET; + + if (mindate) { + mindate = DateMath.clearTime(mindate); + } + if (maxdate) { + maxdate = DateMath.clearTime(maxdate); + } + + html[html.length] = '<tbody class="m' + (useDate.getMonth()+1) + ' ' + this.Style.CSS_BODY + '">'; + + var i = 0, + tempDiv = document.createElement("div"), + cell = document.createElement("td"); + + tempDiv.appendChild(cell); + + var cal = this.parent || this; + + for (var r=0;r<6;r++) { + weekNum = DateMath.getWeekNumber(workingDate, startDay); + weekClass = weekPrefix + weekNum; + + // Local OOM check for performance, since we already have pagedate + if (r !== 0 && hideBlankWeeks === true && workingDate.getMonth() != useDate.getMonth()) { + break; + } else { + html[html.length] = '<tr class="' + weekClass + '">'; + + if (showWeekHeader) { html = this.renderRowHeader(weekNum, html); } + + for (var d=0; d < 7; d++){ // Render actual days + + cellRenderers = []; + + this.clearElement(cell); + cell.className = this.Style.CSS_CELL; + cell.id = this.id + cellPrefix + i; + this.logger.log("Rendering cell " + cell.id + " (" + workingDate.getFullYear() + yearOffset + "-" + (workingDate.getMonth()+1) + "-" + workingDate.getDate() + ")", "cellrender"); + + if (workingDate.getDate() == todayDate && + workingDate.getMonth() == todayMonth && + workingDate.getFullYear() == todayYear) { + cellRenderers[cellRenderers.length]=cal.renderCellStyleToday; + } + + var workingArray = [workingDate.getFullYear(),workingDate.getMonth()+1,workingDate.getDate()]; + this.cellDates[this.cellDates.length] = workingArray; // Add this date to cellDates + + // Local OOM check for performance, since we already have pagedate + if (workingDate.getMonth() != useDate.getMonth()) { + cellRenderers[cellRenderers.length]=cal.renderCellNotThisMonth; + } else { + Dom.addClass(cell, workingDayPrefix + workingDate.getDay()); + Dom.addClass(cell, dayPrefix + workingDate.getDate()); + + for (var s=0;s<this.renderStack.length;++s) { + + renderer = null; + + var rArray = this.renderStack[s], + type = rArray[0], + month, + day, + year; + + switch (type) { + case Calendar.DATE: + month = rArray[1][1]; + day = rArray[1][2]; + year = rArray[1][0]; + + if (workingDate.getMonth()+1 == month && workingDate.getDate() == day && workingDate.getFullYear() == year) { + renderer = rArray[2]; + this.renderStack.splice(s,1); + } + break; + case Calendar.MONTH_DAY: + month = rArray[1][0]; + day = rArray[1][1]; + + if (workingDate.getMonth()+1 == month && workingDate.getDate() == day) { + renderer = rArray[2]; + this.renderStack.splice(s,1); + } + break; + case Calendar.RANGE: + var date1 = rArray[1][0], + date2 = rArray[1][1], + d1month = date1[1], + d1day = date1[2], + d1year = date1[0], + d1 = DateMath.getDate(d1year, d1month-1, d1day), + d2month = date2[1], + d2day = date2[2], + d2year = date2[0], + d2 = DateMath.getDate(d2year, d2month-1, d2day); + + if (workingDate.getTime() >= d1.getTime() && workingDate.getTime() <= d2.getTime()) { + renderer = rArray[2]; + + if (workingDate.getTime()==d2.getTime()) { + this.renderStack.splice(s,1); + } + } + break; + case Calendar.WEEKDAY: + var weekday = rArray[1][0]; + if (workingDate.getDay()+1 == weekday) { + renderer = rArray[2]; + } + break; + case Calendar.MONTH: + month = rArray[1][0]; + if (workingDate.getMonth()+1 == month) { + renderer = rArray[2]; + } + break; + } + + if (renderer) { + cellRenderers[cellRenderers.length]=renderer; + } + } + + } + + if (this._indexOfSelectedFieldArray(workingArray) > -1) { + cellRenderers[cellRenderers.length]=cal.renderCellStyleSelected; + } + + if ((mindate && (workingDate.getTime() < mindate.getTime())) || + (maxdate && (workingDate.getTime() > maxdate.getTime())) + ) { + cellRenderers[cellRenderers.length]=cal.renderOutOfBoundsDate; + } else { + cellRenderers[cellRenderers.length]=cal.styleCellDefault; + cellRenderers[cellRenderers.length]=cal.renderCellDefault; + } + + for (var x=0; x < cellRenderers.length; ++x) { + this.logger.log("renderer[" + x + "] for (" + workingDate.getFullYear() + yearOffset + "-" + (workingDate.getMonth()+1) + "-" + workingDate.getDate() + ")", "cellrender"); + if (cellRenderers[x].call(cal, workingDate, cell) == Calendar.STOP_RENDER) { + break; + } + } + + workingDate.setTime(workingDate.getTime() + DateMath.ONE_DAY_MS); + // Just in case we crossed DST/Summertime boundaries + workingDate = DateMath.clearTime(workingDate); + + if (i >= 0 && i <= 6) { + Dom.addClass(cell, this.Style.CSS_CELL_TOP); + } + if ((i % 7) === 0) { + Dom.addClass(cell, this.Style.CSS_CELL_LEFT); + } + if (((i+1) % 7) === 0) { + Dom.addClass(cell, this.Style.CSS_CELL_RIGHT); + } + + var postDays = this.postMonthDays; + if (hideBlankWeeks && postDays >= 7) { + var blankWeeks = Math.floor(postDays/7); + for (var p=0;p<blankWeeks;++p) { + postDays -= 7; + } + } + + if (i >= ((this.preMonthDays+postDays+this.monthDays)-7)) { + Dom.addClass(cell, this.Style.CSS_CELL_BOTTOM); + } + + html[html.length] = tempDiv.innerHTML; + i++; + } + + if (showWeekFooter) { html = this.renderRowFooter(weekNum, html); } + + html[html.length] = '</tr>'; + } + } + + html[html.length] = '</tbody>'; + + return html; + }, + + /** + * Renders the calendar footer. In the default implementation, there is + * no footer. + * @method renderFooter + * @param {Array} html The current working HTML array + * @return {Array} The current working HTML array + */ + renderFooter : function(html) { return html; }, + + /** + * Renders the calendar after it has been configured. The render() method has a specific call chain that will execute + * when the method is called: renderHeader, renderBody, renderFooter. + * Refer to the documentation for those methods for information on + * individual render tasks. + * @method render + */ + render : function() { + this.beforeRenderEvent.fire(); + + // Find starting day of the current month + var workingDate = DateMath.findMonthStart(this.cfg.getProperty(DEF_CFG.PAGEDATE.key)); + + this.resetRenderers(); + this.cellDates.length = 0; + + Event.purgeElement(this.oDomContainer, true); + + var html = []; + + html[html.length] = '<table cellSpacing="0" class="' + this.Style.CSS_CALENDAR + ' y' + (workingDate.getFullYear() + this.Locale.YEAR_OFFSET) +'" id="' + this.id + '">'; + html = this.renderHeader(html); + html = this.renderBody(workingDate, html); + html = this.renderFooter(html); + html[html.length] = '</table>'; + + this.oDomContainer.innerHTML = html.join("\n"); + + this.applyListeners(); + this.cells = Dom.getElementsByClassName(this.Style.CSS_CELL, "td", this.id); + + this.cfg.refireEvent(DEF_CFG.TITLE.key); + this.cfg.refireEvent(DEF_CFG.CLOSE.key); + this.cfg.refireEvent(DEF_CFG.IFRAME.key); + + this.renderEvent.fire(); + }, + + /** + * Applies the Calendar's DOM listeners to applicable elements. + * @method applyListeners + */ + applyListeners : function() { + var root = this.oDomContainer, + cal = this.parent || this, + anchor = "a", + click = "click"; + + var linkLeft = Dom.getElementsByClassName(this.Style.CSS_NAV_LEFT, anchor, root), + linkRight = Dom.getElementsByClassName(this.Style.CSS_NAV_RIGHT, anchor, root); + + if (linkLeft && linkLeft.length > 0) { + this.linkLeft = linkLeft[0]; + Event.addListener(this.linkLeft, click, this.doPreviousMonthNav, cal, true); + } + + if (linkRight && linkRight.length > 0) { + this.linkRight = linkRight[0]; + Event.addListener(this.linkRight, click, this.doNextMonthNav, cal, true); + } + + if (cal.cfg.getProperty("navigator") !== null) { + this.applyNavListeners(); + } + + if (this.domEventMap) { + var el,elements; + for (var cls in this.domEventMap) { + if (Lang.hasOwnProperty(this.domEventMap, cls)) { + var items = this.domEventMap[cls]; + + if (! (items instanceof Array)) { + items = [items]; + } + + for (var i=0;i<items.length;i++) { + var item = items[i]; + elements = Dom.getElementsByClassName(cls, item.tag, this.oDomContainer); + + for (var c=0;c<elements.length;c++) { + el = elements[c]; + Event.addListener(el, item.event, item.handler, item.scope, item.correct ); + } + } + } + } + } + + Event.addListener(this.oDomContainer, "click", this.doSelectCell, this); + Event.addListener(this.oDomContainer, "mouseover", this.doCellMouseOver, this); + Event.addListener(this.oDomContainer, "mouseout", this.doCellMouseOut, this); + }, + + applyNavListeners : function() { + var calParent = this.parent || this, + cal = this, + navBtns = Dom.getElementsByClassName(this.Style.CSS_NAV, "a", this.oDomContainer); + + if (navBtns.length > 0) { + + Event.addListener(navBtns, "click", function (e, obj) { + var target = Event.getTarget(e); + // this == navBtn + if (this === target || Dom.isAncestor(this, target)) { + Event.preventDefault(e); + } + var navigator = calParent.oNavigator; + if (navigator) { + var pgdate = cal.cfg.getProperty("pagedate"); + navigator.setYear(pgdate.getFullYear() + cal.Locale.YEAR_OFFSET); + navigator.setMonth(pgdate.getMonth()); + navigator.show(); + } + }); + } + }, + + /** + * Retrieves the Date object for the specified Calendar cell + * @method getDateByCellId + * @param {String} id The id of the cell + * @return {Date} The Date object for the specified Calendar cell + */ + getDateByCellId : function(id) { + var date = this.getDateFieldsByCellId(id); + return (date) ? DateMath.getDate(date[0],date[1]-1,date[2]) : null; + }, + + /** + * Retrieves the Date object for the specified Calendar cell + * @method getDateFieldsByCellId + * @param {String} id The id of the cell + * @return {Array} The array of Date fields for the specified Calendar cell + */ + getDateFieldsByCellId : function(id) { + id = this.getIndexFromId(id); + return (id > -1) ? this.cellDates[id] : null; + }, + + /** + * Find the Calendar's cell index for a given date. + * If the date is not found, the method returns -1. + * <p> + * The returned index can be used to lookup the cell HTMLElement + * using the Calendar's cells array or passed to selectCell to select + * cells by index. + * </p> + * + * See <a href="#cells">cells</a>, <a href="#selectCell">selectCell</a>. + * + * @method getCellIndex + * @param {Date} date JavaScript Date object, for which to find a cell index. + * @return {Number} The index of the date in Calendars cellDates/cells arrays, or -1 if the date + * is not on the curently rendered Calendar page. + */ + getCellIndex : function(date) { + var idx = -1; + if (date) { + var m = date.getMonth(), + y = date.getFullYear(), + d = date.getDate(), + dates = this.cellDates; + + for (var i = 0; i < dates.length; ++i) { + var cellDate = dates[i]; + if (cellDate[0] === y && cellDate[1] === m+1 && cellDate[2] === d) { + idx = i; + break; + } + } + } + return idx; + }, + + /** + * Given the id used to mark each Calendar cell, this method + * extracts the index number from the id. + * + * @param {String} strId The cell id + * @return {Number} The index of the cell, or -1 if id does not contain an index number + */ + getIndexFromId : function(strId) { + var idx = -1, + li = strId.lastIndexOf("_cell"); + + if (li > -1) { + idx = parseInt(strId.substring(li + 5), 10); + } + + return idx; + }, + + // BEGIN BUILT-IN TABLE CELL RENDERERS + + /** + * Renders a cell that falls before the minimum date or after the maximum date. + * widget class. + * @method renderOutOfBoundsDate + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering + * should not be terminated + */ + renderOutOfBoundsDate : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_OOB); + cell.innerHTML = workingDate.getDate(); + return Calendar.STOP_RENDER; + }, + + /** + * Renders the row header for a week. + * @method renderRowHeader + * @param {Number} weekNum The week number of the current row + * @param {Array} cell The current working HTML array + */ + renderRowHeader : function(weekNum, html) { + html[html.length] = '<th class="' + this.Style.CSS_ROW_HEADER + '">' + weekNum + '</th>'; + return html; + }, + + /** + * Renders the row footer for a week. + * @method renderRowFooter + * @param {Number} weekNum The week number of the current row + * @param {Array} cell The current working HTML array + */ + renderRowFooter : function(weekNum, html) { + html[html.length] = '<th class="' + this.Style.CSS_ROW_FOOTER + '">' + weekNum + '</th>'; + return html; + }, + + /** + * Renders a single standard calendar cell in the calendar widget table. + * All logic for determining how a standard default cell will be rendered is + * encapsulated in this method, and must be accounted for when extending the + * widget class. + * @method renderCellDefault + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellDefault : function(workingDate, cell) { + cell.innerHTML = '<a href="#" class="' + this.Style.CSS_CELL_SELECTOR + '">' + this.buildDayLabel(workingDate) + "</a>"; + }, + + /** + * Styles a selectable cell. + * @method styleCellDefault + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + styleCellDefault : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_SELECTABLE); + }, + + + /** + * Renders a single standard calendar cell using the CSS hightlight1 style + * @method renderCellStyleHighlight1 + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellStyleHighlight1 : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT1); + }, + + /** + * Renders a single standard calendar cell using the CSS hightlight2 style + * @method renderCellStyleHighlight2 + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellStyleHighlight2 : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT2); + }, + + /** + * Renders a single standard calendar cell using the CSS hightlight3 style + * @method renderCellStyleHighlight3 + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellStyleHighlight3 : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT3); + }, + + /** + * Renders a single standard calendar cell using the CSS hightlight4 style + * @method renderCellStyleHighlight4 + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellStyleHighlight4 : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT4); + }, + + /** + * Applies the default style used for rendering today's date to the current calendar cell + * @method renderCellStyleToday + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + */ + renderCellStyleToday : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_TODAY); + }, + + /** + * Applies the default style used for rendering selected dates to the current calendar cell + * @method renderCellStyleSelected + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering + * should not be terminated + */ + renderCellStyleSelected : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_SELECTED); + }, + + /** + * Applies the default style used for rendering dates that are not a part of the current + * month (preceding or trailing the cells for the current month) + * @method renderCellNotThisMonth + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering + * should not be terminated + */ + renderCellNotThisMonth : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL_OOM); + cell.innerHTML=workingDate.getDate(); + return Calendar.STOP_RENDER; + }, + + /** + * Renders the current calendar cell as a non-selectable "black-out" date using the default + * restricted style. + * @method renderBodyCellRestricted + * @param {Date} workingDate The current working Date object being used to generate the calendar + * @param {HTMLTableCellElement} cell The current working cell in the calendar + * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering + * should not be terminated + */ + renderBodyCellRestricted : function(workingDate, cell) { + Dom.addClass(cell, this.Style.CSS_CELL); + Dom.addClass(cell, this.Style.CSS_CELL_RESTRICTED); + cell.innerHTML=workingDate.getDate(); + return Calendar.STOP_RENDER; + }, + + // END BUILT-IN TABLE CELL RENDERERS + + // BEGIN MONTH NAVIGATION METHODS + + /** + * Adds the designated number of months to the current calendar month, and sets the current + * calendar page date to the new month. + * @method addMonths + * @param {Number} count The number of months to add to the current calendar + */ + addMonths : function(count) { + var cfgPageDate = DEF_CFG.PAGEDATE.key, + + prevDate = this.cfg.getProperty(cfgPageDate), + newDate = DateMath.add(prevDate, DateMath.MONTH, count); + + this.cfg.setProperty(cfgPageDate, newDate); + this.resetRenderers(); + this.changePageEvent.fire(prevDate, newDate); + }, + + /** + * Subtracts the designated number of months from the current calendar month, and sets the current + * calendar page date to the new month. + * @method subtractMonths + * @param {Number} count The number of months to subtract from the current calendar + */ + subtractMonths : function(count) { + this.addMonths(-1*count); + }, + + /** + * Adds the designated number of years to the current calendar, and sets the current + * calendar page date to the new month. + * @method addYears + * @param {Number} count The number of years to add to the current calendar + */ + addYears : function(count) { + var cfgPageDate = DEF_CFG.PAGEDATE.key, + + prevDate = this.cfg.getProperty(cfgPageDate), + newDate = DateMath.add(prevDate, DateMath.YEAR, count); + + this.cfg.setProperty(cfgPageDate, newDate); + this.resetRenderers(); + this.changePageEvent.fire(prevDate, newDate); + }, + + /** + * Subtcats the designated number of years from the current calendar, and sets the current + * calendar page date to the new month. + * @method subtractYears + * @param {Number} count The number of years to subtract from the current calendar + */ + subtractYears : function(count) { + this.addYears(-1*count); + }, + + /** + * Navigates to the next month page in the calendar widget. + * @method nextMonth + */ + nextMonth : function() { + this.addMonths(1); + }, + + /** + * Navigates to the previous month page in the calendar widget. + * @method previousMonth + */ + previousMonth : function() { + this.addMonths(-1); + }, + + /** + * Navigates to the next year in the currently selected month in the calendar widget. + * @method nextYear + */ + nextYear : function() { + this.addYears(1); + }, + + /** + * Navigates to the previous year in the currently selected month in the calendar widget. + * @method previousYear + */ + previousYear : function() { + this.addYears(-1); + }, + + // END MONTH NAVIGATION METHODS + + // BEGIN SELECTION METHODS + + /** + * Resets the calendar widget to the originally selected month and year, and + * sets the calendar to the initial selection(s). + * @method reset + */ + reset : function() { + this.cfg.resetProperty(DEF_CFG.SELECTED.key); + this.cfg.resetProperty(DEF_CFG.PAGEDATE.key); + this.resetEvent.fire(); + }, + + /** + * Clears the selected dates in the current calendar widget and sets the calendar + * to the current month and year. + * @method clear + */ + clear : function() { + this.cfg.setProperty(DEF_CFG.SELECTED.key, []); + this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.today.getTime())); + this.clearEvent.fire(); + }, + + /** + * Selects a date or a collection of dates on the current calendar. This method, by default, + * does not call the render method explicitly. Once selection has completed, render must be + * called for the changes to be reflected visually. + * + * Any dates which are OOB (out of bounds, not selectable) will not be selected and the array of + * selected dates passed to the selectEvent will not contain OOB dates. + * + * If all dates are OOB, the no state change will occur; beforeSelect and select events will not be fired. + * + * @method select + * @param {String/Date/Date[]} date The date string of dates to select in the current calendar. Valid formats are + * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). + * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). + * This method can also take a JavaScript Date object or an array of Date objects. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + select : function(date) { + this.logger.log("Select: " + date, "info"); + + var aToBeSelected = this._toFieldArray(date), + validDates = [], + selected = [], + cfgSelected = DEF_CFG.SELECTED.key; + + this.logger.log("Selection field array: " + aToBeSelected, "info"); + + for (var a=0; a < aToBeSelected.length; ++a) { + var toSelect = aToBeSelected[a]; + + if (!this.isDateOOB(this._toDate(toSelect))) { + + if (validDates.length === 0) { + this.beforeSelectEvent.fire(); + selected = this.cfg.getProperty(cfgSelected); + } + validDates.push(toSelect); + + if (this._indexOfSelectedFieldArray(toSelect) == -1) { + selected[selected.length] = toSelect; + } + } + } + + if (validDates.length === 0) { this.logger.log("All provided dates were OOB. beforeSelect and select events not fired", "info"); } + + if (validDates.length > 0) { + if (this.parent) { + this.parent.cfg.setProperty(cfgSelected, selected); + } else { + this.cfg.setProperty(cfgSelected, selected); + } + this.selectEvent.fire(validDates); + } + + return this.getSelectedDates(); + }, + + /** + * Selects a date on the current calendar by referencing the index of the cell that should be selected. + * This method is used to easily select a single cell (usually with a mouse click) without having to do + * a full render. The selected style is applied to the cell directly. + * + * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month + * or out of bounds cells), it will not be selected and in such a case beforeSelect and select events will not be fired. + * + * @method selectCell + * @param {Number} cellIndex The index of the cell to select in the current calendar. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + selectCell : function(cellIndex) { + + var cell = this.cells[cellIndex], + cellDate = this.cellDates[cellIndex], + dCellDate = this._toDate(cellDate), + selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE); + + this.logger.log("Select: " + dCellDate, "info"); + if (!selectable) {this.logger.log("The cell at cellIndex:" + cellIndex + " is not a selectable cell. beforeSelect, select events not fired", "info"); } + + if (selectable) { + + this.beforeSelectEvent.fire(); + + var cfgSelected = DEF_CFG.SELECTED.key; + var selected = this.cfg.getProperty(cfgSelected); + + var selectDate = cellDate.concat(); + + if (this._indexOfSelectedFieldArray(selectDate) == -1) { + selected[selected.length] = selectDate; + } + if (this.parent) { + this.parent.cfg.setProperty(cfgSelected, selected); + } else { + this.cfg.setProperty(cfgSelected, selected); + } + this.renderCellStyleSelected(dCellDate,cell); + this.selectEvent.fire([selectDate]); + + this.doCellMouseOut.call(cell, null, this); + } + + return this.getSelectedDates(); + }, + + /** + * Deselects a date or a collection of dates on the current calendar. This method, by default, + * does not call the render method explicitly. Once deselection has completed, render must be + * called for the changes to be reflected visually. + * + * The method will not attempt to deselect any dates which are OOB (out of bounds, and hence not selectable) + * and the array of deselected dates passed to the deselectEvent will not contain any OOB dates. + * + * If all dates are OOB, beforeDeselect and deselect events will not be fired. + * + * @method deselect + * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are + * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). + * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). + * This method can also take a JavaScript Date object or an array of Date objects. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + deselect : function(date) { + this.logger.log("Deselect: " + date, "info"); + + var aToBeDeselected = this._toFieldArray(date), + validDates = [], + selected = [], + cfgSelected = DEF_CFG.SELECTED.key; + + this.logger.log("Deselection field array: " + aToBeDeselected, "info"); + + for (var a=0; a < aToBeDeselected.length; ++a) { + var toDeselect = aToBeDeselected[a]; + + if (!this.isDateOOB(this._toDate(toDeselect))) { + + if (validDates.length === 0) { + this.beforeDeselectEvent.fire(); + selected = this.cfg.getProperty(cfgSelected); + } + + validDates.push(toDeselect); + + var index = this._indexOfSelectedFieldArray(toDeselect); + if (index != -1) { + selected.splice(index,1); + } + } + } + + if (validDates.length === 0) { this.logger.log("All provided dates were OOB. beforeDeselect and deselect events not fired");} + + if (validDates.length > 0) { + if (this.parent) { + this.parent.cfg.setProperty(cfgSelected, selected); + } else { + this.cfg.setProperty(cfgSelected, selected); + } + this.deselectEvent.fire(validDates); + } + + return this.getSelectedDates(); + }, + + /** + * Deselects a date on the current calendar by referencing the index of the cell that should be deselected. + * This method is used to easily deselect a single cell (usually with a mouse click) without having to do + * a full render. The selected style is removed from the cell directly. + * + * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month + * or out of bounds cells), the method will not attempt to deselect it and in such a case, beforeDeselect and + * deselect events will not be fired. + * + * @method deselectCell + * @param {Number} cellIndex The index of the cell to deselect in the current calendar. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + deselectCell : function(cellIndex) { + var cell = this.cells[cellIndex], + cellDate = this.cellDates[cellIndex], + cellDateIndex = this._indexOfSelectedFieldArray(cellDate); + + var selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE); + if (!selectable) { this.logger.log("The cell at cellIndex:" + cellIndex + " is not a selectable/deselectable cell", "info"); } + + if (selectable) { + + this.beforeDeselectEvent.fire(); + + var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key), + dCellDate = this._toDate(cellDate), + selectDate = cellDate.concat(); + + if (cellDateIndex > -1) { + if (this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth() == dCellDate.getMonth() && + this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getFullYear() == dCellDate.getFullYear()) { + Dom.removeClass(cell, this.Style.CSS_CELL_SELECTED); + } + selected.splice(cellDateIndex, 1); + } + + if (this.parent) { + this.parent.cfg.setProperty(DEF_CFG.SELECTED.key, selected); + } else { + this.cfg.setProperty(DEF_CFG.SELECTED.key, selected); + } + + this.deselectEvent.fire([selectDate]); + } + + return this.getSelectedDates(); + }, + + /** + * Deselects all dates on the current calendar. + * @method deselectAll + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + * Assuming that this function executes properly, the return value should be an empty array. + * However, the empty array is returned for the sake of being able to check the selection status + * of the calendar. + */ + deselectAll : function() { + this.beforeDeselectEvent.fire(); + + var cfgSelected = DEF_CFG.SELECTED.key, + selected = this.cfg.getProperty(cfgSelected), + count = selected.length, + sel = selected.concat(); + + if (this.parent) { + this.parent.cfg.setProperty(cfgSelected, []); + } else { + this.cfg.setProperty(cfgSelected, []); + } + + if (count > 0) { + this.deselectEvent.fire(sel); + } + + return this.getSelectedDates(); + }, + + // END SELECTION METHODS + + // BEGIN TYPE CONVERSION METHODS + + /** + * Converts a date (either a JavaScript Date object, or a date string) to the internal data structure + * used to represent dates: [[yyyy,mm,dd],[yyyy,mm,dd]]. + * @method _toFieldArray + * @private + * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are + * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). + * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). + * This method can also take a JavaScript Date object or an array of Date objects. + * @return {Array[](Number[])} Array of date field arrays + */ + _toFieldArray : function(date) { + var returnDate = []; + + if (date instanceof Date) { + returnDate = [[date.getFullYear(), date.getMonth()+1, date.getDate()]]; + } else if (Lang.isString(date)) { + returnDate = this._parseDates(date); + } else if (Lang.isArray(date)) { + for (var i=0;i<date.length;++i) { + var d = date[i]; + returnDate[returnDate.length] = [d.getFullYear(),d.getMonth()+1,d.getDate()]; + } + } + + return returnDate; + }, + + /** + * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. The date field array + * is the format in which dates are as provided as arguments to selectEvent and deselectEvent listeners. + * + * @method toDate + * @param {Number[]} dateFieldArray The date field array to convert to a JavaScript Date. + * @return {Date} JavaScript Date object representing the date field array. + */ + toDate : function(dateFieldArray) { + return this._toDate(dateFieldArray); + }, + + /** + * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. + * @method _toDate + * @private + * @deprecated Made public, toDate + * @param {Number[]} dateFieldArray The date field array to convert to a JavaScript Date. + * @return {Date} JavaScript Date object representing the date field array + */ + _toDate : function(dateFieldArray) { + if (dateFieldArray instanceof Date) { + return dateFieldArray; + } else { + return DateMath.getDate(dateFieldArray[0],dateFieldArray[1]-1,dateFieldArray[2]); + } + }, + + // END TYPE CONVERSION METHODS + + // BEGIN UTILITY METHODS + + /** + * Determines if 2 field arrays are equal. + * @method _fieldArraysAreEqual + * @private + * @param {Number[]} array1 The first date field array to compare + * @param {Number[]} array2 The first date field array to compare + * @return {Boolean} The boolean that represents the equality of the two arrays + */ + _fieldArraysAreEqual : function(array1, array2) { + var match = false; + + if (array1[0]==array2[0]&&array1[1]==array2[1]&&array1[2]==array2[2]) { + match=true; + } + + return match; + }, + + /** + * Gets the index of a date field array [yyyy,mm,dd] in the current list of selected dates. + * @method _indexOfSelectedFieldArray + * @private + * @param {Number[]} find The date field array to search for + * @return {Number} The index of the date field array within the collection of selected dates. + * -1 will be returned if the date is not found. + */ + _indexOfSelectedFieldArray : function(find) { + var selected = -1, + seldates = this.cfg.getProperty(DEF_CFG.SELECTED.key); + + for (var s=0;s<seldates.length;++s) { + var sArray = seldates[s]; + if (find[0]==sArray[0]&&find[1]==sArray[1]&&find[2]==sArray[2]) { + selected = s; + break; + } + } + + return selected; + }, + + /** + * Determines whether a given date is OOM (out of month). + * @method isDateOOM + * @param {Date} date The JavaScript Date object for which to check the OOM status + * @return {Boolean} true if the date is OOM + */ + isDateOOM : function(date) { + return (date.getMonth() != this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth()); + }, + + /** + * Determines whether a given date is OOB (out of bounds - less than the mindate or more than the maxdate). + * + * @method isDateOOB + * @param {Date} date The JavaScript Date object for which to check the OOB status + * @return {Boolean} true if the date is OOB + */ + isDateOOB : function(date) { + var minDate = this.cfg.getProperty(DEF_CFG.MINDATE.key), + maxDate = this.cfg.getProperty(DEF_CFG.MAXDATE.key), + dm = DateMath; + + if (minDate) { + minDate = dm.clearTime(minDate); + } + if (maxDate) { + maxDate = dm.clearTime(maxDate); + } + + var clearedDate = new Date(date.getTime()); + clearedDate = dm.clearTime(clearedDate); + + return ((minDate && clearedDate.getTime() < minDate.getTime()) || (maxDate && clearedDate.getTime() > maxDate.getTime())); + }, + + /** + * Parses a pagedate configuration property value. The value can either be specified as a string of form "mm/yyyy" or a Date object + * and is parsed into a Date object normalized to the first day of the month. If no value is passed in, the month and year from today's date are used to create the Date object + * @method _parsePageDate + * @private + * @param {Date|String} date Pagedate value which needs to be parsed + * @return {Date} The Date object representing the pagedate + */ + _parsePageDate : function(date) { + var parsedDate; + + if (date) { + if (date instanceof Date) { + parsedDate = DateMath.findMonthStart(date); + } else { + var month, year, aMonthYear; + aMonthYear = date.split(this.cfg.getProperty(DEF_CFG.DATE_FIELD_DELIMITER.key)); + month = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_MONTH_POSITION.key)-1], 10)-1; + year = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_YEAR_POSITION.key)-1], 10) - this.Locale.YEAR_OFFSET; + + parsedDate = DateMath.getDate(year, month, 1); + } + } else { + parsedDate = DateMath.getDate(this.today.getFullYear(), this.today.getMonth(), 1); + } + return parsedDate; + }, + + // END UTILITY METHODS + + // BEGIN EVENT HANDLERS + + /** + * Event executed before a date is selected in the calendar widget. + * @deprecated Event handlers for this event should be susbcribed to beforeSelectEvent. + */ + onBeforeSelect : function() { + if (this.cfg.getProperty(DEF_CFG.MULTI_SELECT.key) === false) { + if (this.parent) { + this.parent.callChildFunction("clearAllBodyCellStyles", this.Style.CSS_CELL_SELECTED); + this.parent.deselectAll(); + } else { + this.clearAllBodyCellStyles(this.Style.CSS_CELL_SELECTED); + this.deselectAll(); + } + } + }, + + /** + * Event executed when a date is selected in the calendar widget. + * @param {Array} selected An array of date field arrays representing which date or dates were selected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ] + * @deprecated Event handlers for this event should be susbcribed to selectEvent. + */ + onSelect : function(selected) { }, + + /** + * Event executed before a date is deselected in the calendar widget. + * @deprecated Event handlers for this event should be susbcribed to beforeDeselectEvent. + */ + onBeforeDeselect : function() { }, + + /** + * Event executed when a date is deselected in the calendar widget. + * @param {Array} selected An array of date field arrays representing which date or dates were deselected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ] + * @deprecated Event handlers for this event should be susbcribed to deselectEvent. + */ + onDeselect : function(deselected) { }, + + /** + * Event executed when the user navigates to a different calendar page. + * @deprecated Event handlers for this event should be susbcribed to changePageEvent. + */ + onChangePage : function() { + this.render(); + }, + + /** + * Event executed when the calendar widget is rendered. + * @deprecated Event handlers for this event should be susbcribed to renderEvent. + */ + onRender : function() { }, + + /** + * Event executed when the calendar widget is reset to its original state. + * @deprecated Event handlers for this event should be susbcribed to resetEvemt. + */ + onReset : function() { this.render(); }, + + /** + * Event executed when the calendar widget is completely cleared to the current month with no selections. + * @deprecated Event handlers for this event should be susbcribed to clearEvent. + */ + onClear : function() { this.render(); }, + + /** + * Validates the calendar widget. This method has no default implementation + * and must be extended by subclassing the widget. + * @return Should return true if the widget validates, and false if + * it doesn't. + * @type Boolean + */ + validate : function() { return true; }, + + // END EVENT HANDLERS + + // BEGIN DATE PARSE METHODS + + /** + * Converts a date string to a date field array + * @private + * @param {String} sDate Date string. Valid formats are mm/dd and mm/dd/yyyy. + * @return A date field array representing the string passed to the method + * @type Array[](Number[]) + */ + _parseDate : function(sDate) { + var aDate = sDate.split(this.Locale.DATE_FIELD_DELIMITER), + rArray; + + if (aDate.length == 2) { + rArray = [aDate[this.Locale.MD_MONTH_POSITION-1],aDate[this.Locale.MD_DAY_POSITION-1]]; + rArray.type = Calendar.MONTH_DAY; + } else { + rArray = [aDate[this.Locale.MDY_YEAR_POSITION-1] - this.Locale.YEAR_OFFSET, aDate[this.Locale.MDY_MONTH_POSITION-1],aDate[this.Locale.MDY_DAY_POSITION-1]]; + rArray.type = Calendar.DATE; + } + + for (var i=0;i<rArray.length;i++) { + rArray[i] = parseInt(rArray[i], 10); + } + + return rArray; + }, + + /** + * Converts a multi or single-date string to an array of date field arrays + * @private + * @param {String} sDates Date string with one or more comma-delimited dates. Valid formats are mm/dd, mm/dd/yyyy, mm/dd/yyyy-mm/dd/yyyy + * @return An array of date field arrays + * @type Array[](Number[]) + */ + _parseDates : function(sDates) { + var aReturn = [], + aDates = sDates.split(this.Locale.DATE_DELIMITER); + + for (var d=0;d<aDates.length;++d) { + var sDate = aDates[d]; + + if (sDate.indexOf(this.Locale.DATE_RANGE_DELIMITER) != -1) { + // This is a range + var aRange = sDate.split(this.Locale.DATE_RANGE_DELIMITER), + dateStart = this._parseDate(aRange[0]), + dateEnd = this._parseDate(aRange[1]), + fullRange = this._parseRange(dateStart, dateEnd); + + aReturn = aReturn.concat(fullRange); + } else { + // This is not a range + var aDate = this._parseDate(sDate); + aReturn.push(aDate); + } + } + return aReturn; + }, + + /** + * Converts a date range to the full list of included dates + * @private + * @param {Number[]} startDate Date field array representing the first date in the range + * @param {Number[]} endDate Date field array representing the last date in the range + * @return An array of date field arrays + * @type Array[](Number[]) + */ + _parseRange : function(startDate, endDate) { + var dCurrent = DateMath.add(DateMath.getDate(startDate[0],startDate[1]-1,startDate[2]),DateMath.DAY,1), + dEnd = DateMath.getDate(endDate[0], endDate[1]-1, endDate[2]), + results = []; + + results.push(startDate); + while (dCurrent.getTime() <= dEnd.getTime()) { + results.push([dCurrent.getFullYear(),dCurrent.getMonth()+1,dCurrent.getDate()]); + dCurrent = DateMath.add(dCurrent,DateMath.DAY,1); + } + return results; + }, + + // END DATE PARSE METHODS + + // BEGIN RENDERER METHODS + + /** + * Resets the render stack of the current calendar to its original pre-render value. + */ + resetRenderers : function() { + this.renderStack = this._renderStack.concat(); + }, + + /** + * Removes all custom renderers added to the Calendar through the addRenderer, addMonthRenderer and + * addWeekdayRenderer methods. Calendar's render method needs to be called after removing renderers + * to re-render the Calendar without custom renderers applied. + */ + removeRenderers : function() { + this._renderStack = []; + this.renderStack = []; + }, + + /** + * Clears the inner HTML, CSS class and style information from the specified cell. + * @method clearElement + * @param {HTMLTableCellElement} cell The cell to clear + */ + clearElement : function(cell) { + cell.innerHTML = " "; + cell.className=""; + }, + + /** + * Adds a renderer to the render stack. The function reference passed to this method will be executed + * when a date cell matches the conditions specified in the date string for this renderer. + * @method addRenderer + * @param {String} sDates A date string to associate with the specified renderer. Valid formats + * include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005) + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addRenderer : function(sDates, fnRender) { + var aDates = this._parseDates(sDates); + for (var i=0;i<aDates.length;++i) { + var aDate = aDates[i]; + + if (aDate.length == 2) { // this is either a range or a month/day combo + if (aDate[0] instanceof Array) { // this is a range + this._addRenderer(Calendar.RANGE,aDate,fnRender); + } else { // this is a month/day combo + this._addRenderer(Calendar.MONTH_DAY,aDate,fnRender); + } + } else if (aDate.length == 3) { + this._addRenderer(Calendar.DATE,aDate,fnRender); + } + } + }, + + /** + * The private method used for adding cell renderers to the local render stack. + * This method is called by other methods that set the renderer type prior to the method call. + * @method _addRenderer + * @private + * @param {String} type The type string that indicates the type of date renderer being added. + * Values are YAHOO.widget.Calendar.DATE, YAHOO.widget.Calendar.MONTH_DAY, YAHOO.widget.Calendar.WEEKDAY, + * YAHOO.widget.Calendar.RANGE, YAHOO.widget.Calendar.MONTH + * @param {Array} aDates An array of dates used to construct the renderer. The format varies based + * on the renderer type + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + _addRenderer : function(type, aDates, fnRender) { + var add = [type,aDates,fnRender]; + this.renderStack.unshift(add); + this._renderStack = this.renderStack.concat(); + }, + + /** + * Adds a month to the render stack. The function reference passed to this method will be executed + * when a date cell matches the month passed to this method. + * @method addMonthRenderer + * @param {Number} month The month (1-12) to associate with this renderer + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addMonthRenderer : function(month, fnRender) { + this._addRenderer(Calendar.MONTH,[month],fnRender); + }, + + /** + * Adds a weekday to the render stack. The function reference passed to this method will be executed + * when a date cell matches the weekday passed to this method. + * @method addWeekdayRenderer + * @param {Number} weekday The weekday (Sunday = 1, Monday = 2 ... Saturday = 7) to associate with this renderer + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addWeekdayRenderer : function(weekday, fnRender) { + this._addRenderer(Calendar.WEEKDAY,[weekday],fnRender); + }, + + // END RENDERER METHODS + + // BEGIN CSS METHODS + + /** + * Removes all styles from all body cells in the current calendar table. + * @method clearAllBodyCellStyles + * @param {style} style The CSS class name to remove from all calendar body cells + */ + clearAllBodyCellStyles : function(style) { + for (var c=0;c<this.cells.length;++c) { + Dom.removeClass(this.cells[c],style); + } + }, + + // END CSS METHODS + + // BEGIN GETTER/SETTER METHODS + /** + * Sets the calendar's month explicitly + * @method setMonth + * @param {Number} month The numeric month, from 0 (January) to 11 (December) + */ + setMonth : function(month) { + var cfgPageDate = DEF_CFG.PAGEDATE.key, + current = this.cfg.getProperty(cfgPageDate); + current.setMonth(parseInt(month, 10)); + this.cfg.setProperty(cfgPageDate, current); + }, + + /** + * Sets the calendar's year explicitly. + * @method setYear + * @param {Number} year The numeric 4-digit year + */ + setYear : function(year) { + var cfgPageDate = DEF_CFG.PAGEDATE.key, + current = this.cfg.getProperty(cfgPageDate); + + current.setFullYear(parseInt(year, 10) - this.Locale.YEAR_OFFSET); + this.cfg.setProperty(cfgPageDate, current); + }, + + /** + * Gets the list of currently selected dates from the calendar. + * @method getSelectedDates + * @return {Date[]} An array of currently selected JavaScript Date objects. + */ + getSelectedDates : function() { + var returnDates = [], + selected = this.cfg.getProperty(DEF_CFG.SELECTED.key); + + for (var d=0;d<selected.length;++d) { + var dateArray = selected[d]; + + var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]); + returnDates.push(date); + } + + returnDates.sort( function(a,b) { return a-b; } ); + return returnDates; + }, + + /// END GETTER/SETTER METHODS /// + + /** + * Hides the Calendar's outer container from view. + * @method hide + */ + hide : function() { + if (this.beforeHideEvent.fire()) { + this.oDomContainer.style.display = "none"; + this.hideEvent.fire(); + } + }, + + /** + * Shows the Calendar's outer container. + * @method show + */ + show : function() { + if (this.beforeShowEvent.fire()) { + this.oDomContainer.style.display = "block"; + this.showEvent.fire(); + } + }, + + /** + * Returns a string representing the current browser. + * @deprecated As of 2.3.0, environment information is available in YAHOO.env.ua + * @see YAHOO.env.ua + * @property browser + * @type String + */ + browser : (function() { + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf('opera')!=-1) { // Opera (check first in case of spoof) + return 'opera'; + } else if (ua.indexOf('msie 7')!=-1) { // IE7 + return 'ie7'; + } else if (ua.indexOf('msie') !=-1) { // IE + return 'ie'; + } else if (ua.indexOf('safari')!=-1) { // Safari (check before Gecko because it includes "like Gecko") + return 'safari'; + } else if (ua.indexOf('gecko') != -1) { // Gecko + return 'gecko'; + } else { + return false; + } + })(), + /** + * Returns a string representation of the object. + * @method toString + * @return {String} A string representation of the Calendar object. + */ + toString : function() { + return "Calendar " + this.id; + }, + + /** + * Destroys the Calendar instance. The method will remove references + * to HTML elements, remove any event listeners added by the Calendar, + * and destroy the Config and CalendarNavigator instances it has created. + * + * @method destroy + */ + destroy : function() { + + if (this.beforeDestroyEvent.fire()) { + var cal = this; + + // Child objects + if (cal.navigator) { + cal.navigator.destroy(); + } + + if (cal.cfg) { + cal.cfg.destroy(); + } + + // DOM event listeners + Event.purgeElement(cal.oDomContainer, true); + + // Generated markup/DOM - Not removing the container DIV since we didn't create it. + Dom.removeClass(cal.oDomContainer, cal.Style.CSS_WITH_TITLE); + Dom.removeClass(cal.oDomContainer, cal.Style.CSS_CONTAINER); + Dom.removeClass(cal.oDomContainer, cal.Style.CSS_SINGLE); + cal.oDomContainer.innerHTML = ""; + + // JS-to-DOM references + cal.oDomContainer = null; + cal.cells = null; + + this.destroyEvent.fire(); + } + } +}; + +YAHOO.widget.Calendar = Calendar; + +/** +* @namespace YAHOO.widget +* @class Calendar_Core +* @extends YAHOO.widget.Calendar +* @deprecated The old Calendar_Core class is no longer necessary. +*/ +YAHOO.widget.Calendar_Core = YAHOO.widget.Calendar; + +YAHOO.widget.Cal_Core = YAHOO.widget.Calendar; + +})(); +(function() { + + var Dom = YAHOO.util.Dom, + DateMath = YAHOO.widget.DateMath, + Event = YAHOO.util.Event, + Lang = YAHOO.lang, + Calendar = YAHOO.widget.Calendar; + +/** +* YAHOO.widget.CalendarGroup is a special container class for YAHOO.widget.Calendar. This class facilitates +* the ability to have multi-page calendar views that share a single dataset and are +* dependent on each other. +* +* The calendar group instance will refer to each of its elements using a 0-based index. +* For example, to construct the placeholder for a calendar group widget with id "cal1" and +* containerId of "cal1Container", the markup would be as follows: +* <xmp> +* <div id="cal1Container_0"></div> +* <div id="cal1Container_1"></div> +* </xmp> +* The tables for the calendars ("cal1_0" and "cal1_1") will be inserted into those containers. +* +* <p> +* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong> +* The CalendarGroup can be constructed by simply providing a container ID string, +* or a reference to a container DIV HTMLElement (the element needs to exist +* in the document). +* +* E.g.: +* <xmp> +* var c = new YAHOO.widget.CalendarGroup("calContainer", configOptions); +* </xmp> +* or: +* <xmp> +* var containerDiv = YAHOO.util.Dom.get("calContainer"); +* var c = new YAHOO.widget.CalendarGroup(containerDiv, configOptions); +* </xmp> +* </p> +* <p> +* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix. +* For example if an ID is not provided, and the container's ID is "calContainer", the CalendarGroup's ID will be set to "calContainer_t". +* </p> +* +* @namespace YAHOO.widget +* @class CalendarGroup +* @constructor +* @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional. +* @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document. +* @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup. +*/ +function CalendarGroup(id, containerId, config) { + if (arguments.length > 0) { + this.init.apply(this, arguments); + } +} + +/** +* The set of default Config property keys and values for the CalendarGroup. +* +* <p> +* NOTE: This property is made public in order to allow users to change +* the default values of configuration properties. Users should not +* modify the key string, unless they are overriding the Calendar implementation +* </p> +* +* @property YAHOO.widget.CalendarGroup.DEFAULT_CONFIG +* @static +* @type Object An object with key/value pairs, the key being the +* uppercase configuration property name and the value being an objec +* literal with a key string property, and a value property, specifying the +* default value of the property +*/ + +/** +* The set of default Config property keys and values for the CalendarGroup +* @property YAHOO.widget.CalendarGroup._DEFAULT_CONFIG +* @deprecated Made public. See the public DEFAULT_CONFIG property for details +* @private +* @static +* @type Object +*/ +CalendarGroup.DEFAULT_CONFIG = CalendarGroup._DEFAULT_CONFIG = Calendar.DEFAULT_CONFIG; +CalendarGroup.DEFAULT_CONFIG.PAGES = {key:"pages", value:2}; + +var DEF_CFG = CalendarGroup.DEFAULT_CONFIG; + +CalendarGroup.prototype = { + + /** + * Initializes the calendar group. All subclasses must call this method in order for the + * group to be initialized properly. + * @method init + * @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional. + * @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document. + * @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup. + */ + init : function(id, container, config) { + + // Normalize 2.4.0, pre 2.4.0 args + var nArgs = this._parseArgs(arguments); + + id = nArgs.id; + container = nArgs.container; + config = nArgs.config; + + this.oDomContainer = Dom.get(container); + if (!this.oDomContainer) { this.logger.log("Container not found in document.", "error"); } + + if (!this.oDomContainer.id) { + this.oDomContainer.id = Dom.generateId(); + } + if (!id) { + id = this.oDomContainer.id + "_t"; + } + + /** + * The unique id associated with the CalendarGroup + * @property id + * @type String + */ + this.id = id; + + /** + * The unique id associated with the CalendarGroup container + * @property containerId + * @type String + */ + this.containerId = this.oDomContainer.id; + + this.logger = new YAHOO.widget.LogWriter("CalendarGroup " + this.id); + this.initEvents(); + this.initStyles(); + + /** + * The collection of Calendar pages contained within the CalendarGroup + * @property pages + * @type YAHOO.widget.Calendar[] + */ + this.pages = []; + + Dom.addClass(this.oDomContainer, CalendarGroup.CSS_CONTAINER); + Dom.addClass(this.oDomContainer, CalendarGroup.CSS_MULTI_UP); + + /** + * The Config object used to hold the configuration variables for the CalendarGroup + * @property cfg + * @type YAHOO.util.Config + */ + this.cfg = new YAHOO.util.Config(this); + + /** + * The local object which contains the CalendarGroup's options + * @property Options + * @type Object + */ + this.Options = {}; + + /** + * The local object which contains the CalendarGroup's locale settings + * @property Locale + * @type Object + */ + this.Locale = {}; + + this.setupConfig(); + + if (config) { + this.cfg.applyConfig(config, true); + } + + this.cfg.fireQueue(); + + // OPERA HACK FOR MISWRAPPED FLOATS + if (YAHOO.env.ua.opera){ + this.renderEvent.subscribe(this._fixWidth, this, true); + this.showEvent.subscribe(this._fixWidth, this, true); + } + + this.logger.log("Initialized " + this.pages.length + "-page CalendarGroup", "info"); + }, + + setupConfig : function() { + + var cfg = this.cfg; + + /** + * The number of pages to include in the CalendarGroup. This value can only be set once, in the CalendarGroup's constructor arguments. + * @config pages + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.PAGES.key, { value:DEF_CFG.PAGES.value, validator:cfg.checkNumber, handler:this.configPages } ); + + /** + * The positive or negative year offset from the Gregorian calendar year (assuming a January 1st rollover) to + * be used when displaying or parsing dates. NOTE: All JS Date objects returned by methods, or expected as input by + * methods will always represent the Gregorian year, in order to maintain date/month/week values. + * + * @config year_offset + * @type Number + * @default 0 + */ + cfg.addProperty(DEF_CFG.YEAR_OFFSET.key, { value:DEF_CFG.YEAR_OFFSET.value, handler: this.delegateConfig, supercedes:DEF_CFG.YEAR_OFFSET.supercedes, suppressEvent:true } ); + + /** + * The date to use to represent "Today". + * + * @config today + * @type Date + * @default Today's date + */ + cfg.addProperty(DEF_CFG.TODAY.key, { value: new Date(DEF_CFG.TODAY.value.getTime()), supercedes:DEF_CFG.TODAY.supercedes, handler: this.configToday, suppressEvent:false } ); + + /** + * The month/year representing the current visible Calendar date (mm/yyyy) + * @config pagedate + * @type String | Date + * @default Today's date + */ + cfg.addProperty(DEF_CFG.PAGEDATE.key, { value: DEF_CFG.PAGEDATE.value || new Date(DEF_CFG.TODAY.value.getTime()), handler:this.configPageDate } ); + + /** + * The date or range of dates representing the current Calendar selection + * + * @config selected + * @type String + * @default [] + */ + cfg.addProperty(DEF_CFG.SELECTED.key, { value:[], handler:this.configSelected } ); + + /** + * The title to display above the CalendarGroup's month header + * @config title + * @type String + * @default "" + */ + cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } ); + + /** + * Whether or not a close button should be displayed for this CalendarGroup + * @config close + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } ); + + /** + * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below. + * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be + * enabled if required. + * + * @config iframe + * @type Boolean + * @default true for IE6 and below, false for all other browsers + */ + cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } ); + + /** + * The minimum selectable date in the current Calendar (mm/dd/yyyy) + * @config mindate + * @type String | Date + * @default null + */ + cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.delegateConfig } ); + + /** + * The maximum selectable date in the current Calendar (mm/dd/yyyy) + * @config maxdate + * @type String | Date + * @default null + */ + cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.delegateConfig } ); + + // Options properties + + /** + * True if the Calendar should allow multiple selections. False by default. + * @config MULTI_SELECT + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.MULTI_SELECT.key, { value:DEF_CFG.MULTI_SELECT.value, handler:this.delegateConfig, validator:cfg.checkBoolean } ); + + /** + * The weekday the week begins on. Default is 0 (Sunday). + * @config START_WEEKDAY + * @type number + * @default 0 + */ + cfg.addProperty(DEF_CFG.START_WEEKDAY.key, { value:DEF_CFG.START_WEEKDAY.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * True if the Calendar should show weekday labels. True by default. + * @config SHOW_WEEKDAYS + * @type Boolean + * @default true + */ + cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key, { value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should show week row headers. False by default. + * @config SHOW_WEEK_HEADER + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key,{ value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should show week row footers. False by default. + * @config SHOW_WEEK_FOOTER + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } ); + + /** + * True if the Calendar should suppress weeks that are not a part of the current month. False by default. + * @config HIDE_BLANK_WEEKS + * @type Boolean + * @default false + */ + cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key,{ value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } ); + + /** + * The image that should be used for the left navigation arrow. + * @config NAV_ARROW_LEFT + * @type String + * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft" + * @default null + */ + cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key, { value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.delegateConfig } ); + + /** + * The image that should be used for the right navigation arrow. + * @config NAV_ARROW_RIGHT + * @type String + * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright" + * @default null + */ + cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key, { value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.delegateConfig } ); + + // Locale properties + + /** + * The short month labels for the current locale. + * @config MONTHS_SHORT + * @type String[] + * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + */ + cfg.addProperty(DEF_CFG.MONTHS_SHORT.key, { value:DEF_CFG.MONTHS_SHORT.value, handler:this.delegateConfig } ); + + /** + * The long month labels for the current locale. + * @config MONTHS_LONG + * @type String[] + * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + */ + cfg.addProperty(DEF_CFG.MONTHS_LONG.key, { value:DEF_CFG.MONTHS_LONG.value, handler:this.delegateConfig } ); + + /** + * The 1-character weekday labels for the current locale. + * @config WEEKDAYS_1CHAR + * @type String[] + * @default ["S", "M", "T", "W", "T", "F", "S"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key, { value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.delegateConfig } ); + + /** + * The short weekday labels for the current locale. + * @config WEEKDAYS_SHORT + * @type String[] + * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key, { value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.delegateConfig } ); + + /** + * The medium weekday labels for the current locale. + * @config WEEKDAYS_MEDIUM + * @type String[] + * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key, { value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.delegateConfig } ); + + /** + * The long weekday labels for the current locale. + * @config WEEKDAYS_LONG + * @type String[] + * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + */ + cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key, { value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.delegateConfig } ); + + /** + * The setting that determines which length of month labels should be used. Possible values are "short" and "long". + * @config LOCALE_MONTHS + * @type String + * @default "long" + */ + cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key, { value:DEF_CFG.LOCALE_MONTHS.value, handler:this.delegateConfig } ); + + /** + * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long". + * @config LOCALE_WEEKDAYS + * @type String + * @default "short" + */ + cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key, { value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.delegateConfig } ); + + /** + * The value used to delimit individual dates in a date string passed to various Calendar functions. + * @config DATE_DELIMITER + * @type String + * @default "," + */ + cfg.addProperty(DEF_CFG.DATE_DELIMITER.key, { value:DEF_CFG.DATE_DELIMITER.value, handler:this.delegateConfig } ); + + /** + * The value used to delimit date fields in a date string passed to various Calendar functions. + * @config DATE_FIELD_DELIMITER + * @type String + * @default "/" + */ + cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key,{ value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.delegateConfig } ); + + /** + * The value used to delimit date ranges in a date string passed to various Calendar functions. + * @config DATE_RANGE_DELIMITER + * @type String + * @default "-" + */ + cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key,{ value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.delegateConfig } ); + + /** + * The position of the month in a month/year date string + * @config MY_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key, { value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the year in a month/year date string + * @config MY_YEAR_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key, { value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the month in a month/day date string + * @config MD_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key, { value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the day in a month/year date string + * @config MD_DAY_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key, { value:DEF_CFG.MD_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the month in a month/day/year date string + * @config MDY_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key, { value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the day in a month/day/year date string + * @config MDY_DAY_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key, { value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the year in a month/day/year date string + * @config MDY_YEAR_POSITION + * @type Number + * @default 3 + */ + cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key, { value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the month in the month year label string used as the Calendar header + * @config MY_LABEL_MONTH_POSITION + * @type Number + * @default 1 + */ + cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key, { value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The position of the year in the month year label string used as the Calendar header + * @config MY_LABEL_YEAR_POSITION + * @type Number + * @default 2 + */ + cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key, { value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } ); + + /** + * The suffix used after the month when rendering the Calendar header + * @config MY_LABEL_MONTH_SUFFIX + * @type String + * @default " " + */ + cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key, { value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.delegateConfig } ); + + /** + * The suffix used after the year when rendering the Calendar header + * @config MY_LABEL_YEAR_SUFFIX + * @type String + * @default "" + */ + cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.delegateConfig } ); + + /** + * Configuration for the Month Year Navigation UI. By default it is disabled + * @config NAV + * @type Object + * @default null + */ + cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } ); + + /** + * The map of UI strings which the CalendarGroup UI uses. + * + * @config strings + * @type {Object} + * @default An object with the properties shown below: + * <dl> + * <dt>previousMonth</dt><dd><em>String</em> : The string to use for the "Previous Month" navigation UI. Defaults to "Previous Month".</dd> + * <dt>nextMonth</dt><dd><em>String</em> : The string to use for the "Next Month" navigation UI. Defaults to "Next Month".</dd> + * <dt>close</dt><dd><em>String</em> : The string to use for the close button label. Defaults to "Close".</dd> + * </dl> + */ + cfg.addProperty(DEF_CFG.STRINGS.key, { + value:DEF_CFG.STRINGS.value, + handler:this.configStrings, + validator: function(val) { + return Lang.isObject(val); + }, + supercedes: DEF_CFG.STRINGS.supercedes + }); + }, + + /** + * Initializes CalendarGroup's built-in CustomEvents + * @method initEvents + */ + initEvents : function() { + + var me = this, + strEvent = "Event", + CE = YAHOO.util.CustomEvent; + + /** + * Proxy subscriber to subscribe to the CalendarGroup's child Calendars' CustomEvents + * @method sub + * @private + * @param {Function} fn The function to subscribe to this CustomEvent + * @param {Object} obj The CustomEvent's scope object + * @param {Boolean} bOverride Whether or not to apply scope correction + */ + var sub = function(fn, obj, bOverride) { + for (var p=0;p<me.pages.length;++p) { + var cal = me.pages[p]; + cal[this.type + strEvent].subscribe(fn, obj, bOverride); + } + }; + + /** + * Proxy unsubscriber to unsubscribe from the CalendarGroup's child Calendars' CustomEvents + * @method unsub + * @private + * @param {Function} fn The function to subscribe to this CustomEvent + * @param {Object} obj The CustomEvent's scope object + */ + var unsub = function(fn, obj) { + for (var p=0;p<me.pages.length;++p) { + var cal = me.pages[p]; + cal[this.type + strEvent].unsubscribe(fn, obj); + } + }; + + var defEvents = Calendar._EVENT_TYPES; + + /** + * Fired before a date selection is made + * @event beforeSelectEvent + */ + me.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT); + me.beforeSelectEvent.subscribe = sub; me.beforeSelectEvent.unsubscribe = unsub; + + /** + * Fired when a date selection is made + * @event selectEvent + * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. + */ + me.selectEvent = new CE(defEvents.SELECT); + me.selectEvent.subscribe = sub; me.selectEvent.unsubscribe = unsub; + + /** + * Fired before a date or set of dates is deselected + * @event beforeDeselectEvent + */ + me.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT); + me.beforeDeselectEvent.subscribe = sub; me.beforeDeselectEvent.unsubscribe = unsub; + + /** + * Fired when a date or set of dates has been deselected + * @event deselectEvent + * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. + */ + me.deselectEvent = new CE(defEvents.DESELECT); + me.deselectEvent.subscribe = sub; me.deselectEvent.unsubscribe = unsub; + + /** + * Fired when the Calendar page is changed + * @event changePageEvent + */ + me.changePageEvent = new CE(defEvents.CHANGE_PAGE); + me.changePageEvent.subscribe = sub; me.changePageEvent.unsubscribe = unsub; + + /** + * Fired before the Calendar is rendered + * @event beforeRenderEvent + */ + me.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER); + me.beforeRenderEvent.subscribe = sub; me.beforeRenderEvent.unsubscribe = unsub; + + /** + * Fired when the Calendar is rendered + * @event renderEvent + */ + me.renderEvent = new CE(defEvents.RENDER); + me.renderEvent.subscribe = sub; me.renderEvent.unsubscribe = unsub; + + /** + * Fired when the Calendar is reset + * @event resetEvent + */ + me.resetEvent = new CE(defEvents.RESET); + me.resetEvent.subscribe = sub; me.resetEvent.unsubscribe = unsub; + + /** + * Fired when the Calendar is cleared + * @event clearEvent + */ + me.clearEvent = new CE(defEvents.CLEAR); + me.clearEvent.subscribe = sub; me.clearEvent.unsubscribe = unsub; + + /** + * Fired just before the CalendarGroup is to be shown + * @event beforeShowEvent + */ + me.beforeShowEvent = new CE(defEvents.BEFORE_SHOW); + + /** + * Fired after the CalendarGroup is shown + * @event showEvent + */ + me.showEvent = new CE(defEvents.SHOW); + + /** + * Fired just before the CalendarGroup is to be hidden + * @event beforeHideEvent + */ + me.beforeHideEvent = new CE(defEvents.BEFORE_HIDE); + + /** + * Fired after the CalendarGroup is hidden + * @event hideEvent + */ + me.hideEvent = new CE(defEvents.HIDE); + + /** + * Fired just before the CalendarNavigator is to be shown + * @event beforeShowNavEvent + */ + me.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV); + + /** + * Fired after the CalendarNavigator is shown + * @event showNavEvent + */ + me.showNavEvent = new CE(defEvents.SHOW_NAV); + + /** + * Fired just before the CalendarNavigator is to be hidden + * @event beforeHideNavEvent + */ + me.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV); + + /** + * Fired after the CalendarNavigator is hidden + * @event hideNavEvent + */ + me.hideNavEvent = new CE(defEvents.HIDE_NAV); + + /** + * Fired just before the CalendarNavigator is to be rendered + * @event beforeRenderNavEvent + */ + me.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV); + + /** + * Fired after the CalendarNavigator is rendered + * @event renderNavEvent + */ + me.renderNavEvent = new CE(defEvents.RENDER_NAV); + + /** + * Fired just before the CalendarGroup is to be destroyed + * @event beforeDestroyEvent + */ + me.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY); + + /** + * Fired after the CalendarGroup is destroyed. This event should be used + * for notification only. When this event is fired, important CalendarGroup instance + * properties, dom references and event listeners have already been + * removed/dereferenced, and hence the CalendarGroup instance is not in a usable + * state. + * + * @event destroyEvent + */ + me.destroyEvent = new CE(defEvents.DESTROY); + }, + + /** + * The default Config handler for the "pages" property + * @method configPages + * @param {String} type The CustomEvent type (usually the property name) + * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. + * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. + */ + configPages : function(type, args, obj) { + var pageCount = args[0], + cfgPageDate = DEF_CFG.PAGEDATE.key, + sep = "_", + caldate, + firstPageDate = null, + groupCalClass = "groupcal", + firstClass = "first-of-type", + lastClass = "last-of-type"; + + for (var p=0;p<pageCount;++p) { + var calId = this.id + sep + p, + calContainerId = this.containerId + sep + p, + childConfig = this.cfg.getConfig(); + + childConfig.close = false; + childConfig.title = false; + childConfig.navigator = null; + + if (p > 0) { + caldate = new Date(firstPageDate); + this._setMonthOnDate(caldate, caldate.getMonth() + p); + childConfig.pageDate = caldate; + } + + var cal = this.constructChild(calId, calContainerId, childConfig); + + Dom.removeClass(cal.oDomContainer, this.Style.CSS_SINGLE); + Dom.addClass(cal.oDomContainer, groupCalClass); + + if (p===0) { + firstPageDate = cal.cfg.getProperty(cfgPageDate); + Dom.addClass(cal.oDomContainer, firstClass); + } + + if (p==(pageCount-1)) { + Dom.addClass(cal.oDomContainer, lastClass); + } + + cal.parent = this; + cal.index = p; + + this.pages[this.pages.length] = cal; + } + }, + + /** + * The default Config handler for the "pagedate" property + * @method configPageDate + * @param {String} type The CustomEvent type (usually the property name) + * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. + * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. + */ + configPageDate : function(type, args, obj) { + var val = args[0], + firstPageDate; + + var cfgPageDate = DEF_CFG.PAGEDATE.key; + + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + if (p === 0) { + firstPageDate = cal._parsePageDate(val); + cal.cfg.setProperty(cfgPageDate, firstPageDate); + } else { + var pageDate = new Date(firstPageDate); + this._setMonthOnDate(pageDate, pageDate.getMonth() + p); + cal.cfg.setProperty(cfgPageDate, pageDate); + } + } + }, + + /** + * The default Config handler for the CalendarGroup "selected" property + * @method configSelected + * @param {String} type The CustomEvent type (usually the property name) + * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. + * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. + */ + configSelected : function(type, args, obj) { + var cfgSelected = DEF_CFG.SELECTED.key; + this.delegateConfig(type, args, obj); + var selected = (this.pages.length > 0) ? this.pages[0].cfg.getProperty(cfgSelected) : []; + this.cfg.setProperty(cfgSelected, selected, true); + }, + + + /** + * Delegates a configuration property to the CustomEvents associated with the CalendarGroup's children + * @method delegateConfig + * @param {String} type The CustomEvent type (usually the property name) + * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. + * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. + */ + delegateConfig : function(type, args, obj) { + var val = args[0]; + var cal; + + for (var p=0;p<this.pages.length;p++) { + cal = this.pages[p]; + cal.cfg.setProperty(type, val); + } + }, + + /** + * Adds a function to all child Calendars within this CalendarGroup. + * @method setChildFunction + * @param {String} fnName The name of the function + * @param {Function} fn The function to apply to each Calendar page object + */ + setChildFunction : function(fnName, fn) { + var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key); + + for (var p=0;p<pageCount;++p) { + this.pages[p][fnName] = fn; + } + }, + + /** + * Calls a function within all child Calendars within this CalendarGroup. + * @method callChildFunction + * @param {String} fnName The name of the function + * @param {Array} args The arguments to pass to the function + */ + callChildFunction : function(fnName, args) { + var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key); + + for (var p=0;p<pageCount;++p) { + var page = this.pages[p]; + if (page[fnName]) { + var fn = page[fnName]; + fn.call(page, args); + } + } + }, + + /** + * Constructs a child calendar. This method can be overridden if a subclassed version of the default + * calendar is to be used. + * @method constructChild + * @param {String} id The id of the table element that will represent the calendar widget + * @param {String} containerId The id of the container div element that will wrap the calendar table + * @param {Object} config The configuration object containing the Calendar's arguments + * @return {YAHOO.widget.Calendar} The YAHOO.widget.Calendar instance that is constructed + */ + constructChild : function(id,containerId,config) { + var container = document.getElementById(containerId); + if (! container) { + container = document.createElement("div"); + container.id = containerId; + this.oDomContainer.appendChild(container); + } + return new Calendar(id,containerId,config); + }, + + /** + * Sets the calendar group's month explicitly. This month will be set into the first + * page of the multi-page calendar, and all other months will be iterated appropriately. + * @method setMonth + * @param {Number} month The numeric month, from 0 (January) to 11 (December) + */ + setMonth : function(month) { + month = parseInt(month, 10); + var currYear; + + var cfgPageDate = DEF_CFG.PAGEDATE.key; + + for (var p=0; p<this.pages.length; ++p) { + var cal = this.pages[p]; + var pageDate = cal.cfg.getProperty(cfgPageDate); + if (p === 0) { + currYear = pageDate.getFullYear(); + } else { + pageDate.setFullYear(currYear); + } + this._setMonthOnDate(pageDate, month+p); + cal.cfg.setProperty(cfgPageDate, pageDate); + } + }, + + /** + * Sets the calendar group's year explicitly. This year will be set into the first + * page of the multi-page calendar, and all other months will be iterated appropriately. + * @method setYear + * @param {Number} year The numeric 4-digit year + */ + setYear : function(year) { + + var cfgPageDate = DEF_CFG.PAGEDATE.key; + + year = parseInt(year, 10); + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + var pageDate = cal.cfg.getProperty(cfgPageDate); + + if ((pageDate.getMonth()+1) == 1 && p>0) { + year+=1; + } + cal.setYear(year); + } + }, + + /** + * Calls the render function of all child calendars within the group. + * @method render + */ + render : function() { + this.renderHeader(); + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.render(); + } + this.renderFooter(); + }, + + /** + * Selects a date or a collection of dates on the current calendar. This method, by default, + * does not call the render method explicitly. Once selection has completed, render must be + * called for the changes to be reflected visually. + * @method select + * @param {String/Date/Date[]} date The date string of dates to select in the current calendar. Valid formats are + * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). + * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). + * This method can also take a JavaScript Date object or an array of Date objects. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + select : function(date) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.select(date); + } + return this.getSelectedDates(); + }, + + /** + * Selects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly. + * The value of the MULTI_SELECT Configuration attribute will determine the set of dates which get selected. + * <ul> + * <li>If MULTI_SELECT is false, selectCell will select the cell at the specified index for only the last displayed Calendar page.</li> + * <li>If MULTI_SELECT is true, selectCell will select the cell at the specified index, on each displayed Calendar page.</li> + * </ul> + * @method selectCell + * @param {Number} cellIndex The index of the cell to be selected. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + selectCell : function(cellIndex) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.selectCell(cellIndex); + } + return this.getSelectedDates(); + }, + + /** + * Deselects a date or a collection of dates on the current calendar. This method, by default, + * does not call the render method explicitly. Once deselection has completed, render must be + * called for the changes to be reflected visually. + * @method deselect + * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are + * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). + * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). + * This method can also take a JavaScript Date object or an array of Date objects. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + deselect : function(date) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.deselect(date); + } + return this.getSelectedDates(); + }, + + /** + * Deselects all dates on the current calendar. + * @method deselectAll + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + * Assuming that this function executes properly, the return value should be an empty array. + * However, the empty array is returned for the sake of being able to check the selection status + * of the calendar. + */ + deselectAll : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.deselectAll(); + } + return this.getSelectedDates(); + }, + + /** + * Deselects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly. + * deselectCell will deselect the cell at the specified index on each displayed Calendar page. + * + * @method deselectCell + * @param {Number} cellIndex The index of the cell to deselect. + * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. + */ + deselectCell : function(cellIndex) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.deselectCell(cellIndex); + } + return this.getSelectedDates(); + }, + + /** + * Resets the calendar widget to the originally selected month and year, and + * sets the calendar to the initial selection(s). + * @method reset + */ + reset : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.reset(); + } + }, + + /** + * Clears the selected dates in the current calendar widget and sets the calendar + * to the current month and year. + * @method clear + */ + clear : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.clear(); + } + + this.cfg.setProperty(DEF_CFG.SELECTED.key, []); + this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.pages[0].today.getTime())); + this.render(); + }, + + /** + * Navigates to the next month page in the calendar widget. + * @method nextMonth + */ + nextMonth : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.nextMonth(); + } + }, + + /** + * Navigates to the previous month page in the calendar widget. + * @method previousMonth + */ + previousMonth : function() { + for (var p=this.pages.length-1;p>=0;--p) { + var cal = this.pages[p]; + cal.previousMonth(); + } + }, + + /** + * Navigates to the next year in the currently selected month in the calendar widget. + * @method nextYear + */ + nextYear : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.nextYear(); + } + }, + + /** + * Navigates to the previous year in the currently selected month in the calendar widget. + * @method previousYear + */ + previousYear : function() { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.previousYear(); + } + }, + + /** + * Gets the list of currently selected dates from the calendar. + * @return An array of currently selected JavaScript Date objects. + * @type Date[] + */ + getSelectedDates : function() { + var returnDates = []; + var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key); + for (var d=0;d<selected.length;++d) { + var dateArray = selected[d]; + + var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]); + returnDates.push(date); + } + + returnDates.sort( function(a,b) { return a-b; } ); + return returnDates; + }, + + /** + * Adds a renderer to the render stack. The function reference passed to this method will be executed + * when a date cell matches the conditions specified in the date string for this renderer. + * @method addRenderer + * @param {String} sDates A date string to associate with the specified renderer. Valid formats + * include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005) + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addRenderer : function(sDates, fnRender) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.addRenderer(sDates, fnRender); + } + }, + + /** + * Adds a month to the render stack. The function reference passed to this method will be executed + * when a date cell matches the month passed to this method. + * @method addMonthRenderer + * @param {Number} month The month (1-12) to associate with this renderer + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addMonthRenderer : function(month, fnRender) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.addMonthRenderer(month, fnRender); + } + }, + + /** + * Adds a weekday to the render stack. The function reference passed to this method will be executed + * when a date cell matches the weekday passed to this method. + * @method addWeekdayRenderer + * @param {Number} weekday The weekday (1-7) to associate with this renderer. 1=Sunday, 2=Monday etc. + * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. + */ + addWeekdayRenderer : function(weekday, fnRender) { + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + cal.addWeekdayRenderer(weekday, fnRender); + } + }, + + /** + * Removes all custom renderers added to the CalendarGroup through the addRenderer, addMonthRenderer and + * addWeekRenderer methods. CalendarGroup's render method needs to be called to after removing renderers + * to see the changes applied. + * + * @method removeRenderers + */ + removeRenderers : function() { + this.callChildFunction("removeRenderers"); + }, + + /** + * Renders the header for the CalendarGroup. + * @method renderHeader + */ + renderHeader : function() { + // EMPTY DEFAULT IMPL + }, + + /** + * Renders a footer for the 2-up calendar container. By default, this method is + * unimplemented. + * @method renderFooter + */ + renderFooter : function() { + // EMPTY DEFAULT IMPL + }, + + /** + * Adds the designated number of months to the current calendar month, and sets the current + * calendar page date to the new month. + * @method addMonths + * @param {Number} count The number of months to add to the current calendar + */ + addMonths : function(count) { + this.callChildFunction("addMonths", count); + }, + + /** + * Subtracts the designated number of months from the current calendar month, and sets the current + * calendar page date to the new month. + * @method subtractMonths + * @param {Number} count The number of months to subtract from the current calendar + */ + subtractMonths : function(count) { + this.callChildFunction("subtractMonths", count); + }, + + /** + * Adds the designated number of years to the current calendar, and sets the current + * calendar page date to the new month. + * @method addYears + * @param {Number} count The number of years to add to the current calendar + */ + addYears : function(count) { + this.callChildFunction("addYears", count); + }, + + /** + * Subtcats the designated number of years from the current calendar, and sets the current + * calendar page date to the new month. + * @method subtractYears + * @param {Number} count The number of years to subtract from the current calendar + */ + subtractYears : function(count) { + this.callChildFunction("subtractYears", count); + }, + + /** + * Returns the Calendar page instance which has a pagedate (month/year) matching the given date. + * Returns null if no match is found. + * + * @method getCalendarPage + * @param {Date} date The JavaScript Date object for which a Calendar page is to be found. + * @return {Calendar} The Calendar page instance representing the month to which the date + * belongs. + */ + getCalendarPage : function(date) { + var cal = null; + if (date) { + var y = date.getFullYear(), + m = date.getMonth(); + + var pages = this.pages; + for (var i = 0; i < pages.length; ++i) { + var pageDate = pages[i].cfg.getProperty("pagedate"); + if (pageDate.getFullYear() === y && pageDate.getMonth() === m) { + cal = pages[i]; + break; + } + } + } + return cal; + }, + + /** + * Sets the month on a Date object, taking into account year rollover if the month is less than 0 or greater than 11. + * The Date object passed in is modified. It should be cloned before passing it into this method if the original value needs to be maintained + * @method _setMonthOnDate + * @private + * @param {Date} date The Date object on which to set the month index + * @param {Number} iMonth The month index to set + */ + _setMonthOnDate : function(date, iMonth) { + // Bug in Safari 1.3, 2.0 (WebKit build < 420), Date.setMonth does not work consistently if iMonth is not 0-11 + if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420 && (iMonth < 0 || iMonth > 11)) { + var newDate = DateMath.add(date, DateMath.MONTH, iMonth-date.getMonth()); + date.setTime(newDate.getTime()); + } else { + date.setMonth(iMonth); + } + }, + + /** + * Fixes the width of the CalendarGroup container element, to account for miswrapped floats + * @method _fixWidth + * @private + */ + _fixWidth : function() { + var w = 0; + for (var p=0;p<this.pages.length;++p) { + var cal = this.pages[p]; + w += cal.oDomContainer.offsetWidth; + } + if (w > 0) { + this.oDomContainer.style.width = w + "px"; + } + }, + + /** + * Returns a string representation of the object. + * @method toString + * @return {String} A string representation of the CalendarGroup object. + */ + toString : function() { + return "CalendarGroup " + this.id; + }, + + /** + * Destroys the CalendarGroup instance. The method will remove references + * to HTML elements, remove any event listeners added by the CalendarGroup. + * + * It will also destroy the Config and CalendarNavigator instances created by the + * CalendarGroup and the individual Calendar instances created for each page. + * + * @method destroy + */ + destroy : function() { + + if (this.beforeDestroyEvent.fire()) { + + var cal = this; + + // Child objects + if (cal.navigator) { + cal.navigator.destroy(); + } + + if (cal.cfg) { + cal.cfg.destroy(); + } + + // DOM event listeners + Event.purgeElement(cal.oDomContainer, true); + + // Generated markup/DOM - Not removing the container DIV since we didn't create it. + Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_CONTAINER); + Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_MULTI_UP); + + for (var i = 0, l = cal.pages.length; i < l; i++) { + cal.pages[i].destroy(); + cal.pages[i] = null; + } + + cal.oDomContainer.innerHTML = ""; + + // JS-to-DOM references + cal.oDomContainer = null; + + this.destroyEvent.fire(); + } + } +}; + +/** +* CSS class representing the container for the calendar +* @property YAHOO.widget.CalendarGroup.CSS_CONTAINER +* @static +* @final +* @type String +*/ +CalendarGroup.CSS_CONTAINER = "yui-calcontainer"; + +/** +* CSS class representing the container for the calendar +* @property YAHOO.widget.CalendarGroup.CSS_MULTI_UP +* @static +* @final +* @type String +*/ +CalendarGroup.CSS_MULTI_UP = "multi"; + +/** +* CSS class representing the title for the 2-up calendar +* @property YAHOO.widget.CalendarGroup.CSS_2UPTITLE +* @static +* @final +* @type String +*/ +CalendarGroup.CSS_2UPTITLE = "title"; + +/** +* CSS class representing the close icon for the 2-up calendar +* @property YAHOO.widget.CalendarGroup.CSS_2UPCLOSE +* @static +* @final +* @deprecated Along with Calendar.IMG_ROOT and NAV_ARROW_LEFT, NAV_ARROW_RIGHT configuration properties. +* Calendar's <a href="YAHOO.widget.Calendar.html#Style.CSS_CLOSE">Style.CSS_CLOSE</a> property now represents the CSS class used to render the close icon +* @type String +*/ +CalendarGroup.CSS_2UPCLOSE = "close-icon"; + +YAHOO.lang.augmentProto(CalendarGroup, Calendar, "buildDayLabel", + "buildMonthLabel", + "renderOutOfBoundsDate", + "renderRowHeader", + "renderRowFooter", + "renderCellDefault", + "styleCellDefault", + "renderCellStyleHighlight1", + "renderCellStyleHighlight2", + "renderCellStyleHighlight3", + "renderCellStyleHighlight4", + "renderCellStyleToday", + "renderCellStyleSelected", + "renderCellNotThisMonth", + "renderBodyCellRestricted", + "initStyles", + "configTitle", + "configClose", + "configIframe", + "configStrings", + "configToday", + "configNavigator", + "createTitleBar", + "createCloseButton", + "removeTitleBar", + "removeCloseButton", + "hide", + "show", + "toDate", + "_toDate", + "_parseArgs", + "browser"); + +YAHOO.widget.CalGrp = CalendarGroup; +YAHOO.widget.CalendarGroup = CalendarGroup; + +/** +* @class YAHOO.widget.Calendar2up +* @extends YAHOO.widget.CalendarGroup +* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default. +*/ +YAHOO.widget.Calendar2up = function(id, containerId, config) { + this.init(id, containerId, config); +}; + +YAHOO.extend(YAHOO.widget.Calendar2up, CalendarGroup); + +/** +* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default. +*/ +YAHOO.widget.Cal2up = YAHOO.widget.Calendar2up; + +})(); +/** + * The CalendarNavigator is used along with a Calendar/CalendarGroup to + * provide a Month/Year popup navigation control, allowing the user to navigate + * to a specific month/year in the Calendar/CalendarGroup without having to + * scroll through months sequentially + * + * @namespace YAHOO.widget + * @class CalendarNavigator + * @constructor + * @param {Calendar|CalendarGroup} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached. + */ +YAHOO.widget.CalendarNavigator = function(cal) { + this.init(cal); +}; + +(function() { + // Setup static properties (inside anon fn, so that we can use shortcuts) + var CN = YAHOO.widget.CalendarNavigator; + + /** + * YAHOO.widget.CalendarNavigator.CLASSES contains constants + * for the class values applied to the CalendarNaviatgator's + * DOM elements + * @property YAHOO.widget.CalendarNavigator.CLASSES + * @type Object + * @static + */ + CN.CLASSES = { + /** + * Class applied to the Calendar Navigator's bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV + * @type String + * @static + */ + NAV :"yui-cal-nav", + /** + * Class applied to the Calendar/CalendarGroup's bounding box to indicate + * the Navigator is currently visible + * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV_VISIBLE + * @type String + * @static + */ + NAV_VISIBLE: "yui-cal-nav-visible", + /** + * Class applied to the Navigator mask's bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.MASK + * @type String + * @static + */ + MASK : "yui-cal-nav-mask", + /** + * Class applied to the year label/control bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR + * @type String + * @static + */ + YEAR : "yui-cal-nav-y", + /** + * Class applied to the month label/control bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH + * @type String + * @static + */ + MONTH : "yui-cal-nav-m", + /** + * Class applied to the submit/cancel button's bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTONS + * @type String + * @static + */ + BUTTONS : "yui-cal-nav-b", + /** + * Class applied to buttons wrapping element + * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTON + * @type String + * @static + */ + BUTTON : "yui-cal-nav-btn", + /** + * Class applied to the validation error area's bounding box + * @property YAHOO.widget.CalendarNavigator.CLASSES.ERROR + * @type String + * @static + */ + ERROR : "yui-cal-nav-e", + /** + * Class applied to the year input control + * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR_CTRL + * @type String + * @static + */ + YEAR_CTRL : "yui-cal-nav-yc", + /** + * Class applied to the month input control + * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH_CTRL + * @type String + * @static + */ + MONTH_CTRL : "yui-cal-nav-mc", + /** + * Class applied to controls with invalid data (e.g. a year input field with invalid an year) + * @property YAHOO.widget.CalendarNavigator.CLASSES.INVALID + * @type String + * @static + */ + INVALID : "yui-invalid", + /** + * Class applied to default controls + * @property YAHOO.widget.CalendarNavigator.CLASSES.DEFAULT + * @type String + * @static + */ + DEFAULT : "yui-default" + }; + + /** + * Object literal containing the default configuration values for the CalendarNavigator + * The configuration object is expected to follow the format below, with the properties being + * case sensitive. + * <dl> + * <dt>strings</dt> + * <dd><em>Object</em> : An object with the properties shown below, defining the string labels to use in the Navigator's UI + * <dl> + * <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd> + * <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd> + * <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd> + * <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd> + * <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd> + * </dl> + * </dd> + * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd> + * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd> + * </dl> + * @property DEFAULT_CONFIG + * @type Object + * @static + */ + CN.DEFAULT_CONFIG = { + strings : { + month: "Month", + year: "Year", + submit: "Okay", + cancel: "Cancel", + invalidYear : "Year needs to be a number" + }, + monthFormat: YAHOO.widget.Calendar.LONG, + initialFocus: "year" + }; + + /** + * Object literal containing the default configuration values for the CalendarNavigator + * @property _DEFAULT_CFG + * @protected + * @deprecated Made public. See the public DEFAULT_CONFIG property + * @type Object + * @static + */ + CN._DEFAULT_CFG = CN.DEFAULT_CONFIG; + + + /** + * The suffix added to the Calendar/CalendarGroup's ID, to generate + * a unique ID for the Navigator and it's bounding box. + * @property YAHOO.widget.CalendarNavigator.ID_SUFFIX + * @static + * @type String + * @final + */ + CN.ID_SUFFIX = "_nav"; + /** + * The suffix added to the Navigator's ID, to generate + * a unique ID for the month control. + * @property YAHOO.widget.CalendarNavigator.MONTH_SUFFIX + * @static + * @type String + * @final + */ + CN.MONTH_SUFFIX = "_month"; + /** + * The suffix added to the Navigator's ID, to generate + * a unique ID for the year control. + * @property YAHOO.widget.CalendarNavigator.YEAR_SUFFIX + * @static + * @type String + * @final + */ + CN.YEAR_SUFFIX = "_year"; + /** + * The suffix added to the Navigator's ID, to generate + * a unique ID for the error bounding box. + * @property YAHOO.widget.CalendarNavigator.ERROR_SUFFIX + * @static + * @type String + * @final + */ + CN.ERROR_SUFFIX = "_error"; + /** + * The suffix added to the Navigator's ID, to generate + * a unique ID for the "Cancel" button. + * @property YAHOO.widget.CalendarNavigator.CANCEL_SUFFIX + * @static + * @type String + * @final + */ + CN.CANCEL_SUFFIX = "_cancel"; + /** + * The suffix added to the Navigator's ID, to generate + * a unique ID for the "Submit" button. + * @property YAHOO.widget.CalendarNavigator.SUBMIT_SUFFIX + * @static + * @type String + * @final + */ + CN.SUBMIT_SUFFIX = "_submit"; + + /** + * The number of digits to which the year input control is to be limited. + * @property YAHOO.widget.CalendarNavigator.YR_MAX_DIGITS + * @static + * @type Number + */ + CN.YR_MAX_DIGITS = 4; + + /** + * The amount by which to increment the current year value, + * when the arrow up/down key is pressed on the year control + * @property YAHOO.widget.CalendarNavigator.YR_MINOR_INC + * @static + * @type Number + */ + CN.YR_MINOR_INC = 1; + + /** + * The amount by which to increment the current year value, + * when the page up/down key is pressed on the year control + * @property YAHOO.widget.CalendarNavigator.YR_MAJOR_INC + * @static + * @type Number + */ + CN.YR_MAJOR_INC = 10; + + /** + * Artificial delay (in ms) between the time the Navigator is hidden + * and the Calendar/CalendarGroup state is updated. Allows the user + * the see the Calendar/CalendarGroup page changing. If set to 0 + * the Calendar/CalendarGroup page will be updated instantly + * @property YAHOO.widget.CalendarNavigator.UPDATE_DELAY + * @static + * @type Number + */ + CN.UPDATE_DELAY = 50; + + /** + * Regular expression used to validate the year input + * @property YAHOO.widget.CalendarNavigator.YR_PATTERN + * @static + * @type RegExp + */ + CN.YR_PATTERN = /^\d+$/; + /** + * Regular expression used to trim strings + * @property YAHOO.widget.CalendarNavigator.TRIM + * @static + * @type RegExp + */ + CN.TRIM = /^\s*(.*?)\s*$/; +})(); + +YAHOO.widget.CalendarNavigator.prototype = { + + /** + * The unique ID for this CalendarNavigator instance + * @property id + * @type String + */ + id : null, + + /** + * The Calendar/CalendarGroup instance to which the navigator belongs + * @property cal + * @type {Calendar|CalendarGroup} + */ + cal : null, + + /** + * Reference to the HTMLElement used to render the navigator's bounding box + * @property navEl + * @type HTMLElement + */ + navEl : null, + + /** + * Reference to the HTMLElement used to render the navigator's mask + * @property maskEl + * @type HTMLElement + */ + maskEl : null, + + /** + * Reference to the HTMLElement used to input the year + * @property yearEl + * @type HTMLElement + */ + yearEl : null, + + /** + * Reference to the HTMLElement used to input the month + * @property monthEl + * @type HTMLElement + */ + monthEl : null, + + /** + * Reference to the HTMLElement used to display validation errors + * @property errorEl + * @type HTMLElement + */ + errorEl : null, + + /** + * Reference to the HTMLElement used to update the Calendar/Calendar group + * with the month/year values + * @property submitEl + * @type HTMLElement + */ + submitEl : null, + + /** + * Reference to the HTMLElement used to hide the navigator without updating the + * Calendar/Calendar group + * @property cancelEl + * @type HTMLElement + */ + cancelEl : null, + + /** + * Reference to the first focusable control in the navigator (by default monthEl) + * @property firstCtrl + * @type HTMLElement + */ + firstCtrl : null, + + /** + * Reference to the last focusable control in the navigator (by default cancelEl) + * @property lastCtrl + * @type HTMLElement + */ + lastCtrl : null, + + /** + * The document containing the Calendar/Calendar group instance + * @protected + * @property _doc + * @type HTMLDocument + */ + _doc : null, + + /** + * Internal state property for the current year displayed in the navigator + * @protected + * @property _year + * @type Number + */ + _year: null, + + /** + * Internal state property for the current month index displayed in the navigator + * @protected + * @property _month + * @type Number + */ + _month: 0, + + /** + * Private internal state property which indicates whether or not the + * Navigator has been rendered. + * @private + * @property __rendered + * @type Boolean + */ + __rendered: false, + + /** + * Init lifecycle method called as part of construction + * + * @method init + * @param {Calendar} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached + */ + init : function(cal) { + var calBox = cal.oDomContainer; + + this.cal = cal; + this.id = calBox.id + YAHOO.widget.CalendarNavigator.ID_SUFFIX; + this._doc = calBox.ownerDocument; + + /** + * Private flag, to identify IE Quirks + * @private + * @property __isIEQuirks + */ + var ie = YAHOO.env.ua.ie; + this.__isIEQuirks = (ie && ((ie <= 6) || (this._doc.compatMode == "BackCompat"))); + }, + + /** + * Displays the navigator and mask, updating the input controls to reflect the + * currently set month and year. The show method will invoke the render method + * if the navigator has not been renderered already, allowing for lazy rendering + * of the control. + * + * The show method will fire the Calendar/CalendarGroup's beforeShowNav and showNav events + * + * @method show + */ + show : function() { + var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES; + + if (this.cal.beforeShowNavEvent.fire()) { + if (!this.__rendered) { + this.render(); + } + this.clearErrors(); + + this._updateMonthUI(); + this._updateYearUI(); + this._show(this.navEl, true); + + this.setInitialFocus(); + this.showMask(); + + YAHOO.util.Dom.addClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE); + this.cal.showNavEvent.fire(); + } + }, + + /** + * Hides the navigator and mask + * + * The show method will fire the Calendar/CalendarGroup's beforeHideNav event and hideNav events + * @method hide + */ + hide : function() { + var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES; + + if (this.cal.beforeHideNavEvent.fire()) { + this._show(this.navEl, false); + this.hideMask(); + YAHOO.util.Dom.removeClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE); + this.cal.hideNavEvent.fire(); + } + }, + + + /** + * Displays the navigator's mask element + * + * @method showMask + */ + showMask : function() { + this._show(this.maskEl, true); + if (this.__isIEQuirks) { + this._syncMask(); + } + }, + + /** + * Hides the navigator's mask element + * + * @method hideMask + */ + hideMask : function() { + this._show(this.maskEl, false); + }, + + /** + * Returns the current month set on the navigator + * + * Note: This may not be the month set in the UI, if + * the UI contains an invalid value. + * + * @method getMonth + * @return {Number} The Navigator's current month index + */ + getMonth: function() { + return this._month; + }, + + /** + * Returns the current year set on the navigator + * + * Note: This may not be the year set in the UI, if + * the UI contains an invalid value. + * + * @method getYear + * @return {Number} The Navigator's current year value + */ + getYear: function() { + return this._year; + }, + + /** + * Sets the current month on the Navigator, and updates the UI + * + * @method setMonth + * @param {Number} nMonth The month index, from 0 (Jan) through 11 (Dec). + */ + setMonth : function(nMonth) { + if (nMonth >= 0 && nMonth < 12) { + this._month = nMonth; + } + this._updateMonthUI(); + }, + + /** + * Sets the current year on the Navigator, and updates the UI. If the + * provided year is invalid, it will not be set. + * + * @method setYear + * @param {Number} nYear The full year value to set the Navigator to. + */ + setYear : function(nYear) { + var yrPattern = YAHOO.widget.CalendarNavigator.YR_PATTERN; + if (YAHOO.lang.isNumber(nYear) && yrPattern.test(nYear+"")) { + this._year = nYear; + } + this._updateYearUI(); + }, + + /** + * Renders the HTML for the navigator, adding it to the + * document and attaches event listeners if it has not + * already been rendered. + * + * @method render + */ + render: function() { + this.cal.beforeRenderNavEvent.fire(); + if (!this.__rendered) { + this.createNav(); + this.createMask(); + this.applyListeners(); + this.__rendered = true; + } + this.cal.renderNavEvent.fire(); + }, + + /** + * Creates the navigator's containing HTMLElement, it's contents, and appends + * the containg element to the Calendar/CalendarGroup's container. + * + * @method createNav + */ + createNav : function() { + var NAV = YAHOO.widget.CalendarNavigator; + var doc = this._doc; + + var d = doc.createElement("div"); + d.className = NAV.CLASSES.NAV; + + var htmlBuf = this.renderNavContents([]); + + d.innerHTML = htmlBuf.join(''); + this.cal.oDomContainer.appendChild(d); + + this.navEl = d; + + this.yearEl = doc.getElementById(this.id + NAV.YEAR_SUFFIX); + this.monthEl = doc.getElementById(this.id + NAV.MONTH_SUFFIX); + this.errorEl = doc.getElementById(this.id + NAV.ERROR_SUFFIX); + this.submitEl = doc.getElementById(this.id + NAV.SUBMIT_SUFFIX); + this.cancelEl = doc.getElementById(this.id + NAV.CANCEL_SUFFIX); + + if (YAHOO.env.ua.gecko && this.yearEl && this.yearEl.type == "text") { + // Avoid XUL error on focus, select [ https://bugzilla.mozilla.org/show_bug.cgi?id=236791, + // supposedly fixed in 1.8.1, but there are reports of it still being around for methods other than blur ] + this.yearEl.setAttribute("autocomplete", "off"); + } + + this._setFirstLastElements(); + }, + + /** + * Creates the Mask HTMLElement and appends it to the Calendar/CalendarGroups + * container. + * + * @method createMask + */ + createMask : function() { + var C = YAHOO.widget.CalendarNavigator.CLASSES; + + var d = this._doc.createElement("div"); + d.className = C.MASK; + + this.cal.oDomContainer.appendChild(d); + this.maskEl = d; + }, + + /** + * Used to set the width/height of the mask in pixels to match the Calendar Container. + * Currently only used for IE6 or IE in quirks mode. The other A-Grade browser are handled using CSS (width/height 100%). + * <p> + * The method is also registered as an HTMLElement resize listener on the Calendars container element. + * </p> + * @protected + * @method _syncMask + */ + _syncMask : function() { + var c = this.cal.oDomContainer; + if (c && this.maskEl) { + var r = YAHOO.util.Dom.getRegion(c); + YAHOO.util.Dom.setStyle(this.maskEl, "width", r.right - r.left + "px"); + YAHOO.util.Dom.setStyle(this.maskEl, "height", r.bottom - r.top + "px"); + } + }, + + /** + * Renders the contents of the navigator + * + * @method renderNavContents + * + * @param {Array} html The HTML buffer to append the HTML to. + * @return {Array} A reference to the buffer passed in. + */ + renderNavContents : function(html) { + var NAV = YAHOO.widget.CalendarNavigator, + C = NAV.CLASSES, + h = html; // just to use a shorter name + + h[h.length] = '<div class="' + C.MONTH + '">'; + this.renderMonth(h); + h[h.length] = '</div>'; + h[h.length] = '<div class="' + C.YEAR + '">'; + this.renderYear(h); + h[h.length] = '</div>'; + h[h.length] = '<div class="' + C.BUTTONS + '">'; + this.renderButtons(h); + h[h.length] = '</div>'; + h[h.length] = '<div class="' + C.ERROR + '" id="' + this.id + NAV.ERROR_SUFFIX + '"></div>'; + + return h; + }, + + /** + * Renders the month label and control for the navigator + * + * @method renderNavContents + * @param {Array} html The HTML buffer to append the HTML to. + * @return {Array} A reference to the buffer passed in. + */ + renderMonth : function(html) { + var NAV = YAHOO.widget.CalendarNavigator, + C = NAV.CLASSES; + + var id = this.id + NAV.MONTH_SUFFIX, + mf = this.__getCfg("monthFormat"), + months = this.cal.cfg.getProperty((mf == YAHOO.widget.Calendar.SHORT) ? "MONTHS_SHORT" : "MONTHS_LONG"), + h = html; + + if (months && months.length > 0) { + h[h.length] = '<label for="' + id + '">'; + h[h.length] = this.__getCfg("month", true); + h[h.length] = '</label>'; + h[h.length] = '<select name="' + id + '" id="' + id + '" class="' + C.MONTH_CTRL + '">'; + for (var i = 0; i < months.length; i++) { + h[h.length] = '<option value="' + i + '">'; + h[h.length] = months[i]; + h[h.length] = '</option>'; + } + h[h.length] = '</select>'; + } + return h; + }, + + /** + * Renders the year label and control for the navigator + * + * @method renderYear + * @param {Array} html The HTML buffer to append the HTML to. + * @return {Array} A reference to the buffer passed in. + */ + renderYear : function(html) { + var NAV = YAHOO.widget.CalendarNavigator, + C = NAV.CLASSES; + + var id = this.id + NAV.YEAR_SUFFIX, + size = NAV.YR_MAX_DIGITS, + h = html; + + h[h.length] = '<label for="' + id + '">'; + h[h.length] = this.__getCfg("year", true); + h[h.length] = '</label>'; + h[h.length] = '<input type="text" name="' + id + '" id="' + id + '" class="' + C.YEAR_CTRL + '" maxlength="' + size + '"/>'; + return h; + }, + + /** + * Renders the submit/cancel buttons for the navigator + * + * @method renderButton + * @return {String} The HTML created for the Button UI + */ + renderButtons : function(html) { + var C = YAHOO.widget.CalendarNavigator.CLASSES; + var h = html; + + h[h.length] = '<span class="' + C.BUTTON + ' ' + C.DEFAULT + '">'; + h[h.length] = '<button type="button" id="' + this.id + '_submit' + '">'; + h[h.length] = this.__getCfg("submit", true); + h[h.length] = '</button>'; + h[h.length] = '</span>'; + h[h.length] = '<span class="' + C.BUTTON +'">'; + h[h.length] = '<button type="button" id="' + this.id + '_cancel' + '">'; + h[h.length] = this.__getCfg("cancel", true); + h[h.length] = '</button>'; + h[h.length] = '</span>'; + + return h; + }, + + /** + * Attaches DOM event listeners to the rendered elements + * <p> + * The method will call applyKeyListeners, to setup keyboard specific + * listeners + * </p> + * @method applyListeners + */ + applyListeners : function() { + var E = YAHOO.util.Event; + + function yearUpdateHandler() { + if (this.validate()) { + this.setYear(this._getYearFromUI()); + } + } + + function monthUpdateHandler() { + this.setMonth(this._getMonthFromUI()); + } + + E.on(this.submitEl, "click", this.submit, this, true); + E.on(this.cancelEl, "click", this.cancel, this, true); + E.on(this.yearEl, "blur", yearUpdateHandler, this, true); + E.on(this.monthEl, "change", monthUpdateHandler, this, true); + + if (this.__isIEQuirks) { + YAHOO.util.Event.on(this.cal.oDomContainer, "resize", this._syncMask, this, true); + } + + this.applyKeyListeners(); + }, + + /** + * Removes/purges DOM event listeners from the rendered elements + * + * @method purgeListeners + */ + purgeListeners : function() { + var E = YAHOO.util.Event; + E.removeListener(this.submitEl, "click", this.submit); + E.removeListener(this.cancelEl, "click", this.cancel); + E.removeListener(this.yearEl, "blur"); + E.removeListener(this.monthEl, "change"); + if (this.__isIEQuirks) { + E.removeListener(this.cal.oDomContainer, "resize", this._syncMask); + } + + this.purgeKeyListeners(); + }, + + /** + * Attaches DOM listeners for keyboard support. + * Tab/Shift-Tab looping, Enter Key Submit on Year element, + * Up/Down/PgUp/PgDown year increment on Year element + * <p> + * NOTE: MacOSX Safari 2.x doesn't let you tab to buttons and + * MacOSX Gecko does not let you tab to buttons or select controls, + * so for these browsers, Tab/Shift-Tab looping is limited to the + * elements which can be reached using the tab key. + * </p> + * @method applyKeyListeners + */ + applyKeyListeners : function() { + var E = YAHOO.util.Event, + ua = YAHOO.env.ua; + + // IE/Safari 3.1 doesn't fire keypress for arrow/pg keys (non-char keys) + var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress"; + + // - IE/Safari 3.1 doesn't fire keypress for non-char keys + // - Opera doesn't allow us to cancel keydown or keypress for tab, but + // changes focus successfully on keydown (keypress is too late to change focus - opera's already moved on). + var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress"; + + // Everyone likes keypress for Enter (char keys) - whoo hoo! + E.on(this.yearEl, "keypress", this._handleEnterKey, this, true); + + E.on(this.yearEl, arrowEvt, this._handleDirectionKeys, this, true); + E.on(this.lastCtrl, tabEvt, this._handleTabKey, this, true); + E.on(this.firstCtrl, tabEvt, this._handleShiftTabKey, this, true); + }, + + /** + * Removes/purges DOM listeners for keyboard support + * + * @method purgeKeyListeners + */ + purgeKeyListeners : function() { + var E = YAHOO.util.Event, + ua = YAHOO.env.ua; + + var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress"; + var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress"; + + E.removeListener(this.yearEl, "keypress", this._handleEnterKey); + E.removeListener(this.yearEl, arrowEvt, this._handleDirectionKeys); + E.removeListener(this.lastCtrl, tabEvt, this._handleTabKey); + E.removeListener(this.firstCtrl, tabEvt, this._handleShiftTabKey); + }, + + /** + * Updates the Calendar/CalendarGroup's pagedate with the currently set month and year if valid. + * <p> + * If the currently set month/year is invalid, a validation error will be displayed and the + * Calendar/CalendarGroup's pagedate will not be updated. + * </p> + * @method submit + */ + submit : function() { + if (this.validate()) { + this.hide(); + + this.setMonth(this._getMonthFromUI()); + this.setYear(this._getYearFromUI()); + + var cal = this.cal; + + // Artificial delay, just to help the user see something changed + var delay = YAHOO.widget.CalendarNavigator.UPDATE_DELAY; + if (delay > 0) { + var nav = this; + window.setTimeout(function(){ nav._update(cal); }, delay); + } else { + this._update(cal); + } + } + }, + + /** + * Updates the Calendar rendered state, based on the state of the CalendarNavigator + * @method _update + * @param cal The Calendar instance to update + * @protected + */ + _update : function(cal) { + var date = YAHOO.widget.DateMath.getDate(this.getYear() - cal.cfg.getProperty("YEAR_OFFSET"), this.getMonth(), 1); + cal.cfg.setProperty("pagedate", date); + cal.render(); + }, + + /** + * Hides the navigator and mask, without updating the Calendar/CalendarGroup's state + * + * @method cancel + */ + cancel : function() { + this.hide(); + }, + + /** + * Validates the current state of the UI controls + * + * @method validate + * @return {Boolean} true, if the current UI state contains valid values, false if not + */ + validate : function() { + if (this._getYearFromUI() !== null) { + this.clearErrors(); + return true; + } else { + this.setYearError(); + this.setError(this.__getCfg("invalidYear", true)); + return false; + } + }, + + /** + * Displays an error message in the Navigator's error panel + * @method setError + * @param {String} msg The error message to display + */ + setError : function(msg) { + if (this.errorEl) { + this.errorEl.innerHTML = msg; + this._show(this.errorEl, true); + } + }, + + /** + * Clears the navigator's error message and hides the error panel + * @method clearError + */ + clearError : function() { + if (this.errorEl) { + this.errorEl.innerHTML = ""; + this._show(this.errorEl, false); + } + }, + + /** + * Displays the validation error UI for the year control + * @method setYearError + */ + setYearError : function() { + YAHOO.util.Dom.addClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID); + }, + + /** + * Removes the validation error UI for the year control + * @method clearYearError + */ + clearYearError : function() { + YAHOO.util.Dom.removeClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID); + }, + + /** + * Clears all validation and error messages in the UI + * @method clearErrors + */ + clearErrors : function() { + this.clearError(); + this.clearYearError(); + }, + + /** + * Sets the initial focus, based on the configured value + * @method setInitialFocus + */ + setInitialFocus : function() { + var el = this.submitEl, + f = this.__getCfg("initialFocus"); + + if (f && f.toLowerCase) { + f = f.toLowerCase(); + if (f == "year") { + el = this.yearEl; + try { + this.yearEl.select(); + } catch (selErr) { + // Ignore; + } + } else if (f == "month") { + el = this.monthEl; + } + } + + if (el && YAHOO.lang.isFunction(el.focus)) { + try { + el.focus(); + } catch (focusErr) { + // TODO: Fall back if focus fails? + } + } + }, + + /** + * Removes all renderered HTML elements for the Navigator from + * the DOM, purges event listeners and clears (nulls) any property + * references to HTML references + * @method erase + */ + erase : function() { + if (this.__rendered) { + this.purgeListeners(); + + // Clear out innerHTML references + this.yearEl = null; + this.monthEl = null; + this.errorEl = null; + this.submitEl = null; + this.cancelEl = null; + this.firstCtrl = null; + this.lastCtrl = null; + if (this.navEl) { + this.navEl.innerHTML = ""; + } + + var p = this.navEl.parentNode; + if (p) { + p.removeChild(this.navEl); + } + this.navEl = null; + + var pm = this.maskEl.parentNode; + if (pm) { + pm.removeChild(this.maskEl); + } + this.maskEl = null; + this.__rendered = false; + } + }, + + /** + * Destroys the Navigator object and any HTML references + * @method destroy + */ + destroy : function() { + this.erase(); + this._doc = null; + this.cal = null; + this.id = null; + }, + + /** + * Protected implementation to handle how UI elements are + * hidden/shown. + * + * @method _show + * @protected + */ + _show : function(el, bShow) { + if (el) { + YAHOO.util.Dom.setStyle(el, "display", (bShow) ? "block" : "none"); + } + }, + + /** + * Returns the month value (index), from the month UI element + * @protected + * @method _getMonthFromUI + * @return {Number} The month index, or 0 if a UI element for the month + * is not found + */ + _getMonthFromUI : function() { + if (this.monthEl) { + return this.monthEl.selectedIndex; + } else { + return 0; // Default to Jan + } + }, + + /** + * Returns the year value, from the Navitator's year UI element + * @protected + * @method _getYearFromUI + * @return {Number} The year value set in the UI, if valid. null is returned if + * the UI does not contain a valid year value. + */ + _getYearFromUI : function() { + var NAV = YAHOO.widget.CalendarNavigator; + + var yr = null; + if (this.yearEl) { + var value = this.yearEl.value; + value = value.replace(NAV.TRIM, "$1"); + + if (NAV.YR_PATTERN.test(value)) { + yr = parseInt(value, 10); + } + } + return yr; + }, + + /** + * Updates the Navigator's year UI, based on the year value set on the Navigator object + * @protected + * @method _updateYearUI + */ + _updateYearUI : function() { + if (this.yearEl && this._year !== null) { + this.yearEl.value = this._year; + } + }, + + /** + * Updates the Navigator's month UI, based on the month value set on the Navigator object + * @protected + * @method _updateMonthUI + */ + _updateMonthUI : function() { + if (this.monthEl) { + this.monthEl.selectedIndex = this._month; + } + }, + + /** + * Sets up references to the first and last focusable element in the Navigator's UI + * in terms of tab order (Naviagator's firstEl and lastEl properties). The references + * are used to control modality by looping around from the first to the last control + * and visa versa for tab/shift-tab navigation. + * <p> + * See <a href="#applyKeyListeners">applyKeyListeners</a> + * </p> + * @protected + * @method _setFirstLastElements + */ + _setFirstLastElements : function() { + this.firstCtrl = this.monthEl; + this.lastCtrl = this.cancelEl; + + // Special handling for MacOSX. + // - Safari 2.x can't focus on buttons + // - Gecko can't focus on select boxes or buttons + if (this.__isMac) { + if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420){ + this.firstCtrl = this.monthEl; + this.lastCtrl = this.yearEl; + } + if (YAHOO.env.ua.gecko) { + this.firstCtrl = this.yearEl; + this.lastCtrl = this.yearEl; + } + } + }, + + /** + * Default Keyboard event handler to capture Enter + * on the Navigator's year control (yearEl) + * + * @method _handleEnterKey + * @protected + * @param {Event} e The DOM event being handled + */ + _handleEnterKey : function(e) { + var KEYS = YAHOO.util.KeyListener.KEY; + + if (YAHOO.util.Event.getCharCode(e) == KEYS.ENTER) { + YAHOO.util.Event.preventDefault(e); + this.submit(); + } + }, + + /** + * Default Keyboard event handler to capture up/down/pgup/pgdown + * on the Navigator's year control (yearEl). + * + * @method _handleDirectionKeys + * @protected + * @param {Event} e The DOM event being handled + */ + _handleDirectionKeys : function(e) { + var E = YAHOO.util.Event, + KEYS = YAHOO.util.KeyListener.KEY, + NAV = YAHOO.widget.CalendarNavigator; + + var value = (this.yearEl.value) ? parseInt(this.yearEl.value, 10) : null; + if (isFinite(value)) { + var dir = false; + switch(E.getCharCode(e)) { + case KEYS.UP: + this.yearEl.value = value + NAV.YR_MINOR_INC; + dir = true; + break; + case KEYS.DOWN: + this.yearEl.value = Math.max(value - NAV.YR_MINOR_INC, 0); + dir = true; + break; + case KEYS.PAGE_UP: + this.yearEl.value = value + NAV.YR_MAJOR_INC; + dir = true; + break; + case KEYS.PAGE_DOWN: + this.yearEl.value = Math.max(value - NAV.YR_MAJOR_INC, 0); + dir = true; + break; + default: + break; + } + if (dir) { + E.preventDefault(e); + try { + this.yearEl.select(); + } catch(err) { + // Ignore + } + } + } + }, + + /** + * Default Keyboard event handler to capture Tab + * on the last control (lastCtrl) in the Navigator. + * + * @method _handleTabKey + * @protected + * @param {Event} e The DOM event being handled + */ + _handleTabKey : function(e) { + var E = YAHOO.util.Event, + KEYS = YAHOO.util.KeyListener.KEY; + + if (E.getCharCode(e) == KEYS.TAB && !e.shiftKey) { + try { + E.preventDefault(e); + this.firstCtrl.focus(); + } catch (err) { + // Ignore - mainly for focus edge cases + } + } + }, + + /** + * Default Keyboard event handler to capture Shift-Tab + * on the first control (firstCtrl) in the Navigator. + * + * @method _handleShiftTabKey + * @protected + * @param {Event} e The DOM event being handled + */ + _handleShiftTabKey : function(e) { + var E = YAHOO.util.Event, + KEYS = YAHOO.util.KeyListener.KEY; + + if (e.shiftKey && E.getCharCode(e) == KEYS.TAB) { + try { + E.preventDefault(e); + this.lastCtrl.focus(); + } catch (err) { + // Ignore - mainly for focus edge cases + } + } + }, + + /** + * Retrieve Navigator configuration values from + * the parent Calendar/CalendarGroup's config value. + * <p> + * If it has not been set in the user provided configuration, the method will + * return the default value of the configuration property, as set in DEFAULT_CONFIG + * </p> + * @private + * @method __getCfg + * @param {String} Case sensitive property name. + * @param {Boolean} true, if the property is a string property, false if not. + * @return The value of the configuration property + */ + __getCfg : function(prop, bIsStr) { + var DEF_CFG = YAHOO.widget.CalendarNavigator.DEFAULT_CONFIG; + var cfg = this.cal.cfg.getProperty("navigator"); + + if (bIsStr) { + return (cfg !== true && cfg.strings && cfg.strings[prop]) ? cfg.strings[prop] : DEF_CFG.strings[prop]; + } else { + return (cfg !== true && cfg[prop]) ? cfg[prop] : DEF_CFG[prop]; + } + }, + + /** + * Private flag, to identify MacOS + * @private + * @property __isMac + */ + __isMac : (navigator.userAgent.toLowerCase().indexOf("macintosh") != -1) + +}; +YAHOO.register("calendar", YAHOO.widget.Calendar, {version: "2.8.0r4", build: "2449"});