UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

518 lines (410 loc) 17.7 kB
import $ from './jquery'; import datepickerUI from 'jquery-ui/ui/widgets/datepicker'; import * as logger from './internal/log'; import { supportsDateField } from './internal/browser'; import globalize from './internal/globalize'; import keyCode from './key-code'; import i18n from './i18n'; import InlineDialogEl from './inline-dialog2'; import generateUniqueId from './unique-id'; const makePopup = ({horizontalAlignment, datePickerUUID}) => { const popupInlineDialogElement = new InlineDialogEl(); popupInlineDialogElement.id = datePickerUUID; const popup = $(popupInlineDialogElement); popup.attr('persistent', ''); popup.attr('data-aui-focus', 'false'); popup.attr('alignment', `bottom ${horizontalAlignment}`); popup.addClass('aui-datepicker-dialog'); return popup; }; const makeConfig = ({dateFormat, $field, onSelect, hide, onChangeMonthYear}) => ({ 'dateFormat': dateFormat, 'defaultDate': $field.val(), 'maxDate': $field.attr('max'), 'minDate': $field.attr('min'), 'nextText': '>', 'onSelect': function (dateText) { $field.val(dateText); $field.trigger('change'); hide(); // TODO make sure the docs are explicit about the fact that onSelect cannot be an arrow function onSelect && onSelect.call(this, dateText); }, onChangeMonthYear, 'prevText': '<' }); const initCalendar = ({config, popupContents, getCalendarNode, hint}) => { const calendar = $(getCalendarNode()); calendar.datepicker(config); let $hint; if (hint) { $hint = $('<div/>').addClass('aui-datepicker-hint'); $hint.append('<span/>').text(hint); popupContents.append($hint); } return calendar; }; const makeDefaultPopupController = ($field, datePickerUUID) => { let popup; let popupContents; let parentPopup; let isTrackingDatePickerFocus = false; // used to prevent multiple bindings of handleDatePickerFocus within handleFieldBlur const $body = $('body'); const handleDatePickerFocus = (event) => { let $eventTarget = $(event.target); let isTargetInput = $eventTarget.closest(popupContents).length || $eventTarget.is($field); let isTargetPopup = $eventTarget.closest('.ui-datepicker-header').length; // Hide if we're clicking anywhere else but the input or popup OR if esc is pressed. if ((!isTargetInput && !isTargetPopup) || event.keyCode === keyCode.ESCAPE) { hideDatePicker(); isTrackingDatePickerFocus = false; return; } if ($eventTarget.get(0) !== $field.get(0)) { event.preventDefault(); } }; const handleFieldBlur = () => { // Trigger blur if event is keydown and esc OR is focusout. if (!(isTrackingDatePickerFocus)) { $body.on('focus blur click mousedown', '*', handleDatePickerFocus); isTrackingDatePickerFocus = true; } }; const createPolyfill = function () { // bind additional field processing events $body.on('keydown', handleDatePickerFocus); $field.on('focusout keydown', handleFieldBlur); }; const getPopupContents = ({$field}) => { const calculateHorizontalAlignment = $field => { let inLeftHalf = $field.offset().left < window.innerWidth / 2; return inLeftHalf ? 'left' : 'right'; }; popup = makePopup({horizontalAlignment: calculateHorizontalAlignment($field), datePickerUUID}); parentPopup = $field.closest('aui-inline-dialog').get(0); if (parentPopup) { parentPopup._datePickerPopup = popup; // AUI-2696 - hackish coupling to control inline-dialog close behaviour. $(parentPopup).on('aui-hide', e => { if (isTrackingDatePickerFocus) { e.preventDefault(); } $body.off('focus blur', '*', handleDatePickerFocus); if (parentPopup && parentPopup._datePickerPopup) { delete parentPopup._datePickerPopup; } }); } $body.append(popup); popupContents = popup; return popup; }; const handleFieldFocus = () => { if (!popup.get(0).open) { showDatePicker() } }; const showDatePicker = () => { popup.get(0).open = true; }; const hideDatePicker = () => { popup.get(0).open = false; }; const handleChangeMonthYear = () => { // defer refresh call until current stack has cleared (after month has rendered) setTimeout(popup.refresh, 0); }; const getCalendarNode = () => popup.get(0).childNodes[0]; const destroyPolyfill = () => { // goodbye, cruel world! hideDatePicker(); $field.attr('placeholder', null); $field.off('focus click', handleFieldFocus); $field.off('focusout keydown', handleFieldBlur); $body.off('keydown', handleFieldBlur); $body.off('focus blur click mousedown keydown', handleDatePickerFocus); }; return { calendarContainerSelector: null, getPopupContents, handleFieldFocus, showDatePicker, hideDatePicker, handleChangeMonthYear, getCalendarNode, destroyPolyfill, createPolyfill } }; const initPolyfill = function ( {datePicker}, { calendarContainerSelector, getPopupContents, handleFieldFocus, showDatePicker, hideDatePicker, handleChangeMonthYear, getCalendarNode, destroyPolyfill, createPolyfill } ) { const $field = datePicker.getField(); const options = datePicker.getOptions(); const datePickerUUID = datePicker.getUUID(); let calendar; let attributeHandler; let popupContents; // ----------------------------------------------------------------- // extend datePicker public API (side effects)---------------------- // ----------------------------------------------------------------- { const withCalendar = callback => value => { if (typeof calendar !== 'undefined') { return callback(value) } }; const destroyCalendar = withCalendar(() => { calendar.datepicker('destroy'); }); datePicker.show = showDatePicker; datePicker.hide = hideDatePicker; datePicker.destroyPolyfill = () => { destroyPolyfill(); $field.off('propertychange keyup input paste', handleFieldUpdate); if (attributeHandler) { attributeHandler.disconnect(); attributeHandler = null; } if (DatePicker.prototype.browserSupportsDateField) { $field.get(0).type = 'date'; } destroyCalendar(); // TODO: figure out a way to tear down the popup (if necessary) delete datePicker.destroyPolyfill; delete datePicker.show; delete datePicker.hide; }; datePicker.setDate = withCalendar(value => { calendar.datepicker('setDate', value) }); datePicker.getDate = withCalendar(() => calendar.datepicker('getDate')); datePicker.setMin = withCalendar(value => calendar.datepicker('option', 'minDate', value)); datePicker.setMax = withCalendar(value => calendar.datepicker('option', 'maxDate', value)); } const handleFieldUpdate = (event) => { let val = $(event.currentTarget).val(); // IE10/11 fire the 'input' event when internally showing and hiding // the placeholder of an input. This was cancelling the initial click // event and preventing the selection of the first date. The val check here // is a workaround to assure we have legitimate user input that should update // the calendar if (val) { calendar.datepicker('setDate', $field.val()); } }; // ----------------------------------------------------------------- // polyfill bootstrap ---------------------------------------------- // ----------------------------------------------------------------- if (!(options.languageCode in DatePicker.prototype.localisations)) { options.languageCode = ''; } const i18nConfig = DatePicker.prototype.localisations; $field.attr('aria-controls', datePickerUUID); if (!calendarContainerSelector) { popupContents = getPopupContents({$field}); } else { popupContents = $(calendarContainerSelector); popupContents.addClass('aui-datepicker-dialog'); } if (typeof calendar === 'undefined') { if (typeof $field.attr('step') !== 'undefined') { logger.log('WARNING: The date picker polyfill currently does not support the step attribute!'); } const baseConfig = makeConfig({ dateFormat: options.dateFormat, $field, onSelect: options.onSelect, hide: datePicker.hide, onChangeMonthYear: handleChangeMonthYear }); const config = $.extend(undefined, baseConfig, i18nConfig); if (options.firstDay > -1) { config.firstDay = options.firstDay; } calendar = initCalendar({ config, popupContents, getCalendarNode: () => calendarContainerSelector || getCalendarNode(), hint: options.hint }); createPolyfill(); $field.on('propertychange keyup input paste', handleFieldUpdate); // bind attribute handlers to account for html5 attributes attributeHandler = new MutationObserver(function (mutationsList) { mutationsList.forEach(function (mutation) { if (mutation.attributeName === 'min') { datePicker.setMin(mutation.target.getAttribute('min')); } else if (mutation.attributeName === 'max') { datePicker.setMax(mutation.target.getAttribute('max')); } }); }); attributeHandler.observe($field.get(0), {attributes: true}); } // bind what we need to start off with $field.on('focus click', handleFieldFocus); // the click is for fucking opera... Y U NO FIRE FOCUS EVENTS PROPERLY??? // give users a hint that this is a date field; note that placeholder isn't technically a valid attribute // according to the spec... $field.attr('placeholder', options.dateFormat); // override the browser's default date field implementation (if applicable) // since IE doesn't support date input fields, we should be fine... if (options.overrideBrowserDefault && DatePicker.prototype.browserSupportsDateField) { $field.get(0).type = 'text'; //workaround for this issue in Edge: https://connect.microsoft.com/IE/feedback/details/1603512/changing-an-input-type-to-text-does-not-set-the-value let value = $field.get(0).getAttribute('value'); //can't use jquery to get the attribute because it doesn't work in Edge if (value) { $field.get(0).value = value; } } }; const noop = () => { }; function DatePicker(field, baseOptions) { let options = {}; const datePickerUUID = generateUniqueId('date-picker'); const $field = $(field); $field.attr('data-aui-dp-uuid', datePickerUUID); const datePicker = { getUUID: () => datePickerUUID, destroyPolyfill: noop, // may be overridden by initPolyfill getField: () => $field, getOptions: () => options, reset: () => { datePicker.destroyPolyfill(); const browserDoesNotSupportDateField = !DatePicker.prototype.browserSupportsDateField; const shouldOverrideBrowserDefault = options.overrideBrowserDefault; if ( browserDoesNotSupportDateField || shouldOverrideBrowserDefault ) { const popupController = makeDefaultPopupController($field, datePickerUUID); initPolyfill({datePicker}, popupController); } }, reconfigure: newOptions => { options = $.extend(undefined, DatePicker.prototype.defaultOptions, newOptions); datePicker.reset(); } }; datePicker.reconfigure(baseOptions); return datePicker; } // ------------------------------------------------------------------------- // things that should be common -------------------------------------------- // ------------------------------------------------------------------------- DatePicker.prototype.browserSupportsDateField = supportsDateField(); DatePicker.prototype.defaultOptions = { overrideBrowserDefault: false, firstDay: -1, languageCode: $('html').attr('lang') || 'en-AU', dateFormat: datepickerUI.W3C // same as $.datepicker.ISO_8601 }; function CalendarWidget(calendarNode, baseOptions) { const options = $.extend({ 'nextText': '>', 'prevText': '<' }, baseOptions); const $calendarNode = $(calendarNode); const $result = $calendarNode.addClass('aui-datepicker-dialog').datepicker(options); if (options.hint) { const $hint = $('<div/>').addClass('aui-datepicker-hint'); $hint.append('<span/>').text(options.hint); $result.append($hint); } $result.reconfigure = (options) => { $result.datepicker('destroy'); $result.datepicker(options); }; $result.destroy = () => { $result.datepicker('destroy'); }; return $result } // adapted from the jQuery UI Datepicker widget (v1.8.16), with the following changes: // - dayNamesShort -> dayNamesMin // - unnecessary attributes omitted /* CODE to extract codes out: var langCode, langs, out; langs = jQuery.datepicker.regional; out = {}; for (langCode in langs) { if (langs.hasOwnProperty(langCode)) { out[langCode] = { 'dayNames': langs[langCode].dayNames, 'dayNamesMin': langs[langCode].dayNamesShort, // this is deliberate 'firstDay': langs[langCode].firstDay, 'isRTL': langs[langCode].isRTL, 'monthNames': langs[langCode].monthNames, 'showMonthAfterYear': langs[langCode].showMonthAfterYear, 'yearSuffix': langs[langCode].yearSuffix }; } } */ DatePicker.prototype.localisations = { 'dayNames': [i18n.getText('ajs.datepicker.localisations.day-names.sunday'), i18n.getText('ajs.datepicker.localisations.day-names.monday'), i18n.getText('ajs.datepicker.localisations.day-names.tuesday'), i18n.getText('ajs.datepicker.localisations.day-names.wednesday'), i18n.getText('ajs.datepicker.localisations.day-names.thursday'), i18n.getText('ajs.datepicker.localisations.day-names.friday'), i18n.getText('ajs.datepicker.localisations.day-names.saturday')], 'dayNamesMin': [i18n.getText('ajs.datepicker.localisations.day-names-min.sunday'), i18n.getText('ajs.datepicker.localisations.day-names-min.monday'), i18n.getText('ajs.datepicker.localisations.day-names-min.tuesday'), i18n.getText('ajs.datepicker.localisations.day-names-min.wednesday'), i18n.getText('ajs.datepicker.localisations.day-names-min.thursday'), i18n.getText('ajs.datepicker.localisations.day-names-min.friday'), i18n.getText('ajs.datepicker.localisations.day-names-min.saturday')], 'firstDay': i18n.getText('ajs.datepicker.localisations.first-day'), 'isRTL': i18n.getText('ajs.datepicker.localisations.is-RTL') === 'true', 'monthNames': [i18n.getText('ajs.datepicker.localisations.month-names.january'), i18n.getText('ajs.datepicker.localisations.month-names.february'), i18n.getText('ajs.datepicker.localisations.month-names.march'), i18n.getText('ajs.datepicker.localisations.month-names.april'), i18n.getText('ajs.datepicker.localisations.month-names.may'), i18n.getText('ajs.datepicker.localisations.month-names.june'), i18n.getText('ajs.datepicker.localisations.month-names.july'), i18n.getText('ajs.datepicker.localisations.month-names.august'), i18n.getText('ajs.datepicker.localisations.month-names.september'), i18n.getText('ajs.datepicker.localisations.month-names.october'), i18n.getText('ajs.datepicker.localisations.month-names.november'), i18n.getText('ajs.datepicker.localisations.month-names.december')], 'showMonthAfterYear': i18n.getText('ajs.datepicker.localisations.show-month-after-year') === 'true', 'yearSuffix': i18n.getText('ajs.datepicker.localisations.year-suffix') }; // ------------------------------------------------------------------------- // finally, integrate with jQuery for convenience -------------------------- // ------------------------------------------------------------------------- const key = 'aui-datepicker'; const makePlugin = (WidgetConstructor) => function (options) { let picker = this.data(key); if (!picker) { picker = new WidgetConstructor(this, options); this.data(key, picker); } else if (typeof options === 'object') { picker.reconfigure(options); } return picker; }; $.fn.datePicker = makePlugin(DatePicker); globalize('DatePicker', DatePicker); $.fn.calendarWidget = makePlugin(CalendarWidget); globalize('CalendarWidget', CalendarWidget); export default DatePicker; export { CalendarWidget }