UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

963 lines (959 loc) 36.5 kB
'use strict'; var vue = require('vue'); var CButton = require('../button/CButton.js'); var CCalendar = require('../calendar/CCalendar.js'); var CFormControlWrapper = require('../form/CFormControlWrapper.js'); var CPicker = require('../picker/CPicker.js'); var CTimePicker = require('../time-picker/CTimePicker.js'); var utils = require('./utils.js'); var useDebouncedCallback = require('../../composables/useDebouncedCallback.js'); require('@popperjs/core'); var props = require('../props.js'); const CDateRangePicker = vue.defineComponent({ name: 'CDateRangePicker', props: { /** * A string that provides an accessible label for the button that navigates to the next month in the calendar. This label is read by screen readers to describe the action associated with the button. * * @since 5.4.0 */ ariaNavNextMonthLabel: { type: String, default: 'Next month', }, /** * A string that provides an accessible label for the button that navigates to the next year in the calendar. This label is intended for screen readers to help users understand the button's functionality. * * @since 5.4.0 */ ariaNavNextYearLabel: { type: String, default: 'Next year', }, /** * A string that provides an accessible label for the button that navigates to the previous month in the calendar. Screen readers will use this label to explain the purpose of the button. * * @since 5.4.0 */ ariaNavPrevMonthLabel: { type: String, default: 'Previous month', }, /** * A string that provides an accessible label for the button that navigates to the previous year in the calendar. This label helps screen reader users understand the button's function. * * @since 5.4.0 */ ariaNavPrevYearLabel: { type: String, default: 'Previous year', }, /** * The number of calendars that render on desktop devices. */ calendars: { type: Number, default: 2, }, /** * Default date of the component */ calendarDate: [Date, String], /** * Toggle visibility or set the content of cancel button. */ cancelButton: { type: [Boolean, String], default: 'Cancel', }, /** * Sets the color context of the cancel button to one of CoreUI’s themed colors. * * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light' */ cancelButtonColor: { ...props.Color, default: 'primary', }, /** * Size the cancel button small or large. * * @values 'sm', 'lg' */ cancelButtonSize: { type: String, default: 'sm', validator: (value) => { return ['sm', 'lg'].includes(value); }, }, /** * Set the cancel button variant to an outlined button or a ghost button. * * @values 'ghost', 'outline' */ cancelButtonVariant: { type: String, default: 'ghost', validator: (value) => { return ['ghost', 'outline'].includes(value); }, }, /** * Toggle visibility of the cleaner button. */ cleaner: { type: Boolean, default: true, }, /** * If true the dropdown will be immediately closed after submitting the full date. * * @since 4.7.0 */ closeOnSelect: { type: Boolean, default: true, }, /** * Toggle visibility or set the content of confirm button. */ confirmButton: { type: [Boolean, String], default: 'OK', }, /** * Sets the color context of the confirm button to one of CoreUI’s themed colors. * * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light' */ confirmButtonColor: { ...props.Color, default: 'primary', }, /** * Size the confirm button small or large. * * @values 'sm', 'lg' */ confirmButtonSize: { type: String, default: 'sm', validator: (value) => { return ['sm', 'lg'].includes(value); }, }, /** * Set the confirm button variant to an outlined button or a ghost button. * * @values 'ghost', 'outline' */ confirmButtonVariant: { type: String, validator: (value) => { return ['ghost', 'outline'].includes(value); }, }, /** * Set the format of day name. * * @default 'numeric' * @since 4.6.0 */ dayFormat: { type: [Function, String], default: 'numeric', required: false, validator: (value) => { if (typeof value === 'string') { return ['numeric', '2-digit'].includes(value); } if (typeof value === 'function') { return true; } if (typeof value === 'function') { return true; } return false; }, }, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * Specify the list of dates that cannot be selected. */ disabledDates: Array, /** * Initial selected to date (range). */ endDate: [Date, String], /** * Provide valuable, actionable feedback. * * @since 4.6.0 */ feedback: String, /** * Provide valuable, actionable feedback. * * @since 4.6.0 */ feedbackInvalid: String, /** * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`. * * @since 4.6.0 */ feedbackValid: String, /** * Sets the day of start week. * - 0 - Sunday, * - 1 - Monday, * - 2 - Tuesday, * - 3 - Wednesday, * - 4 - Thursday, * - 5 - Friday, * - 6 - Saturday, */ firstDayOfWeek: { type: Number, default: 1, }, /** * Set date format. * We use date-fns to format dates. Visit https://date-fns.org/v2.28.0/docs/format to check accepted patterns. */ format: String, /** * Toggle visibility of footer element or set the content of footer. */ footer: Boolean, /** * The id attribute for the input elements. It can be a single string for both the start and end dates. If a single string is used, the postfix "-start-date" and "-end-date" will be automatically added to make the IDs unique. Alternatively, you can use an array of two strings for start and end dates separately. * * **[Deprecated since v5.3.0]** If the property is a type of string, the name attributes for input elements are generated based on this property until you define name prop ex.: * - \{id\}-start-date * - \{id\}-end-date */ id: { type: [String, Array], }, /** * Toggle visibility or set the content of the input indicator. */ indicator: { type: Boolean, default: true, }, /** * Custom function to format the selected date into a string according to a custom format. * * @since 5.0.0 */ inputDateFormat: Function, /** * Custom function to parse the input value into a valid Date object. * * @since 5.0.0 */ inputDateParse: Function, /** * Defines the delay (in milliseconds) for the input field's onChange event. * * @since 5.0.0 */ inputOnChangeDelay: { type: Number, default: 750, }, /** * Toggle the readonly state for the component. */ inputReadOnly: Boolean, /** * Set component validation state to invalid. * * @since 4.6.0 */ invalid: { type: Boolean, default: undefined, }, /** * Add a caption for a component. * * @since 4.6.0 */ label: String, /** * Sets the default locale for components. If not set, it is inherited from the navigator.language. */ locale: { type: String, default: 'default', }, /** * Max selectable date. */ maxDate: [Date, String], /** * Min selectable date. */ minDate: [Date, String], /** * The name attribute for the input elements. It can be a single string for both the start and end dates. If a single string is used, the postfix "-start-date" and "-end-date" will be automatically added to make the names unique. Alternatively, you can use an array of two strings for start and end dates separately. * * Example for single string: 'date-input' * Result: 'date-input-start-date', 'date-input-end-date' * * Example for array: ['start-date-input', 'end-date-input'] * Result: 'start-date-input', 'end-date-input' * * @since 5.3.0 */ name: { type: [String, Array], }, /** * Show arrows navigation. */ navigation: { type: Boolean, default: true, }, /** * Reorder year-month navigation, and render year first. * * @since 4.6.0 */ navYearFirst: Boolean, /** * Specifies a short hint that is visible in the input. */ placeholder: { type: [String, Array], default: () => ['Start date', 'End date'], }, /** * @ignore */ range: { type: Boolean, default: true, }, /** * Predefined date ranges the user can select from. */ ranges: Object, /** * When present, it specifies that must be filled out before submitting the form. * * @since 4.9.0 */ required: Boolean, /** * Toggle select mode between start and end date. */ selectEndDate: Boolean, /** * Set whether days in adjacent months shown before or after the current month are selectable. This only applies if the `showAdjacementDays` option is set to true. * * @since 4.9.0 */ selectAdjacementDays: Boolean, /** * Specify the type of date selection as day, week, month, or year. * * @since 5.0.0 */ selectionType: { type: String, default: 'day', validator: (value) => ['day', 'week', 'month', 'year'].includes(value), }, /** * Set whether to display dates in adjacent months (non-selectable) at the start and end of the current month. * * @since 4.9.0 */ showAdjacementDays: { type: Boolean, default: true, }, /** * Set whether to display week numbers in the calendar. * * @since 5.0.0 */ showWeekNumber: Boolean, /** * Default icon or character character that separates two dates. */ separator: { type: Boolean, default: true, }, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, required: false, validator: (value) => { return ['sm', 'lg'].includes(value); }, }, /** * Initial selected date. */ startDate: [Date, String], /** * Add helper text to the component. * * @since 4.6.0 */ text: String, /** * Provide an additional time selection by adding select boxes to choose times. */ timepicker: Boolean, /** * Toggle visibility or set the content of today button. */ todayButton: { type: [Boolean, String], default: 'Today', }, /** * Sets the color context of the today button to one of CoreUI’s themed colors. * * @values 'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'dark', 'light' */ todayButtonColor: { ...props.Color, default: 'primary', }, /** * Size the today button small or large. * * @values 'sm', 'lg' */ todayButtonSize: { type: String, default: 'sm', validator: (value) => { return ['sm', 'lg'].includes(value); }, }, /** * Set the today button variant to an outlined button or a ghost button. * * @values 'ghost', 'outline' */ todayButtonVariant: { type: String, validator: (value) => { return ['ghost', 'outline'].includes(value); }, }, /** * Display validation feedback in a styled tooltip. * * @since 4.6.0 */ tooltipFeedback: Boolean, /** * Set component validation state to valid. * * @since 4.6.0 */ valid: { type: Boolean, default: undefined, }, /** * Toggle the visibility of the component. */ visible: Boolean, /** * Set length or format of day name. * * @type number | 'long' | 'narrow' | 'short' */ weekdayFormat: { type: [Function, Number, String], default: 2, validator: (value) => { if (typeof value === 'string') { return ['long', 'narrow', 'short'].includes(value); } if (typeof value === 'number') { return true; } if (typeof value === 'function') { return true; } return false; }, }, /** * Label displayed over week numbers in the calendar. * * @since 5.0.0 */ weekNumbersLabel: String, }, emits: [ /** * Callback fired when the end date changed. * * @property {Date} date - date object * @property {string} formatedDate - formated date */ 'end-date-change', /** * Callback fired when the component requests to be hidden. */ 'hide', /** * Callback fired when the component requests to be shown. */ 'show', /** * Callback fired when the start date changed. * * @property {Date} date - date object * @property {string} formatedDate - formated date */ 'start-date-change', /** * Callback fired when the start date changed. * * @property {Date | null} date - date object * @since 4.7.0 */ 'update:start-date', /** * Callback fired when the end date changed. * * @property {Date | null} date - date object * @since 4.7.0 */ 'update:end-date', ], setup(props, { slots, attrs, emit }) { const inputEndRef = vue.ref(); const inputStartRef = vue.ref(); const formRef = vue.ref(); const calendarDate = vue.ref(props.calendarDate ?? null); const endDate = vue.ref(props.endDate ?? null); const maxDate = vue.ref(props.maxDate ?? null); const minDate = vue.ref(props.minDate ?? null); const startDate = vue.ref(props.startDate ?? null); const visible = vue.ref(props.visible); const initialStartDate = vue.ref(props.startDate ?? null); const initialEndDate = vue.ref(props.endDate ?? null); const inputStartHoverValue = vue.ref(null); const inputEndHoverValue = vue.ref(null); const isValid = vue.ref(props.valid ?? (props.invalid === true ? false : undefined)); const isMobile = vue.ref(false); const selectEndDate = vue.ref(false); vue.onMounted(() => { isMobile.value = window.innerWidth < 768; }); vue.watch(() => [props.valid, props.invalid], () => { isValid.value = props.valid ?? (props.invalid === true ? false : undefined); }); vue.watch(() => props.startDate, () => { calendarDate.value = props.startDate ? props.startDate : null; startDate.value = props.startDate ? props.startDate : null; }); vue.watch(() => props.endDate, () => { calendarDate.value = props.endDate ? props.endDate : null; endDate.value = props.endDate ? props.endDate : null; }); vue.watch(() => props.maxDate, () => { maxDate.value = props.maxDate ? props.maxDate : null; }); vue.watch(() => props.minDate, () => { minDate.value = props.minDate ? props.minDate : null; }); vue.watch(inputStartRef, () => { if (inputStartRef.value && inputStartRef.value.form) { formRef.value = inputStartRef.value.form; } }); vue.watch([formRef, startDate, endDate], () => { if (formRef.value) { formRef.value.addEventListener('submit', (event) => { setTimeout(() => handleFormValidation(event.target)); }); handleFormValidation(formRef.value); } }); const formatDate = (date) => { if (props.selectionType !== 'day') { return date; } const _date = new Date(date); return props.inputDateFormat ? props.inputDateFormat(_date) : props.timepicker ? _date.toLocaleString(props.locale) : _date.toLocaleDateString(props.locale); }; const setInputValue = (date) => { if (date) { return formatDate(date); } return ''; }; const handleDateHover = (date) => { if (selectEndDate.value) { inputEndHoverValue.value = date; return; } inputStartHoverValue.value = date; }; const handleFormValidation = (form) => { if (!form.classList.contains('was-validated')) { return; } if ((props.range && startDate.value && endDate.value) || (!props.range && startDate.value)) { isValid.value = true; return; } isValid.value = false; }; const handleStartDateChange = (date) => { startDate.value = date; inputStartHoverValue.value = null; if (props.range) { selectEndDate.value = true; } emit('start-date-change', date, date ? formatDate(date) : undefined); emit('update:start-date', date); if (props.timepicker || props.footer) { return; } if (props.closeOnSelect && !props.range) { visible.value = false; } }; const handleEndDateChange = (date) => { endDate.value = date; inputEndHoverValue.value = null; if (props.range) { selectEndDate.value = false; } emit('end-date-change', date, date ? formatDate(date) : undefined); emit('update:end-date', date); if (props.timepicker || props.footer) { return; } if (props.closeOnSelect && startDate.value !== null) { visible.value = false; } }; const handleClear = (event) => { event.stopPropagation(); startDate.value = null; endDate.value = null; inputStartHoverValue.value = null; inputEndHoverValue.value = null; emit('start-date-change', null); emit('end-date-change', null); emit('update:start-date', null); emit('update:end-date', null); }; const handleOnChange = (value, input) => { const date = props.inputDateParse ? props.inputDateParse(value) : utils.getLocalDateFromString(value, props.locale, props.timepicker); if (date instanceof Date && date.getTime()) { calendarDate.value = date; if (input === 'start') { startDate.value = date; } else { endDate.value = date; } } }; const InputGroup = () => vue.h('div', { class: 'date-picker-input-group', }, [ vue.h('input', { autocomplete: 'off', class: [ 'date-picker-input', { hover: inputStartHoverValue.value, }, ], disabled: props.disabled, ...(props.id && { id: utils.getInputIdOrName(props.id, props.range, 'start') }), ...(props.name && { name: utils.getInputIdOrName(props.name, props.range, 'start') }), ...(props.id && !Array.isArray(props.id) && !props.name && { name: props.range ? `${props.id}-start-date` : `${props.id}-date` }), // TODO: remove in v6 onClick: () => { selectEndDate.value = false; }, onChange: (event) => handleOnChange(event.target.value, 'start'), onInput: (event) => useDebouncedCallback.useDebouncedCallback(() => handleOnChange(event.target.value, 'start'), props.inputOnChangeDelay), placeholder: Array.isArray(props.placeholder) ? props.placeholder[0] : props.placeholder, readonly: props.inputReadOnly || typeof props.format === 'string', required: props.required, ref: inputStartRef, value: inputStartHoverValue.value ? setInputValue(inputStartHoverValue.value) : setInputValue(startDate.value), }), props.range && props.separator !== false && vue.h('div', { class: 'date-picker-separator' }), props.range && vue.h('input', { autocomplete: 'off', class: [ 'date-picker-input', { hover: inputEndHoverValue.value, }, ], disabled: props.disabled, ...(props.id && { id: utils.getInputIdOrName(props.id, props.range, 'end') }), ...(props.name && { name: utils.getInputIdOrName(props.name, props.range, 'end') }), ...(props.id && !Array.isArray(props.id) && !props.name && { name: `${props.id}-end-date` }), // TODO: remove in v6 onClick: () => { selectEndDate.value = true; }, onChange: (event) => handleOnChange(event.target.value, 'end'), onInput: (event) => useDebouncedCallback.useDebouncedCallback(() => handleOnChange(event.target.value, 'end'), props.inputOnChangeDelay), placeholder: props.placeholder[1], readonly: props.inputReadOnly || typeof props.format === 'string', required: props.required, ref: inputEndRef, value: inputEndHoverValue.value ? setInputValue(inputEndHoverValue.value) : setInputValue(endDate.value), }), props.indicator && vue.h('div', { class: 'date-picker-indicator' }), props.cleaner && (startDate.value || endDate.value) && vue.h('div', { class: 'date-picker-cleaner', onClick: (event) => handleClear(event), }), ]); return () => vue.h(CFormControlWrapper.CFormControlWrapper, { ...(typeof attrs['aria-describedby'] === 'string' && { describedby: attrs['aria-describedby'], }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, ...(props.id && !Array.isArray(props.id) && { id: props.id }), invalid: isValid.value === false ? true : false, label: props.label, text: props.text, tooltipFeedback: props.tooltipFeedback, valid: isValid.value, }, { default: () => vue.h(CPicker.CPicker, { class: [ 'date-picker', { [`date-picker-${props.size}`]: props.size, disabled: props.disabled, 'is-invalid': isValid.value === false ? true : false, 'is-valid': isValid.value, }, ], disabled: props.disabled, dropdownClassNames: 'date-picker-dropdown', footer: props.footer || props.timepicker, onHide: () => { visible.value = false; emit('hide'); }, onShow: () => { if (startDate.value) { initialStartDate.value = new Date(startDate.value); } if (endDate.value) { initialEndDate.value = new Date(endDate.value); } visible.value = true; emit('show'); }, visible: visible.value, }, { toggler: () => InputGroup(), footer: () => vue.h('div', { class: 'date-picker-footer' }, [ props.todayButton && vue.h(CButton.CButton, { class: 'me-auto', color: props.todayButtonColor, size: props.todayButtonSize, variant: props.todayButtonVariant, onClick: () => { const date = new Date(); startDate.value = date; if (props.range) { endDate.value = date; } calendarDate.value = date; }, }, () => props.todayButton), props.cancelButton && vue.h(CButton.CButton, { color: props.cancelButtonColor, onClick: () => { startDate.value = initialStartDate.value; if (props.range) { endDate.value = initialEndDate.value; } visible.value = false; }, size: props.cancelButtonSize, variant: props.cancelButtonVariant, }, () => props.cancelButton), props.confirmButton && vue.h(CButton.CButton, { color: props.confirmButtonColor, onClick: () => { visible.value = false; }, size: props.confirmButtonSize, variant: props.confirmButtonVariant, }, () => props.confirmButton), ]), default: () => vue.h('div', { class: 'date-picker-body', }, [ props.ranges && vue.h('div', { class: 'date-picker-ranges' }, Object.keys(props.ranges).map((key) => vue.h(CButton.CButton, { color: 'secondary', onClick: () => { if (props.ranges) { startDate.value = props.ranges[key][0]; endDate.value = props.ranges[key][1]; } }, variant: 'ghost', }, () => key))), vue.h('div', { class: 'date-picker-calendars' }, vue.h(CCalendar.CCalendar, { ariaNavNextMonthLabel: props.ariaNavNextMonthLabel, ariaNavNextYearLabel: props.ariaNavNextYearLabel, ariaNavPrevMonthLabel: props.ariaNavPrevMonthLabel, ariaNavPrevYearLabel: props.ariaNavPrevYearLabel, calendarDate: calendarDate.value, calendars: isMobile.value ? 1 : props.calendars, class: 'date-picker-calendars', dayFormat: props.dayFormat, disabledDates: props.disabledDates, endDate: endDate.value, firstDayOfWeek: props.firstDayOfWeek, locale: props.locale, maxDate: maxDate.value, minDate: minDate.value, navigation: props.navigation, navYearFirst: props.navYearFirst, range: props.range, selectAdjacementDays: props.selectAdjacementDays, selectEndDate: selectEndDate.value, selectionType: props.selectionType, showAdjacementDays: props.showAdjacementDays, showWeekNumber: props.showWeekNumber, startDate: startDate.value, weekdayFormat: props.weekdayFormat, weekNumbersLabel: props.weekNumbersLabel, onDateHover: (date) => handleDateHover(date), onCalendarDateChange: (date) => { calendarDate.value = date; }, onStartDateChange: (date) => handleStartDateChange(date), onEndDateChange: (date) => handleEndDateChange(date), onSelectEndChange: (value) => { selectEndDate.value = value; }, }, { /** * @slot Location for next icon. */ ...(slots.navNextIcon && { navNextIcon: () => slots.navNextIcon && slots.navNextIcon(), }), /** * @slot Location for next double icon. */ ...(slots.navNextDoubleIcon && { navNextDoubleIcon: () => slots.navNextDoubleIcon && slots.navNextDoubleIcon(), }), /** * @slot Location for previous icon. */ ...(slots.navPrevIcon && { navPrevIcon: () => slots.navPrevIcon && slots.navPrevIcon(), }), /** * @slot Location for double previous icon. */ ...(slots.navPrevDoubleIcon && { navPrevDoubleIcon: () => slots.navPrevDoubleIcon && slots.navPrevDoubleIcon(), }), })), props.timepicker && vue.h('div', { class: 'date-picker-timepickers' }, isMobile.value || (props.range && props.calendars === 1) ? [ vue.h(CTimePicker.CTimePicker, { container: 'inline', disabled: startDate.value === null ? true : false, locale: props.locale, onChange: (_, __, date) => handleStartDateChange(date), ...(startDate.value && { time: new Date(startDate.value) }), variant: 'select', }), vue.h(CTimePicker.CTimePicker, { container: 'inline', disabled: endDate.value === null ? true : false, locale: props.locale, onChange: (_, __, date) => handleEndDateChange(date), ...(endDate.value && { time: new Date(endDate.value) }), variant: 'select', }), ] : [...Array(props.calendars)].map((_, index) => vue.h(CTimePicker.CTimePicker, { container: 'inline', disabled: index === 0 ? startDate.value === null ? true : false : endDate.value === null ? true : false, locale: props.locale, onChange: (_, __, date) => index === 0 ? handleStartDateChange(date) : handleEndDateChange(date), // @ts-expect-error time: index === 0 ? startDate.value && new Date(startDate.value) : endDate.value && new Date(endDate.value), variant: 'select', }))), ]), }), }); }, }); exports.CDateRangePicker = CDateRangePicker; //# sourceMappingURL=CDateRangePicker.js.map