UNPKG

@mst101/vue-datepicker

Version:

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

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