UNPKG

@gitlab/ui

Version:
417 lines (401 loc) • 12.7 kB
import isString from 'lodash/isString'; import Pikaday from 'pikaday'; import { defaultConfig } from '../../../config'; import { defaultDateFormat, datepickerWidthOptionsMap } from '../../../utils/constants'; import { areDatesEqual } from '../../../utils/datetime_utility'; import GlButton from '../button/button'; import GlFormInput from '../form/form_input/form_input'; import GlIcon from '../icon/icon'; import { translate } from '../../../utils/i18n'; import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js'; // const pad = function (val) { let len = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; return `0${val}`.slice(-len); }; /** * Used `onSelect` method in pickaday * @param {Date} date UTC format * @return {String} Date formated in yyyy-mm-dd */ const defaultDateFormatter = date => { const day = pad(date.getDate()); const month = pad(date.getMonth() + 1); const year = date.getFullYear(); return `${year}-${month}-${day}`; }; const isBefore = (compareTo, date) => compareTo && date && date.getTime() < compareTo.getTime(); const highlightPastDates = pikaday => { const pikaButtons = pikaday.el.querySelectorAll('.pika-button'); const today = new Date(); pikaButtons.forEach(pikaButton => { const { pikaYear, pikaMonth, pikaDay } = pikaButton.dataset; const pikaButtonDate = new Date(pikaYear, pikaMonth, pikaDay); if (isBefore(today, pikaButtonDate)) { pikaButton.classList.add('is-past-date'); } }); }; const addAccessibleLabels = element => { // Pikaday sets `role="heading"`, which requires a corresponding // `aria-level`. Ensure we have one. const titleEl = element.querySelector('.pika-title[role="heading"]'); if (titleEl) { titleEl.setAttribute('aria-level', 3); } // Add aria-label to month & year select dropdowns const monthEl = element.querySelector('select.pika-select-month'); if (monthEl) { monthEl.setAttribute('aria-label', translate('GlDatepicker.monthLabel', 'Month')); } const yearEl = element.querySelector('select.pika-select-year'); if (yearEl) { yearEl.setAttribute('aria-label', translate('GlDatepicker.yearLabel', 'Year')); } }; var script = { name: 'GlDatepicker', components: { GlFormInput, GlIcon, GlButton }, props: { /** * Selector of element that triggers the datepicker. Defaults to the calendar icon. Pass `null` to trigger on input focus. */ target: { type: String, required: false, default: '' }, /** * DOM node to render calendar into. Defaults to the datepicker container. Pass `null` to use Pikaday default. */ container: { type: String, required: false, default: '' }, value: { type: Date, required: false, default: null }, minDate: { type: Date, required: false, default: null }, maxDate: { type: Date, required: false, default: null }, startRange: { type: Date, required: false, default: null }, endRange: { type: Date, required: false, default: null }, /** * Accepts a function that accepts a date as argument and returns true if the date is disabled. */ disableDayFn: { type: Function, required: false, default: null }, firstDay: { type: Number, required: false, default: () => defaultConfig.firstDayOfWeek || 0 // Defaults to 0 (Sunday) }, ariaLabel: { type: String, required: false, default: '' }, placeholder: { type: String, required: false, default: defaultDateFormat }, /** * Defaults to `off` when datepicker opens on focus, otherwise defaults to `null`. */ autocomplete: { type: String, required: false, default: '' }, disabled: { type: Boolean, required: false, default: false }, displayField: { type: Boolean, required: false, default: true }, startOpened: { type: Boolean, required: false, default: false }, /** * Use this prop to set the initial date for the datepicker. */ defaultDate: { type: Date, required: false, default: null }, i18n: { type: Object, required: false, default: null }, theme: { type: String, required: false, default: '' }, showClearButton: { type: Boolean, required: false, default: false }, inputId: { type: String, required: false, default: null }, inputLabel: { type: String, required: false, default: 'Enter date' }, inputName: { type: String, required: false, default: null }, /** * Maximum width of the Datepicker */ width: { type: String, required: false, default: null, validator: value => Object.keys(datepickerWidthOptionsMap).includes(value) }, state: { type: Boolean, required: false, default: null } }, data() { return { textInput: '' }; }, computed: { formattedDate() { return 'calendar' in this ? this.calendar.toString() : ''; }, customTrigger() { return isString(this.target) && this.target !== ''; }, triggerOnFocus() { return this.target === null; }, showDefaultField() { return !this.customTrigger || this.triggerOnFocus; }, renderClearButton() { return this.showClearButton && this.textInput !== '' && !this.disabled; }, inputAutocomplete() { if (this.autocomplete !== '') { return this.autocomplete; } if (this.triggerOnFocus) { return 'off'; } return null; }, datepickerClasses() { return ['gl-datepicker', 'gl-inline-block', 'gl-w-full', // eslint-disable-next-line @gitlab/tailwind-no-interpolation -- Not a CSS utility `gl-form-input-${this.computedWidth}`]; }, computedWidth() { if (this.width) { return this.width; } return 'md'; } }, watch: { value(val) { if (!areDatesEqual(val, this.calendar.getDate())) { this.calendar.setDate(val, true); } }, minDate(minDate) { this.calendar.setMinDate(minDate); }, maxDate(maxDate) { this.calendar.setMaxDate(maxDate); }, startRange(startRange) { this.calendar.setStartRange(startRange); }, endRange(endRange) { this.calendar.setEndRange(endRange); } }, mounted() { const $parentEl = this.$parent.$el; const openedEvent = this.opened.bind(this); const drawEvent = this.draw.bind(this); const pikadayConfig = { field: this.$el.querySelector('input[type="text"]'), // `!gl-absolute` is needed because of this bug: https://github.com/Pikaday/Pikaday/issues/840 theme: `gl-datepicker-theme !gl-absolute ${this.theme}`, defaultDate: this.defaultDate || this.value, setDefaultDate: Boolean(this.value) || Boolean(this.defaultDate), minDate: this.minDate, maxDate: this.maxDate, // Only supports default gitlab format YYYY-MM-DD. We have to decide if we want to support other formats. format: defaultDateFormat, parse: dateString => { const parsedDate = Date.parse(dateString.replace(/-/g, '/')); return Number.isNaN(parsedDate) ? new Date() : new Date(parsedDate); }, disableDayFn: this.disableDayFn, firstDay: this.firstDay, ariaLabel: this.ariaLabel, toString: date => defaultDateFormatter(date), onSelect: this.selected.bind(this), onClose: this.closed.bind(this), onOpen: () => { addAccessibleLabels(this.$el); openedEvent(); }, onDraw: pikaday => { highlightPastDates(pikaday); drawEvent(); } }; // Pass `null` as `target` prop to use the `field` as the trigger (open on focus) if (!this.triggerOnFocus && !this.disabled) { const trigger = this.customTrigger ? $parentEl.querySelector(this.target) : this.$refs.calendarTriggerBtn.$el; pikadayConfig.trigger = trigger; // Set `trigger` as the `field` if `field` element doesn't exist (not passed via the slot) if (!pikadayConfig.field && this.customTrigger) { pikadayConfig.field = trigger; } } // Pass `null` as `container` prop to prevent passing the `container` option to Pikaday if (this.container !== null) { const container = this.container ? $parentEl.querySelector(this.container) : this.$el; pikadayConfig.container = container; } if (this.i18n) { pikadayConfig.i18n = this.i18n; } this.calendar = new Pikaday(pikadayConfig); if (this.startOpened) { this.calendar.show(); } }, beforeDestroy() { this.calendar.destroy(); }, methods: { // is used to open datepicker programmatically show() { this.calendar.show(); }, selected(date) { /** * Emitted when a new date has been selected. * @property {Date} date The selected date */ this.$emit('input', date); }, closed() { /** * Emitted when the datepicker is hidden. */ this.$emit('close'); }, opened() { /** * Emitted when the datepicker becomes visible. */ this.$emit('open'); }, cleared() { this.textInput = ''; /** * Emitted when the clear button is clicked. */ this.$emit('clear'); }, draw() { /** * Emitted when the datepicker draws a new month. */ this.$emit('monthChange'); }, onKeydown() { if (this.textInput === '') { const resetDate = this.minDate || null; this.calendar.setDate(resetDate); this.selected(resetDate); } } } }; /* script */ const __vue_script__ = script; /* template */ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:_vm.datepickerClasses},[(_vm.showDefaultField)?_c('div',{staticClass:"gl-flex gl-items-start gl-gap-3"},[_c('div',{staticClass:"gl-relative gl-flex gl-grow"},[_vm._t("default",function(){return [_c('gl-form-input',{class:_vm.renderClearButton ? '!gl-pr-9' : '!gl-pr-7',attrs:{"id":_vm.inputId,"name":_vm.inputName,"data-testid":"gl-datepicker-input","value":_vm.formattedDate,"placeholder":_vm.placeholder,"autocomplete":_vm.inputAutocomplete,"disabled":_vm.disabled,"aria-label":_vm.inputLabel,"state":_vm.state},on:{"keydown":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }return _vm.onKeydown.apply(null, arguments)}},model:{value:(_vm.textInput),callback:function ($$v) {_vm.textInput=$$v;},expression:"textInput"}})]},{"formattedDate":_vm.formattedDate}),_vm._v(" "),_c('div',{staticClass:"gl-datepicker-actions"},[(_vm.renderClearButton)?_c('gl-button',{staticClass:"gl-pointer-events-auto",attrs:{"data-testid":"clear-button","aria-label":"Clear date","category":"tertiary","size":"small","icon":"clear"},on:{"click":_vm.cleared}}):_vm._e(),_vm._v(" "),(_vm.triggerOnFocus || _vm.disabled)?_c('span',{staticClass:"gl-px-2"},[_c('gl-icon',{staticClass:"gl-block",attrs:{"data-testid":"datepicker-calendar-icon","name":"calendar","size":16,"variant":_vm.disabled ? 'disabled' : 'default'}})],1):_c('gl-button',{ref:"calendarTriggerBtn",staticClass:"gl-pointer-events-auto",attrs:{"aria-label":"Open datepicker","category":"tertiary","size":"small","icon":"calendar"}})],1)],2),_vm._v(" "),_vm._t("after")],2):_vm._t("default",null,{"formattedDate":_vm.formattedDate})],2)}; var __vue_staticRenderFns__ = []; /* style */ const __vue_inject_styles__ = undefined; /* scoped */ const __vue_scope_id__ = undefined; /* module identifier */ const __vue_module_identifier__ = undefined; /* functional template */ const __vue_is_functional_template__ = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ const __vue_component__ = /*#__PURE__*/__vue_normalize__( { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, false, undefined, undefined, undefined ); export { __vue_component__ as default, defaultDateFormatter, pad };