2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('calendar', function (Y, NAME) {
11 * The Calendar component is a UI widget that allows users
12 * to view dates in a two-dimensional month grid, as well as
13 * to select one or more dates, or ranges of dates. Calendar
14 * is generated dynamically and relies on the developer to
15 * provide for a progressive enhancement alternative.
21 var getCN = Y.ClassNameManager.getClassName,
22 CALENDAR = 'calendar',
29 CAL_DAY_SELECTED = getCN(CALENDAR, 'day-selected'),
30 CAL_DAY_HILITED = getCN(CALENDAR, 'day-highlighted'),
31 CAL_DAY = getCN(CALENDAR, 'day'),
32 CAL_PREVMONTH_DAY = getCN(CALENDAR, 'prevmonth-day'),
33 CAL_NEXTMONTH_DAY = getCN(CALENDAR, 'nextmonth-day'),
34 CAL_GRID = getCN(CALENDAR, 'grid'),
35 ydate = Y.DataType.Date,
36 CAL_PANE = getCN(CALENDAR, 'pane'),
39 /** Create a calendar view to represent a single or multiple
40 * month range of dates, rendered as a grid with date and
44 * @extends CalendarBase
45 * @param config {Object} Configuration object (see Configuration attributes)
49 Calendar.superclass.constructor.apply ( this, arguments );
52 Y.Calendar = Y.extend(Calendar, Y.CalendarBase, {
56 _highlightedDateNode: null,
59 * A property tracking the last selected date on the calendar, for the
60 * purposes of multiple selection.
62 * @property _lastSelectedDate
67 _lastSelectedDate: null,
70 * Designated initializer. Activates the navigation plugin for the calendar.
74 initializer : function () {
75 this.plug(Y.Plugin.CalendarNavigator);
78 this._highlightedDateNode = null;
79 this._lastSelectedDate = null;
83 * Overrides the _bindCalendarEvents placeholder in CalendarBase
84 * and binds calendar events during bindUI stage.
85 * @method _bindCalendarEvents
88 _bindCalendarEvents : function () {
89 var contentBox = this.get('contentBox'),
90 pane = contentBox.one("." + CAL_PANE);
92 pane.on("selectstart", this._preventSelectionStart);
93 pane.delegate("click", this._clickCalendar, "." + CAL_DAY + ", ." + CAL_PREVMONTH_DAY + ", ." + CAL_NEXTMONTH_DAY, this);
94 pane.delegate("keydown", this._keydownCalendar, "." + CAL_GRID, this);
95 pane.delegate("focus", this._focusCalendarGrid, "." + CAL_GRID, this);
96 pane.delegate("focus", this._focusCalendarCell, "." + CAL_DAY, this);
97 pane.delegate("blur", this._blurCalendarGrid, "." + CAL_GRID + ",." + CAL_DAY, this);
100 this.after(['minimumDateChange', 'maximumDateChange'], this._afterCustomRendererChange);
104 * Prevents text selection if it is started within the calendar pane
105 * @method _preventSelectionStart
106 * @param event {Event} The selectstart event
109 _preventSelectionStart : function (event) {
110 event.preventDefault();
114 * Highlights a specific date node with keyboard highlight class
115 * @method _highlightDateNode
116 * @param oDate {Date} Date corresponding the node to be highlighted
119 _highlightDateNode : function (oDate) {
120 this._unhighlightCurrentDateNode();
121 var newNode = this._dateToNode(oDate);
123 newNode.addClass(CAL_DAY_HILITED);
127 * Unhighlights a specific date node currently highlighted with keyboard highlight class
128 * @method _unhighlightCurrentDateNode
131 _unhighlightCurrentDateNode : function () {
132 var allHilitedNodes = this.get("contentBox").all("." + CAL_DAY_HILITED);
133 if (allHilitedNodes) {
134 allHilitedNodes.removeClass(CAL_DAY_HILITED);
139 * Returns the grid number for a specific calendar grid (for multi-grid templates)
140 * @method _getGridNumber
141 * @param gridNode {Node} Node corresponding to a specific grid
144 _getGridNumber : function (gridNode) {
145 var idParts = gridNode.get("id").split("_").reverse();
147 return parseInt(idParts[0], 10);
151 * Handler for loss of focus of calendar grid
152 * @method _blurCalendarGrid
155 _blurCalendarGrid : function () {
156 this._unhighlightCurrentDateNode();
161 * Handler for gain of focus of calendar cell
162 * @method _focusCalendarCell
165 _focusCalendarCell : function (ev) {
166 this._highlightedDateNode = ev.target;
167 ev.stopPropagation();
171 * Handler for gain of focus of calendar grid
172 * @method _focusCalendarGrid
175 _focusCalendarGrid : function () {
176 this._unhighlightCurrentDateNode();
177 this._highlightedDateNode = null;
181 * Handler for keyboard press on a calendar grid
182 * @method _keydownCalendar
185 _keydownCalendar : function (ev) {
186 var gridNum = this._getGridNumber(ev.target),
187 curDate = !this._highlightedDateNode ? null : this._nodeToDate(this._highlightedDateNode),
188 keyCode = ev.keyCode,
214 case KEY_SPACE: case KEY_ENTER:
216 if (this._highlightedDateNode) {
217 selMode = this.get("selectionMode");
218 if (selMode === "single" && !this._highlightedDateNode.hasClass(CAL_DAY_SELECTED)) {
219 this._clearSelection(true);
220 this._addDateToSelection(curDate);
221 } else if (selMode === "multiple" || selMode === "multiple-sticky") {
222 if (this._highlightedDateNode.hasClass(CAL_DAY_SELECTED)) {
223 this._removeDateFromSelection(curDate);
225 this._addDateToSelection(curDate);
233 if (keyCode === KEY_DOWN || keyCode === KEY_UP || keyCode === KEY_LEFT || keyCode === KEY_RIGHT) {
236 curDate = ydate.addMonths(this.get("date"), gridNum);
242 newDate = ydate.addDays(curDate, dayNum);
243 startDate = this.get("date");
244 endDate = ydate.addMonths(this.get("date"), this._paneNumber - 1);
245 lastPaneDate = new Date(endDate);
246 endDate.setDate(ydate.daysInMonth(endDate));
248 if (ydate.isInRange(newDate, startDate, endDate)) {
250 var paneShift = (newDate.getMonth() - curDate.getMonth()) % 10;
252 if (paneShift != 0) {
253 var newGridNum = gridNum + paneShift,
254 contentBox = this.get('contentBox'),
255 newPane = contentBox.one("#" + this._calendarId + "_pane_" + newGridNum);
259 this._highlightDateNode(newDate);
260 } else if (ydate.isGreater(startDate, newDate)) {
261 if (!ydate.isGreaterOrEqual(this.get("minimumDate"), startDate)) {
262 this.set("date", ydate.addMonths(startDate, -1));
263 this._highlightDateNode(newDate);
265 } else if (ydate.isGreater(newDate, endDate)) {
266 if (!ydate.isGreaterOrEqual(lastPaneDate, this.get("maximumDate"))) {
267 this.set("date", ydate.addMonths(startDate, 1));
268 this._highlightDateNode(newDate);
275 * Handles the calendar clicks based on selection mode.
276 * @method _clickCalendar
277 * @param {Event} ev A click event
280 _clickCalendar : function (ev) {
281 var clickedCell = ev.currentTarget,
282 clickedCellIsDay = clickedCell.hasClass(CAL_DAY) &&
283 !clickedCell.hasClass(CAL_PREVMONTH_DAY) &&
284 !clickedCell.hasClass(CAL_NEXTMONTH_DAY),
286 clickedCellIsSelected = clickedCell.hasClass(CAL_DAY_SELECTED),
289 switch (this.get("selectionMode")) {
291 if (clickedCellIsDay) {
292 if (!clickedCellIsSelected) {
293 this._clearSelection(true);
294 this._addDateToSelection(this._nodeToDate(clickedCell));
298 case("multiple-sticky"):
299 if (clickedCellIsDay) {
300 if (clickedCellIsSelected) {
301 this._removeDateFromSelection(this._nodeToDate(clickedCell));
303 this._addDateToSelection(this._nodeToDate(clickedCell));
308 if (clickedCellIsDay) {
309 if (!ev.metaKey && !ev.ctrlKey && !ev.shiftKey) {
310 this._clearSelection(true);
311 this._lastSelectedDate = this._nodeToDate(clickedCell);
312 this._addDateToSelection(this._lastSelectedDate);
313 } else if (((os === 'macintosh' && ev.metaKey) || (os !== 'macintosh' && ev.ctrlKey)) && !ev.shiftKey) {
314 if (clickedCellIsSelected) {
315 this._removeDateFromSelection(this._nodeToDate(clickedCell));
316 this._lastSelectedDate = null;
318 this._lastSelectedDate = this._nodeToDate(clickedCell);
319 this._addDateToSelection(this._lastSelectedDate);
321 } else if (((os === 'macintosh' && ev.metaKey) || (os !== 'macintosh' && ev.ctrlKey)) && ev.shiftKey) {
322 if (this._lastSelectedDate) {
323 selectedDate = this._nodeToDate(clickedCell);
324 this._addDateRangeToSelection(selectedDate, this._lastSelectedDate);
325 this._lastSelectedDate = selectedDate;
327 this._lastSelectedDate = this._nodeToDate(clickedCell);
328 this._addDateToSelection(this._lastSelectedDate);
330 } else if (ev.shiftKey) {
331 if (this._lastSelectedDate) {
332 selectedDate = this._nodeToDate(clickedCell);
333 this._clearSelection(true);
334 this._addDateRangeToSelection(selectedDate, this._lastSelectedDate);
335 this._lastSelectedDate = selectedDate;
337 this._clearSelection(true);
338 this._lastSelectedDate = this._nodeToDate(clickedCell);
339 this._addDateToSelection(this._lastSelectedDate);
346 if (clickedCellIsDay) {
348 * Fired when a specific date cell in the calendar is clicked. The event carries a
349 * payload which includes a `cell` property corresponding to the node of the actual
350 * date cell, and a `date` property, with the `Date` that was clicked.
354 this.fire("dateClick", {cell: clickedCell, date: this._nodeToDate(clickedCell)});
355 } else if (clickedCell.hasClass(CAL_PREVMONTH_DAY)) {
357 * Fired when any of the previous month's days displayed before the calendar grid
360 * @event prevMonthClick
362 this.fire("prevMonthClick");
363 } else if (clickedCell.hasClass(CAL_NEXTMONTH_DAY)) {
365 * Fired when any of the next month's days displayed after the calendar grid
368 * @event nextMonthClick
370 this.fire("nextMonthClick");
375 * Overrides CalendarBase.prototype._canBeSelected to disable
376 * nodes earlier than minimumDate and later than maximumDate
377 * @method _canBeSelected
380 _canBeSelected : function (date) {
381 var minDate = this.get('minimumDate'),
382 maxDate = this.get('maximumDate');
384 if ((minDate && !ydate.isGreaterOrEqual(date, minDate)) ||
385 (maxDate && ydate.isGreater(date, maxDate))) {
389 return Calendar.superclass._canBeSelected.call(this, date);
393 * Overrides CalendarBase.prototype._renderCustomRules to disable
394 * nodes earlier than minimumDate and later than maximumDate
395 * @method _renderCustomRules
398 _renderCustomRules: function () {
399 Calendar.superclass._renderCustomRules.call(this);
401 var minDate = this.get('minimumDate'),
402 maxDate = this.get('maximumDate'),
407 if (!minDate && !maxDate) {
411 for (paneNum = 0; paneNum < this._paneNumber; paneNum++) {
412 paneDate = ydate.addMonths(this.get("date"), paneNum);
413 dates = dates.concat(ydate.listOfDatesInMonth(paneDate));
417 for (i = 0, l = dates.length; i < l; i++) {
418 if (!ydate.isGreaterOrEqual(dates[i], minDate)) {
419 this._disableDate(dates[i]);
427 for (i = dates.length - 1; i >= 0; i--) {
428 if (ydate.isGreater(dates[i], maxDate)) {
429 this._disableDate(dates[i]);
438 * Subtracts one month from the current calendar view.
439 * @method subtractMonth
440 * @return {Calendar} A reference to this object
443 subtractMonth : function (e) {
444 this.set("date", ydate.addMonths(this.get("date"), -1));
452 * Subtracts one year from the current calendar view.
453 * @method subtractYear
454 * @return {Calendar} A reference to this object
457 subtractYear : function (e) {
458 this.set("date", ydate.addYears(this.get("date"), -1));
466 * Adds one month to the current calendar view.
468 * @return {Calendar} A reference to this object
471 addMonth : function (e) {
472 this.set("date", ydate.addMonths(this.get("date"), 1));
480 * Adds one year to the current calendar view.
482 * @return {Calendar} A reference to this object
485 addYear : function (e) {
486 this.set("date", ydate.addYears(this.get("date"), 1));
494 * The identity of the widget.
498 * @default 'calendar'
506 * Static property used to define the default attribute configuration of
517 * A setting specifying the type of selection the calendar allows.
518 * Possible values include:
520 * <li>`single` - One date at a time</li>
521 * <li>`multiple-sticky` - Multiple dates, selected one at a time (the dates "stick"). This option
522 * is appropriate for mobile devices, where function keys from the keyboard are not available.</li>
523 * <li>`multiple` - Multiple dates, selected with Ctrl/Meta keys for additional single
524 * dates, and Shift key for date ranges.</li>
526 * @attribute selectionMode
535 * The date corresponding to the current calendar view. Always
536 * normalized to the first of the month that contains the date
537 * at assignment time. Used as the first date visible in the
542 * @default Today's date as set on the user's computer.
547 setter: function (val) {
549 var newDate = this._normalizeDate(val),
550 newEndDate = ydate.addMonths(newDate, this._paneNumber - 1),
551 minDate = this.get("minimumDate"),
552 maxDate = this.get("maximumDate");
554 if ((!minDate || ydate.isGreaterOrEqual(newDate, minDate)) &&
555 (!maxDate || ydate.isGreaterOrEqual(maxDate, newEndDate))
558 } else if (minDate && ydate.isGreater(minDate, newDate)) {
559 return this._normalizeDate(minDate);
560 } else if (maxDate && ydate.isGreater(newEndDate, maxDate)) {
561 return ydate.addMonths(this._normalizeDate(maxDate), 1 - this._paneNumber);
567 * Unless minimumDate is null, it will not be possible to display and select dates earlier than this one.
569 * @attribute minimumDate
575 setter: function (val) {
576 if (Y.Lang.isDate(val)) {
577 var curDate = this.get('date'),
578 newMin = this._normalizeTime(val);
579 if (curDate && !ydate.isGreaterOrEqual(curDate, newMin)) {
580 this.set('date', val);
590 * Unless maximumDate is null, it will not be possible to display and select dates later than this one.
592 * @attribute maximumDate
598 setter: function (val) {
599 if (Y.Lang.isDate(val)) {
600 var curDate = this.get('date');
602 if (curDate && !ydate.isGreaterOrEqual(val, ydate.addMonths(curDate, this._paneNumber - 1))) {
603 this.set('date', ydate.addMonths(this._normalizeDate(val), 1 - this._paneNumber));
606 return this._normalizeTime(val);
616 }, '3.13.0', {"requires": ["calendar-base", "calendarnavigator"], "skinnable": true});