UNPKG

@mst101/vue-datepicker

Version:

A simple, but powerful, Vue 3 datepicker component. Supports disabling of dates, inline mode, translations & custom slots

1 lines 204 kB
{"version":3,"file":"vue-datepicker.mjs","sources":["../src/locale/Language.js","../src/locale/translations/en.js","../src/utils/calendarSlots.js","../src/utils/DateUtils.js","../src/mixins/inputProps.vue","../src/components/DateInput.vue","../src/utils/cellUtils.js","../src/utils/DisabledDate.js","../src/mixins/navMixin.vue","../src/components/PickerHeader.vue","../src/mixins/pickerMixin.vue","../src/utils/HighlightedDate.js","../src/components/PickerCells.vue","../src/components/PickerDay.vue","../src/components/PickerMonth.vue","../src/components/PickerYear.vue","../src/utils/dom.js","../src/components/Popup.vue","../src/components/DatePicker.vue"],"sourcesContent":["export default class Language {\n // eslint-disable-next-line max-params\n constructor(\n language,\n months,\n monthsAbbr,\n days,\n rtl = false,\n ymd = false,\n yearSuffix = '',\n ) {\n this.language = language\n this.months = months\n this.monthsAbbr = monthsAbbr\n this.days = days\n this.rtl = rtl\n this.ymd = ymd\n this.yearSuffix = yearSuffix\n }\n\n /* eslint-disable no-underscore-dangle */\n get language() {\n return this._language\n }\n\n set language(language) {\n if (typeof language !== 'string') {\n throw new TypeError('Language must be a string')\n }\n this._language = language\n }\n\n get months() {\n return this._months\n }\n\n set months(months) {\n if (months.length !== 12) {\n throw new RangeError(\n `There must be 12 months for ${this.language} language`,\n )\n }\n this._months = months\n }\n\n get monthsAbbr() {\n return this._monthsAbbr\n }\n\n set monthsAbbr(monthsAbbr) {\n if (monthsAbbr.length !== 12) {\n throw new RangeError(\n `There must be 12 abbreviated months for ${this.language} language`,\n )\n }\n this._monthsAbbr = monthsAbbr\n }\n\n get days() {\n return this._days\n }\n\n set days(days) {\n if (days.length !== 7) {\n throw new RangeError(`There must be 7 days for ${this.language} language`)\n }\n this._days = days\n }\n\n getDaysStartingOn(firstDayOfWeek) {\n const firstDays = this._days.slice(firstDayOfWeek)\n const lastDays = this._days.slice(0, firstDayOfWeek)\n\n return firstDays.concat(lastDays)\n }\n\n getMonthByAbbrName(name) {\n const monthValue = this._monthsAbbr.findIndex((month) => month === name) + 1\n return monthValue < 10 ? `0${monthValue}` : `${monthValue}`\n }\n\n getMonthByName(name) {\n const monthValue = this._months.findIndex((month) => month === name) + 1\n return monthValue < 10 ? `0${monthValue}` : `${monthValue}`\n }\n}\n","import Language from '../Language'\n\nexport default new Language(\n 'English',\n [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n ],\n [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n ],\n ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],\n)\n","export default [\n 'beforeCalendarHeaderDay',\n 'calendarFooterDay',\n 'beforeCalendarHeaderMonth',\n 'calendarFooterMonth',\n 'beforeCalendarHeaderYear',\n 'calendarFooterYear',\n 'nextIntervalBtn',\n 'prevIntervalBtn',\n]\n","import en from '~/locale/translations/en'\n\n/**\n * Attempts to return a parseable date in the format 'yyyy-MM-dd'\n * @param {String} dateStr\n * @param {String} formatStr\n * @param {Object} translation\n * @param {Number} currentYear\n * @param {String} time\n * @return String\n */\n// eslint-disable-next-line complexity,max-statements\nconst getParsableDate = ({\n dateStr,\n formatStr,\n translation,\n currentYear,\n time,\n}) => {\n const splitter = formatStr.match(/-|\\/|\\s|\\./) || ['-']\n const df = formatStr.split(splitter[0])\n const ds = dateStr.split(splitter[0])\n const ymd = [currentYear.toString(), '01', '01']\n\n for (let i = 0; i < df.length; i += 1) {\n if (/yyyy/i.test(df[i])) {\n ymd[0] = ds[i]\n } else if (/mmmm/i.test(df[i])) {\n ymd[1] = translation.getMonthByName(ds[i])\n } else if (/mmm/i.test(df[i])) {\n ymd[1] = translation.getMonthByAbbrName(ds[i])\n } else if (/mm/i.test(df[i])) {\n ymd[1] = ds[i]\n } else if (/m/i.test(df[i])) {\n ymd[1] = ds[i]\n } else if (/dd/i.test(df[i])) {\n ymd[2] = ds[i]\n } else if (/d/i.test(df[i])) {\n const tmp = ds[i].replace(/st|rd|nd|th/g, '')\n ymd[2] = tmp < 10 ? `0${tmp}` : `${tmp}`\n }\n }\n\n return `${ymd.join('-')}${time}`\n}\n\n/**\n * Parses a date using a function passed in via the `parser` prop\n * @param {String} dateStr The string to parse\n * @param {Function} format The function that should be used to format the date\n * @param {Function} parser The function that should be used to parse the date\n * @return {Date | String}\n */\nfunction parseDateWithLibrary(dateStr, format, parser) {\n if (typeof parser !== 'function') {\n throw new Error('Parser needs to be a function')\n }\n\n if (typeof format !== 'function') {\n throw new Error('Format needs to be a function when using a custom parser')\n }\n\n return parser(dateStr)\n}\n\nconst utils = {\n /**\n * @type {Boolean}\n */\n useUtc: false,\n\n /**\n * Returns the full year, using UTC or not\n * @param {Date} date\n */\n getFullYear(date) {\n return this.useUtc ? date.getUTCFullYear() : date.getFullYear()\n },\n\n /**\n * Returns the month, using UTC or not\n * @param {Date} date\n */\n getMonth(date) {\n return this.useUtc ? date.getUTCMonth() : date.getMonth()\n },\n\n /**\n * Returns the number of days in the month, using UTC or not\n * @param {Date} date\n */\n getDaysInMonth(date) {\n return this.daysInMonth(this.getFullYear(date), this.getMonth(date))\n },\n\n /**\n * Returns the date, using UTC or not\n * @param {Date} date\n */\n getDate(date) {\n return this.useUtc ? date.getUTCDate() : date.getDate()\n },\n\n /**\n * Returns the day, using UTC or not\n * @param {Date} date\n */\n getDay(date) {\n return this.useUtc ? date.getUTCDay() : date.getDay()\n },\n\n /**\n * Returns the hours, using UTC or not\n * @param {Date} date\n */\n getHours(date) {\n return this.useUtc ? date.getUTCHours() : date.getHours()\n },\n\n /**\n * Returns the minutes, using UTC or not\n * @param {Date} date\n */\n getMinutes(date) {\n return this.useUtc ? date.getUTCMinutes() : date.getMinutes()\n },\n\n /**\n * Sets the full year, using UTC or not\n * @param {Date} date\n * @param {String, Number} value\n */\n setFullYear(date, value) {\n return this.useUtc ? date.setUTCFullYear(value) : date.setFullYear(value)\n },\n\n /**\n * Sets the month, using UTC or not\n * @param {Date} date\n * @param {String, Number} value\n */\n setMonth(date, value) {\n return this.useUtc ? date.setUTCMonth(value) : date.setMonth(value)\n },\n\n /**\n * Sets the date, using UTC or not\n * @param {Date} date\n * @param {String, Number} value\n */\n setDate(date, value) {\n return this.useUtc ? date.setUTCDate(value) : date.setDate(value)\n },\n\n /**\n * Check if date1 is equivalent to date2, without comparing the time\n * @see https://stackoverflow.com/a/6202196/4455925\n * @param {Date|null} date1\n * @param {Date|null} date2\n */\n // eslint-disable-next-line complexity\n compareDates(date1, date2) {\n if (date1 === null && date2 === null) {\n return true\n }\n\n if (\n (date1 !== null && date2 === null) ||\n (date2 !== null && date1 === null)\n ) {\n return false\n }\n\n const d1 = new Date(date1.valueOf())\n const d2 = new Date(date2.valueOf())\n\n this.resetDateTime(d1)\n this.resetDateTime(d2)\n return d1.valueOf() === d2.valueOf()\n },\n\n /**\n * Validates a date object\n * @param {Date} date - an object instantiated with the new Date constructor\n * @return {Boolean}\n */\n isValidDate(date) {\n if (Object.prototype.toString.call(date) !== '[object Date]') {\n return false\n }\n return !Number.isNaN(date.valueOf())\n },\n\n /**\n * Return abbreviated week day name\n * @param {Date} date\n * @param {Array} days\n * @return {String}\n */\n getDayNameAbbr(date, days) {\n if (typeof date !== 'object') {\n throw TypeError('Invalid Type')\n }\n return days[this.getDay(date)]\n },\n\n /**\n * Return day number from abbreviated week day name\n * @param {String} abbr\n * @return {Number}\n */\n getDayFromAbbr(abbr) {\n for (let i = 0; i < en.days.length; i += 1) {\n if (abbr.toLowerCase() === en.days[i].toLowerCase()) {\n return i\n }\n }\n throw TypeError('Invalid week day')\n },\n\n /**\n * Return name of the month\n * @param {Number|Date} month\n * @param {Array} months\n * @return {String}\n */\n getMonthName(month, months) {\n if (!months) {\n throw Error('missing 2nd parameter Months array')\n }\n if (typeof month === 'object') {\n return months[this.getMonth(month)]\n }\n if (typeof month === 'number') {\n return months[month]\n }\n throw TypeError('Invalid type')\n },\n\n /**\n * Return an abbreviated version of the month\n * @param {Number|Date} month\n * @param {Array} monthsAbbr\n * @return {String}\n */\n getMonthNameAbbr(month, monthsAbbr) {\n if (!monthsAbbr) {\n throw Error('missing 2nd parameter Months array')\n }\n if (typeof month === 'object') {\n return monthsAbbr[this.getMonth(month)]\n }\n if (typeof month === 'number') {\n return monthsAbbr[month]\n }\n throw TypeError('Invalid type')\n },\n\n /**\n * Alternative get total number of days in month\n * @param {Number} year\n * @param {Number} month\n * @return {Number}\n */\n // eslint-disable-next-line complexity\n daysInMonth(year, month) {\n if (/8|3|5|10/.test(month.toString())) {\n return 30\n }\n if (month === 1) {\n return (!(year % 4) && year % 100) || !(year % 400) ? 29 : 28\n }\n return 31\n },\n\n /**\n * Get nth suffix for date\n * @param {Number} day\n * @return {String}\n */\n // eslint-disable-next-line complexity\n getNthSuffix(day) {\n switch (day) {\n case 1:\n case 21:\n case 31:\n return 'st'\n case 2:\n case 22:\n return 'nd'\n case 3:\n case 23:\n return 'rd'\n default:\n return 'th'\n }\n },\n\n /**\n * Formats date object\n * @param {Date} date\n * @param {String} formatStr\n * @param {Object} translation\n * @return {String}\n */\n formatDate(date, formatStr, translation = en) {\n const year = this.getFullYear(date)\n const month = this.getMonth(date) + 1\n const day = this.getDate(date)\n\n const matches = {\n d: day,\n dd: `0${day}`.slice(-2),\n E: this.getDayNameAbbr(date, translation.days),\n o: this.getNthSuffix(this.getDate(date)),\n M: month,\n MM: `0${month}`.slice(-2),\n MMM: this.getMonthNameAbbr(this.getMonth(date), translation.monthsAbbr),\n MMMM: this.getMonthName(this.getMonth(date), translation.months),\n yy: String(year).slice(2),\n yyyy: year,\n }\n\n const REGEX_FORMAT = /y{4}|y{2}|M{1,4}|d{1,2}|o|E/g\n\n return formatStr.replace(REGEX_FORMAT, (match) => matches[match])\n },\n\n /**\n * Parses a date from a string, or returns the original string\n * @param {String} dateStr\n * @param {String|Function} format\n * @param {Object} translation\n * @param {Function} parser\n * @return {Date | String}\n */\n // eslint-disable-next-line max-params\n parseDate(dateStr, format, translation = en, parser = null) {\n if (!(dateStr && format)) {\n return dateStr\n }\n\n if (parser) {\n return parseDateWithLibrary(dateStr, format, parser)\n }\n\n const parseableDate = getParsableDate({\n dateStr,\n formatStr: format,\n translation,\n currentYear: this.getFullYear(new Date()),\n time: this.getTime(),\n })\n const parsedDate = Date.parse(parseableDate)\n\n if (Number.isNaN(parsedDate)) {\n return dateStr\n }\n\n return new Date(parsedDate)\n },\n\n /**\n * Parses a string/number to a date, or returns null\n * @param {Date|String|Number|undefined} date\n * @returns {Date|null}\n */\n parseAsDate(date) {\n if (typeof date === 'string' || typeof date === 'number') {\n const parsed = new Date(date)\n return this.isValidDate(parsed) ? parsed : null\n }\n return this.isValidDate(date) ? date : null\n },\n\n getTime() {\n const time = 'T00:00:00'\n\n return this.useUtc ? `${time}Z` : time\n },\n\n /**\n * Remove hours/minutes/seconds/milliseconds from a date object\n * @param {Date} date\n * @return {Date}\n */\n resetDateTime(date) {\n return new Date(\n this.useUtc ? date.setUTCHours(0, 0, 0, 0) : date.setHours(0, 0, 0, 0),\n )\n },\n\n /**\n * Return a new date object with hours/minutes/seconds/milliseconds removed.\n * Defaults to today's date, if no parameter is provided\n * @param {Date=} date\n * @return {Date}\n */\n getNewDateObject(date) {\n return date\n ? this.resetDateTime(new Date(date))\n : this.resetDateTime(new Date())\n },\n\n /**\n * Returns the `open date` at a given view\n * @param {Date|null} openDate the date on which the datepicker should open\n * @param {View} view Either `day`, `month`, or `year`\n * @return {Date|null}\n */\n getOpenDate(openDate, selectedDate, view) {\n const parsedOpenDate = this.parseAsDate(openDate)\n const openDateOrToday = this.getNewDateObject(parsedOpenDate)\n const newOpenDate = selectedDate || openDateOrToday\n\n return this.adjustDateToView(newOpenDate, view)\n },\n\n /**\n * Converts a date according to a given view\n * e.g. '2025-05-15' becomes '2025-05-01' at `month view and\n * '2025-01-01' at `year` view\n * @param {Date} dateToConvert The date to convert\n * @param {String} view The view for which to adjust the date\n * @return {Date}\n */\n adjustDateToView(dateToConvert, view) {\n const date = this.getNewDateObject(dateToConvert)\n\n if (view === 'year') {\n const resetDay = new Date(this.setDate(date, 1))\n const resetMonth = this.setMonth(resetDay, 0)\n return new Date(resetMonth)\n }\n\n if (view === 'month') {\n return new Date(this.setDate(date, 1))\n }\n\n return date\n },\n}\n\nexport default (useUtc) => ({\n ...utils,\n useUtc,\n})\n","<script>\nexport default {\n props: {\n autofocus: {\n type: Boolean,\n default: false,\n },\n bootstrapStyling: {\n type: Boolean,\n default: false,\n },\n clearButton: {\n type: Boolean,\n default: null,\n },\n calendarButton: {\n type: Boolean,\n default: false,\n },\n disabled: {\n type: Boolean,\n default: false,\n },\n format: {\n type: [String, Function],\n default: 'dd MMM yyyy',\n },\n id: {\n type: String,\n default: null,\n },\n inline: {\n type: Boolean,\n default: false,\n },\n inputClass: {\n type: [String, Object, Array],\n default: null,\n },\n maxlength: {\n type: [Number, String],\n default: null,\n },\n name: {\n type: String,\n default: null,\n },\n openDate: {\n type: [String, Date, Number],\n default: null,\n },\n parser: {\n type: Function,\n default: null,\n },\n pattern: {\n type: String,\n default: null,\n },\n placeholder: {\n type: String,\n default: null,\n },\n refName: {\n type: String,\n default: '',\n },\n required: {\n type: Boolean,\n default: false,\n },\n showCalendarOnButtonClick: {\n type: Boolean,\n default: false,\n },\n showCalendarOnFocus: {\n type: Boolean,\n default: false,\n },\n tabindex: {\n type: [Number, String],\n default: null,\n },\n typeable: {\n type: Boolean,\n default: false,\n },\n useUtc: {\n type: Boolean,\n default: false,\n },\n },\n}\n</script>\n","<template>\n <div :class=\"{ 'input-group': bootstrapStyling }\">\n <slot name=\"beforeDateInput\" />\n <!-- Calendar Button -->\n <button\n v-if=\"calendarButton\"\n ref=\"calendarButton\"\n class=\"vdp-datepicker__calendar-button\"\n :class=\"{ 'btn input-group-prepend': bootstrapStyling }\"\n data-test-calendar-button\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"toggle('calendarButton')\"\n @focus=\"handleButtonFocus\"\n >\n <span :class=\"{ 'input-group-text': bootstrapStyling }\">\n <slot name=\"calendarBtn\">&hellip;</slot>\n </span>\n </button>\n <!-- Input -->\n <input\n :id=\"id\"\n :ref=\"refName\"\n autocomplete=\"off\"\n :autofocus=\"autofocus\"\n :class=\"computedInputClass\"\n :clear-button=\"clearButton\"\n data-test-input\n :disabled=\"disabled\"\n :maxlength=\"maxlength\"\n :name=\"name\"\n :pattern=\"pattern\"\n :placeholder=\"placeholder\"\n :readonly=\"!typeable\"\n :required=\"required\"\n :tabindex=\"tabindex\"\n :type=\"inline ? 'hidden' : null\"\n :value=\"formattedValue\"\n @blur=\"handleInputBlur\"\n @click=\"handleInputClick\"\n @focus=\"handleInputFocus\"\n @keydown.delete=\"handleDelete\"\n @keydown.down.prevent=\"handleKeydownDown\"\n @keydown.enter.prevent=\"handleKeydownEnter\"\n @keydown.esc.prevent=\"handleKeydownEscape\"\n @keydown.space=\"handleKeydownSpace($event)\"\n @keydown.tab=\"$emit('tab', $event)\"\n @keyup=\"handleKeyup($event)\"\n @keyup.space=\"handleKeyupSpace($event)\"\n />\n <!-- Clear Button -->\n <button\n v-if=\"clearButton && selectedDate\"\n class=\"vdp-datepicker__clear-button\"\n :class=\"{ 'btn input-group-append': bootstrapStyling }\"\n data-test-clear-button\n :disabled=\"disabled\"\n type=\"button\"\n @click=\"clearDate\"\n >\n <span :class=\"{ 'input-group-text': bootstrapStyling }\">\n <slot name=\"clearBtn\">&times;</slot>\n </span>\n </button>\n <slot name=\"afterDateInput\" />\n </div>\n</template>\n\n<script>\nimport makeDateUtils from '~/utils/DateUtils'\nimport inputProps from '~/mixins/inputProps.vue'\n\nexport default {\n name: 'DateInput',\n mixins: [inputProps],\n props: {\n isOpen: {\n type: Boolean,\n default: false,\n },\n selectedDate: {\n type: Date,\n default: null,\n },\n slideDuration: {\n type: Number,\n default: 250,\n },\n translation: {\n type: Object,\n default() {\n return {}\n },\n },\n },\n emits: {\n blur: null,\n clearDate: null,\n close: null,\n focus: null,\n open: null,\n selectTypedDate: (date) => {\n return date === null || date instanceof Date\n },\n setFocus: (refArray) => {\n return refArray.every((ref) => {\n return [\n 'calendarButton',\n 'input',\n 'prev',\n 'up',\n 'next',\n 'tabbableCell',\n ].includes(ref)\n })\n },\n tab: null,\n typedDate: (date) => {\n return date === null || date instanceof Date\n },\n },\n data() {\n return {\n input: null,\n isInputFocused: false,\n shouldToggleOnFocus: false,\n shouldToggleOnClick: true,\n typedDate: '',\n utils: makeDateUtils(this.useUtc),\n }\n },\n computed: {\n computedInputClass() {\n if (this.bootstrapStyling) {\n if (typeof this.inputClass === 'string') {\n return [this.inputClass, 'form-control'].join(' ')\n }\n return { 'form-control': true, ...this.inputClass }\n }\n return this.inputClass\n },\n formattedValue() {\n if (this.typeable) {\n return this.typedDate\n }\n\n return this.formatDate(this.selectedDate)\n },\n },\n watch: {\n showCalendarOnFocus: {\n immediate: true,\n handler(showCalendarOnFocus) {\n if (showCalendarOnFocus) {\n this.shouldToggleOnFocus = !this.isOpen\n }\n },\n },\n isOpen(isOpen, wasOpen) {\n this.$nextTick(() => {\n if (isOpen && this.showCalendarOnFocus) {\n if (wasOpen && !this.isInputFocused) {\n this.shouldToggleOnFocus = true\n return\n }\n this.shouldToggleOnFocus = false\n }\n })\n },\n selectedDate: {\n immediate: true,\n handler(selectedDate) {\n if (this.typeable) {\n this.typedDate = this.formatDate(selectedDate)\n }\n },\n },\n },\n mounted() {\n this.input = this.$el.querySelector('input')\n },\n methods: {\n /**\n * Emits a `clear-date` event\n */\n clearDate() {\n this.input.value = ''\n this.$emit('clearDate')\n },\n /**\n * Formats a date\n * @param {Date} date The date to be formatted\n * @returns {String}\n */\n formatDate(date) {\n if (!date) {\n return ''\n }\n\n return typeof this.format === 'function'\n ? this.format(new Date(date))\n : this.utils.formatDate(new Date(date), this.format, this.translation)\n },\n /**\n * Formats a typed date, or clears it if invalid\n */\n formatTypedDate() {\n const parsedDate = this.parseInput()\n\n if (this.utils.isValidDate(parsedDate)) {\n this.typedDate = this.formatDate(parsedDate)\n } else {\n this.input.value = ''\n this.typedDate = ''\n }\n },\n /**\n * Validates typedDate\n */\n handleInputBlur() {\n if (this.showCalendarOnFocus && !this.isOpen) {\n this.shouldToggleOnFocus = true\n }\n\n if (this.typeable) {\n this.formatTypedDate()\n }\n this.isInputFocused = false\n },\n /**\n * Resets `shouldToggleOnFocus` to true\n */\n handleButtonFocus() {\n if (this.showCalendarOnFocus) {\n this.shouldToggleOnFocus = true\n }\n },\n /**\n * Clears the calendar when the `delete` or `backspace` key is pressed\n */\n handleDelete() {\n if (!this.typeable && this.selectedDate) {\n this.clearDate()\n }\n },\n /**\n * Toggles the calendar (unless `show-calendar-on-button-click` is true)\n */\n handleInputClick() {\n if (this.showCalendarOnButtonClick) return\n\n if (this.shouldToggleOnClick) {\n this.toggle()\n }\n },\n /**\n * Opens the calendar when `show-calendar-on-focus` is true (unless `show-calendar-on-button-click` is true)\n */\n // eslint-disable-next-line complexity\n handleInputFocus() {\n if (this.showCalendarOnButtonClick) return\n\n this.isInputFocused = true\n\n if (!this.isOpen && this.shouldToggleOnFocus) {\n this.shouldToggleOnClick = false\n }\n\n if (this.shouldToggleOnFocus && !this.isOpen) {\n this.$emit('open')\n\n setTimeout(() => {\n this.shouldToggleOnClick = true\n }, this.slideDuration)\n }\n },\n /**\n * Opens the calendar, or sets the focus to the next focusable element down\n */\n handleKeydownDown() {\n if (!this.isOpen) {\n this.$emit('open')\n }\n\n if (!this.typeable) {\n return\n }\n\n this.$emit('setFocus', ['prev', 'up', 'next', 'tabbableCell'])\n },\n /**\n * Selects a typed date and closes the calendar\n */\n handleKeydownEnter() {\n if (!this.typeable) {\n return\n }\n\n if (!this.input.value) {\n this.$emit('selectTypedDate', null)\n return\n }\n\n const parsedDate = this.parseInput()\n\n if (this.utils.isValidDate(parsedDate)) {\n this.$emit('selectTypedDate', parsedDate)\n }\n },\n /**\n * Closes the calendar\n */\n handleKeydownEscape() {\n if (this.isOpen) {\n this.$emit('close')\n }\n },\n /**\n * Prevents scrolling when not typeable\n */\n handleKeydownSpace(event) {\n if (!this.typeable) {\n event.preventDefault()\n }\n },\n /**\n * Parses a typed date and emits `typed-date` event, if valid\n * @param {object} event Used to exclude certain keystrokes\n */\n handleKeyup(event) {\n if (\n !this.typeable ||\n [\n 'Control',\n 'Escape',\n 'Shift',\n 'Tab',\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n ].includes(event.key)\n ) {\n return\n }\n\n this.typedDate = this.input.value\n\n if (!this.input.value) {\n this.$emit('typedDate', null)\n return\n }\n\n const parsedDate = this.parseInput()\n\n if (this.utils.isValidDate(parsedDate)) {\n this.$emit('typedDate', parsedDate)\n }\n },\n /**\n * Toggles the calendar unless a typed date has been entered or `show-calendar-on-button-click` is true\n */\n handleKeyupSpace(event) {\n if (this.typeable) {\n if (this.input.value === '') {\n this.toggle()\n }\n return\n }\n\n event.preventDefault()\n if (!this.showCalendarOnButtonClick) {\n this.toggle()\n }\n },\n /**\n * Parses the value of the input field\n */\n parseInput() {\n return new Date(\n this.utils.parseDate(\n this.input.value.trim(),\n this.format,\n this.translation,\n this.parser,\n ),\n )\n },\n /**\n * Opens or closes the calendar\n */\n toggle(calendarButton) {\n if (this.isOpen) {\n this.$emit('setFocus', [calendarButton || 'input'])\n }\n\n this.$emit(this.isOpen ? 'close' : 'open')\n },\n },\n}\n</script>\n","const cellUtils = {\n isDefined(obj, prop) {\n return obj && typeof obj[prop] !== 'undefined'\n },\n\n hasArray(obj, prop) {\n return obj && Array.isArray(obj[prop])\n },\n\n hasDate(obj, prop) {\n return this.isDefined(obj, prop) && this.utils.isValidDate(obj[prop])\n },\n\n dayMonthYear(obj, prop) {\n const { utils } = this\n const hasDate = this.hasDate(obj, prop)\n\n if (!hasDate) {\n return {\n day: undefined,\n month: undefined,\n year: undefined,\n }\n }\n\n const d = obj[prop]\n\n return {\n day: utils.getDate(d),\n month: utils.getMonth(d),\n year: utils.getFullYear(d),\n }\n },\n}\n\nexport default (utils) => ({\n ...cellUtils,\n utils,\n})\n","/* eslint-disable no-underscore-dangle */\n\nimport makeCellUtils from './cellUtils'\n\nexport default class DisabledDate {\n constructor(utils, disabledDates) {\n this._utils = utils\n this._disabledDates = disabledDates\n }\n\n get config() {\n const disabledDates = this._disabledDates\n const utils = makeCellUtils(this._utils)\n const has = {\n customPredictor: utils.isDefined(disabledDates, 'customPredictor'),\n daysOfMonth: utils.hasArray(disabledDates, 'daysOfMonth'),\n daysOfWeek: utils.hasArray(disabledDates, 'days'),\n from: utils.hasDate(disabledDates, 'from'),\n ranges: utils.hasArray(disabledDates, 'ranges'),\n specificDates: utils.hasArray(disabledDates, 'dates'),\n to: utils.hasDate(disabledDates, 'to'),\n }\n\n return {\n to: utils.dayMonthYear(disabledDates, 'to'),\n from: utils.dayMonthYear(disabledDates, 'from'),\n has,\n }\n }\n\n daysInMonth(date) {\n const utils = this._utils\n const month = utils.getMonth(date)\n const year = utils.getFullYear(date)\n\n return utils.daysInMonth(year, month)\n }\n\n isDateDisabledVia(date) {\n const disabledDates = this._disabledDates\n const { has } = this.config\n\n return {\n to: () => {\n return has.to && date <= disabledDates.to\n },\n from: () => {\n return has.from && date >= disabledDates.from\n },\n range: () => {\n if (!has.ranges) return false\n\n const { ranges } = disabledDates\n const u = makeCellUtils(this._utils)\n\n return ranges.some((thisRange) => {\n const hasFrom = u.isDefined(thisRange, 'from')\n const hasTo = u.isDefined(thisRange, 'to')\n\n return (\n hasFrom && hasTo && date <= thisRange.to && date >= thisRange.from\n )\n })\n },\n customPredictor: () => {\n return has.customPredictor && disabledDates.customPredictor(date)\n },\n specificDate: () => {\n if (!has.specificDates) return false\n\n return disabledDates.dates.some((d) => {\n return this._utils.compareDates(date, d)\n })\n },\n daysOfWeek: () => {\n if (!has.daysOfWeek) return false\n\n return disabledDates.days.indexOf(this._utils.getDay(date)) !== -1\n },\n daysOfMonth: () => {\n if (!has.daysOfMonth) return false\n\n return (\n disabledDates.daysOfMonth.indexOf(this._utils.getDate(date)) !== -1\n )\n },\n }\n }\n\n isMonthDisabledVia(date) {\n const { from, has, to } = this.config\n const month = this._utils.getMonth(date)\n const year = this._utils.getFullYear(date)\n\n return {\n to: () => {\n const isYearInPast = has.to && year < to.year\n\n if (isYearInPast) {\n return true\n }\n\n return has.to && month < to.month && year <= to.year\n },\n from: () => {\n const isYearInFuture = has.from && year > from.year\n\n if (isYearInFuture) {\n return true\n }\n\n return has.from && month > from.month && year >= from.year\n },\n }\n }\n\n isYearDisabledVia(date) {\n const { from, has, to } = this.config\n const year = this._utils.getFullYear(date)\n\n return {\n to: () => {\n return has.to && year < to.year\n },\n from: () => {\n return has.from && year > from.year\n },\n }\n }\n\n /**\n * Checks if the given date should be disabled\n * @param {Date} date\n * @return {Boolean}\n */\n // eslint-disable-next-line complexity,max-statements\n isDateDisabled(date) {\n const isDisabledVia = this.isDateDisabledVia(date)\n\n return (\n isDisabledVia.to() ||\n isDisabledVia.from() ||\n isDisabledVia.range() ||\n isDisabledVia.specificDate() ||\n isDisabledVia.daysOfWeek() ||\n isDisabledVia.daysOfMonth() ||\n isDisabledVia.customPredictor()\n )\n }\n\n /**\n * Checks if the given month should be disabled\n * @param {Date} date\n * @return {Boolean}\n */\n // eslint-disable-next-line complexity,max-statements\n isMonthDisabled(date) {\n const isDisabledVia = this.isMonthDisabledVia(date)\n\n if (isDisabledVia.to() || isDisabledVia.from()) {\n return true\n }\n\n // now we have to check each day of the month\n for (let i = 1; i <= this.daysInMonth(date); i += 1) {\n const dayDate = new Date(date)\n dayDate.setDate(i)\n // if at least one day of this month is NOT disabled,\n // we can conclude that this month SHOULD be selectable\n if (!this.isDateDisabled(dayDate)) {\n return false\n }\n }\n\n return true\n }\n\n /**\n * Checks if the given year should be disabled\n * @param {Date} date\n * @return {Boolean}\n */\n // eslint-disable-next-line complexity,max-statements\n isYearDisabled(date) {\n const isDisabledVia = this.isYearDisabledVia(date)\n\n if (isDisabledVia.to() || isDisabledVia.from()) {\n return true\n }\n\n // now we have to check each month of the year\n for (let i = 0; i < 12; i += 1) {\n const monthDate = new Date(date)\n monthDate.setMonth(i)\n // if at least one month of this year is NOT disabled,\n // we can conclude that this year SHOULD be selectable\n if (!this.isMonthDisabled(monthDate)) {\n return false\n }\n }\n\n return true\n }\n\n getEarliestPossibleDate(date) {\n if (!date) {\n return null\n }\n const utils = this._utils\n\n if (this.isDateDisabled(date)) {\n const nextDate = new Date(\n utils.getFullYear(date),\n utils.getMonth(date),\n utils.getDate(date) + 1,\n )\n\n return this.getEarliestPossibleDate(nextDate)\n }\n\n return date\n }\n\n getLatestPossibleDate(date) {\n if (!date) {\n return null\n }\n const utils = this._utils\n\n if (this.isDateDisabled(date)) {\n const nextDate = new Date(\n utils.getFullYear(date),\n utils.getMonth(date),\n utils.getDate(date) - 1,\n )\n\n return this.getLatestPossibleDate(nextDate)\n }\n\n return date\n }\n}\n","<script>\nexport default {\n data() {\n return {\n focus: {\n delay: 0,\n refs: [],\n },\n inlineTabbableCell: null,\n isActive: false,\n isRevertingToOpenDate: false,\n navElements: [],\n navElementsFocusedIndex: 0,\n resetTabbableCell: false,\n skipReviewFocus: false,\n tabbableCell: null,\n transitionName: '',\n }\n },\n computed: {\n fallbackElementsToFocus() {\n const elements = ['tabbableCell', 'prev', 'next']\n\n if (this.typeable) {\n elements.unshift('input')\n }\n\n return elements\n },\n focusedDateTimestamp() {\n const pageDate = new Date(this.pageTimestamp)\n\n if (this.hasClass(this.tabbableCell, 'day')) {\n return this.utils.setDate(pageDate, this.tabbableCell.innerHTML.trim())\n }\n\n if (this.hasClass(this.tabbableCell, 'month')) {\n return this.utils.setMonth(pageDate, this.tabbableCellId)\n }\n\n const fullYear = this.utils.getFullYear(pageDate) - 1\n return this.utils.setFullYear(pageDate, fullYear + this.tabbableCellId)\n },\n tabbableCellId() {\n return (\n this.tabbableCell && Number(this.tabbableCell.getAttribute('data-id'))\n )\n },\n },\n methods: {\n /**\n * Converts a date to first in month for `month` view or first in year for `year` view\n * @param {Date} date The date to convert\n * @returns {Date}\n */\n getCellDate(date) {\n switch (this.view) {\n case 'month':\n return new Date(this.utils.setDate(date, 1))\n case 'year':\n return new Date(\n this.utils.setMonth(new Date(this.utils.setDate(date, 1)), 0),\n )\n default:\n return date\n }\n },\n /**\n * Returns true, unless tabbing should be focus-trapped\n * @return {Boolean}\n */\n allowNormalTabbing(event) {\n if (!this.isOpen) {\n return true\n }\n\n return this.isTabbingAwayFromInlineDatepicker(event)\n },\n /**\n * Focuses the first non-disabled element found in the `focus.refs` array and sets `navElementsFocusedIndex`\n */\n applyFocus() {\n const focusRefs = [...this.focus.refs, ...this.fallbackElementsToFocus]\n\n for (let i = 0; i < focusRefs.length; i += 1) {\n const element = this.getElementByRef(focusRefs[i])\n\n if (element && element.getAttribute('disabled') === null) {\n element.focus()\n this.setNavElementsFocusedIndex()\n break\n }\n }\n },\n /**\n * Ensures the most recently focused tabbable cell is focused when tabbing backwards to an inline calendar\n * If no element has previously been focused, the tabbable cell is reset and focused\n */\n focusInlineTabbableCell() {\n if (this.inlineTabbableCell) {\n this.inlineTabbableCell.focus()\n\n return\n }\n\n this.resetTabbableCell = true\n this.setTabbableCell()\n this.tabbableCell.focus()\n this.resetTabbableCell = false\n },\n /**\n * Returns the currently focused cell element, if there is one...\n */\n getActiveCell() {\n const activeElement = this.getActiveElement()\n const isActiveElementACell = this.hasClass(activeElement, 'cell')\n const isOnSameView = this.hasClass(activeElement, this.view)\n\n if (isActiveElementACell && isOnSameView && !this.resetTabbableCell) {\n return activeElement\n }\n\n return null\n },\n /**\n * Returns the currently focused element, using shadowRoot for web-components...\n */\n getActiveElement() {\n return document.activeElement.shadowRoot\n ? document.activeElement.shadowRoot.activeElement\n : document.activeElement\n },\n /**\n * Returns the `cellId` for a given a date\n * @param {Date} date The date for which we need the cellId\n * @returns {Number|null}\n */\n getCellId(date) {\n if (!date || !this.$refs.picker.$refs.cells) {\n return null\n }\n\n const cellDate = this.getCellDate(date)\n const { cells } = this.$refs.picker.$refs.cells\n\n for (let i = 0; i < cells.length; i += 1) {\n if (cells[i].timestamp === cellDate.valueOf()) {\n return i\n }\n }\n\n return null\n },\n /**\n * Finds an element by its `ref` attribute\n * @param {string} ref The `ref` name of the wanted element\n * @returns {HTMLElement|Vue} A Vue element\n */\n // eslint-disable-next-line complexity,max-statements\n getElementByRef(ref) {\n if (ref === 'tabbableCell') {\n return this.tabbableCell\n }\n if (ref === 'input') {\n return this.$refs.dateInput && this.$refs.dateInput.$refs[this.refName]\n }\n if (ref === 'calendarButton') {\n return this.$refs.dateInput && this.$refs.dateInput.$refs.calendarButton\n }\n if (ref === 'openDate') {\n return this.$refs.picker.$refs.cells.$refs.openDate[0]\n }\n if (this.showHeader) {\n return (\n this.$refs.picker &&\n this.$refs.picker.$refs.pickerHeader &&\n this.$refs.picker.$refs.pickerHeader.$refs[ref]\n )\n }\n return null\n },\n /**\n * Returns an array of all HTML elements which should be focus-trapped in the calendarFooter slot\n * @returns {Array} An array of HTML elements\n */\n getElementsFromCalendarFooter() {\n const footerSlotIndex = this.hasSlot('beforeCalendarHeader') ? 2 : 1\n\n return this.getFocusableElements(\n this.$refs.view.children[footerSlotIndex],\n )\n },\n /**\n * Returns an array of all HTML elements which should be focus-trapped in the specified slot\n * @returns {Array} An array of HTML elements\n */\n getElementsFromSlot(slotName) {\n if (!this.hasSlot(slotName)) {\n return []\n }\n\n if (slotName === 'beforeCalendarHeader') {\n return this.getFocusableElements(this.$refs.view.children[0])\n }\n\n if (slotName === 'calendarFooter') {\n return this.getElementsFromCalendarFooter()\n }\n\n const isBeforeHeader = slotName.indexOf('beforeCalendarHeader') > -1\n const picker = this.$refs.picker.$el\n const index = isBeforeHeader ? 0 : picker.children.length - 1\n\n return this.getFocusableElements(picker.children[index])\n },\n /**\n * Returns an array of all HTMLButtonElements which should be focus-trapped in the header\n * @returns {Array} An array of HTMLButtonElements\n */\n getElementsFromHeader() {\n if (!this.$refs.picker.$refs.pickerHeader) {\n return []\n }\n const header = this.$refs.picker.$refs.pickerHeader.$el\n const navNodeList = header.querySelectorAll('button:enabled')\n\n return [...Array.prototype.slice.call(navNodeList)]\n },\n /**\n * Returns an array of focusable elements in a given HTML fragment\n * @param {Element} fragment The HTML fragment to search\n * @returns {Array}\n */\n getFocusableElements(fragment) {\n if (!fragment) {\n return []\n }\n\n const navNodeList = fragment.querySelectorAll(\n 'button:enabled:not([tabindex=\"-1\"]), [href]:not([tabindex=\"-1\"]), input:not([tabindex=\"-1\"]):not([type=hidden]), select:enabled:not([tabindex=\"-1\"]), textarea:enabled:not([tabindex=\"-1\"]), [tabindex]:not([tabindex=\"-1\"])',\n )\n\n return [...Array.prototype.slice.call(navNodeList)]\n },\n /**\n * Returns the first focusable element of an inline datepicker\n * @returns {HTMLElement}\n */\n getFirstInlineFocusableElement() {\n const popupElements = this.getFocusableElements(this.$refs.popup.$el)\n\n return popupElements[0]\n },\n /**\n * Returns the last focusable element of an inline datepicker\n * @returns {HTMLElement}\n */\n getLastInlineFocusableElement() {\n const popupElements = this.getFocusableElements(this.$refs.popup.$el)\n\n return popupElements[popupElements.length - 1]\n },\n /**\n * Returns the input element (when typeable)\n * @returns {Element}\n */\n getInputField() {\n if (!this.typeable || this.inline) {\n return null\n }\n\n return this.$refs.dateInput.$refs[this.refName]\n },\n /**\n * Used for a typeable datepicker: returns the cell element that corresponds to latestValidTypedDate...\n */\n getTypedCell() {\n if (!this.typeable) {\n return null\n }\n\n const cellId = this.getCellId(this.latestValidTypedDate)\n\n return cellId ? this.$refs.picker.$refs.cells.$el.children[cellId] : null\n },\n /**\n * Sets `datepickerId` (as a global) and keeps track of focusable elements\n */\n handleFocusIn() {\n document.datepickerId = this.datepickerId\n this.globalDatepickerId = this.datepickerId\n\n this.isActive = true\n this.setInlineTabbableCell()\n this.setNavElements()\n },\n /**\n * Sets the datepicker's `isActive` state to false and resets `globalDatepickerId`\n */\n handleFocusOut() {\n this.isActive = false\n this.globalDatepickerId = ''\n },\n /**\n * Returns true if the calendar has been passed the given slot\n * @param {String} slotName The name of the slot\n * @return {Boolean}\n */\n hasSlot(slotName) {\n return !!this.$slots[slotName]\n },\n /**\n * Returns true if the user is tabbing away from an inline datepicker\n * @return {Boolean}\n */\n isTabbingAwayFromInlineDatepicker(event) {\n if (!this.inline) {\n return false\n }\n\n if (this.isTabbingAwayFromFirstNavElement(event)) {\n this.tabAwayFromFirstElement()\n\n return true\n }\n\n if (this.isTabbingAwayFromLastNavElement(event)) {\n this.tabAwayFromLastElement()\n\n return true\n }\n\n return false\n },\n /**\n * Used for inline calendars; returns true if the user tabs backwards from the first focusable element\n * @param {object} event Used to determine whether we are tabbing forwards or backwards\n * @return {Boolean}\n */\n isTabbingAwayFromFirstNavElement(event) {\n if (!event.shiftKey) {\n return false\n }\n\n const activeElement = this.getActiveElement()\n const firstNavElement = this.navElements[0]\n\n return activeElement === firstNavElement\n },\n /**\n * Used for inline calendars; returns true if the user tabs forwards from the last focusable element\n * @param {object} event Used to determine whether we are tabbing forwards or backwards\n * @return {Boolean}\n */\n isTabbingAwayFromLastNavElement(event) {\n if (event.shiftKey) {\n return false\n }\n\n const activeElement = this.getActiveElement()\n const lastNavElement = this.navElements[this.navElements.length - 1]\n\n return activeElement === lastNavElement\n },\n /**\n * Resets the focus to the open date\n */\n resetFocusToOpenDate(focusedDateTimestamp) {\n this.focus.refs = ['openDate']\n this.setTransitionAndFocusDelay(\n focusedDateTimestamp,\n this.computedOpenDate,\n )\n\n if (!this.isMinimumView) {\n this.isRevertingToOpenDate = true\n this.view = this.minimumView\n }\n\n this.setPageDate(this.computedOpenDate)\n this.reviewFocus()\n },\n /**\n * Sets the correct focus on next tick\n */\n reviewFocus() {\n if (this.skipReviewFocus) {\n return\n }\n\n this.tabbableCell = null\n this.resetTabbableCell = true\n\n this.$nextTick(() => {\n this.setNavElements()\n\n setTimeout(() => {\n this.applyFocus()\n }, this.focus.delay)\n\n this.resetTabbableCell = false\n })\n },\n /**\n * Stores the current tabbableCell of an inline datepicker\n * N.B. This is used when tabbing back (shift + tab) to an inline calendar from further down the page\n */\n setInlineTabbableCell() {\n if (!this.inline) {\n return\n }\n\n this.inlineTabbableCell = this.tabbableCell\n },\n /**\n * Sets the direction of the slide transition and whether or not to delay application of the focus\n * @param {Date|Number} startDate The date from which to measure\n * @param {Date|Number} endDate Is this before or after the startDate? And is it on the same page?\n */\n setTransitionAndFocusDelay(startDate, endDate) {\n const startPageDate = this.utils.setDate(new Date(startDate), 1)\n const endPageDate = this.utils.setDate(new Date(endDate), 1)\n const isInTheFuture = startPageDate < endPageDate\n\n if (this.isMinimumView) {\n this.focus.delay = isInTheFuture ? this.slideDuration : 0\n } else {\n this.focus.delay = 0\n }\n\n this.setTransitionName(endDate - startDate)\n },\n /**\n * Set the focus\n * @param {Array} refs An array of `refs` to focus (in order of preference)\n */\n setFocus(refs) {\n this.focus.refs = refs\n this.applyFocus()\n },\n /**\n * Determines which elements in datepicker should be focus-trapped\n */\n setNavElements() {\n if (!this.view) return\n\n this.updateTabbableCell()\n\n const view = this.ucFirst(this.view)\n\n this.navElements = [\n this.getInputField(),\n this.getElementsFromSlot('beforeCalendarHeader'),\n this.getElementsFromSlot(`beforeCalendarHeader${view}`),\n this.getElementsFromHeader(),\n this.tabbableCell,\n this.getElementsFromSlot(`calendarFooter${view}`),\n this.getElementsFromSlot('calendarFooter'),\n ]\n .filter((item) => !!item)\n .reduce((acc, val) => acc.concat(val), [])\n },\n /**\n * Keeps track of the currently focused index in the navElements array\n */\n setNavElementsFocusedIndex() {\n const activeElement = this.getActiveElement()\n\n for (let i = 0; i < this.navElements.length; i += 1) {\n if (activeElement === this.navElements[i]) {\n this.navElementsFocusedIndex = i\n return\n }\n }\n\n this.navElementsFocusedIndex = 0\n },\n /**\n * Sets the focus-trapped cell in the picker\n */\n // eslint-disable-next-line complexity\n setTabbableCell() {\n if (!this.$refs.picker || !this.$refs.picker.$refs.cells) {\n return\n }\n\n const pickerCells = this.$refs.picker.$refs.cells.$el\n\n this.tabbableCell =\n this.getActiveCell() ||\n this.getTypedCell() ||\n pickerCells.querySelector('button.selected:not(.muted):enabled') ||\n pickerCells.querySelector('button.open:not(.muted):enabled') ||\n pickerCells.querySelector('button.today:not(.muted):enabled') ||\n pickerCells.querySelector('button.cell:not(.muted):enabled')\n },\n /**\n * Sets the direction of the slide transition\n * @param {Number} plusOrMinus Positive for the future; negative for the past\n */\n setTransitionName(plusOrMinus) {\n const isInTheFuture = plusOrMinus > 0\n\n if (this.isRtl) {\n this.transitionName = isInTheFuture ? 'slide-left' : 'slide-right'\n } else {\n this.transitionName = isInTheFuture ? 'slide-right' : 'slide-left'\n }\n },\n /**\n * Focuses the first focusable element of an inline datepicker, so that the previous element on the page will be tabbed to\n */\n tabAwayFromFirstElement() {\n const firstElement = this.getFirstInlineFocusableElement()\n\n // Keep a record of `tabbableCell` in case `showHeader=false` and this is the first date\n // in the month (with no edge dates from the previous month) to which we may want to\n // tab back down to later.\n this.setInlineTabbableCell()\n\n firstElement.focus()\n\n // Reset the tabbableCell as we don't want it to be the `firstElement` if the latter is\n // an edge date from the previous month\n this.tabbableCell = this.inlineTabbableCell\n },\n /**\n * Focuses the last focusable element of an inline datepicker, so that the