UNPKG

@komgrip/vue-flatpickr

Version:

A lightweight and customizable date/time picker component for Vue.js applications

253 lines (227 loc) 8.46 kB
import { PropType, defineComponent, h, nextTick } from 'vue' import flatpickr from '@komgrip/flatpickr' import { excludedEvents, includedEvents, eventMappings } from './events' import { arrayify, nullify } from './util' // Keep a copy of all events for later use const allEvents = [...includedEvents, ...excludedEvents] // Passing these properties in `fp.set()` method will cause flatpickr to trigger some callbacks const configCallbacks = [ 'locale', 'showMonths', ] as (keyof flatpickr.Options.Options)[] export default defineComponent({ name: 'Flatpickr', render() { if (this.options.wrap || this.wrap) { if (this.$slots.default && this.$slots.default?.().length == 1) { return this.$slots.default?.()[0] } if (!this.$slots.default || this.$slots.default?.().length == 0) { return h( 'div', {}, h('input', { type: 'text', 'data-input': true, disabled: this.disabled, onInput: this.onInput, }) ) } return h('div', {}, this.$slots.default?.()) } else { return h('input', { type: 'text', 'data-input': true, disabled: this.disabled, onInput: this.onInput, }) } }, emits: ['blur', 'update:modelValue', ...Object.values(eventMappings)], props: { options: { type: Object as PropType<flatpickr.Options.Options>, default: () => ({ defaultDate: null, wrap: false, }), }, modelValue: { type: [String, Number, Date, Array, null] as PropType< | flatpickr.Options.DateOption | flatpickr.Options.DateOption[] | null >, required: true, }, disabled: { type: Boolean, default: false, }, wrap: { type: Boolean, default: false, }, mode: { type: String as PropType<'single' | 'multiple' | 'range' | 'time'>, default: 'single', }, events: { type: Array as PropType<flatpickr.Options.HookKey[]>, default: () => includedEvents, }, }, data() { return { fp: null as flatpickr.Instance | null, //todo make it non-reactive } }, methods: { prepareConfig(): flatpickr.Options.Options { // Don't mutate original object on parent component let safeConfig = { ...this.options } this.events.forEach((hook) => { // Respect global callbacks registered via setDefault() method let globalCallbacks = flatpickr.defaultConfig[hook] || [] // Inject our own method along with user's callbacks let localCallback: flatpickr.Options.Hook = (...args) => { this.$emit(eventMappings[hook], ...args) } // Overwrite with merged array safeConfig[hook] = arrayify(safeConfig[hook] || []).concat( globalCallbacks, localCallback ) }) const onCloseCb = this.onClose.bind(this) as flatpickr.Options.Hook const onChangeCb = this.onChange.bind( this ) as flatpickr.Options.Hook safeConfig['onClose'] = arrayify( safeConfig['onClose'] || [] ).concat(onCloseCb) as flatpickr.Options.Hook[] safeConfig['onChange'] = arrayify( safeConfig['onChange'] || [] ).concat(onChangeCb) as flatpickr.Options.Hook[] // Set initial date without emitting any event safeConfig.defaultDate = this.modelValue || safeConfig.defaultDate if (this.wrap) safeConfig.wrap = this.wrap safeConfig.mode = this.mode return safeConfig }, /** * Get the HTML node where flatpickr to be attached * Bind on parent element if wrap is true */ getElem(): HTMLInputElement { return this.$el //return this.options.wrap ? (this.$el || this.$el.parentNode) : this.$el; }, /** * Watch for value changed by date-picker itself and notify parent component */ onInput(_event: InputEvent) { //const input = event.target as HTMLInputElement; // Let's wait for DOM to be updated nextTick().then(() => { //this.$emit('update:modelValue', nullify(input.value)); }) }, onChange( selectedDates: Date[], _dateStr: string, _instance: flatpickr.Instance ) { if (this.mode == 'single' || this.mode == 'time') { this.$emit('update:modelValue', selectedDates[0] || null) } else { this.$emit('update:modelValue', selectedDates) } }, fpInput(): HTMLInputElement { return this.fp!.altInput || this.fp!.input }, /** * Blur event is required by many validation libraries */ onBlur(event: Event): void { this.$emit( 'blur', nullify((event.target as HTMLInputElement).value) ) }, /** * Flatpickr does not emit input event in some cases */ onClose(_selectedDates: Date[], _dateStr: string) { //this.$emit('update:modelValue', selectedDates); }, /** * Watch for the disabled property and sets the value to the real input. */ watchDisabled(newState: boolean) { if (newState) { this.fpInput().setAttribute('disabled', '') } else { this.fpInput().removeAttribute('disabled') } }, }, watch: { /** * Watch for any config changes and redraw date-picker */ options: { deep: true, handler(newConfig) { if (!this.fp) return let safeConfig: flatpickr.Options.Options = { ...newConfig } // Workaround: Don't pass hooks to configs again otherwise // previously registered hooks will stop working // Notice: we are looping through all events // This also means that new callbacks can not be passed once component has been initialized allEvents.forEach((hook) => { delete safeConfig[hook] }) this.fp!.set(safeConfig) // Workaround: Allow to change locale dynamically configCallbacks.forEach((name) => { if (typeof safeConfig[name] !== 'undefined') { this.fp!.set(name as any, safeConfig[name]) } }) }, }, /** * Watch for changes from parent component and update DOM */ modelValue(newValue: any) { // Prevent updates if v-model value is same as input's current value if (!this.$el || newValue === nullify(this.$el.value)) return // Notify flatpickr instance that there is a change in value this.fp?.setDate(newValue, false) }, }, beforeUnmount() { /* istanbul ignore else */ if (!this.fp) return this.fpInput().removeEventListener('blur', this.onBlur) this.fp.destroy() this.fp = null }, mounted() { // Return early if flatpickr is already loaded /* istanbul ignore if */ if (this.fp) return // Init flatpickr this.fp = flatpickr(this.getElem(), this.prepareConfig()) // Attach blur event this.fpInput().addEventListener('blur', this.onBlur) // Immediate watch will fail before fp is set, // so we need to start watching after mount this.$watch('disabled', this.watchDisabled, { immediate: true, }) }, })