@coreui/vue-pro
Version:
UI Components Library for Vue.js
815 lines (783 loc) • 23.4 kB
text/typescript
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 }