NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / calendar / calendar.js
blob90d7c745cc0bee58e6bec9993bc00a0d6a91180e
1 /*
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/
6 */
8 YUI.add('calendar', function (Y, NAME) {
10 /**
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.
16  *
17  *
18  * @module calendar
19  */
21 var getCN             = Y.ClassNameManager.getClassName,
22     CALENDAR          = 'calendar',
23     KEY_DOWN          = 40,
24     KEY_UP            = 38,
25     KEY_LEFT          = 37,
26     KEY_RIGHT         = 39,
27     KEY_ENTER         = 13,
28     KEY_SPACE         = 32,
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'),
37     os                = Y.UA.os;
39 /** Create a calendar view to represent a single or multiple
40     * month range of dates, rendered as a grid with date and
41     * weekday labels.
42     *
43     * @class Calendar
44     * @extends CalendarBase
45     * @param config {Object} Configuration object (see Configuration attributes)
46     * @constructor
47     */
48 function Calendar() {
49     Calendar.superclass.constructor.apply ( this, arguments );
52 Y.Calendar = Y.extend(Calendar, Y.CalendarBase, {
54     _keyEvents: [],
56     _highlightedDateNode: null,
58     /**
59      * A property tracking the last selected date on the calendar, for the
60      * purposes of multiple selection.
61      *
62      * @property _lastSelectedDate
63      * @type Date
64      * @default null
65      * @private
66      */
67     _lastSelectedDate: null,
69     /**
70      * Designated initializer. Activates the navigation plugin for the calendar.
71      *
72      * @method initializer
73      */
74     initializer : function () {
75         this.plug(Y.Plugin.CalendarNavigator);
77         this._keyEvents = [];
78         this._highlightedDateNode = null;
79         this._lastSelectedDate = null;
80     },
82     /**
83      * Overrides the _bindCalendarEvents placeholder in CalendarBase
84      * and binds calendar events during bindUI stage.
85      * @method _bindCalendarEvents
86      * @protected
87      */
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);
101     },
103     /**
104      * Prevents text selection if it is started within the calendar pane
105      * @method _preventSelectionStart
106      * @param event {Event} The selectstart event
107      * @protected
108      */
109     _preventSelectionStart : function (event) {
110         event.preventDefault();
111     },
113     /**
114      * Highlights a specific date node with keyboard highlight class
115      * @method _highlightDateNode
116      * @param oDate {Date} Date corresponding the node to be highlighted
117      * @protected
118      */
119     _highlightDateNode : function (oDate) {
120         this._unhighlightCurrentDateNode();
121         var newNode = this._dateToNode(oDate);
122         newNode.focus();
123         newNode.addClass(CAL_DAY_HILITED);
124     },
126     /**
127      * Unhighlights a specific date node currently highlighted with keyboard highlight class
128      * @method _unhighlightCurrentDateNode
129      * @protected
130      */
131     _unhighlightCurrentDateNode : function () {
132         var allHilitedNodes = this.get("contentBox").all("." + CAL_DAY_HILITED);
133         if (allHilitedNodes) {
134             allHilitedNodes.removeClass(CAL_DAY_HILITED);
135         }
136     },
138     /**
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
142      * @protected
143      */
144     _getGridNumber : function (gridNode) {
145         var idParts = gridNode.get("id").split("_").reverse();
147         return parseInt(idParts[0], 10);
148     },
150     /**
151      * Handler for loss of focus of calendar grid
152      * @method _blurCalendarGrid
153      * @protected
154      */
155     _blurCalendarGrid : function () {
156         this._unhighlightCurrentDateNode();
157     },
160     /**
161      * Handler for gain of focus of calendar cell
162      * @method _focusCalendarCell
163      * @protected
164      */
165     _focusCalendarCell : function (ev) {
166         this._highlightedDateNode = ev.target;
167         ev.stopPropagation();
168     },
170     /**
171      * Handler for gain of focus of calendar grid
172      * @method _focusCalendarGrid
173      * @protected
174      */
175     _focusCalendarGrid : function () {
176         this._unhighlightCurrentDateNode();
177         this._highlightedDateNode = null;
178     },
180     /**
181      * Handler for keyboard press on a calendar grid
182      * @method _keydownCalendar
183      * @protected
184      */
185     _keydownCalendar : function (ev) {
186         var gridNum = this._getGridNumber(ev.target),
187             curDate = !this._highlightedDateNode ? null : this._nodeToDate(this._highlightedDateNode),
188             keyCode = ev.keyCode,
189             dayNum = 0,
190             dir = '',
191             selMode,
192             newDate,
193             startDate,
194             endDate,
195             lastPaneDate;
197         switch(keyCode) {
198             case KEY_DOWN:
199                 dayNum = 7;
200                 dir = 's';
201                 break;
202             case KEY_UP:
203                 dayNum = -7;
204                 dir = 'n';
205                 break;
206             case KEY_LEFT:
207                 dayNum = -1;
208                 dir = 'w';
209                 break;
210             case KEY_RIGHT:
211                 dayNum = 1;
212                 dir = 'e';
213                 break;
214             case KEY_SPACE: case KEY_ENTER:
215                 ev.preventDefault();
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);
224                         } else {
225                             this._addDateToSelection(curDate);
226                         }
227                     }
228                 }
229                 break;
230         }
233         if (keyCode === KEY_DOWN || keyCode === KEY_UP || keyCode === KEY_LEFT || keyCode === KEY_RIGHT) {
235             if (!curDate) {
236                 curDate = ydate.addMonths(this.get("date"), gridNum);
237                 dayNum = 0;
238             }
240             ev.preventDefault();
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);
256                             newPane.focus();
257                 }
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);
264                 }
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);
269                 }
270             }
271         }
272     },
274     /**
275      * Handles the calendar clicks based on selection mode.
276      * @method _clickCalendar
277      * @param {Event} ev A click event
278      * @private
279      */
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),
287         selectedDate;
289         switch (this.get("selectionMode")) {
290             case("single"):
291                 if (clickedCellIsDay) {
292                     if (!clickedCellIsSelected) {
293                         this._clearSelection(true);
294                         this._addDateToSelection(this._nodeToDate(clickedCell));
295                     }
296                 }
297                 break;
298             case("multiple-sticky"):
299                 if (clickedCellIsDay) {
300                     if (clickedCellIsSelected) {
301                         this._removeDateFromSelection(this._nodeToDate(clickedCell));
302                     } else {
303                         this._addDateToSelection(this._nodeToDate(clickedCell));
304                     }
305                 }
306                 break;
307             case("multiple"):
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;
317                         } else {
318                             this._lastSelectedDate = this._nodeToDate(clickedCell);
319                             this._addDateToSelection(this._lastSelectedDate);
320                         }
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;
326                         } else {
327                             this._lastSelectedDate = this._nodeToDate(clickedCell);
328                             this._addDateToSelection(this._lastSelectedDate);
329                         }
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;
336                         } else {
337                             this._clearSelection(true);
338                             this._lastSelectedDate = this._nodeToDate(clickedCell);
339                             this._addDateToSelection(this._lastSelectedDate);
340                         }
341                     }
342                 }
343                 break;
344         }
346         if (clickedCellIsDay) {
347             /**
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.
351             *
352             * @event dateClick
353             */
354             this.fire("dateClick", {cell: clickedCell, date: this._nodeToDate(clickedCell)});
355         } else if (clickedCell.hasClass(CAL_PREVMONTH_DAY)) {
356             /**
357             * Fired when any of the previous month's days displayed before the calendar grid
358             * are clicked.
359             *
360             * @event prevMonthClick
361             */
362             this.fire("prevMonthClick");
363         } else if (clickedCell.hasClass(CAL_NEXTMONTH_DAY)) {
364             /**
365             * Fired when any of the next month's days displayed after the calendar grid
366             * are clicked.
367             *
368             * @event nextMonthClick
369             */
370             this.fire("nextMonthClick");
371         }
372     },
374     /**
375      * Overrides CalendarBase.prototype._canBeSelected to disable
376      * nodes earlier than minimumDate and later than maximumDate
377      * @method _canBeSelected
378      * @private
379      */
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))) {
386             return false;
387         }
389         return Calendar.superclass._canBeSelected.call(this, date);
390     },
392     /**
393      * Overrides CalendarBase.prototype._renderCustomRules to disable
394      * nodes earlier than minimumDate and later than maximumDate
395      * @method _renderCustomRules
396      * @private
397      */
398     _renderCustomRules: function () {
399         Calendar.superclass._renderCustomRules.call(this);
401         var minDate = this.get('minimumDate'),
402             maxDate = this.get('maximumDate'),
403             dates = [],
404             i, l,
405             paneNum;
407         if (!minDate && !maxDate) {
408             return;
409         }
411         for (paneNum = 0; paneNum < this._paneNumber; paneNum++) {
412             paneDate = ydate.addMonths(this.get("date"), paneNum);
413             dates = dates.concat(ydate.listOfDatesInMonth(paneDate));
414         }
416         if (minDate) {
417             for (i = 0, l = dates.length; i < l; i++) {
418                 if (!ydate.isGreaterOrEqual(dates[i], minDate)) {
419                     this._disableDate(dates[i]);
420                 } else {
421                     break;
422                 }
423             }
424         }
426         if (maxDate) {
427             for (i = dates.length - 1; i >= 0; i--) {
428                 if (ydate.isGreater(dates[i], maxDate)) {
429                     this._disableDate(dates[i]);
430                 } else {
431                     break;
432                 }
433             }
434         }
435     },
437     /**
438      * Subtracts one month from the current calendar view.
439      * @method subtractMonth
440      * @return {Calendar} A reference to this object
441      * @chainable
442      */
443     subtractMonth : function (e) {
444         this.set("date", ydate.addMonths(this.get("date"), -1));
445         if (e) {
446             e.halt();
447         }
448         return this;
449     },
451     /**
452      * Subtracts one year from the current calendar view.
453      * @method subtractYear
454      * @return {Calendar} A reference to this object
455      * @chainable
456      */
457     subtractYear : function (e) {
458         this.set("date", ydate.addYears(this.get("date"), -1));
459         if (e) {
460             e.halt();
461         }
462         return this;
463     },
465     /**
466      * Adds one month to the current calendar view.
467      * @method addMonth
468      * @return {Calendar} A reference to this object
469      * @chainable
470      */
471     addMonth : function (e) {
472         this.set("date", ydate.addMonths(this.get("date"), 1));
473         if (e) {
474             e.halt();
475         }
476         return this;
477     },
479     /**
480      * Adds one year to the current calendar view.
481      * @method addYear
482      * @return {Calendar} A reference to this object
483      * @chainable
484      */
485     addYear : function (e) {
486         this.set("date", ydate.addYears(this.get("date"), 1));
487         if (e) {
488             e.halt();
489         }
490         return this;
491     }
492 }, {
493     /**
494     * The identity of the widget.
495     *
496     * @property NAME
497     * @type String
498     * @default 'calendar'
499     * @readOnly
500     * @protected
501     * @static
502     */
503     NAME: "calendar",
505     /**
506     * Static property used to define the default attribute configuration of
507     * the Widget.
508     *
509     * @property ATTRS
510     * @type {Object}
511     * @protected
512     * @static
513     */
514     ATTRS: {
516         /**
517          * A setting specifying the type of selection the calendar allows.
518          * Possible values include:
519          * <ul>
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>
525          *
526          * @attribute selectionMode
527          * @type String
528          * @default single
529          */
530         selectionMode: {
531             value: "single"
532         },
534         /**
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
538          * calendar.
539          *
540          * @attribute date
541          * @type Date
542          * @default Today's date as set on the user's computer.
543          */
544         date: {
545             value: new Date(),
546             lazyAdd: false,
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))
556                 ) {
557                     return newDate;
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);
562                 }
563             }
564         },
566         /**
567          * Unless minimumDate is null, it will not be possible to display and select dates earlier than this one.
568          *
569          * @attribute minimumDate
570          * @type Date
571          * @default null
572          */
573         minimumDate: {
574             value: null,
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);
581                     }
582                     return newMin;
583                 } else {
584                     return null;
585                 }
586             }
587         },
589         /**
590          * Unless maximumDate is null, it will not be possible to display and select dates later than this one.
591          *
592          * @attribute maximumDate
593          * @type Date
594          * @default null
595          */
596         maximumDate: {
597             value: null,
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));
604                     }
606                     return this._normalizeTime(val);
607                 } else {
608                     return null;
609                 }
610             }
611         }
612     }
616 }, '3.13.0', {"requires": ["calendar-base", "calendarnavigator"], "skinnable": true});