|
1 YUI.add('calendar', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 * The Calendar component is a UI widget that allows users |
|
5 * to view dates in a two-dimensional month grid, as well as |
|
6 * to select one or more dates, or ranges of dates. Calendar |
|
7 * is generated dynamically and relies on the developer to |
|
8 * provide for a progressive enhancement alternative. |
|
9 * |
|
10 * |
|
11 * @module calendar |
|
12 */ |
|
13 |
|
14 var getCN = Y.ClassNameManager.getClassName, |
|
15 CALENDAR = 'calendar', |
|
16 KEY_DOWN = 40, |
|
17 KEY_UP = 38, |
|
18 KEY_LEFT = 37, |
|
19 KEY_RIGHT = 39, |
|
20 KEY_ENTER = 13, |
|
21 KEY_SPACE = 32, |
|
22 CAL_DAY_SELECTED = getCN(CALENDAR, 'day-selected'), |
|
23 CAL_DAY_HILITED = getCN(CALENDAR, 'day-highlighted'), |
|
24 CAL_DAY = getCN(CALENDAR, 'day'), |
|
25 CAL_PREVMONTH_DAY = getCN(CALENDAR, 'prevmonth-day'), |
|
26 CAL_NEXTMONTH_DAY = getCN(CALENDAR, 'nextmonth-day'), |
|
27 CAL_GRID = getCN(CALENDAR, 'grid'), |
|
28 ydate = Y.DataType.Date, |
|
29 CAL_PANE = getCN(CALENDAR, 'pane'), |
|
30 os = Y.UA.os; |
|
31 |
|
32 /** Create a calendar view to represent a single or multiple |
|
33 * month range of dates, rendered as a grid with date and |
|
34 * weekday labels. |
|
35 * |
|
36 * @class Calendar |
|
37 * @extends CalendarBase |
|
38 * @param config {Object} Configuration object (see Configuration attributes) |
|
39 * @constructor |
|
40 */ |
|
41 function Calendar() { |
|
42 Calendar.superclass.constructor.apply ( this, arguments ); |
|
43 } |
|
44 |
|
45 Y.Calendar = Y.extend(Calendar, Y.CalendarBase, { |
|
46 |
|
47 _keyEvents: [], |
|
48 |
|
49 _highlightedDateNode: null, |
|
50 |
|
51 /** |
|
52 * A property tracking the last selected date on the calendar, for the |
|
53 * purposes of multiple selection. |
|
54 * |
|
55 * @property _lastSelectedDate |
|
56 * @type Date |
|
57 * @default null |
|
58 * @private |
|
59 */ |
|
60 _lastSelectedDate: null, |
|
61 |
|
62 /** |
|
63 * Designated initializer. Activates the navigation plugin for the calendar. |
|
64 * |
|
65 * @method initializer |
|
66 */ |
|
67 initializer : function () { |
|
68 this.plug(Y.Plugin.CalendarNavigator); |
|
69 |
|
70 this._keyEvents = []; |
|
71 this._highlightedDateNode = null; |
|
72 this._lastSelectedDate = null; |
|
73 }, |
|
74 |
|
75 /** |
|
76 * Overrides the _bindCalendarEvents placeholder in CalendarBase |
|
77 * and binds calendar events during bindUI stage. |
|
78 * @method _bindCalendarEvents |
|
79 * @protected |
|
80 */ |
|
81 _bindCalendarEvents : function () { |
|
82 var contentBox = this.get('contentBox'), |
|
83 pane = contentBox.one("." + CAL_PANE); |
|
84 |
|
85 pane.on("selectstart", this._preventSelectionStart); |
|
86 pane.delegate("click", this._clickCalendar, "." + CAL_DAY + ", ." + CAL_PREVMONTH_DAY + ", ." + CAL_NEXTMONTH_DAY, this); |
|
87 pane.delegate("keydown", this._keydownCalendar, "." + CAL_GRID, this); |
|
88 pane.delegate("focus", this._focusCalendarGrid, "." + CAL_GRID, this); |
|
89 pane.delegate("focus", this._focusCalendarCell, "." + CAL_DAY, this); |
|
90 pane.delegate("blur", this._blurCalendarGrid, "." + CAL_GRID + ",." + CAL_DAY, this); |
|
91 |
|
92 |
|
93 this.after(['minimumDateChange', 'maximumDateChange'], this._afterCustomRendererChange); |
|
94 }, |
|
95 |
|
96 /** |
|
97 * Prevents text selection if it is started within the calendar pane |
|
98 * @method _preventSelectionStart |
|
99 * @param event {Event} The selectstart event |
|
100 * @protected |
|
101 */ |
|
102 _preventSelectionStart : function (event) { |
|
103 event.preventDefault(); |
|
104 }, |
|
105 |
|
106 /** |
|
107 * Highlights a specific date node with keyboard highlight class |
|
108 * @method _highlightDateNode |
|
109 * @param oDate {Date} Date corresponding the node to be highlighted |
|
110 * @protected |
|
111 */ |
|
112 _highlightDateNode : function (oDate) { |
|
113 this._unhighlightCurrentDateNode(); |
|
114 var newNode = this._dateToNode(oDate); |
|
115 newNode.focus(); |
|
116 newNode.addClass(CAL_DAY_HILITED); |
|
117 }, |
|
118 |
|
119 /** |
|
120 * Unhighlights a specific date node currently highlighted with keyboard highlight class |
|
121 * @method _unhighlightCurrentDateNode |
|
122 * @protected |
|
123 */ |
|
124 _unhighlightCurrentDateNode : function () { |
|
125 var allHilitedNodes = this.get("contentBox").all("." + CAL_DAY_HILITED); |
|
126 if (allHilitedNodes) { |
|
127 allHilitedNodes.removeClass(CAL_DAY_HILITED); |
|
128 } |
|
129 }, |
|
130 |
|
131 /** |
|
132 * Returns the grid number for a specific calendar grid (for multi-grid templates) |
|
133 * @method _getGridNumber |
|
134 * @param gridNode {Node} Node corresponding to a specific grid |
|
135 * @protected |
|
136 */ |
|
137 _getGridNumber : function (gridNode) { |
|
138 var idParts = gridNode.get("id").split("_").reverse(); |
|
139 |
|
140 return parseInt(idParts[0], 10); |
|
141 }, |
|
142 |
|
143 /** |
|
144 * Handler for loss of focus of calendar grid |
|
145 * @method _blurCalendarGrid |
|
146 * @protected |
|
147 */ |
|
148 _blurCalendarGrid : function () { |
|
149 this._unhighlightCurrentDateNode(); |
|
150 }, |
|
151 |
|
152 |
|
153 /** |
|
154 * Handler for gain of focus of calendar cell |
|
155 * @method _focusCalendarCell |
|
156 * @protected |
|
157 */ |
|
158 _focusCalendarCell : function (ev) { |
|
159 this._highlightedDateNode = ev.target; |
|
160 ev.stopPropagation(); |
|
161 }, |
|
162 |
|
163 /** |
|
164 * Handler for gain of focus of calendar grid |
|
165 * @method _focusCalendarGrid |
|
166 * @protected |
|
167 */ |
|
168 _focusCalendarGrid : function () { |
|
169 this._unhighlightCurrentDateNode(); |
|
170 this._highlightedDateNode = null; |
|
171 }, |
|
172 |
|
173 /** |
|
174 * Handler for keyboard press on a calendar grid |
|
175 * @method _keydownCalendar |
|
176 * @protected |
|
177 */ |
|
178 _keydownCalendar : function (ev) { |
|
179 var gridNum = this._getGridNumber(ev.target), |
|
180 curDate = !this._highlightedDateNode ? null : this._nodeToDate(this._highlightedDateNode), |
|
181 keyCode = ev.keyCode, |
|
182 dayNum = 0, |
|
183 dir = '', |
|
184 selMode, |
|
185 newDate, |
|
186 startDate, |
|
187 endDate, |
|
188 lastPaneDate; |
|
189 |
|
190 switch(keyCode) { |
|
191 case KEY_DOWN: |
|
192 dayNum = 7; |
|
193 dir = 's'; |
|
194 break; |
|
195 case KEY_UP: |
|
196 dayNum = -7; |
|
197 dir = 'n'; |
|
198 break; |
|
199 case KEY_LEFT: |
|
200 dayNum = -1; |
|
201 dir = 'w'; |
|
202 break; |
|
203 case KEY_RIGHT: |
|
204 dayNum = 1; |
|
205 dir = 'e'; |
|
206 break; |
|
207 case KEY_SPACE: case KEY_ENTER: |
|
208 ev.preventDefault(); |
|
209 if (this._highlightedDateNode) { |
|
210 selMode = this.get("selectionMode"); |
|
211 if (selMode === "single" && !this._highlightedDateNode.hasClass(CAL_DAY_SELECTED)) { |
|
212 this._clearSelection(true); |
|
213 this._addDateToSelection(curDate); |
|
214 } else if (selMode === "multiple" || selMode === "multiple-sticky") { |
|
215 if (this._highlightedDateNode.hasClass(CAL_DAY_SELECTED)) { |
|
216 this._removeDateFromSelection(curDate); |
|
217 } else { |
|
218 this._addDateToSelection(curDate); |
|
219 } |
|
220 } |
|
221 } |
|
222 break; |
|
223 } |
|
224 |
|
225 |
|
226 if (keyCode === KEY_DOWN || keyCode === KEY_UP || keyCode === KEY_LEFT || keyCode === KEY_RIGHT) { |
|
227 |
|
228 if (!curDate) { |
|
229 curDate = ydate.addMonths(this.get("date"), gridNum); |
|
230 dayNum = 0; |
|
231 } |
|
232 |
|
233 ev.preventDefault(); |
|
234 |
|
235 newDate = ydate.addDays(curDate, dayNum); |
|
236 startDate = this.get("date"); |
|
237 endDate = ydate.addMonths(this.get("date"), this._paneNumber - 1); |
|
238 lastPaneDate = new Date(endDate); |
|
239 endDate.setDate(ydate.daysInMonth(endDate)); |
|
240 |
|
241 if (ydate.isInRange(newDate, startDate, endDate)) { |
|
242 /* |
|
243 var paneShift = (newDate.getMonth() - curDate.getMonth()) % 10; |
|
244 |
|
245 if (paneShift != 0) { |
|
246 var newGridNum = gridNum + paneShift, |
|
247 contentBox = this.get('contentBox'), |
|
248 newPane = contentBox.one("#" + this._calendarId + "_pane_" + newGridNum); |
|
249 newPane.focus(); |
|
250 } |
|
251 */ |
|
252 this._highlightDateNode(newDate); |
|
253 } else if (ydate.isGreater(startDate, newDate)) { |
|
254 if (!ydate.isGreaterOrEqual(this.get("minimumDate"), startDate)) { |
|
255 this.set("date", ydate.addMonths(startDate, -1)); |
|
256 this._highlightDateNode(newDate); |
|
257 } |
|
258 } else if (ydate.isGreater(newDate, endDate)) { |
|
259 if (!ydate.isGreaterOrEqual(lastPaneDate, this.get("maximumDate"))) { |
|
260 this.set("date", ydate.addMonths(startDate, 1)); |
|
261 this._highlightDateNode(newDate); |
|
262 } |
|
263 } |
|
264 } |
|
265 }, |
|
266 |
|
267 /** |
|
268 * Handles the calendar clicks based on selection mode. |
|
269 * @method _clickCalendar |
|
270 * @param {Event} ev A click event |
|
271 * @private |
|
272 */ |
|
273 _clickCalendar : function (ev) { |
|
274 var clickedCell = ev.currentTarget, |
|
275 clickedCellIsDay = clickedCell.hasClass(CAL_DAY) && |
|
276 !clickedCell.hasClass(CAL_PREVMONTH_DAY) && |
|
277 !clickedCell.hasClass(CAL_NEXTMONTH_DAY), |
|
278 |
|
279 clickedCellIsSelected = clickedCell.hasClass(CAL_DAY_SELECTED), |
|
280 selectedDate; |
|
281 |
|
282 switch (this.get("selectionMode")) { |
|
283 case("single"): |
|
284 if (clickedCellIsDay) { |
|
285 if (!clickedCellIsSelected) { |
|
286 this._clearSelection(true); |
|
287 this._addDateToSelection(this._nodeToDate(clickedCell)); |
|
288 } |
|
289 } |
|
290 break; |
|
291 case("multiple-sticky"): |
|
292 if (clickedCellIsDay) { |
|
293 if (clickedCellIsSelected) { |
|
294 this._removeDateFromSelection(this._nodeToDate(clickedCell)); |
|
295 } else { |
|
296 this._addDateToSelection(this._nodeToDate(clickedCell)); |
|
297 } |
|
298 } |
|
299 break; |
|
300 case("multiple"): |
|
301 if (clickedCellIsDay) { |
|
302 if (!ev.metaKey && !ev.ctrlKey && !ev.shiftKey) { |
|
303 this._clearSelection(true); |
|
304 this._lastSelectedDate = this._nodeToDate(clickedCell); |
|
305 this._addDateToSelection(this._lastSelectedDate); |
|
306 } else if (((os === 'macintosh' && ev.metaKey) || (os !== 'macintosh' && ev.ctrlKey)) && !ev.shiftKey) { |
|
307 if (clickedCellIsSelected) { |
|
308 this._removeDateFromSelection(this._nodeToDate(clickedCell)); |
|
309 this._lastSelectedDate = null; |
|
310 } else { |
|
311 this._lastSelectedDate = this._nodeToDate(clickedCell); |
|
312 this._addDateToSelection(this._lastSelectedDate); |
|
313 } |
|
314 } else if (((os === 'macintosh' && ev.metaKey) || (os !== 'macintosh' && ev.ctrlKey)) && ev.shiftKey) { |
|
315 if (this._lastSelectedDate) { |
|
316 selectedDate = this._nodeToDate(clickedCell); |
|
317 this._addDateRangeToSelection(selectedDate, this._lastSelectedDate); |
|
318 this._lastSelectedDate = selectedDate; |
|
319 } else { |
|
320 this._lastSelectedDate = this._nodeToDate(clickedCell); |
|
321 this._addDateToSelection(this._lastSelectedDate); |
|
322 } |
|
323 } else if (ev.shiftKey) { |
|
324 if (this._lastSelectedDate) { |
|
325 selectedDate = this._nodeToDate(clickedCell); |
|
326 this._clearSelection(true); |
|
327 this._addDateRangeToSelection(selectedDate, this._lastSelectedDate); |
|
328 this._lastSelectedDate = selectedDate; |
|
329 } else { |
|
330 this._clearSelection(true); |
|
331 this._lastSelectedDate = this._nodeToDate(clickedCell); |
|
332 this._addDateToSelection(this._lastSelectedDate); |
|
333 } |
|
334 } |
|
335 } |
|
336 break; |
|
337 } |
|
338 |
|
339 if (clickedCellIsDay) { |
|
340 /** |
|
341 * Fired when a specific date cell in the calendar is clicked. The event carries a |
|
342 * payload which includes a `cell` property corresponding to the node of the actual |
|
343 * date cell, and a `date` property, with the `Date` that was clicked. |
|
344 * |
|
345 * @event dateClick |
|
346 */ |
|
347 this.fire("dateClick", {cell: clickedCell, date: this._nodeToDate(clickedCell)}); |
|
348 } else if (clickedCell.hasClass(CAL_PREVMONTH_DAY)) { |
|
349 /** |
|
350 * Fired when any of the previous month's days displayed before the calendar grid |
|
351 * are clicked. |
|
352 * |
|
353 * @event prevMonthClick |
|
354 */ |
|
355 this.fire("prevMonthClick"); |
|
356 } else if (clickedCell.hasClass(CAL_NEXTMONTH_DAY)) { |
|
357 /** |
|
358 * Fired when any of the next month's days displayed after the calendar grid |
|
359 * are clicked. |
|
360 * |
|
361 * @event nextMonthClick |
|
362 */ |
|
363 this.fire("nextMonthClick"); |
|
364 } |
|
365 }, |
|
366 |
|
367 /** |
|
368 * Overrides CalendarBase.prototype._canBeSelected to disable |
|
369 * nodes earlier than minimumDate and later than maximumDate |
|
370 * @method _canBeSelected |
|
371 * @private |
|
372 */ |
|
373 _canBeSelected : function (date) { |
|
374 var minDate = this.get('minimumDate'), |
|
375 maxDate = this.get('maximumDate'); |
|
376 |
|
377 if ((minDate && !ydate.isGreaterOrEqual(date, minDate)) || |
|
378 (maxDate && ydate.isGreater(date, maxDate))) { |
|
379 return false; |
|
380 } |
|
381 |
|
382 return Calendar.superclass._canBeSelected.call(this, date); |
|
383 }, |
|
384 |
|
385 /** |
|
386 * Overrides CalendarBase.prototype._renderCustomRules to disable |
|
387 * nodes earlier than minimumDate and later than maximumDate |
|
388 * @method _renderCustomRules |
|
389 * @private |
|
390 */ |
|
391 _renderCustomRules: function () { |
|
392 Calendar.superclass._renderCustomRules.call(this); |
|
393 |
|
394 var minDate = this.get('minimumDate'), |
|
395 maxDate = this.get('maximumDate'), |
|
396 dates = [], |
|
397 i, l, |
|
398 paneDate, |
|
399 paneNum; |
|
400 |
|
401 if (!minDate && !maxDate) { |
|
402 return; |
|
403 } |
|
404 |
|
405 for (paneNum = 0; paneNum < this._paneNumber; paneNum++) { |
|
406 paneDate = ydate.addMonths(this.get("date"), paneNum); |
|
407 dates = dates.concat(ydate.listOfDatesInMonth(paneDate)); |
|
408 } |
|
409 |
|
410 if (minDate) { |
|
411 for (i = 0, l = dates.length; i < l; i++) { |
|
412 if (!ydate.isGreaterOrEqual(dates[i], minDate)) { |
|
413 this._disableDate(dates[i]); |
|
414 } else { |
|
415 break; |
|
416 } |
|
417 } |
|
418 } |
|
419 |
|
420 if (maxDate) { |
|
421 for (i = dates.length - 1; i >= 0; i--) { |
|
422 if (ydate.isGreater(dates[i], maxDate)) { |
|
423 this._disableDate(dates[i]); |
|
424 } else { |
|
425 break; |
|
426 } |
|
427 } |
|
428 } |
|
429 }, |
|
430 |
|
431 /** |
|
432 * Subtracts one month from the current calendar view. |
|
433 * @method subtractMonth |
|
434 * @return {Calendar} A reference to this object |
|
435 * @chainable |
|
436 */ |
|
437 subtractMonth : function (e) { |
|
438 this.set("date", ydate.addMonths(this.get("date"), -1)); |
|
439 if (e) { |
|
440 e.halt(); |
|
441 } |
|
442 return this; |
|
443 }, |
|
444 |
|
445 /** |
|
446 * Subtracts one year from the current calendar view. |
|
447 * @method subtractYear |
|
448 * @return {Calendar} A reference to this object |
|
449 * @chainable |
|
450 */ |
|
451 subtractYear : function (e) { |
|
452 this.set("date", ydate.addYears(this.get("date"), -1)); |
|
453 if (e) { |
|
454 e.halt(); |
|
455 } |
|
456 return this; |
|
457 }, |
|
458 |
|
459 /** |
|
460 * Adds one month to the current calendar view. |
|
461 * @method addMonth |
|
462 * @return {Calendar} A reference to this object |
|
463 * @chainable |
|
464 */ |
|
465 addMonth : function (e) { |
|
466 this.set("date", ydate.addMonths(this.get("date"), 1)); |
|
467 if (e) { |
|
468 e.halt(); |
|
469 } |
|
470 return this; |
|
471 }, |
|
472 |
|
473 /** |
|
474 * Adds one year to the current calendar view. |
|
475 * @method addYear |
|
476 * @return {Calendar} A reference to this object |
|
477 * @chainable |
|
478 */ |
|
479 addYear : function (e) { |
|
480 this.set("date", ydate.addYears(this.get("date"), 1)); |
|
481 if (e) { |
|
482 e.halt(); |
|
483 } |
|
484 return this; |
|
485 } |
|
486 }, { |
|
487 /** |
|
488 * The identity of the widget. |
|
489 * |
|
490 * @property NAME |
|
491 * @type String |
|
492 * @default 'calendar' |
|
493 * @readOnly |
|
494 * @protected |
|
495 * @static |
|
496 */ |
|
497 NAME: "calendar", |
|
498 |
|
499 /** |
|
500 * Static property used to define the default attribute configuration of |
|
501 * the Widget. |
|
502 * |
|
503 * @property ATTRS |
|
504 * @type {Object} |
|
505 * @protected |
|
506 * @static |
|
507 */ |
|
508 ATTRS: { |
|
509 |
|
510 /** |
|
511 * A setting specifying the type of selection the calendar allows. |
|
512 * Possible values include: |
|
513 * <ul> |
|
514 * <li>`single` - One date at a time</li> |
|
515 * <li>`multiple-sticky` - Multiple dates, selected one at a time (the dates "stick"). This option |
|
516 * is appropriate for mobile devices, where function keys from the keyboard are not available.</li> |
|
517 * <li>`multiple` - Multiple dates, selected with Ctrl/Meta keys for additional single |
|
518 * dates, and Shift key for date ranges.</li> |
|
519 * |
|
520 * @attribute selectionMode |
|
521 * @type String |
|
522 * @default single |
|
523 */ |
|
524 selectionMode: { |
|
525 value: "single" |
|
526 }, |
|
527 |
|
528 /** |
|
529 * The date corresponding to the current calendar view. Always |
|
530 * normalized to the first of the month that contains the date |
|
531 * at assignment time. Used as the first date visible in the |
|
532 * calendar. |
|
533 * |
|
534 * @attribute date |
|
535 * @type Date |
|
536 * @default Today's date as set on the user's computer. |
|
537 */ |
|
538 date: { |
|
539 value: new Date(), |
|
540 lazyAdd: false, |
|
541 setter: function (val) { |
|
542 |
|
543 var newDate = this._normalizeDate(val), |
|
544 newEndDate = ydate.addMonths(newDate, this._paneNumber - 1), |
|
545 minDate = this.get("minimumDate"), |
|
546 maxDate = this.get("maximumDate"); |
|
547 |
|
548 if ((!minDate || ydate.isGreaterOrEqual(newDate, minDate)) && |
|
549 (!maxDate || ydate.isGreaterOrEqual(maxDate, newEndDate)) |
|
550 ) { |
|
551 return newDate; |
|
552 } else if (minDate && ydate.isGreater(minDate, newDate)) { |
|
553 return this._normalizeDate(minDate); |
|
554 } else if (maxDate && ydate.isGreater(newEndDate, maxDate)) { |
|
555 return ydate.addMonths(this._normalizeDate(maxDate), 1 - this._paneNumber); |
|
556 } |
|
557 } |
|
558 }, |
|
559 |
|
560 /** |
|
561 * Unless minimumDate is null, it will not be possible to display and select dates earlier than this one. |
|
562 * |
|
563 * @attribute minimumDate |
|
564 * @type Date |
|
565 * @default null |
|
566 */ |
|
567 minimumDate: { |
|
568 value: null, |
|
569 setter: function (val) { |
|
570 if (Y.Lang.isDate(val)) { |
|
571 var curDate = this.get('date'), |
|
572 newMin = this._normalizeTime(val); |
|
573 if (curDate && !ydate.isGreaterOrEqual(curDate, newMin)) { |
|
574 this.set('date', val); |
|
575 } |
|
576 return newMin; |
|
577 } else { |
|
578 return null; |
|
579 } |
|
580 } |
|
581 }, |
|
582 |
|
583 /** |
|
584 * Unless maximumDate is null, it will not be possible to display and select dates later than this one. |
|
585 * |
|
586 * @attribute maximumDate |
|
587 * @type Date |
|
588 * @default null |
|
589 */ |
|
590 maximumDate: { |
|
591 value: null, |
|
592 setter: function (val) { |
|
593 if (Y.Lang.isDate(val)) { |
|
594 var curDate = this.get('date'); |
|
595 |
|
596 if (curDate && !ydate.isGreaterOrEqual(val, ydate.addMonths(curDate, this._paneNumber - 1))) { |
|
597 this.set('date', ydate.addMonths(this._normalizeDate(val), 1 - this._paneNumber)); |
|
598 } |
|
599 |
|
600 return this._normalizeTime(val); |
|
601 } else { |
|
602 return null; |
|
603 } |
|
604 } |
|
605 } |
|
606 } |
|
607 }); |
|
608 |
|
609 |
|
610 }, '@VERSION@', {"requires": ["calendar-base", "calendarnavigator"], "skinnable": true}); |