UNPKG

18.1 kBJavaScriptView Raw
1import $ from './jquery';
2import datepickerUI from 'jquery-ui/ui/widgets/datepicker';
3import * as logger from './internal/log';
4import { supportsDateField } from './internal/browser';
5import globalize from './internal/globalize';
6import keyCode from './key-code';
7import { I18n } from './i18n';
8import InlineDialogEl from './inline-dialog2';
9import generateUniqueId from './unique-id';
10
11const makePopup = ({horizontalAlignment, datePickerUUID}) => {
12 const popupInlineDialogElement = new InlineDialogEl();
13 popupInlineDialogElement.id = datePickerUUID;
14
15 const popup = $(popupInlineDialogElement);
16 popup.attr('persistent', '');
17 popup.attr('data-aui-focus', 'false');
18 popup.attr('alignment', `bottom ${horizontalAlignment}`);
19 popup.addClass('aui-datepicker-dialog');
20
21 return popup;
22};
23
24const makeConfig = ({ minDate, maxDate, dateFormat, $field, onSelect, hide, onChangeMonthYear}) => ({
25 dateFormat,
26 defaultDate: $field.val(),
27 maxDate: maxDate || $field.attr('max'),
28 minDate: minDate || $field.attr('min'),
29 nextText: '>',
30 onSelect: function (dateText) {
31 $field.val(dateText);
32 $field.trigger('change');
33 hide();
34 // TODO make sure the docs are explicit about the fact that onSelect cannot be an arrow function
35 onSelect && onSelect.call(this, dateText);
36 },
37 onChangeMonthYear,
38 prevText: '<'
39});
40
41const initCalendar = ({config, popupContents, getCalendarNode, hint}) => {
42 const calendar = $(getCalendarNode());
43
44 calendar.datepicker(config);
45
46 let $hint;
47 if (hint) {
48 $hint = $('<div/>').addClass('aui-datepicker-hint');
49 $hint.append('<span/>').text(hint);
50 popupContents.append($hint);
51 }
52
53 return calendar;
54};
55
56const makeDefaultPopupController = ($field, datePickerUUID) => {
57 let popup;
58 let popupContents;
59 let parentPopup;
60 let isTrackingDatePickerFocus = false; // used to prevent multiple bindings of handleDatePickerFocus within handleFieldBlur
61
62
63 const $body = $('body');
64
65 const handleDatePickerFocus = (event) => {
66 let $eventTarget = $(event.target);
67 let isTargetInput = $eventTarget.closest(popupContents).length || $eventTarget.is($field);
68 let isTargetPopup = $eventTarget.closest('.ui-datepicker-header').length;
69
70 // Hide if we're clicking anywhere else but the input or popup OR if esc is pressed.
71 if ((!isTargetInput && !isTargetPopup) || event.keyCode === keyCode.ESCAPE) {
72 hideDatePicker();
73 isTrackingDatePickerFocus = false;
74 return;
75 }
76
77 if ($eventTarget.get(0) !== $field.get(0)) {
78 event.preventDefault();
79 }
80 };
81
82 const handleFieldBlur = () => {
83 // Trigger blur if event is keydown and esc OR is focusout.
84 if (!(isTrackingDatePickerFocus)) {
85 $body.on('focus blur click mousedown', '*', handleDatePickerFocus);
86 isTrackingDatePickerFocus = true;
87 }
88 };
89
90
91 const createPolyfill = function () {
92 // bind additional field processing events
93 $body.on('keydown', handleDatePickerFocus);
94 $field.on('focusout keydown', handleFieldBlur);
95
96
97 };
98
99 const getPopupContents = ({$field}) => {
100 const calculateHorizontalAlignment = $field => {
101 let inLeftHalf = $field.offset().left < window.innerWidth / 2;
102 return inLeftHalf ? 'left' : 'right';
103 };
104
105 popup = makePopup({horizontalAlignment: calculateHorizontalAlignment($field), datePickerUUID});
106
107 parentPopup = $field.closest('aui-inline-dialog').get(0);
108 if (parentPopup) {
109 parentPopup._datePickerPopup = popup; // AUI-2696 - hackish coupling to control inline-dialog close behaviour.
110 $(parentPopup).on('aui-hide', e => {
111 if (isTrackingDatePickerFocus) {
112 e.preventDefault();
113 }
114 $body.off('focus blur', '*', handleDatePickerFocus);
115 if (parentPopup && parentPopup._datePickerPopup) {
116 delete parentPopup._datePickerPopup;
117 }
118 });
119 }
120
121 $body.append(popup);
122
123 popupContents = popup;
124
125 return popup;
126 };
127
128 const handleFieldFocus = () => {
129 if (!popup.get(0).open) {
130 showDatePicker()
131 }
132 };
133
134 const showDatePicker = () => {
135 popup.get(0).open = true;
136 };
137
138 const hideDatePicker = () => {
139 popup.get(0).open = false;
140 };
141
142 const handleChangeMonthYear = () => {
143 // defer refresh call until current stack has cleared (after month has rendered)
144 setTimeout(popup.refresh, 0);
145 };
146
147 const getCalendarNode = () =>
148 popup.get(0).childNodes[0];
149
150 const destroyPolyfill = () => {
151 // goodbye, cruel world!
152 hideDatePicker();
153
154 $field.off('focus click', handleFieldFocus);
155 $field.off('focusout keydown', handleFieldBlur);
156
157 $body.off('keydown', handleFieldBlur);
158 $body.off('focus blur click mousedown keydown', handleDatePickerFocus);
159 };
160
161 return {
162 calendarContainerSelector: null,
163 getPopupContents,
164 handleFieldFocus,
165 showDatePicker,
166 hideDatePicker,
167 handleChangeMonthYear,
168 getCalendarNode,
169 destroyPolyfill,
170 createPolyfill
171 }
172};
173
174const initPolyfill = function (datePicker) {
175 const $field = datePicker.getField();
176 const options = datePicker.getOptions();
177 const datePickerUUID = datePicker.getUUID();
178
179 let calendar;
180
181 const {
182 getPopupContents,
183 handleFieldFocus,
184 showDatePicker,
185 hideDatePicker,
186 handleChangeMonthYear,
187 getCalendarNode,
188 destroyPolyfill,
189 createPolyfill,
190 } = makeDefaultPopupController($field, datePickerUUID);
191
192 const handleFieldUpdate = (event) => {
193 let val = $(event.currentTarget).val();
194 // IE10/11 fire the 'input' event when internally showing and hiding
195 // the placeholder of an input. This was cancelling the initial click
196 // event and preventing the selection of the first date. The val check here
197 // is a workaround to assure we have legitimate user input that should update
198 // the calendar
199 if (val) {
200 calendar.datepicker('setDate', $field.val());
201 }
202 };
203
204 // keep track of things we mutate within `initPolyfill`.
205 // these should all be restored in the `destroyPolyfill` function.
206 const originalPlaceholder = $field.attr('placeholder');
207 const originalType = $field.prop('type');
208 let attributeHandler;
209
210 // -----------------------------------------------------------------
211 // mutate datePicker public API
212 // -----------------------------------------------------------------
213 {
214 const withCalendar = callback => value => {
215 if (typeof calendar !== 'undefined') {
216 return callback(value)
217 }
218 };
219
220 const destroyCalendar = withCalendar(() => {
221 calendar.datepicker('destroy');
222 });
223
224 datePicker.show = showDatePicker;
225
226 datePicker.hide = hideDatePicker;
227
228 // un-does everything the `initPolyfill` function constructed or mutated.
229 datePicker.destroyPolyfill = () => {
230 destroyPolyfill();
231
232 $field.off('propertychange keyup input paste', handleFieldUpdate);
233
234 if (attributeHandler) {
235 attributeHandler.disconnect();
236 attributeHandler = null;
237 }
238
239 if (originalPlaceholder) {
240 $field.attr('placeholder', originalPlaceholder);
241 }
242
243 if (originalType) {
244 $field.prop('type', originalType);
245 }
246
247 $field.removeAttr('data-aui-dp-uuid');
248
249 destroyCalendar();
250
251 // TODO: figure out a way to tear down the popup (if necessary)
252
253 delete datePicker.destroyPolyfill;
254
255 delete datePicker.show;
256 delete datePicker.hide;
257
258 };
259
260 datePicker.setDate = withCalendar(value => {
261 calendar.datepicker('setDate', value)
262 });
263
264 datePicker.getDate = withCalendar(() => calendar.datepicker('getDate'));
265
266 datePicker.setMin = withCalendar(value => calendar.datepicker('option', 'minDate', value));
267
268 datePicker.setMax = withCalendar(value => calendar.datepicker('option', 'maxDate', value));
269 }
270
271
272 // -----------------------------------------------------------------
273 // polyfill bootstrap ----------------------------------------------
274 // -----------------------------------------------------------------
275
276
277 if (!(options.languageCode in DatePicker.prototype.localisations)) {
278 options.languageCode = '';
279 }
280 const i18nConfig = DatePicker.prototype.localisations;
281
282 $field.attr('aria-controls', datePickerUUID);
283
284 if (typeof calendar === 'undefined') {
285 if (typeof $field.attr('step') !== 'undefined') {
286 logger.log('WARNING: The date picker polyfill currently does not support the step attribute!');
287 }
288 const baseConfig = makeConfig({
289 dateFormat: options.dateFormat,
290 minDate: options.minDate,
291 maxDate: options.maxDate,
292 $field,
293 onSelect: options.onSelect,
294 hide: datePicker.hide,
295 onChangeMonthYear: handleChangeMonthYear
296 });
297 const config = $.extend(undefined, baseConfig, i18nConfig);
298
299 // If this is a string value, it will be coerced to numeric.
300 // Since nothing only numbers can be larger than a negative number, this works as a defense.
301 if (options.firstDay > -1) {
302 config.firstDay = options.firstDay;
303 }
304
305 calendar = initCalendar({
306 config,
307 popupContents: getPopupContents({$field}),
308 getCalendarNode,
309 hint: options.hint
310 });
311
312 createPolyfill();
313
314 $field.on('propertychange keyup input paste', handleFieldUpdate);
315
316
317 // bind attribute handlers to account for html5 attributes
318 attributeHandler = new MutationObserver(function (mutationsList) {
319 mutationsList.forEach(function (mutation) {
320 if (mutation.attributeName === 'min') {
321 datePicker.setMin(mutation.target.getAttribute('min'));
322 } else if (mutation.attributeName === 'max') {
323 datePicker.setMax(mutation.target.getAttribute('max'));
324 }
325 });
326 });
327 attributeHandler.observe($field.get(0), {attributes: true});
328 }
329
330 // bind what we need to start off with
331 $field.on('focus click', handleFieldFocus); // the click is for fucking opera... Y U NO FIRE FOCUS EVENTS PROPERLY???
332
333 // give users a hint that this is a date field; note that placeholder isn't technically a valid attribute
334 // according to the spec...
335 $field.attr('placeholder', options.placeholder);
336
337 // Override the browser's default date field implementation.
338 // There used to be a fix for AUI-3681 here, too, but my testing of Edge 15
339 // shows that changing the `type` of input does not erase its value.
340 // see https://codepen.io/chrisdarroch/pen/YzwgjyJ
341 $field.prop('type', 'text');
342 // Set default value on initialization to handle all date formats.
343 // It is possible, because of changing type to text on the line above.
344 $field.val($field.attr('value'));
345 // Trigger change to update calendar popup value.
346 $field.trigger('propertychange');
347
348 // demonstrate the polyfill is initialised
349 $field.attr('data-aui-dp-uuid', datePickerUUID);
350};
351
352function DatePicker(field, baseOptions) {
353 let options = {};
354
355 const datePickerUUID = generateUniqueId('date-picker');
356
357 const $field = $(field);
358
359 const datePicker = {
360 getUUID: () => datePickerUUID,
361 getField: () => $field,
362 getOptions: () => options,
363 destroy: () => {
364 if (typeof datePicker.destroyPolyfill === 'function') {
365 datePicker.destroyPolyfill();
366 }
367 },
368 reset: () => {
369 datePicker.destroy();
370 const browserDoesNotSupportDateField = !DatePicker.prototype.browserSupportsDateField;
371 const shouldOverrideBrowserDefault = options.overrideBrowserDefault !== false;
372
373 if (
374 browserDoesNotSupportDateField ||
375 shouldOverrideBrowserDefault
376 ) {
377 initPolyfill(datePicker);
378 }
379 },
380 reconfigure: newOptions => {
381 options = $.extend(undefined, DatePicker.prototype.defaultOptions, newOptions);
382 datePicker.reset();
383 }
384 };
385
386 datePicker.reconfigure(baseOptions);
387
388 return datePicker;
389}
390
391// -------------------------------------------------------------------------
392// things that should be common --------------------------------------------
393// -------------------------------------------------------------------------
394
395DatePicker.prototype.browserSupportsDateField = supportsDateField();
396
397DatePicker.prototype.defaultOptions = {
398 overrideBrowserDefault: false,
399 firstDay: -1,
400 languageCode: $('html').attr('lang') || 'en-AU',
401 dateFormat: datepickerUI.W3C // same as $.datepicker.ISO_8601
402};
403
404function CalendarWidget(calendarNode, baseOptions) {
405 const options = $.extend({
406 'nextText': '>',
407 'prevText': '<'
408 }, baseOptions);
409
410 const $calendarNode = $(calendarNode);
411
412 const $result = $calendarNode
413 .addClass('aui-datepicker-dialog')
414 .addClass('aui-calendar-widget')
415 .datepicker(options);
416
417 if (options.hint) {
418 const $hint = $('<div/>').addClass('aui-datepicker-hint');
419 $hint.append('<span/>').text(options.hint);
420 $result.append($hint);
421 }
422
423 $result.reconfigure = (options) => {
424 $result.datepicker('destroy');
425 $result.datepicker(options);
426 };
427
428 $result.destroy = () => {
429 $result.datepicker('destroy');
430 };
431
432 return $result
433}
434
435// adapted from the jQuery UI Datepicker widget (v1.8.16), with the following changes:
436// - dayNamesShort -> dayNamesMin
437// - unnecessary attributes omitted
438/*
439CODE to extract codes out:
440
441var langCode, langs, out;
442langs = jQuery.datepicker.regional;
443out = {};
444
445for (langCode in langs) {
446 if (langs.hasOwnProperty(langCode)) {
447 out[langCode] = {
448 'dayNames': langs[langCode].dayNames,
449 'dayNamesMin': langs[langCode].dayNamesShort, // this is deliberate
450 'firstDay': langs[langCode].firstDay,
451 'isRTL': langs[langCode].isRTL,
452 'monthNames': langs[langCode].monthNames,
453 'showMonthAfterYear': langs[langCode].showMonthAfterYear,
454 'yearSuffix': langs[langCode].yearSuffix
455 };
456 }
457}
458
459 */
460
461DatePicker.prototype.localisations = {
462 'dayNames': [I18n.getText('ajs.datepicker.localisations.day-names.sunday'),
463 I18n.getText('ajs.datepicker.localisations.day-names.monday'),
464 I18n.getText('ajs.datepicker.localisations.day-names.tuesday'),
465 I18n.getText('ajs.datepicker.localisations.day-names.wednesday'),
466 I18n.getText('ajs.datepicker.localisations.day-names.thursday'),
467 I18n.getText('ajs.datepicker.localisations.day-names.friday'),
468 I18n.getText('ajs.datepicker.localisations.day-names.saturday')],
469 'dayNamesMin': [I18n.getText('ajs.datepicker.localisations.day-names-min.sunday'),
470 I18n.getText('ajs.datepicker.localisations.day-names-min.monday'),
471 I18n.getText('ajs.datepicker.localisations.day-names-min.tuesday'),
472 I18n.getText('ajs.datepicker.localisations.day-names-min.wednesday'),
473 I18n.getText('ajs.datepicker.localisations.day-names-min.thursday'),
474 I18n.getText('ajs.datepicker.localisations.day-names-min.friday'),
475 I18n.getText('ajs.datepicker.localisations.day-names-min.saturday')],
476 'firstDay': I18n.getText('ajs.datepicker.localisations.first-day'),
477 'isRTL': I18n.getText('ajs.datepicker.localisations.is-RTL') === 'true',
478 'monthNames': [I18n.getText('ajs.datepicker.localisations.month-names.january'),
479 I18n.getText('ajs.datepicker.localisations.month-names.february'),
480 I18n.getText('ajs.datepicker.localisations.month-names.march'),
481 I18n.getText('ajs.datepicker.localisations.month-names.april'),
482 I18n.getText('ajs.datepicker.localisations.month-names.may'),
483 I18n.getText('ajs.datepicker.localisations.month-names.june'),
484 I18n.getText('ajs.datepicker.localisations.month-names.july'),
485 I18n.getText('ajs.datepicker.localisations.month-names.august'),
486 I18n.getText('ajs.datepicker.localisations.month-names.september'),
487 I18n.getText('ajs.datepicker.localisations.month-names.october'),
488 I18n.getText('ajs.datepicker.localisations.month-names.november'),
489 I18n.getText('ajs.datepicker.localisations.month-names.december')],
490 'showMonthAfterYear': I18n.getText('ajs.datepicker.localisations.show-month-after-year') === 'true',
491 'yearSuffix': I18n.getText('ajs.datepicker.localisations.year-suffix')
492};
493
494
495// -------------------------------------------------------------------------
496// finally, integrate with jQuery for convenience --------------------------
497// -------------------------------------------------------------------------
498const key = 'aui-datepicker';
499
500const makePlugin = (WidgetConstructor) => function (options) {
501 let picker = this.data(key);
502 if (!picker) {
503 picker = new WidgetConstructor(this, options);
504 this.data(key, picker);
505 } else if (typeof options === 'object') {
506 picker.reconfigure(options);
507 } else if (options === 'destroy') {
508 picker.destroy();
509 }
510 return picker;
511};
512
513$.fn.datePicker = makePlugin(DatePicker);
514globalize('DatePicker', DatePicker);
515
516$.fn.calendarWidget = makePlugin(CalendarWidget);
517globalize('CalendarWidget', CalendarWidget);
518
519export default DatePicker;
520export { CalendarWidget }