@ecwenze1/vue-datepicker
Version:
A clean datepicker made with VueJs
427 lines (405 loc) • 12.5 kB
JavaScript
import { clearAllBodyScrollLocks } from 'body-scroll-lock';
// Styles
import './VDPicker.scss';
// Mixins
import Localable from '../../mixins/localable';
import Mobile from '../../mixins/mobile';
// Directives
import ClickOutside from '../../directives/click-outside';
// Components
import VDMenu from '../VDMenu';
import VDPickerCustomInput from './VDPickerCustomInput/VDPickerCustomInput';
import VDPickerAgenda from './VDPickerAgenda/VDPickerAgenda';
// Constants
import { Z_INDEX_LIST, KEYCODES } from '../../constants';
// Date Helpers
import {
getDefaultInputFormat,
getDefaultHeaderFormat,
getDefaultOutputFormat,
initDate,
genFormattedInputDate,
transformDateForModel,
} from './utils/helpers';
import { generateRandomId, validateAttachTarget } from '../../utils/helpers';
import { deprecate, removed } from '../../utils/console';
import mixins from '../../utils/mixins';
export const defaultMenuProps = {
minWidth: '290px',
maxWidth: '315px',
};
const baseMixins = mixins(
Localable,
Mobile,
);
export default baseMixins.extend({
name: 'VDPicker',
provide () {
return {
VDPicker: this,
};
},
directives: { ClickOutside },
props: {
id: { type: String, default: undefined },
name: { type: String, default: 'datepicker' },
// Add input clear functionality
clearable: { type: Boolean, default: false },
// Validation Buttons
validate: { type: Boolean, default: false },
buttonValidate: { type: String, default: undefined },
buttonCancel: { type: String, default: undefined },
// type (date, month, quarter, year picker)
type: { type: String, default: 'date' },
// Range
range: { type: Boolean, default: false },
rangeInputText: { type: String, default: '%d ~ %d' },
rangeHeaderText: { type: String, default: undefined },
rangePresets: { type: Array, default: undefined },
// Current Value from v-model
value: { type: [String, Object, Number, Date] },
// Format
format: { type: String, default: undefined },
formatHeader: { type: String, default: undefined },
// Show/hide datepicker
visible: { type: Boolean, default: false },
// Sets the locale.
placeholder: { type: String, default: 'YYYY-MM-DD' },
// Applies specified color to the control
color: { type: String, default: '#4f88ff' },
// Applies custom class to datepicker content
contentClass: { type: String, default: '' },
// Allowed dates
allowedDates: { type: Function },
minDate: { type: [String, Number, Date] },
maxDate: { type: [String, Number, Date] },
// Range for year picker
visibleYearsNumber: { type: Number, default: 10 },
// Disabled all datepicker
disabled: { type: Boolean, default: false },
// Inline
inline: { type: Boolean, default: false },
// Set if header in agenda should be visible
noHeader: { type: Boolean, default: false },
// Allow to hide calendar icon
noCalendarIcon: { type: Boolean, default: false },
// Responsive bottom sheet
fullscreenMobile: { type: Boolean, default: false },
// tabindex
tabindex: { type: [String, Number], default: '0' },
// Right to Left
rtl: { type: Boolean, default: false },
// Highlight Range
highlightRange: { type: Object, default: undefined },
// --> Menu Props
// Allow to set origin
origin: { type: String, default: undefined },
// Allows the menu to overflow off the screen
allowOverflow: { type: Boolean, default: true },
// attach
attach: { validator: validateAttachTarget, default: false },
// Specificy a z-index for agenda & overlay
zIndex: { type: Number, default: Z_INDEX_LIST.datepicker },
},
data: () => ({
date: undefined,
highlightDate: undefined,
isMenuActive: false,
isBooted: false,
activator: undefined,
}),
computed: {
classes () {
return {
'vd-wrapper--inline': this.inline,
'vd-wrapper--disabled': this.disabled,
'vd-wrapper--rtl': this.rtl,
};
},
// use a computed to have a dynamicId for each instance
componentId () {
return this.id || `datepicker_${generateRandomId()}`;
},
// If format isnt specificed, select default format from type
inputFormat () {
if (!this.format) return getDefaultInputFormat(this.range ? 'range' : this.type);
return this.format;
},
headerFormat () {
if (!this.formatHeader) return getDefaultHeaderFormat(this.range ? 'range' : this.type);
return this.formatHeader;
},
outputFormat () {
return getDefaultOutputFormat(this.range ? 'range' : this.type);
},
textsFormat () {
const { buttonValidate, buttonCancel, rangeHeaderText } = this.currentLocale.lang;
return {
buttonValidate: this.buttonValidate || buttonValidate,
buttonCancel: this.buttonCancel || buttonCancel,
rangeHeaderText: this.rangeHeaderText || rangeHeaderText,
};
},
internalDate: {
get () {
return initDate(this.value, {
range: this.range,
locale: this.currentLocale,
type: this.type,
});
},
set (date) {
this.date = date;
},
},
internalHighlightDate: {
get () {
return initDate(this.highlightRange, {
range: true,
locale: this.currentLocale,
type: this.type,
});
},
set (date) {
this.highlightDate = date;
},
},
isDateDefined () {
const isDateDefined = !this.range && this.internalDate;
const isDateRangeDefined = this.range &&
this.internalDate &&
this.internalDate.start &&
this.internalDate.end;
return Boolean(isDateDefined) || Boolean(isDateRangeDefined);
},
formattedInputDate () {
return genFormattedInputDate({
inputFormat: this.inputFormat,
internalDate: this.internalDate,
isDateDefined: this.isDateDefined,
locale: this.currentLocale,
range: this.range,
rangeInputText: this.rangeInputText,
type: this.type,
});
},
isFullScreenMode () {
return this.fullscreenMobile && this.isMobile;
},
},
watch: {
visible: {
handler (isMenuActive) {
this.isMenuActive = isMenuActive;
},
immediate: true,
},
isFullScreenMode () {
if (!this.isMenuActive) return;
this.hideDatePicker();
setTimeout(() => {
this.showDatePicker();
}, 200);
},
},
created () {
/* istanbul ignore next */
['no-input'].forEach(prop => {
if (this.$attrs.hasOwnProperty(prop)) removed({ original: prop, vm: this });
});
/* istanbul ignore next */
if (this.$attrs.hasOwnProperty('fullscreen-breakpoint')) {
deprecate({ original: 'fullscreen-mobile', replacement: 'mobile-breakpoint', vm: this });
}
},
mounted () {
this.activator = this.$refs.activator;
},
beforeDestroy () {
this.hideDatePicker();
this.$emit('onDestroy');
},
methods: {
// ------------------------------
// Events
// ------------------------------
showDatePicker () {
if (this.disabled) return;
this.isMenuActive = true;
this.$emit('onOpen');
},
hideDatePicker () {
if (!this.isMenuActive) return;
this.isMenuActive = false;
this.isBooted = false;
clearAllBodyScrollLocks();
this.$emit('onClose');
},
changeDate (date) {
this.internalDate = date;
if (this.validate) return;
this.validateDate();
},
validateDate () {
// if there is no date selected, return;
if (!this.date) {
this.hideDatePicker();
return;
}
this.$emit('input', transformDateForModel(this.date, this.outputFormat, this.range));
this.$emit('onChange');
this.hideDatePicker();
},
onKeyDown (event) {
const keyCode = event.keyCode;
const menu = this.$refs.menu;
if (!menu) return;
// close menu on esc|tab
if ([
KEYCODES.esc,
KEYCODES.tab,
].includes(keyCode)) return this.hideDatePicker(event);
},
onClearDate () {
this.$emit('input', undefined);
this.$emit('onChange');
},
// ------------------------------
// Generate Template
// ------------------------------
genContent () {
if (this.inline) return [this.genAgenda()];
return [
this.$scopedSlots.activator ? this.genActivator() : this.genCustomInput(),
this.genMenuWithContent(),
];
},
genActivator () {
return this.$createElement('div', {
staticClass: 'vd-activator',
directives: [{
name: 'click-outside',
value: {
isActive: this.isMenuActive && !this.isFullScreenMode,
handler: this.hideDatePicker,
},
}],
on: {
click: this.showDatePicker,
keydown: this.onKeyDown,
},
ref: 'activator',
}, [
this.$scopedSlots.activator({
date: this.formattedInputDate,
}),
]);
},
genCustomInput () {
return this.$createElement(VDPickerCustomInput, {
props: {
clearable: this.clearable,
color: this.color,
date: this.formattedInputDate,
disabled: this.disabled,
id: this.componentId,
isDateDefined: this.isDateDefined,
isMenuActive: this.isMenuActive,
name: this.name,
noCalendarIcon: this.noCalendarIcon,
placeholder: this.placeholder,
tabindex: this.tabindex,
},
directives: [{
name: 'click-outside',
value: {
isActive: this.isMenuActive && !this.isFullScreenMode,
handler: this.hideDatePicker,
},
}],
nativeOn: {
click: this.showDatePicker,
},
on: {
keydown: this.onKeyDown,
clearDate: this.onClearDate,
},
ref: 'activator',
});
},
genMenuWithContent () {
const shouldShowBottomSheet = this.isFullScreenMode;
const menuProps = {
...defaultMenuProps,
value: this.isMenuActive,
origin: this.origin,
allowOverflow: this.allowOverflow,
attach: !shouldShowBottomSheet ? this.attach : false,
transition: shouldShowBottomSheet ? 'slide-in-out-transition' : 'scale-transition',
// Allow GMenu to act like a bottomSheet
// TODO create a GBottomSheet component
bottomSheet: shouldShowBottomSheet,
};
const activator = this.activator;
return this.$createElement(VDMenu, {
attrs: { role: 'menu' },
props: {
...menuProps,
activator,
},
on: {
transitionEnd: () => {
this.isBooted = true;
},
},
ref: 'menu',
}, [this.genAgenda()]);
},
genAgenda () {
return this.$createElement(VDPickerAgenda, {
props: {
allowedDates: this.allowedDates,
buttonCancel: this.textsFormat.buttonCancel,
buttonValidate: this.textsFormat.buttonValidate,
color: this.color,
date: this.internalDate,
fullscreen: this.isBooted && this.isMenuActive && this.isFullScreenMode,
headerFormat: this.headerFormat,
locale: this.currentLocale,
maxDate: this.maxDate,
minDate: this.minDate,
name: this.name,
noHeader: this.noHeader,
range: this.range,
rangeHeaderText: this.textsFormat.rangeHeaderText,
rangePresets: this.rangePresets,
highlightDate: this.internalHighlightDate,
rtl: this.rtl,
type: this.type,
validate: this.validate,
value: this.isMenuActive,
visibleYearsNumber: this.visibleYearsNumber,
},
on: {
selectDate: this.changeDate,
validateDate: this.validateDate,
close: this.hideDatePicker,
},
directives: [{
name: 'click-outside',
value: {
isActive: this.isBooted && this.isMenuActive,
handler: this.hideDatePicker,
},
}],
ref: 'agenda',
});
},
},
render (h) {
return h('div', {
staticClass: 'vd-wrapper',
class: this.classes,
}, this.genContent());
},
});