/** * The CalendarBase submodule is a basic UI calendar view that displays * a range of dates in a two-dimensional month grid, with one or more * months visible at a single time. CalendarBase supports custom date * rendering, multiple calendar panes, and selection. * @module calendar * @submodule calendar-base */ var getCN = Y.ClassNameManager.getClassName, CALENDAR = 'calendar', CAL_GRID = getCN(CALENDAR, 'grid'), CAL_LEFT_GRID = getCN(CALENDAR, 'left-grid'), CAL_RIGHT_GRID = getCN(CALENDAR, 'right-grid'), CAL_BODY = getCN(CALENDAR, 'body'), CAL_HD = getCN(CALENDAR, 'header'), CAL_HD_LABEL = getCN(CALENDAR, 'header-label'), CAL_WDAYROW = getCN(CALENDAR, 'weekdayrow'), CAL_WDAY = getCN(CALENDAR, 'weekday'), CAL_COL_HIDDEN = getCN(CALENDAR, 'column-hidden'), CAL_DAY_SELECTED = getCN(CALENDAR, 'day-selected'), SELECTION_DISABLED = getCN(CALENDAR, 'selection-disabled'), CAL_ROW = getCN(CALENDAR, 'row'), CAL_DAY = getCN(CALENDAR, 'day'), CAL_PREVMONTH_DAY = getCN(CALENDAR, 'prevmonth-day'), CAL_NEXTMONTH_DAY = getCN(CALENDAR, 'nextmonth-day'), CAL_ANCHOR = getCN(CALENDAR, 'anchor'), CAL_PANE = getCN(CALENDAR, 'pane'), CAL_STATUS = getCN(CALENDAR, 'status'), L = Y.Lang, substitute = L.sub, arrayEach = Y.Array.each, objEach = Y.Object.each, iOf = Y.Array.indexOf, hasKey = Y.Object.hasKey, setVal = Y.Object.setValue, isEmpty = Y.Object.isEmpty, ydate = Y.DataType.Date; /** Create a calendar view to represent a single or multiple * month range of dates, rendered as a grid with date and * weekday labels. * * @class CalendarBase * @extends Widget * @param config {Object} Configuration object (see Configuration * attributes) * @constructor */ function CalendarBase() { CalendarBase.superclass.constructor.apply ( this, arguments ); } Y.CalendarBase = Y.extend( CalendarBase, Y.Widget, { /** * A storage for various properties of individual month * panes. * * @property _paneProperties * @type Object * @private */ _paneProperties : {}, /** * The number of month panes in the calendar, deduced * from the CONTENT_TEMPLATE's number of {calendar_grid} * tokens. * * @property _paneNumber * @type Number * @private */ _paneNumber : 1, /** * The unique id used to prefix various elements of this * calendar instance. * * @property _calendarId * @type String * @private */ _calendarId : null, /** * The hash map of selected dates, populated with * selectDates() and deselectDates() methods * * @property _selectedDates * @type Object * @private */ _selectedDates : {}, /** * A private copy of the rules object, populated * by setting the customRenderer attribute. * * @property _rules * @type Object * @private */ _rules : {}, /** * A private copy of the filterFunction, populated * by setting the customRenderer attribute. * * @property _filterFunction * @type Function * @private */ _filterFunction : null, /** * Storage for calendar cells modified by any custom * formatting. The storage is cleared, used to restore * cells to the original state, and repopulated accordingly * when the calendar is rerendered. * * @property _storedDateCells * @type Object * @private */ _storedDateCells : {}, /** * Designated initializer * Initializes instance-level properties of * calendar. * * @method initializer */ initializer : function () { this._paneProperties = {}; this._calendarId = Y.guid('calendar'); this._selectedDates = {}; if (isEmpty(this._rules)) { this._rules = {}; } this._storedDateCells = {}; }, /** * renderUI implementation * * Creates a visual representation of the calendar based on existing parameters. * @method renderUI */ renderUI : function () { var contentBox = this.get('contentBox'); contentBox.appendChild(this._initCalendarHTML(this.get('date'))); if (this.get('showPrevMonth')) { this._afterShowPrevMonthChange(); } if (this.get('showNextMonth')) { this._afterShowNextMonthChange(); } this._renderCustomRules(); this._renderSelectedDates(); this.get("boundingBox").setAttribute("aria-labelledby", this._calendarId + "_header"); }, /** * bindUI implementation * * Assigns listeners to relevant events that change the state * of the calendar. * @method bindUI */ bindUI : function () { this.after('dateChange', this._afterDateChange); this.after('showPrevMonthChange', this._afterShowPrevMonthChange); this.after('showNextMonthChange', this._afterShowNextMonthChange); this.after('headerRendererChange', this._afterHeaderRendererChange); this.after('customRendererChange', this._afterCustomRendererChange); this.after('enabledDatesRuleChange', this._afterCustomRendererChange); this.after('disabledDatesRuleChange', this._afterCustomRendererChange); this.after('focusedChange', this._afterFocusedChange); this.after('selectionChange', this._renderSelectedDates); this._bindCalendarEvents(); }, /** * An internal utility method that generates a list of selected dates * from the hash storage. * * @method _getSelectedDatesList * @protected * @return {Array} The array of `Date`s that are currently selected. */ _getSelectedDatesList : function () { var output = []; objEach (this._selectedDates, function (year) { objEach (year, function (month) { objEach (month, function (day) { output.push (day); }, this); }, this); }, this); return output; }, /** * A utility method that returns all dates selected in a specific month. * * @method _getSelectedDatesInMonth * @param {Date} oDate corresponding to the month for which selected dates * are requested. * @protected * @return {Array} The array of `Date`s in a given month that are currently selected. */ _getSelectedDatesInMonth : function (oDate) { var year = oDate.getFullYear(), month = oDate.getMonth(); if (hasKey(this._selectedDates, year) && hasKey(this._selectedDates[year], month)) { return Y.Object.values(this._selectedDates[year][month]); } else { return []; } }, /** * An internal parsing method that receives a String list of numbers * and number ranges (of the form "1,2,3,4-6,7-9,10,11" etc.) and checks * whether a specific number is included in this list. Used for looking * up dates in the customRenderer rule set. * * @method _isNumInList * @param {Number} num The number to look for in a list. * @param {String} strList The list of numbers of the form "1,2,3,4-6,7-8,9", etc. * @private * @return {boolean} Returns true if the given number is in the given list. */ _isNumInList : function (num, strList) { if (strList === "all") { return true; } else { var elements = strList.split(","), i = elements.length, range; while (i--) { range = elements[i].split("-"); if (range.length === 2 && num >= parseInt(range[0], 10) && num <= parseInt(range[1], 10)) { return true; } else if (range.length === 1 && (parseInt(elements[i], 10) === num)) { return true; } } return false; } }, /** * Given a specific date, returns an array of rules (from the customRenderer rule set) * that the given date matches. * * @method _getRulesForDate * @param {Date} oDate The date for which an array of rules is needed * @private * @return {Array} Returns an array of `String`s, each containg the name of * a rule that the given date matches. */ _getRulesForDate : function (oDate) { var year = oDate.getFullYear(), month = oDate.getMonth(), date = oDate.getDate(), wday = oDate.getDay(), rules = this._rules, outputRules = [], years, months, dates, days; for (years in rules) { if (this._isNumInList(year, years)) { if (L.isString(rules[years])) { outputRules.push(rules[years]); } else { for (months in rules[years]) { if (this._isNumInList(month, months)) { if (L.isString(rules[years][months])) { outputRules.push(rules[years][months]); } else { for (dates in rules[years][months]) { if (this._isNumInList(date, dates)) { if (L.isString(rules[years][months][dates])) { outputRules.push(rules[years][months][dates]); } else { for (days in rules[years][months][dates]) { if (this._isNumInList(wday, days)) { if (L.isString(rules[years][months][dates][days])) { outputRules.push(rules[years][months][dates][days]); } } } } } } } } } } } } return outputRules; }, /** * A utility method which, given a specific date and a name of the rule, * checks whether the date matches the given rule. * * @method _matchesRule * @param {Date} oDate The date to check * @param {String} rule The name of the rule that the date should match. * @private * @return {boolean} Returns true if the date matches the given rule. * */ _matchesRule : function (oDate, rule) { return (iOf(this._getRulesForDate(oDate), rule) >= 0); }, /** * A utility method which checks whether a given date matches the `enabledDatesRule` * or does not match the `disabledDatesRule` and therefore whether it can be selected. * @method _canBeSelected * @param {Date} oDate The date to check * @private * @return {boolean} Returns true if the date can be selected; false otherwise. */ _canBeSelected : function (oDate) { var enabledDatesRule = this.get("enabledDatesRule"), disabledDatesRule = this.get("disabledDatesRule"); if (enabledDatesRule) { return this._matchesRule(oDate, enabledDatesRule); } else if (disabledDatesRule) { return !this._matchesRule(oDate, disabledDatesRule); } else { return true; } }, /** * Selects a given date or array of dates. * @method selectDates * @param {Date|Array} dates A `Date` or `Array` of `Date`s. * @return {CalendarBase} A reference to this object * @chainable */ selectDates : function (dates) { if (ydate.isValidDate(dates)) { this._addDateToSelection(dates); } else if (L.isArray(dates)) { this._addDatesToSelection(dates); } return this; }, /** * Deselects a given date or array of dates, or deselects * all dates if no argument is specified. * @method deselectDates * @param {Date|Array} [dates] A `Date` or `Array` of `Date`s, or no * argument if all dates should be deselected. * @return {CalendarBase} A reference to this object * @chainable */ deselectDates : function (dates) { if (!dates) { this._clearSelection(); } else if (ydate.isValidDate(dates)) { this._removeDateFromSelection(dates); } else if (L.isArray(dates)) { this._removeDatesFromSelection(dates); } return this; }, /** * A utility method that adds a given date to selection.. * @method _addDateToSelection * @param {Date} oDate The date to add to selection. * @param {Number} [index] An optional parameter that is used * to differentiate between individual date selections and multiple * date selections. * @private */ _addDateToSelection : function (oDate, index) { oDate = this._normalizeTime(oDate); if (this._canBeSelected(oDate)) { var year = oDate.getFullYear(), month = oDate.getMonth(), day = oDate.getDate(); if (hasKey(this._selectedDates, year)) { if (hasKey(this._selectedDates[year], month)) { this._selectedDates[year][month][day] = oDate; } else { this._selectedDates[year][month] = {}; this._selectedDates[year][month][day] = oDate; } } else { this._selectedDates[year] = {}; this._selectedDates[year][month] = {}; this._selectedDates[year][month][day] = oDate; } this._selectedDates = setVal(this._selectedDates, [year, month, day], oDate); if (!index) { this._fireSelectionChange(); } } }, /** * A utility method that adds a given list of dates to selection. * @method _addDatesToSelection * @param {Array} datesArray The list of dates to add to selection. * @private */ _addDatesToSelection : function (datesArray) { arrayEach(datesArray, this._addDateToSelection, this); this._fireSelectionChange(); }, /** * A utility method that adds a given range of dates to selection. * @method _addDateRangeToSelection * @param {Date} startDate The first date of the given range. * @param {Date} endDate The last date of the given range. * @private */ _addDateRangeToSelection : function (startDate, endDate) { var timezoneDifference = (endDate.getTimezoneOffset() - startDate.getTimezoneOffset())*60000, startTime = startDate.getTime(), endTime = endDate.getTime(), tempTime, time, addedDate; if (startTime > endTime) { tempTime = startTime; startTime = endTime; endTime = tempTime + timezoneDifference; } else { endTime = endTime - timezoneDifference; } for (time = startTime; time <= endTime; time += 86400000) { addedDate = new Date(time); addedDate.setHours(12); this._addDateToSelection(addedDate, time); } this._fireSelectionChange(); }, /** * A utility method that removes a given date from selection.. * @method _removeDateFromSelection * @param {Date} oDate The date to remove from selection. * @param {Number} [index] An optional parameter that is used * to differentiate between individual date selections and multiple * date selections. * @private */ _removeDateFromSelection : function (oDate, index) { var year = oDate.getFullYear(), month = oDate.getMonth(), day = oDate.getDate(); if (hasKey(this._selectedDates, year) && hasKey(this._selectedDates[year], month) && hasKey(this._selectedDates[year][month], day) ) { delete this._selectedDates[year][month][day]; if (!index) { this._fireSelectionChange(); } } }, /** * A utility method that removes a given list of dates from selection. * @method _removeDatesFromSelection * @param {Array} datesArray The list of dates to remove from selection. * @private */ _removeDatesFromSelection : function (datesArray) { arrayEach(datesArray, this._removeDateFromSelection, this); this._fireSelectionChange(); }, /** * A utility method that removes a given range of dates from selection. * @method _removeDateRangeFromSelection * @param {Date} startDate The first date of the given range. * @param {Date} endDate The last date of the given range. * @private */ _removeDateRangeFromSelection : function (startDate, endDate) { var startTime = startDate.getTime(), endTime = endDate.getTime(), time; for (time = startTime; time <= endTime; time += 86400000) { this._removeDateFromSelection(new Date(time), time); } this._fireSelectionChange(); }, /** * A utility method that removes all dates from selection. * @method _clearSelection * @param {boolean} noevent A Boolean specifying whether a selectionChange * event should be fired. If true, the event is not fired. * @private */ _clearSelection : function (noevent) { this._selectedDates = {}; this.get("contentBox").all("." + CAL_DAY_SELECTED).removeClass(CAL_DAY_SELECTED).setAttribute("aria-selected", false); if (!noevent) { this._fireSelectionChange(); } }, /** * A utility method that fires a selectionChange event. * @method _fireSelectionChange * @private */ _fireSelectionChange : function () { /** * Fired when the set of selected dates changes. Contains a payload with * a `newSelection` property with an array of selected dates. * * @event selectionChange */ this.fire("selectionChange", {newSelection: this._getSelectedDatesList()}); }, /** * A utility method that restores cells modified by custom formatting. * @method _restoreModifiedCells * @private */ _restoreModifiedCells : function () { var contentbox = this.get("contentBox"), id; for (id in this._storedDateCells) { contentbox.one("#" + id).replace(this._storedDateCells[id]); delete this._storedDateCells[id]; } }, /** * A rendering assist method that renders all cells modified by the customRenderer * rules, as well as the enabledDatesRule and disabledDatesRule. * @method _renderCustomRules * @private */ _renderCustomRules : function () { this.get("contentBox").all("." + CAL_DAY + ",." + CAL_NEXTMONTH_DAY).removeClass(SELECTION_DISABLED).setAttribute("aria-disabled", false); if (!isEmpty(this._rules)) { var paneNum, paneDate, dateArray; for (paneNum = 0; paneNum < this._paneNumber; paneNum++) { paneDate = ydate.addMonths(this.get("date"), paneNum); dateArray = ydate.listOfDatesInMonth(paneDate); arrayEach(dateArray, Y.bind(this._renderCustomRulesHelper, this)); } } }, /** * A handler for a date selection event (either a click or a keyboard * selection) that adds the appropriate CSS class to a specific DOM * node corresponding to the date and sets its aria-selected * attribute to true. * * @method _renderCustomRulesHelper * @private */ _renderCustomRulesHelper: function (date) { var enRule = this.get("enabledDatesRule"), disRule = this.get("disabledDatesRule"), matchingRules, dateNode; matchingRules = this._getRulesForDate(date); if (matchingRules.length > 0) { if ((enRule && iOf(matchingRules, enRule) < 0) || (!enRule && disRule && iOf(matchingRules, disRule) >= 0)) { this._disableDate(date); } if (L.isFunction(this._filterFunction)) { dateNode = this._dateToNode(date); this._storedDateCells[dateNode.get("id")] = dateNode.cloneNode(true); this._filterFunction (date, dateNode, matchingRules); } } else if (enRule) { this._disableDate(date); } }, /** * A rendering assist method that renders all cells that are currently selected. * @method _renderSelectedDates * @private */ _renderSelectedDates : function () { this.get("contentBox").all("." + CAL_DAY_SELECTED).removeClass(CAL_DAY_SELECTED).setAttribute("aria-selected", false); var paneNum, paneDate, dateArray; for (paneNum = 0; paneNum < this._paneNumber; paneNum++) { paneDate = ydate.addMonths(this.get("date"), paneNum); dateArray = this._getSelectedDatesInMonth(paneDate); arrayEach(dateArray, Y.bind(this._renderSelectedDatesHelper, this)); } }, /** * Takes in a date and determines whether that date has any rules * matching it in the customRenderer; then calls the specified * filterFunction if that's the case and/or disables the date * if the rule is specified as a disabledDatesRule. * * @method _renderSelectedDatesHelper * @private */ _renderSelectedDatesHelper: function (date) { this._dateToNode(date).addClass(CAL_DAY_SELECTED).setAttribute("aria-selected", true); }, /** * Add the selection-disabled class and aria-disabled attribute to a node corresponding * to a given date. * * @method _disableDate * @param {Date} date The date to disable * @private */ _disableDate: function (date) { this._dateToNode(date).addClass(SELECTION_DISABLED).setAttribute("aria-disabled", true); }, /** * A utility method that converts a date to the node wrapping the calendar cell * the date corresponds to.. * @method _dateToNode * @param {Date} oDate The date to convert to Node * @protected * @return {Node} The node wrapping the DOM element of the cell the date * corresponds to. */ _dateToNode : function (oDate) { var day = oDate.getDate(), col = 0, daymod = day%7, paneNum = (12 + oDate.getMonth() - this.get("date").getMonth()) % 12, paneId = this._calendarId + "_pane_" + paneNum, cutoffCol = this._paneProperties[paneId].cutoffCol; switch (daymod) { case (0): if (cutoffCol >= 6) { col = 12; } else { col = 5; } break; case (1): col = 6; break; case (2): if (cutoffCol > 0) { col = 7; } else { col = 0; } break; case (3): if (cutoffCol > 1) { col = 8; } else { col = 1; } break; case (4): if (cutoffCol > 2) { col = 9; } else { col = 2; } break; case (5): if (cutoffCol > 3) { col = 10; } else { col = 3; } break; case (6): if (cutoffCol > 4) { col = 11; } else { col = 4; } break; } return(this.get("contentBox").one("#" + this._calendarId + "_pane_" + paneNum + "_" + col + "_" + day)); }, /** * A utility method that converts a node corresponding to the DOM element of * the cell for a particular date to that date. * @method _nodeToDate * @param {Node} oNode The Node wrapping the DOM element of a particular date cell. * @protected * @return {Date} The date corresponding to the DOM element that the given node wraps. */ _nodeToDate : function (oNode) { var idParts = oNode.get("id").split("_").reverse(), paneNum = parseInt(idParts[2], 10), day = parseInt(idParts[0], 10), shiftedDate = ydate.addMonths(this.get("date"), paneNum), year = shiftedDate.getFullYear(), month = shiftedDate.getMonth(); return new Date(year, month, day, 12, 0, 0, 0); }, /** * A placeholder method, called from bindUI, to bind the Calendar events. * @method _bindCalendarEvents * @protected */ _bindCalendarEvents : function () {}, /** * A utility method that normalizes a given date by converting it to the 1st * day of the month the date is in, with the time set to noon. * @method _normalizeDate * @param {Date} oDate The date to normalize * @protected * @return {Date} The normalized date, set to the first of the month, with time * set to noon. */ _normalizeDate : function (date) { if (date) { return new Date(date.getFullYear(), date.getMonth(), 1, 12, 0, 0, 0); } else { return null; } }, /** * A utility method that normalizes a given date by setting its time to noon. * @method _normalizeTime * @param {Date} oDate The date to normalize * @protected * @return {Date} The normalized date * set to noon. */ _normalizeTime : function (date) { if (date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0, 0); } else { return null; } }, /** * A render assist utility method that computes the cutoff column for the calendar * rendering mask. * @method _getCutoffColumn * @param {Date} date The date of the month grid to compute the cutoff column for. * @param {Number} firstday The first day of the week (modified by internationalized calendars) * @private * @return {Number} The number of the cutoff column. */ _getCutoffColumn : function (date, firstday) { var distance = this._normalizeDate(date).getDay() - firstday, cutOffColumn = 6 - (distance + 7) % 7; return cutOffColumn; }, /** * A render assist method that turns on the view of the previous month's dates * in a given calendar pane. * @method _turnPrevMonthOn * @param {Node} pane The calendar pane that needs its previous month's dates view * modified. * @protected */ _turnPrevMonthOn : function (pane) { var pane_id = pane.get("id"), pane_date = this._paneProperties[pane_id].paneDate, daysInPrevMonth = ydate.daysInMonth(ydate.addMonths(pane_date, -1)), cell; if (!this._paneProperties[pane_id].hasOwnProperty("daysInPrevMonth")) { this._paneProperties[pane_id].daysInPrevMonth = 0; } if (daysInPrevMonth !== this._paneProperties[pane_id].daysInPrevMonth) { this._paneProperties[pane_id].daysInPrevMonth = daysInPrevMonth; for (cell = 5; cell >= 0; cell--) { pane.one("#" + pane_id + "_" + cell + "_" + (cell-5)).set('text', daysInPrevMonth--); } } }, /** * A render assist method that turns off the view of the previous month's dates * in a given calendar pane. * @method _turnPrevMonthOff * @param {Node} pane The calendar pane that needs its previous month's dates view * modified. * @protected */ _turnPrevMonthOff : function (pane) { var pane_id = pane.get("id"), cell; this._paneProperties[pane_id].daysInPrevMonth = 0; for (cell = 5; cell >= 0; cell--) { pane.one("#" + pane_id + "_" + cell + "_" + (cell-5)).setContent(" "); } }, /** * A render assist method that cleans up the last few cells in the month grid * when the number of days in the month changes. * @method _cleanUpNextMonthCells * @param {Node} pane The calendar pane that needs the last date cells cleaned up. * @private */ _cleanUpNextMonthCells : function (pane) { var pane_id = pane.get("id"); pane.one("#" + pane_id + "_6_29").removeClass(CAL_NEXTMONTH_DAY); pane.one("#" + pane_id + "_7_30").removeClass(CAL_NEXTMONTH_DAY); pane.one("#" + pane_id + "_8_31").removeClass(CAL_NEXTMONTH_DAY); pane.one("#" + pane_id + "_0_30").removeClass(CAL_NEXTMONTH_DAY); pane.one("#" + pane_id + "_1_31").removeClass(CAL_NEXTMONTH_DAY); }, /** * A render assist method that turns on the view of the next month's dates * in a given calendar pane. * @method _turnNextMonthOn * @param {Node} pane The calendar pane that needs its next month's dates view * modified. * @protected */ _turnNextMonthOn : function (pane) { var dayCounter = 1, pane_id = pane.get("id"), daysInMonth = this._paneProperties[pane_id].daysInMonth, cutoffCol = this._paneProperties[pane_id].cutoffCol, cell, startingCell; for (cell = daysInMonth - 22; cell < cutoffCol + 7; cell++) { pane.one("#" + pane_id + "_" + cell + "_" + (cell+23)).set("text", dayCounter++).addClass(CAL_NEXTMONTH_DAY); } startingCell = cutoffCol; if (daysInMonth === 31 && (cutoffCol <= 1)) { startingCell = 2; } else if (daysInMonth === 30 && cutoffCol === 0) { startingCell = 1; } for (cell = startingCell ; cell < cutoffCol + 7; cell++) { pane.one("#" + pane_id + "_" + cell + "_" + (cell+30)).set("text", dayCounter++).addClass(CAL_NEXTMONTH_DAY); } }, /** * A render assist method that turns off the view of the next month's dates * in a given calendar pane. * @method _turnNextMonthOff * @param {Node} pane The calendar pane that needs its next month's dates view * modified. * @protected */ _turnNextMonthOff : function (pane) { var pane_id = pane.get("id"), daysInMonth = this._paneProperties[pane_id].daysInMonth, cutoffCol = this._paneProperties[pane_id].cutoffCol, cell, startingCell; for (cell = daysInMonth - 22; cell <= 12; cell++) { pane.one("#" + pane_id + "_" + cell + "_" + (cell+23)).setContent(" ").addClass(CAL_NEXTMONTH_DAY); } startingCell = 0; if (daysInMonth === 31 && (cutoffCol <= 1)) { startingCell = 2; } else if (daysInMonth === 30 && cutoffCol === 0) { startingCell = 1; } for (cell = startingCell ; cell <= 12; cell++) { pane.one("#" + pane_id + "_" + cell + "_" + (cell+30)).setContent(" ").addClass(CAL_NEXTMONTH_DAY); } }, /** * The handler for the change in the showNextMonth attribute. * @method _afterShowNextMonthChange * @private */ _afterShowNextMonthChange : function () { var contentBox = this.get('contentBox'), lastPane = contentBox.one("#" + this._calendarId + "_pane_" + (this._paneNumber - 1)); this._cleanUpNextMonthCells(lastPane); if (this.get('showNextMonth')) { this._turnNextMonthOn(lastPane); } else { this._turnNextMonthOff(lastPane); } }, /** * The handler for the change in the showPrevMonth attribute. * @method _afterShowPrevMonthChange * @private */ _afterShowPrevMonthChange : function () { var contentBox = this.get('contentBox'), firstPane = contentBox.one("#" + this._calendarId + "_pane_" + 0); if (this.get('showPrevMonth')) { this._turnPrevMonthOn(firstPane); } else { this._turnPrevMonthOff(firstPane); } }, /** * The handler for the change in the headerRenderer attribute. * @method _afterHeaderRendererChange * @private */ _afterHeaderRendererChange : function () { var headerCell = this.get("contentBox").one("." + CAL_HD_LABEL); headerCell.setContent(this._updateCalendarHeader(this.get('date'))); }, /** * The handler for the change in the customRenderer attribute. * @method _afterCustomRendererChange * @private */ _afterCustomRendererChange : function () { this._restoreModifiedCells(); this._renderCustomRules(); }, /** * The handler for the change in the date attribute. Modifies the calendar * view by shifting the calendar grid mask and running custom rendering and * selection rendering as necessary. * @method _afterDateChange * @private */ _afterDateChange : function () { var contentBox = this.get('contentBox'), headerCell = contentBox.one("." + CAL_HD).one("." + CAL_HD_LABEL), calendarPanes = contentBox.all("." + CAL_GRID), currentDate = this.get("date"), counter = 0; contentBox.setStyle("visibility", "hidden"); headerCell.setContent(this._updateCalendarHeader(currentDate)); this._restoreModifiedCells(); calendarPanes.each(function (curNode) { this._rerenderCalendarPane(ydate.addMonths(currentDate, counter++), curNode); }, this); this._afterShowPrevMonthChange(); this._afterShowNextMonthChange(); this._renderCustomRules(); this._renderSelectedDates(); contentBox.setStyle("visibility", "inherit"); }, /** * A rendering assist method that initializes the HTML for a single * calendar pane. * @method _initCalendarPane * @param {Date} baseDate The date corresponding to the month of the given * calendar pane. * @param {String} pane_id The id of the pane, to be used as a prefix for * element ids in the given pane. * @private */ _initCalendarPane : function (baseDate, pane_id) { // Get a list of short weekdays from the internationalization package, or else use default English ones. var shortWeekDays = this.get('strings.very_short_weekdays') || ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], weekDays = Y.Intl.get('datatype-date-format').A, // Get the first day of the week from the internationalization package, or else use Sunday as default. firstday = this.get('strings.first_weekday') || 0, // Compute the cutoff column of the masked calendar table, based on the start date and the first day of week. cutoffCol = this._getCutoffColumn(baseDate, firstday), // Compute the number of days in the month based on starting date daysInMonth = ydate.daysInMonth(baseDate), // Initialize the array of individual row HTML strings row_array = ['','','','','',''], // Initialize the partial templates object partials = {}, day, row, column, date, id_date, calendar_day_class, column_visibility, output; // Initialize the partial template for the weekday row cells. partials.weekday_row = ''; // Populate the partial template for the weekday row cells with weekday names for (day = firstday; day <= firstday + 6; day++) { partials.weekday_row += substitute(CalendarBase.WEEKDAY_TEMPLATE, { short_weekdayname: shortWeekDays[day%7], weekdayname: weekDays[day%7] }); } // Populate the partial template for the weekday row container with the weekday row cells partials.weekday_row_template = substitute(CalendarBase.WEEKDAY_ROW_TEMPLATE, partials); // Populate the array of individual row HTML strings for (row = 0; row <= 5; row++) { for (column = 0; column <= 12; column++) { // Compute the value of the date that needs to populate the cell date = 7*row - 5 + column; // Compose the value of the unique id of the current calendar cell id_date = pane_id + "_" + column + "_" + date; // Set the calendar day class to one of three possible values calendar_day_class = CAL_DAY; if (date < 1) { calendar_day_class = CAL_PREVMONTH_DAY; } else if (date > daysInMonth) { calendar_day_class = CAL_NEXTMONTH_DAY; } // Cut off dates that fall before the first and after the last date of the month if (date < 1 || date > daysInMonth) { date = " "; } // Decide on whether a column in the masked table is visible or not based on the value of the cutoff column. column_visibility = (column >= cutoffCol && column < (cutoffCol + 7)) ? '' : CAL_COL_HIDDEN; // Substitute the values into the partial calendar day template and add it to the current row HTML string row_array[row] += substitute (CalendarBase.CALDAY_TEMPLATE, { day_content: date, calendar_col_class: "calendar_col" + column, calendar_col_visibility_class: column_visibility, calendar_day_class: calendar_day_class, calendar_day_id: id_date }); } } // Instantiate the partial calendar pane body template partials.body_template = ''; // Populate the body template with the rows templates arrayEach (row_array, function (v) { partials.body_template += substitute(CalendarBase.CALDAY_ROW_TEMPLATE, {calday_row: v}); }); // Populate the calendar grid id partials.calendar_pane_id = pane_id; // Populate the calendar pane tabindex partials.calendar_pane_tabindex = this.get("tabIndex"); partials.pane_arialabel = ydate.format(baseDate, { format: "%B %Y" }); // Generate final output by substituting class names. output = substitute(substitute (CalendarBase.CALENDAR_GRID_TEMPLATE, partials), CalendarBase.CALENDAR_STRINGS); // Store the initialized pane information this._paneProperties[pane_id] = {cutoffCol: cutoffCol, daysInMonth: daysInMonth, paneDate: baseDate}; return output; }, /** * A rendering assist method that rerenders a specified calendar pane, based * on a new Date. * @method _rerenderCalendarPane * @param {Date} newDate The date corresponding to the month of the given * calendar pane. * @param {Node} pane The node corresponding to the calendar pane to be rerenders. * @private */ _rerenderCalendarPane : function (newDate, pane) { // Get the first day of the week from the internationalization package, or else use Sunday as default. var firstday = this.get('strings.first_weekday') || 0, // Compute the cutoff column of the masked calendar table, based on the start date and the first day of week. cutoffCol = this._getCutoffColumn(newDate, firstday), // Compute the number of days in the month based on starting date daysInMonth = ydate.daysInMonth(newDate), // Get pane id for easier reference paneId = pane.get("id"), column, currentColumn, curCell; // Hide the pane before making DOM changes to speed them up pane.setStyle("visibility", "hidden"); pane.setAttribute("aria-label", ydate.format(newDate, {format:"%B %Y"})); // Go through all columns, and flip their visibility setting based on whether they are within the unmasked range. for (column = 0; column <= 12; column++) { currentColumn = pane.all("." + "calendar_col" + column); currentColumn.removeClass(CAL_COL_HIDDEN); if (column < cutoffCol || column >= (cutoffCol + 7)) { currentColumn.addClass(CAL_COL_HIDDEN); } else { // Clean up dates in visible columns to account for the correct number of days in a month switch(column) { case 0: curCell = pane.one("#" + paneId + "_0_30"); if (daysInMonth >= 30) { curCell.set("text", "30"); curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY); } else { curCell.setContent(" "); curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY); } break; case 1: curCell = pane.one("#" + paneId + "_1_31"); if (daysInMonth >= 31) { curCell.set("text", "31"); curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY); } else { curCell.setContent(" "); curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY); } break; case 6: curCell = pane.one("#" + paneId + "_6_29"); if (daysInMonth >= 29) { curCell.set("text", "29"); curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY); } else { curCell.setContent(" "); curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY); } break; case 7: curCell = pane.one("#" + paneId + "_7_30"); if (daysInMonth >= 30) { curCell.set("text", "30"); curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY); } else { curCell.setContent(" "); curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY); } break; case 8: curCell = pane.one("#" + paneId + "_8_31"); if (daysInMonth >= 31) { curCell.set("text", "31"); curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY); } else { curCell.setContent(" "); curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY); } break; } } } // Update stored pane properties this._paneProperties[paneId].cutoffCol = cutoffCol; this._paneProperties[paneId].daysInMonth = daysInMonth; this._paneProperties[paneId].paneDate = newDate; // Bring the pane visibility back after all DOM changes are done pane.setStyle("visibility", "inherit"); }, /** * A rendering assist method that updates the calendar header based * on a given date and potentially the provided headerRenderer. * @method _updateCalendarHeader * @param {Date} baseDate The date with which to update the calendar header. * @private */ _updateCalendarHeader : function (baseDate) { var headerString = "", headerRenderer = this.get("headerRenderer"); if (Y.Lang.isString(headerRenderer)) { headerString = ydate.format(baseDate, {format:headerRenderer}); } else if (headerRenderer instanceof Function) { headerString = headerRenderer.call(this, baseDate); } return headerString; }, /** * A rendering assist method that initializes the calendar header HTML * based on a given date and potentially the provided headerRenderer. * @method _initCalendarHeader * @param {Date} baseDate The date with which to initialize the calendar header. * @private */ _initCalendarHeader : function (baseDate) { return substitute(substitute(CalendarBase.HEADER_TEMPLATE, { calheader: this._updateCalendarHeader(baseDate), calendar_id: this._calendarId }), CalendarBase.CALENDAR_STRINGS ); }, /** * A rendering assist method that initializes the calendar HTML * based on a given date. * @method _initCalendarHTML * @param {Date} baseDate The date with which to initialize the calendar. * @private */ _initCalendarHTML : function (baseDate) { // Instantiate the partials holder var partials = {}, // Counter for iterative template replacement. counter = 0, singlePane, output; // Generate the template for the header partials.header_template = this._initCalendarHeader(baseDate); partials.calendar_id = this._calendarId; partials.body_template = substitute(substitute (CalendarBase.CONTENT_TEMPLATE, partials), CalendarBase.CALENDAR_STRINGS); // Instantiate the iterative template replacer function function paneReplacer () { singlePane = this._initCalendarPane(ydate.addMonths(baseDate, counter), partials.calendar_id + "_pane_" + counter); counter++; return singlePane; } // Go through all occurrences of the calendar_grid_template token and replace it with an appropriate calendar grid. output = partials.body_template.replace(/\{calendar_grid_template\}/g, Y.bind(paneReplacer, this)); // Update the paneNumber count this._paneNumber = counter; return output; } }, { /** * The CSS classnames for the calendar templates. * @property CALENDAR_STRINGS * @type Object * @readOnly * @protected * @static */ CALENDAR_STRINGS: { calendar_grid_class : CAL_GRID, calendar_body_class : CAL_BODY, calendar_hd_class : CAL_HD, calendar_hd_label_class : CAL_HD_LABEL, calendar_weekdayrow_class : CAL_WDAYROW, calendar_weekday_class : CAL_WDAY, calendar_row_class : CAL_ROW, calendar_day_class : CAL_DAY, calendar_dayanchor_class : CAL_ANCHOR, calendar_pane_class : CAL_PANE, calendar_right_grid_class : CAL_RIGHT_GRID, calendar_left_grid_class : CAL_LEFT_GRID, calendar_status_class : CAL_STATUS }, /* ARIA_STATUS_TEMPLATE: '<div role="status" aria-atomic="true" class="{calendar_status_class}"></div>', AriaStatus : null, updateStatus : function (statusString) { if (!CalendarBase.AriaStatus) { CalendarBase.AriaStatus = create( substitute (CalendarBase.ARIA_STATUS_TEMPLATE, CalendarBase.CALENDAR_STRINGS)); Y.one("body").append(CalendarBase.AriaStatus); } CalendarBase.AriaStatus.set("text", statusString); }, */ /** * The main content template for calendar. * @property CONTENT_TEMPLATE * @type String * @protected * @static */ CONTENT_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' + '{header_template}' + '<div class="yui3-u-1">' + '{calendar_grid_template}' + '</div>' + '</div>', /** * A single pane template for calendar (same as default CONTENT_TEMPLATE) * @property ONE_PANE_TEMPLATE * @type String * @protected * @readOnly * @static */ ONE_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' + '{header_template}' + '<div class="yui3-u-1">' + '{calendar_grid_template}' + '</div>' + '</div>', /** * A two pane template for calendar. * @property TWO_PANE_TEMPLATE * @type String * @protected * @readOnly * @static */ TWO_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' + '{header_template}' + '<div class="yui3-u-1-2">'+ '<div class = "{calendar_left_grid_class}">' + '{calendar_grid_template}' + '</div>' + '</div>' + '<div class="yui3-u-1-2">' + '<div class = "{calendar_right_grid_class}">' + '{calendar_grid_template}' + '</div>' + '</div>' + '</div>', /** * A three pane template for calendar. * @property THREE_PANE_TEMPLATE * @type String * @protected * @readOnly * @static */ THREE_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' + '{header_template}' + '<div class="yui3-u-1-3">' + '<div class="{calendar_left_grid_class}">' + '{calendar_grid_template}' + '</div>' + '</div>' + '<div class="yui3-u-1-3">' + '{calendar_grid_template}' + '</div>' + '<div class="yui3-u-1-3">' + '<div class="{calendar_right_grid_class}">' + '{calendar_grid_template}' + '</div>' + '</div>' + '</div>', /** * A template for the calendar grid. * @property CALENDAR_GRID_TEMPLATE * @type String * @protected * @static */ CALENDAR_GRID_TEMPLATE: '<table class="{calendar_grid_class}" id="{calendar_pane_id}" role="grid" aria-readonly="true" ' + 'aria-label="{pane_arialabel}" tabindex="{calendar_pane_tabindex}">' + '<thead>' + '{weekday_row_template}' + '</thead>' + '<tbody>' + '{body_template}' + '</tbody>' + '</table>', /** * A template for the calendar header. * @property HEADER_TEMPLATE * @type String * @protected * @static */ HEADER_TEMPLATE: '<div class="yui3-g {calendar_hd_class}">' + '<div class="yui3-u {calendar_hd_label_class}" id="{calendar_id}_header" aria-role="heading">' + '{calheader}' + '</div>' + '</div>', /** * A template for the row of weekday names. * @property WEEKDAY_ROW_TEMPLATE * @type String * @protected * @static */ WEEKDAY_ROW_TEMPLATE: '<tr class="{calendar_weekdayrow_class}" role="row">' + '{weekday_row}' + '</tr>', /** * A template for a single row of calendar days. * @property CALDAY_ROW_TEMPLATE * @type String * @protected * @static */ CALDAY_ROW_TEMPLATE: '<tr class="{calendar_row_class}" role="row">' + '{calday_row}' + '</tr>', /** * A template for a single cell with a weekday name. * @property WEEKDAY_TEMPLATE * @type String * @protected * @static */ WEEKDAY_TEMPLATE: '<th class="{calendar_weekday_class}" role="columnheader" aria-label="{weekdayname}">{short_weekdayname}</th>', /** * A template for a single cell with a calendar day. * @property CALDAY_TEMPLATE * @type String * @protected * @static */ CALDAY_TEMPLATE: '<td class="{calendar_col_class} {calendar_day_class} {calendar_col_visibility_class}" id="{calendar_day_id}" ' + 'role="gridcell" tabindex="-1">' + '{day_content}' + '</td>', /** * The identity of the widget. * * @property NAME * @type String * @default 'calendarBase' * @readOnly * @protected * @static */ NAME: 'calendarBase', /** * Static property used to define the default attribute configuration of * the Widget. * * @property ATTRS * @type {Object} * @protected * @static */ ATTRS: { tabIndex: { value: 1 }, /** * The date corresponding to the current calendar view. Always * normalized to the first of the month that contains the date * at assignment time. Used as the first date visible in the * calendar. * * @attribute date * @type Date * @default The first of the month containing today's date, as * set on the end user's system. */ date: { value: new Date(), setter: function (val) { var newDate = this._normalizeDate(val); if (ydate.areEqual(newDate, this.get('date'))) { return this.get('date'); } else { return newDate; } } }, /** * A setting specifying whether to shows days from the previous * month in the visible month's grid, if there are empty preceding * cells available. * * @attribute showPrevMonth * @type boolean * @default false */ showPrevMonth: { value: false }, /** * A setting specifying whether to shows days from the next * month in the visible month's grid, if there are empty * cells available at the end. * * @attribute showNextMonth * @type boolean * @default false */ showNextMonth: { value: false }, /** * Strings and properties derived from the internationalization packages * for the calendar. * * @attribute strings * @type Object * @protected */ strings : { valueFn: function() { return Y.Intl.get("calendar-base"); } }, /** * Custom header renderer for the calendar. * * @attribute headerRenderer * @type String | Function */ headerRenderer: { value: "%B %Y" }, /** * The name of the rule which all enabled dates should match. * Either disabledDatesRule or enabledDatesRule should be specified, * or neither, but not both. * * @attribute enabledDatesRule * @type String * @default null */ enabledDatesRule: { value: null }, /** * The name of the rule which all disabled dates should match. * Either disabledDatesRule or enabledDatesRule should be specified, * or neither, but not both. * * @attribute disabledDatesRule * @type String * @default null */ disabledDatesRule: { value: null }, /** * A read-only attribute providing a list of currently selected dates. * * @attribute selectedDates * @readOnly * @type Array */ selectedDates : { readOnly: true, getter: function () { return (this._getSelectedDatesList()); } }, /** * An object of the form {rules:Object, filterFunction:Function}, * providing set of rules and a custom rendering function for * customizing specific calendar cells. * * @attribute customRenderer * @type Object * @default {} */ customRenderer : { lazyAdd: false, value: {}, setter: function (val) { this._rules = val.rules; this._filterFunction = val.filterFunction; } } } });