calendar.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /*eslint max-depth: 0*/
  6. (function (factory) {
  7. 'use strict';
  8. if (typeof define === 'function' && define.amd) {
  9. define([
  10. 'jquery',
  11. 'jquery/ui',
  12. 'jquery/jquery-ui-timepicker-addon'
  13. ], factory);
  14. } else {
  15. factory(window.jQuery);
  16. }
  17. }(function ($) {
  18. 'use strict';
  19. var calendarBasePrototype,
  20. datepickerPrototype = $.datepicker.constructor.prototype;
  21. $.datepicker.markerClassName = '_has-datepicker';
  22. /**
  23. * Extend JQuery date picker prototype with store local time methods
  24. */
  25. $.extend(datepickerPrototype, {
  26. /**
  27. * Get date/time according to store settings.
  28. * We use serverTimezoneOffset (in seconds) instead of serverTimezoneSeconds
  29. * in order to have ability to know actual store time even if page hadn't been reloaded
  30. * @returns {Date}
  31. */
  32. _getTimezoneDate: function (options) {
  33. // local time in ms
  34. var ms = Date.now();
  35. options = options || $.calendarConfig || {};
  36. // Adjust milliseconds according to store timezone offset,
  37. // mind the GMT zero offset
  38. if (typeof options.serverTimezoneOffset !== 'undefined') {
  39. // Make UTC time and add store timezone offset in seconds
  40. ms += new Date().getTimezoneOffset() * 60 * 1000 + options.serverTimezoneOffset * 1000;
  41. } else if (typeof options.serverTimezoneSeconds !== 'undefined') {
  42. //Set milliseconds according to client local timezone offset
  43. ms = (options.serverTimezoneSeconds + new Date().getTimezoneOffset() * 60) * 1000;
  44. }
  45. return new Date(ms);
  46. },
  47. /**
  48. * Set date/time according to store settings.
  49. * @param {String|Object} target - the target input field or division or span
  50. */
  51. _setTimezoneDateDatepicker: function (target) {
  52. this._setDateDatepicker(target, this._getTimezoneDate());
  53. }
  54. });
  55. /**
  56. * Widget calendar
  57. */
  58. $.widget('mage.calendar', {
  59. options: {
  60. autoComplete: true
  61. },
  62. /**
  63. * Merge global options with options passed to widget invoke
  64. * @protected
  65. */
  66. _create: function () {
  67. this._enableAMPM();
  68. this.options = $.extend(
  69. {},
  70. $.calendarConfig ? $.calendarConfig : {},
  71. this.options.showsTime ? {
  72. showTime: true,
  73. showHour: true,
  74. showMinute: true
  75. } : {},
  76. this.options
  77. );
  78. this._initPicker(this.element);
  79. this._overwriteGenerateHtml();
  80. },
  81. /**
  82. * Get picker name
  83. * @protected
  84. */
  85. _picker: function () {
  86. return this.options.showsTime ? 'datetimepicker' : 'datepicker';
  87. },
  88. /**
  89. * Fix for Timepicker - Set ampm option for Timepicker if timeformat contains string 'tt'
  90. * @protected
  91. */
  92. _enableAMPM: function () {
  93. if (this.options.timeFormat && this.options.timeFormat.indexOf('tt') >= 0) {
  94. this.options.ampm = true;
  95. }
  96. },
  97. /**
  98. * Wrapper for overwrite jQuery UI datepicker function.
  99. */
  100. _overwriteGenerateHtml: function () {
  101. /**
  102. * Overwrite jQuery UI datepicker function.
  103. * Reason: magento date could be set before calendar show
  104. * but local date will be styled as current in original _generateHTML
  105. *
  106. * @param {Object} inst - instance datepicker.
  107. * @return {String} html template
  108. */
  109. $.datepicker.constructor.prototype._generateHTML = function (inst) {
  110. var today = this._getTimezoneDate(),
  111. isRTL = this._get(inst, 'isRTL'),
  112. showButtonPanel = this._get(inst, 'showButtonPanel'),
  113. hideIfNoPrevNext = this._get(inst, 'hideIfNoPrevNext'),
  114. navigationAsDateFormat = this._get(inst, 'navigationAsDateFormat'),
  115. numMonths = this._getNumberOfMonths(inst),
  116. showCurrentAtPos = this._get(inst, 'showCurrentAtPos'),
  117. stepMonths = this._get(inst, 'stepMonths'),
  118. isMultiMonth = parseInt(numMonths[0], 10) !== 1 || parseInt(numMonths[1], 10) !== 1,
  119. currentDate = this._daylightSavingAdjust(!inst.currentDay ? new Date(9999, 9, 9) :
  120. new Date(inst.currentYear, inst.currentMonth, inst.currentDay)),
  121. minDate = this._getMinMaxDate(inst, 'min'),
  122. maxDate = this._getMinMaxDate(inst, 'max'),
  123. drawMonth = inst.drawMonth - showCurrentAtPos,
  124. drawYear = inst.drawYear,
  125. maxDraw,
  126. prevText = this._get(inst, 'prevText'),
  127. prev,
  128. nextText = this._get(inst, 'nextText'),
  129. next,
  130. currentText = this._get(inst, 'currentText'),
  131. gotoDate,
  132. controls,
  133. buttonPanel,
  134. firstDay,
  135. showWeek = this._get(inst, 'showWeek'),
  136. dayNames = this._get(inst, 'dayNames'),
  137. dayNamesMin = this._get(inst, 'dayNamesMin'),
  138. monthNames = this._get(inst, 'monthNames'),
  139. monthNamesShort = this._get(inst, 'monthNamesShort'),
  140. beforeShowDay = this._get(inst, 'beforeShowDay'),
  141. showOtherMonths = this._get(inst, 'showOtherMonths'),
  142. selectOtherMonths = this._get(inst, 'selectOtherMonths'),
  143. defaultDate = this._getDefaultDate(inst),
  144. html = '',
  145. row = 0,
  146. col = 0,
  147. selectedDate,
  148. cornerClass = ' ui-corner-all',
  149. group = '',
  150. calender = '',
  151. dow = 0,
  152. thead,
  153. day,
  154. daysInMonth,
  155. leadDays,
  156. curRows,
  157. numRows,
  158. printDate,
  159. dRow = 0,
  160. tbody,
  161. daySettings,
  162. otherMonth,
  163. unselectable;
  164. if (drawMonth < 0) {
  165. drawMonth += 12;
  166. drawYear--;
  167. }
  168. if (maxDate) {
  169. maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(),
  170. maxDate.getMonth() - numMonths[0] * numMonths[1] + 1, maxDate.getDate()));
  171. maxDraw = minDate && maxDraw < minDate ? minDate : maxDraw;
  172. while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) {
  173. drawMonth--;
  174. if (drawMonth < 0) {
  175. drawMonth = 11;
  176. drawYear--;
  177. }
  178. }
  179. }
  180. inst.drawMonth = drawMonth;
  181. inst.drawYear = drawYear;
  182. prevText = !navigationAsDateFormat ? prevText : this.formatDate(prevText,
  183. this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)),
  184. this._getFormatConfig(inst));
  185. prev = this._canAdjustMonth(inst, -1, drawYear, drawMonth) ?
  186. '<a class="ui-datepicker-prev ui-corner-all" data-handler="prev" data-event="click"' +
  187. ' title="' + prevText + '">' +
  188. '<span class="ui-icon ui-icon-circle-triangle-' + (isRTL ? 'e' : 'w') + '">' +
  189. '' + prevText + '</span></a>'
  190. : hideIfNoPrevNext ? ''
  191. : '<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="' +
  192. '' + prevText + '"><span class="ui-icon ui-icon-circle-triangle-' +
  193. '' + (isRTL ? 'e' : 'w') + '">' + prevText + '</span></a>';
  194. nextText = !navigationAsDateFormat ?
  195. nextText
  196. : this.formatDate(nextText,
  197. this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)),
  198. this._getFormatConfig(inst));
  199. next = this._canAdjustMonth(inst, +1, drawYear, drawMonth) ?
  200. '<a class="ui-datepicker-next ui-corner-all" data-handler="next" data-event="click"' +
  201. 'title="' + nextText + '"><span class="ui-icon ui-icon-circle-triangle-' +
  202. '' + (isRTL ? 'w' : 'e') + '">' + nextText + '</span></a>'
  203. : hideIfNoPrevNext ? ''
  204. : '<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="' + nextText + '">' +
  205. '<span class="ui-icon ui-icon-circle-triangle-' + (isRTL ? 'w' : 'e') + '">' + nextText +
  206. '</span></a>';
  207. gotoDate = this._get(inst, 'gotoCurrent') && inst.currentDay ? currentDate : today;
  208. currentText = !navigationAsDateFormat ? currentText :
  209. this.formatDate(currentText, gotoDate, this._getFormatConfig(inst));
  210. controls = !inst.inline ?
  211. '<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ' +
  212. 'ui-corner-all" data-handler="hide" data-event="click">' +
  213. this._get(inst, 'closeText') + '</button>'
  214. : '';
  215. buttonPanel = showButtonPanel ?
  216. '<div class="ui-datepicker-buttonpane ui-widget-content">' + (isRTL ? controls : '') +
  217. (this._isInRange(inst, gotoDate) ? '<button type="button" class="ui-datepicker-current ' +
  218. 'ui-state-default ui-priority-secondary ui-corner-all" data-handler="today" data-event="click"' +
  219. '>' + currentText + '</button>' : '') + (isRTL ? '' : controls) + '</div>' : '';
  220. firstDay = parseInt(this._get(inst, 'firstDay'), 10);
  221. firstDay = isNaN(firstDay) ? 0 : firstDay;
  222. for (row = 0; row < numMonths[0]; row++) {
  223. this.maxRows = 4;
  224. for (col = 0; col < numMonths[1]; col++) {
  225. selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay));
  226. calender = '';
  227. if (isMultiMonth) {
  228. calender += '<div class="ui-datepicker-group';
  229. if (numMonths[1] > 1) {
  230. switch (col) {
  231. case 0: calender += ' ui-datepicker-group-first';
  232. cornerClass = ' ui-corner-' + (isRTL ? 'right' : 'left');
  233. break;
  234. case numMonths[1] - 1: calender += ' ui-datepicker-group-last';
  235. cornerClass = ' ui-corner-' + (isRTL ? 'left' : 'right');
  236. break;
  237. default: calender += ' ui-datepicker-group-middle'; cornerClass = '';
  238. }
  239. }
  240. calender += '">';
  241. }
  242. calender += '<div class="ui-datepicker-header ' +
  243. 'ui-widget-header ui-helper-clearfix' + cornerClass + '">' +
  244. (/all|left/.test(cornerClass) && parseInt(row, 10) === 0 ? isRTL ? next : prev : '') +
  245. (/all|right/.test(cornerClass) && parseInt(row, 10) === 0 ? isRTL ? prev : next : '') +
  246. this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate,
  247. row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers
  248. '</div><table class="ui-datepicker-calendar"><thead>' +
  249. '<tr>';
  250. thead = showWeek ?
  251. '<th class="ui-datepicker-week-col">' + this._get(inst, 'weekHeader') + '</th>' : '';
  252. for (dow = 0; dow < 7; dow++) { // days of the week
  253. day = (dow + firstDay) % 7;
  254. thead += '<th' + ((dow + firstDay + 6) % 7 >= 5 ?
  255. ' class="ui-datepicker-week-end"' : '') + '>' +
  256. '<span title="' + dayNames[day] + '">' + dayNamesMin[day] + '</span></th>';
  257. }
  258. calender += thead + '</tr></thead><tbody>';
  259. daysInMonth = this._getDaysInMonth(drawYear, drawMonth);
  260. if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) {
  261. inst.selectedDay = Math.min(inst.selectedDay, daysInMonth);
  262. }
  263. leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7;
  264. curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate
  265. numRows = isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows;
  266. this.maxRows = numRows;
  267. printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays));
  268. for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows
  269. calender += '<tr>';
  270. tbody = !showWeek ? '' : '<td class="ui-datepicker-week-col">' +
  271. this._get(inst, 'calculateWeek')(printDate) + '</td>';
  272. for (dow = 0; dow < 7; dow++) { // create date picker days
  273. daySettings = beforeShowDay ?
  274. beforeShowDay.apply(inst.input ? inst.input[0] : null, [printDate]) : [true, ''];
  275. otherMonth = printDate.getMonth() !== drawMonth;
  276. unselectable = otherMonth && !selectOtherMonths || !daySettings[0] ||
  277. minDate && printDate < minDate || maxDate && printDate > maxDate;
  278. tbody += '<td class="' +
  279. ((dow + firstDay + 6) % 7 >= 5 ? ' ui-datepicker-week-end' : '') + // highlight weekends
  280. (otherMonth ? ' ui-datepicker-other-month' : '') + // highlight days from other months
  281. (printDate.getTime() === selectedDate.getTime() &&
  282. drawMonth === inst.selectedMonth && inst._keyEvent || // user pressed key
  283. defaultDate.getTime() === printDate.getTime() &&
  284. defaultDate.getTime() === selectedDate.getTime() ?
  285. // or defaultDate is current printedDate and defaultDate is selectedDate
  286. ' ' + this._dayOverClass : '') + // highlight selected day
  287. (unselectable ? ' ' + this._unselectableClass + ' ui-state-disabled' : '') +
  288. (otherMonth && !showOtherMonths ? '' : ' ' + daySettings[1] + // highlight custom dates
  289. (printDate.getTime() === currentDate.getTime() ? ' ' + this._currentClass : '') +
  290. (printDate.getDate() === today.getDate() && printDate.getMonth() === today.getMonth() &&
  291. printDate.getYear() === today.getYear() ? ' ui-datepicker-today' : '')) + '"' +
  292. ((!otherMonth || showOtherMonths) && daySettings[2] ?
  293. ' title="' + daySettings[2] + '"' : '') + // cell title
  294. (unselectable ? '' : ' data-handler="selectDay" data-event="click" data-month="' +
  295. '' + printDate.getMonth() + '" data-year="' + printDate.getFullYear() + '"') + '>' +
  296. (otherMonth && !showOtherMonths ? '&#xa0;' : // display for other months
  297. unselectable ? '<span class="ui-state-default">' + printDate.getDate() + '</span>'
  298. : '<a class="ui-state-default' +
  299. (printDate.getTime() === today.getTime() ? ' ' : '') +
  300. (printDate.getTime() === currentDate.getTime() ? ' ui-state-active' : '') +
  301. (otherMonth ? ' ui-priority-secondary' : '') +
  302. '" href="#">' + printDate.getDate() + '</a>') + '</td>';
  303. printDate.setDate(printDate.getDate() + 1);
  304. printDate = this._daylightSavingAdjust(printDate);
  305. }
  306. calender += tbody + '</tr>';
  307. }
  308. drawMonth++;
  309. if (drawMonth > 11) {
  310. drawMonth = 0;
  311. drawYear++;
  312. }
  313. calender += '</tbody></table>' + (isMultiMonth ? '</div>' +
  314. (numMonths[0] > 0 && col === numMonths[1] - 1 ? '<div class="ui-datepicker-row-break"></div>'
  315. : '') : '');
  316. group += calender;
  317. }
  318. html += group;
  319. }
  320. html += buttonPanel + ($.ui.ie6 && !inst.inline ?
  321. '<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>' : '');
  322. inst._keyEvent = false;
  323. return html;
  324. };
  325. },
  326. /**
  327. * Set current date if the date is not set
  328. * @protected
  329. * @param {Object} element
  330. */
  331. _setCurrentDate: function (element) {
  332. if (!element.val()) {
  333. element[this._picker()]('setTimezoneDate').val('');
  334. }
  335. },
  336. /**
  337. * Init Datetimepicker
  338. * @protected
  339. * @param {Object} element
  340. */
  341. _initPicker: function (element) {
  342. var picker = element[this._picker()](this.options),
  343. pickerButtonText = picker.next('.ui-datepicker-trigger')
  344. .find('img')
  345. .attr('title');
  346. picker.next('.ui-datepicker-trigger')
  347. .addClass('v-middle')
  348. .text('') // Remove jQuery UI datepicker generated image
  349. .append('<span>' + pickerButtonText + '</span>');
  350. $(element).attr('autocomplete', this.options.autoComplete ? 'on' : 'off');
  351. this._setCurrentDate(element);
  352. },
  353. /**
  354. * destroy instance of datetimepicker
  355. */
  356. _destroy: function () {
  357. this.element[this._picker()]('destroy');
  358. this._super();
  359. },
  360. /**
  361. * Method is kept for backward compatibility and unit-tests acceptance
  362. * see \mage\calendar\calendar-test.js
  363. * @return {Object} date
  364. */
  365. getTimezoneDate: function () {
  366. return datepickerPrototype._getTimezoneDate.call(this, this.options);
  367. }
  368. });
  369. calendarBasePrototype = $.mage.calendar.prototype;
  370. /**
  371. * Extension for Calendar - date and time format convert functionality
  372. * @var {Object}
  373. */
  374. $.widget('mage.calendar', $.extend({}, calendarBasePrototype,
  375. /** @lends {$.mage.calendar.prototype} */ {
  376. /**
  377. * key - backend format, value - jquery format
  378. * @type {Object}
  379. * @private
  380. */
  381. dateTimeFormat: {
  382. date: {
  383. 'EEEE': 'DD',
  384. 'EEE': 'D',
  385. 'EE': 'D',
  386. 'E': 'D',
  387. 'D': 'o',
  388. 'MMMM': 'MM',
  389. 'MMM': 'M',
  390. 'MM': 'mm',
  391. 'M': 'mm',
  392. 'yyyy': 'yy',
  393. 'y': 'yy',
  394. 'Y': 'yy',
  395. 'yy': 'yy' // Always long year format on frontend
  396. },
  397. time: {
  398. 'a': 'TT'
  399. }
  400. },
  401. /**
  402. * Add Date and Time converting to _create method
  403. * @protected
  404. */
  405. _create: function () {
  406. if (this.options.dateFormat) {
  407. this.options.dateFormat = this._convertFormat(this.options.dateFormat, 'date');
  408. }
  409. if (this.options.timeFormat) {
  410. this.options.timeFormat = this._convertFormat(this.options.timeFormat, 'time');
  411. }
  412. calendarBasePrototype._create.apply(this, arguments);
  413. },
  414. /**
  415. * Converting date or time format
  416. * @protected
  417. * @param {String} format
  418. * @param {String} type
  419. * @return {String}
  420. */
  421. _convertFormat: function (format, type) {
  422. var symbols = format.match(/([a-z]+)/ig),
  423. separators = format.match(/([^a-z]+)/ig),
  424. self = this,
  425. convertedFormat = '';
  426. if (symbols) {
  427. $.each(symbols, function (key, val) {
  428. convertedFormat +=
  429. (self.dateTimeFormat[type][val] || val) +
  430. (separators[key] || '');
  431. });
  432. }
  433. return convertedFormat;
  434. }
  435. })
  436. );
  437. /**
  438. * Widget dateRange
  439. * @extends $.mage.calendar
  440. */
  441. $.widget('mage.dateRange', $.mage.calendar, {
  442. /**
  443. * creates two instances of datetimepicker for date range selection
  444. * @protected
  445. */
  446. _initPicker: function () {
  447. var from,
  448. to;
  449. if (this.options.from && this.options.to) {
  450. from = this.element.find('#' + this.options.from.id);
  451. to = this.element.find('#' + this.options.to.id);
  452. this.options.onSelect = $.proxy(function (selectedDate) {
  453. to[this._picker()]('option', 'minDate', selectedDate);
  454. }, this);
  455. $.mage.calendar.prototype._initPicker.call(this, from);
  456. from.on('change', $.proxy(function () {
  457. to[this._picker()]('option', 'minDate', from[this._picker()]('getDate'));
  458. }, this));
  459. this.options.onSelect = $.proxy(function (selectedDate) {
  460. from[this._picker()]('option', 'maxDate', selectedDate);
  461. }, this);
  462. $.mage.calendar.prototype._initPicker.call(this, to);
  463. to.on('change', $.proxy(function () {
  464. from[this._picker()]('option', 'maxDate', to[this._picker()]('getDate'));
  465. }, this));
  466. }
  467. },
  468. /**
  469. * destroy two instances of datetimepicker
  470. */
  471. _destroy: function () {
  472. if (this.options.from) {
  473. this.element.find('#' + this.options.from.id)[this._picker()]('destroy');
  474. }
  475. if (this.options.to) {
  476. this.element.find('#' + this.options.to.id)[this._picker()]('destroy');
  477. }
  478. this._super();
  479. }
  480. });
  481. // Overrides the "today" button functionality to select today's date when clicked.
  482. $.datepicker._gotoTodayOriginal = $.datepicker._gotoToday;
  483. /**
  484. * overwrite jQuery UI _showDatepicker function for proper HTML generation conditions.
  485. *
  486. */
  487. $.datepicker._showDatepickerOriginal = $.datepicker._showDatepicker;
  488. /**
  489. * Triggers original method showDataPicker for rendering calendar
  490. * @param {HTMLObject} input
  491. * @private
  492. */
  493. $.datepicker._showDatepicker = function (input) {
  494. if (!input.disabled) {
  495. $.datepicker._showDatepickerOriginal.call(this, input);
  496. }
  497. };
  498. /**
  499. * _gotoToday
  500. * @param {Object} el
  501. */
  502. $.datepicker._gotoToday = function (el) {
  503. //Set date/time according to timezone offset
  504. $(el).datepicker('setTimezoneDate')
  505. // To ensure that user can re-select date field without clicking outside it first.
  506. .blur().trigger('change');
  507. };
  508. return {
  509. dateRange: $.mage.dateRange,
  510. calendar: $.mage.calendar
  511. };
  512. }));