UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

815 lines (783 loc) 23.4 kB
import { defineComponent, h, PropType, ref, watch } from 'vue' import { CButton } from '../button' import { CFormControlWrapper } from '../form/CFormControlWrapper' import { CPicker } from '../picker' import { CTimePickerRollCol } from './CTimePickerRollCol' import { convert12hTo24h, convertTimeToDate, getAmPm, getLocalizedTimePartials, getSelectedHour, getSelectedMinutes, getSelectedSeconds, isValidTime, } from './utils' import type { LocalizedTimePartials } from './types' import { useDebouncedCallback } from '../../composables' import { Color } from '../props' const CTimePicker = defineComponent({ name: 'CTimePicker', props: { /** * Accessible label for the hours selection element. */ ariaSelectHoursLabel: String, /** * Accessible label for the AM/PM selection element. * * @since 5.16.0 */ ariaSelectMeridiemLabel: String, /** * Accessible label for the minutes selection element. * * @since 5.16.0 */ ariaSelectMinutesLabel: String, /** * Accessible label for the seconds selection element. * * @since 5.16.0 */ ariaSelectSecondsLabel: String, /** * Set if the component should use the 12/24 hour format. If `true` forces the interface to a 12-hour format. If `false` forces the interface into a 24-hour format. If `auto` the current locale will determine the 12 or 24-hour interface by default locales. * * @since 4.7.0 */ ampm: { type: [Boolean, String] as PropType<boolean | 'auto'>, default: 'auto', validator: (value: boolean | 'auto') => { if (typeof value == 'string') { return ['auto'].includes(value) } if (typeof value == 'boolean') { return true } return false }, }, /** * 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: { ...Color, default: 'primary', }, /** * Size the cancel button small or large. * * @values 'sm', 'lg' */ cancelButtonSize: { type: String, default: 'sm', validator: (value: string) => { 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: string) => { return ['ghost', 'outline'].includes(value) }, }, /** * Toggle visibility of the cleaner button. */ cleaner: { 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: { ...Color, default: 'primary', }, /** * Size the confirm button small or large. * * @values 'sm', 'lg' */ confirmButtonSize: { type: String, default: 'sm', validator: (value: string) => { 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: string) => { return ['ghost', 'outline'].includes(value) }, }, /** * Set container type for the component. */ container: { type: String as PropType<'dropdown' | 'inline'>, default: 'dropdown', }, /** * Toggle the disabled state for the component. */ disabled: Boolean, /** * 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, /** * Specify a list of available hours using an array, or customize the filtering of hours through a function. * * @since 5.0.0 */ hours: { type: [Array, Function] as PropType<number[] | ((hour: number) => number[])>, }, /** * The id global attribute defines an identifier (ID) that must be unique in the whole document. */ id: String, /** * Toggle visibility or set the content of the input indicator. */ indicator: { type: Boolean, default: true, }, /** * 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', }, /** * Toggle the display of minutes, specify a list of available minutes using an array, or customize the filtering of minutes through a function. * * @since 5.0.0 */ minutes: { type: [Array, Boolean, Function] as PropType< number[] | ((hour: number) => number[]) | boolean >, default: true, }, /** * Set the name attribute for the input element. * * @since 5.3.0 */ name: String, /** * Specifies a short hint that is visible in the input. */ placeholder: { type: String, default: 'Select time', }, /** * When present, it specifies that must be filled out before submitting the form. * * @since 4.9.0 */ required: Boolean, /** * Toggle the display of seconds, specify a list of available seconds using an array, or customize the filtering of seconds through a function. * * @since 4.7.0 */ seconds: { type: [Array, Boolean, Function] as PropType< number[] | ((hour: number) => number[]) | boolean >, default: true, }, /** * Size the component small or large. * * @values 'sm', 'lg' */ size: { type: String, default: undefined, validator: (value: string) => { return ['sm', 'lg'].includes(value) }, }, /** * Generates dropdown menu using Teleport. * * @since 5.8.0 */ teleport: { type: [Boolean], default: false, }, /** * Add helper text to the component. * * @since 4.6.0 */ text: String, /** * Initial selected time. */ time: [Date, String], /** * 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, }, /** * Set the time picker variant to a roll or select. * * @values 'roll', 'select' */ variant: { type: String, default: 'roll', validator: (value: string) => { return ['roll', 'select'].includes(value) }, }, /** * Toggle the visibility of the component. */ visible: Boolean, }, emits: [ /** * Callback fired when the time changed. */ '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 time changed. * * @since 4.7.0 */ 'update:time', ], setup(props, { emit, attrs, slots }) { const formRef = ref() const inputRef = ref() const columnRefs = ref<(HTMLElement | null)[]>([]) const date = ref<Date | null>(convertTimeToDate(props.time)) const ampm = ref<'am' | 'pm' | null>( date.value ? getAmPm(new Date(date.value), props.locale) : null ) const initialDate = ref<Date | null>(null) const visible = ref(props.visible) const localizedTimePartials = ref<LocalizedTimePartials>({ listOfHours: [], listOfMinutes: [], listOfSeconds: [], hour12: false, }) const isValid = ref<boolean | undefined>( props.valid ?? (props.invalid === true ? false : undefined) ) const setColumnRef = (index: number, el: HTMLElement | null) => { columnRefs.value[index] = el } watch( () => props.time, () => { date.value = convertTimeToDate(props.time) } ) watch( () => [props.valid, props.invalid], () => { isValid.value = props.valid ?? (props.invalid === true ? false : undefined) } ) watch( date, () => { localizedTimePartials.value = getLocalizedTimePartials( props.locale, props.ampm, props.hours, props.minutes, props.seconds ) if (date.value) { ampm.value = getAmPm(new Date(date.value), props.locale) } }, { immediate: true } ) watch(inputRef, () => { if (inputRef.value && inputRef.value.form) { formRef.value = inputRef.value.form } }) watch([formRef, date], () => { if (formRef.value) { formRef.value.addEventListener('submit', (event: Event) => { setTimeout(() => handleFormValidation(event.target as HTMLFormElement)) }) handleFormValidation(formRef.value) } }) const handleClear = (event: Event) => { event.stopPropagation() date.value = null emit('change', null) emit('update:time', null) } const handleFormValidation = (form: HTMLFormElement) => { if (!form.classList.contains('was-validated')) { return } if (date.value) { isValid.value = true return } isValid.value = false } const handleTimeChange = (set: 'hours' | 'minutes' | 'seconds' | 'meridiem', value: string) => { const _date = date.value || new Date('1970-01-01') if (set === 'meridiem') { const currentHours = _date.getHours() if (value === 'am') { ampm.value = 'am' // Convert PM hours (12-23) to AM hours (0-11) if (currentHours >= 12) { _date.setHours(currentHours - 12) } } if (value === 'pm') { ampm.value = 'pm' // Convert AM hours (0-11) to PM hours (12-23) if (currentHours < 12) { _date.setHours(currentHours + 12) } } } if (set === 'hours') { if (localizedTimePartials.value && localizedTimePartials.value.hour12) { _date.setHours(convert12hTo24h(ampm.value ?? 'am', Number.parseInt(value))) } else { _date.setHours(Number.parseInt(value)) } } if (set === 'minutes') { _date.setMinutes(Number.parseInt(value)) } if (set === 'seconds') { _date.setSeconds(Number.parseInt(value)) } date.value = new Date(_date) emit('change', _date.toTimeString(), _date.toLocaleTimeString(props.locale), _date) emit('update:time', _date.toLocaleTimeString(props.locale)) } const InputGroup = () => h('div', { class: 'time-picker-input-group' }, [ h('input', { autocomplete: 'off', class: 'time-picker-input', disabled: props.disabled, id: props.id, name: props.name, onInput: useDebouncedCallback((event: Event) => { if (isValidTime((event.target as HTMLInputElement).value)) { const _date = convertTimeToDate((event.target as HTMLInputElement).value) date.value = _date if (_date) { emit('change', _date.toTimeString(), _date.toLocaleTimeString(props.locale), _date) emit('update:time', _date.toLocaleTimeString(props.locale)) } else { emit('change', null) emit('update:time', null) } } else { emit('change', null) emit('update:time', null) } }, props.inputOnChangeDelay), placeholder: props.placeholder, readonly: props.inputReadOnly, ref: inputRef, required: props.required, value: date.value ? date.value.toLocaleTimeString(props.locale, { hour12: localizedTimePartials.value && localizedTimePartials.value.hour12, hour: 'numeric', ...(props.minutes && { minute: 'numeric' }), ...(props.seconds && { second: 'numeric' }), }) : '', }), props.indicator && h('div', { class: 'time-picker-indicator', ...(!props.disabled && { onClick: (event: Event) => { event.stopPropagation() visible.value = !visible.value }, onKeydown: (event: KeyboardEvent) => { if (event.key === 'Enter') { visible.value = !visible.value } }, tabIndex: 0, }), }), props.cleaner && date.value && h('div', { class: 'time-picker-cleaner', onClick: (event: Event) => handleClear(event), }), ]) const TimePickerSelect = () => [ h('span', { class: 'time-picker-inline-icon' }), h( 'select', { class: 'time-picker-inline-select', disabled: props.disabled, onChange: (event: Event) => handleTimeChange('hours', (event.target as HTMLSelectElement).value), ...(date.value && { value: getSelectedHour(date.value, props.locale) }), 'aria-label': props.ariaSelectHoursLabel, }, localizedTimePartials.value && localizedTimePartials.value.listOfHours.map((option) => h( 'option', { value: option.value.toString(), }, option.label ) ) ), props.minutes && ':', props.minutes && h( 'select', { class: 'time-picker-inline-select', disabled: props.disabled, onChange: (event: Event) => handleTimeChange('minutes', (event.target as HTMLSelectElement).value), ...(date.value && { value: getSelectedMinutes(date.value) }), 'aria-label': props.ariaSelectMinutesLabel, }, localizedTimePartials.value && localizedTimePartials.value.listOfMinutes?.map((option) => h( 'option', { value: option.value.toString(), }, option.label ) ) ), props.seconds && ':', props.seconds && h( 'select', { class: 'time-picker-inline-select', disabled: props.disabled, onChange: (event: Event) => handleTimeChange('seconds', (event.target as HTMLSelectElement).value), ...(date.value && { value: getSelectedSeconds(date.value) }), 'aria-label': props.ariaSelectSecondsLabel, }, localizedTimePartials.value && localizedTimePartials.value.listOfSeconds?.map((option) => h( 'option', { value: option.value.toString(), }, option.label ) ) ), localizedTimePartials.value && localizedTimePartials.value.hour12 && h( 'select', { class: 'time-picker-inline-select', disabled: props.disabled, onChange: (event: Event) => handleTimeChange('meridiem', (event.target as HTMLSelectElement).value), value: ampm.value, 'aria-label': props.ariaSelectMeridiemLabel, }, [ h( 'option', { value: 'am', }, 'AM' ), h( 'option', { value: 'pm', }, 'PM' ), ] ), ] const TimePickerRoll = () => [ h(CTimePickerRollCol, { ariaLabel: props.ariaSelectHoursLabel, columnIndex: 0, columnRefs: columnRefs.value, elements: localizedTimePartials.value && localizedTimePartials.value.listOfHours, onClick: (index: number) => handleTimeChange('hours', index.toString()), selected: getSelectedHour(date.value, props.locale, props.ampm), setColumnRef, }), props.minutes && h(CTimePickerRollCol, { ariaLabel: props.ariaSelectMinutesLabel, columnIndex: 1, columnRefs: columnRefs.value, elements: localizedTimePartials.value && localizedTimePartials.value.listOfMinutes, onClick: (index: number) => handleTimeChange('minutes', index.toString()), selected: getSelectedMinutes(date.value), setColumnRef, }), props.seconds && h(CTimePickerRollCol, { ariaLabel: props.ariaSelectSecondsLabel, columnIndex: props.minutes ? 2 : 1, columnRefs: columnRefs.value, elements: localizedTimePartials.value && localizedTimePartials.value.listOfSeconds, onClick: (index: number) => handleTimeChange('seconds', index.toString()), selected: getSelectedSeconds(date.value), setColumnRef, }), localizedTimePartials.value && localizedTimePartials.value.hour12 && h(CTimePickerRollCol, { ariaLabel: props.ariaSelectMeridiemLabel, columnIndex: (props.minutes ? 1 : 0) + (props.seconds ? 1 : 0) + 1, columnRefs: columnRefs.value, elements: [ { value: 'am', label: 'AM' }, { value: 'pm', label: 'PM' }, ], onClick: (value: string) => handleTimeChange('meridiem', value), selected: ampm.value, setColumnRef, }), ] return () => h( CFormControlWrapper, { ...(typeof attrs['aria-describedby'] === 'string' && { describedby: attrs['aria-describedby'], }), feedback: props.feedback, feedbackInvalid: props.feedbackInvalid, feedbackValid: props.feedbackValid, id: props.id, invalid: isValid.value === false ? true : false, label: props.label, text: props.text, tooltipFeedback: props.tooltipFeedback, valid: isValid.value, }, { default: () => h( CPicker, { class: [ 'time-picker', { [`time-picker-${props.size}`]: props.size, disabled: props.disabled, 'is-invalid': isValid.value === false ? true : false, 'is-valid': isValid.value, }, ], container: props.container, disabled: props.disabled, dropdownClassNames: 'time-picker-dropdown', footer: true, onHide: () => { visible.value = false emit('hide') }, onShow: () => { if (date.value) { initialDate.value = new Date(date.value) } visible.value = true emit('show') }, teleport: props.teleport, visible: visible.value, }, { ...(slots.cancelButton && { cancelButton: () => slots.cancelButton && slots.cancelButton(), }), ...(slots.confirmButton && { confirmButton: () => slots.confirmButton && slots.confirmButton(), }), toggler: () => InputGroup(), default: () => h( 'div', { class: [ 'time-picker-body', { ['time-picker-roll']: props.variant === 'roll', }, ], ...(props.variant !== 'select' && { role: 'group' }), }, props.variant === 'select' ? TimePickerSelect() : TimePickerRoll() ), footer: () => h('div', { class: 'time-picker-footer' }, [ props.cancelButton && h( CButton, { color: props.cancelButtonColor, onClick: () => { if (initialDate.value) { date.value = new Date(initialDate.value) } visible.value = false }, size: props.cancelButtonSize, variant: props.cancelButtonVariant, }, () => props.cancelButton ), props.confirmButton && h( CButton, { color: props.confirmButtonColor, onClick: () => { visible.value = false }, size: props.confirmButtonSize, variant: props.confirmButtonVariant, }, () => props.confirmButton ), ]), } ), } ) }, }) export { CTimePicker }