API Docs for: 3.10.3
Show:

File: calendar/js/calendar.js

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