UNPKG

ns-vue-flatpickr

Version:

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

257 lines (229 loc) 9.68 kB
import { PropType, defineComponent, h, nextTick } from "vue"; import flatpickr from 'ns-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 }, changeDateOnly: { type: Boolean, default: false } }, 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") { if (this.changeDateOnly && selectedDates[0]) { const newDate = selectedDates[0]; const oldDate = this.modelValue instanceof Date ? this.modelValue : null; if (oldDate) { this.$emit('update:modelValue', new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate(), oldDate.getHours(), oldDate.getMinutes(), oldDate.getSeconds(), oldDate.getMilliseconds())); } else { this.$emit('update:modelValue', selectedDates[0] || null); } } else { this.$emit('update:modelValue', selectedDates[0] || null); } } else if (this.mode == "time") { const newDate = this.modelValue instanceof Date ? this.modelValue : new Date(); const newTime = selectedDates[0] || null; if (newTime) { this.$emit('update:modelValue', new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate(), newTime.getHours(), newTime.getMinutes(), newTime.getSeconds(), newTime.getMilliseconds())); } else { this.$emit('update:modelValue', 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 }); } })