diff --git a/src/components/CvDatePicker/CvDatePicker.stories.js b/src/components/CvDatePicker/CvDatePicker.stories.js index 2b5a4fa25..e09ff7c6d 100644 --- a/src/components/CvDatePicker/CvDatePicker.stories.js +++ b/src/components/CvDatePicker/CvDatePicker.stories.js @@ -9,7 +9,7 @@ import { ref } from 'vue'; const initArgs = { dateLabel: 'Date label', - invalidText: 'Invalid value', + invalidMessage: '', }; export default { @@ -25,7 +25,7 @@ export default { }, argTypes: { dateLabel: { type: String, description: 'Date picker label' }, - invalidText: { + invalidMessage: { type: String, description: 'Date picker text on invalid value', }, @@ -58,7 +58,7 @@ Default.parameters = storyParametersObject( ); const now = new Date(); -const modelValue = ref(now.toLocaleDateString()); +const modelValue = ref(''); const templateVModel = `
@@ -67,12 +67,11 @@ const templateVModel = `
Sample interaction
- + +
`; -// - const TemplateVModel = args => { return { components: { CvDatePicker }, diff --git a/src/components/CvDatePicker/CvDatePicker.vue b/src/components/CvDatePicker/CvDatePicker.vue index eb5f1165d..9dce376a5 100644 --- a/src/components/CvDatePicker/CvDatePicker.vue +++ b/src/components/CvDatePicker/CvDatePicker.vue @@ -40,6 +40,8 @@ ref="date" type="text" data-date-picker-input + :data-invalid="isInvalid || null" + :disabled="disabled" :data-date-picker-input-from="getKind === 'range'" :id="`${cvId}-input-1`" :class="`${carbonPrefix}--date-picker__input`" @@ -53,14 +55,17 @@ v-if="['single', 'range'].includes(getKind)" :class="`${carbonPrefix}--date-picker__icon`" data-date-picker-icon - @click="console.log($event.target)" />
+ +
+ {{ invalidMessage }} +
+
+ +
@@ -115,6 +125,11 @@ import { Calendar16 } from '@carbon/icons-vue'; import CvWrapper from '../CvWrapper/CvWrapper'; import { props as propsCvTheme, useIsLight } from '../../use/cvTheme'; import flatpickr from 'flatpickr'; +import l10n from 'flatpickr/dist/l10n/index'; +// import carbonFlatpickrAppendToPlugin from './plugins/appendToPlugin'; +import carbonFlatpickrFixEventsPlugin from './plugins/fixEventsPlugin'; +import carbonFlatpickrRangePlugin from './plugins/rangePlugin'; +import carbonFlatpickrMonthSelectPlugin from './plugins/monthSelectPlugin'; const dateWrapper = ref(null); const date = ref(null); @@ -130,7 +145,8 @@ const props = defineProps({ dateLabel: { type: String, default: undefined }, dateEndLabel: { type: String, default: 'End date' }, invalid: { type: Boolean, default: false }, - invalidText: { type: String }, + disabled: { type: Boolean, default: false }, + invalidMessage: { type: String }, pattern: { type: String, default: '\\d{1,2}/\\d{1,2}/\\d{4}' }, placeholder: { type: String, default: 'mm/dd/yyyy' }, calOptions: { @@ -147,6 +163,7 @@ const props = defineProps({ default: 'simple', validator: val => ['short', 'simple', 'single', 'range'].includes(val), }, + value: [String, Object, Array, Date], ...propsCvId, ...propsCvTheme, }); @@ -165,7 +182,7 @@ const getKind = computed({ const getDateLabel = computed({ get() { - if (props.getKind === 'range' && !props.dateLabel) { + if (props.kind === 'range' && !props.dateLabel) { return 'Start date'; } @@ -179,47 +196,133 @@ const getDateLabel = computed({ const getStartDate = computed({ get() { - if (props.modelValue) { - return props.modelValue?.startDate || props.modelValue; - } + return ( + props.modelValue?.startDate || + props.modelValue || + props.value?.startDate || + props.value + ); }, }); const getEndDate = computed({ get() { - if (props.modelValue) { - return props.modelValue?.endDate || props.modelValue; - } + return props.modelValue?.endDate || props.value?.endDate; }, }); +const isRange = computed(() => { + return props.kind === 'range'; +}); + +const isSingle = computed(() => { + return props.kind === 'single'; +}); + +const isInvalid = computed(() => { + return !!props.invalidMessage; +}); + const getFlatpickrOptions = () => { const options = { ...props.calOptions }; + options.plugins = [ + props.kind === 'range' + ? carbonFlatpickrRangePlugin({ + input: todate.value, + }) + : () => {}, + carbonFlatpickrMonthSelectPlugin({ + selectorFlatpickrMonthYearContainer: '.flatpickr-current-month', + selectorFlatpickrYearContainer: '.numInputWrapper', + selectorFlatpickrCurrentMonth: '.cur-month', + classFlatpickrCurrentMonth: 'cur-month', + }), + carbonFlatpickrFixEventsPlugin({ + inputFrom: date.value, + inputTo: todate.value, + }), + ]; + + options.nextArrow = ` + + + + + `; + + options.prevArrow = ` + + + + + `; + options.mode = props.kind; + // add events update based on parameters options.onChange = handleDatePick; + // options.onOpen = onOpen; + options.onReady = onCalReady; return options; }; +const onCalReady = (selectedDates, dateStr, instance) => { + const calendarContainer = instance.calendarContainer; + const options = { + classCalendarContainer: `${carbonPrefix}--date-picker__calendar`, + classMonth: `${carbonPrefix}--date-picker__month`, + classWeekdays: `${carbonPrefix}--date-picker__weekdays`, + classDays: `${carbonPrefix}--date-picker__days`, + classWeekday: `${carbonPrefix}--date-picker__weekday`, + classDay: `${carbonPrefix}--date-picker__day`, + classFocused: `${carbonPrefix}--focused`, + classVisuallyHidden: `${carbonPrefix}--visually-hidden`, + }; + + if (calendarContainer) { + calendarContainer.classList.add(options.classCalendarContainer); + calendarContainer + .querySelector('.flatpickr-month') + .classList.add(options.classMonth); + calendarContainer + .querySelector('.flatpickr-weekdays') + .classList.add(options.classWeekdays); + calendarContainer + .querySelector('.flatpickr-days') + .classList.add(options.classDays); + for (const item of calendarContainer.querySelectorAll( + '.flatpickr-weekday' + )) { + const currentItem = item; + currentItem.innerHTML = currentItem.innerHTML.replace(/\s+/g, ''); + currentItem.classList.add(options.classWeekday); + } + for (const item of calendarContainer.querySelectorAll('.flatpickr-day')) { + item.classList.add(options.classDay); + if (item.classList.contains('today') && selectedDates.length > 0) { + item.classList.add('no-border'); + } else if ( + item.classList.contains('today') && + selectedDates.length === 0 + ) { + item.classList.remove('no-border'); + } + } + } +}; + const initFlatpickr = () => { return flatpickr(date.value, getFlatpickrOptions()); }; let dateToString = val => { - if (typeof val === 'number') { - return this.cal.formatDate(val, this.calOptions.dateFormat); - } else { - return val || ''; - } + return calendar.formatDate(val, props.calOptions.dateFormat); }; const handleDatePick = (selectedDates, dateStr, instance) => { if (selectedDates.length === 1) { - const temp = calendar.formatDate( - selectedDates[0], - props.calOptions.dateFormat - ); + const temp = dateToString(selectedDates[0]); nextTick(() => { date.value.value = temp; @@ -227,14 +330,8 @@ const handleDatePick = (selectedDates, dateStr, instance) => { emit('update:modelValue', temp); } else { - const startDate = calendar.formatDate( - selectedDates[0], - props.calOptions.dateFormat - ); - const endDate = calendar.formatDate( - selectedDates[1], - props.calOptions.dateFormat - ); + const startDate = dateToString(selectedDates[0]); + const endDate = dateToString(selectedDates[1]); nextTick(() => { date.value.value = startDate; @@ -261,4 +358,9 @@ onMounted(() => { }); - + diff --git a/src/components/CvDatePicker/plugins/appendToPlugin.js b/src/components/CvDatePicker/plugins/appendToPlugin.js new file mode 100644 index 000000000..4257a15e1 --- /dev/null +++ b/src/components/CvDatePicker/plugins/appendToPlugin.js @@ -0,0 +1,64 @@ +/** + * @license + * + * Copyright IBM Corp. 2019 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * COPIED FROM carbon-components-react + */ + +/** + * @param {object} config Plugin configuration. + * @returns {Plugin} A Flatpickr plugin to put adjust the position of calendar dropdown. + */ +export default config => fp => { + /** + * Adjusts the floating meun position after Flatpicker sets it. + */ + const handlePreCalendarPosition = () => { + Promise.resolve().then(() => { + const { + calendarContainer, + config: fpConfig, + _positionElement: positionElement, + } = fp; + const { appendTo } = fpConfig; + const { left: containerLeft, top: containerTop } = + appendTo.getBoundingClientRect(); + const { left: refLeft, bottom: refBottom } = + positionElement.getBoundingClientRect(); + if ( + (appendTo !== appendTo.ownerDocument.body || + containerLeft !== 0 || + containerTop !== 0) && + appendTo.ownerDocument.defaultView + .getComputedStyle(appendTo) + .getPropertyValue('position') === 'static' + ) { + throw new Error( + 'Floating menu container must not have `position:static`.' + ); + } + // `2` for negative mergin on calendar dropdown + calendarContainer.style.top = `${refBottom - containerTop + 2}px`; + calendarContainer.style.left = `${refLeft - containerLeft}px`; + }); + }; + + /** + * Registers this Flatpickr plugin. + */ + const register = () => { + fp.loadedPlugins.push('carbonFlatpickrAppendToPlugin'); + }; + + return { + appendTo: config.appendTo, + onReady: register, + onPreCalendarPosition: handlePreCalendarPosition, + }; +}; diff --git a/src/components/CvDatePicker/plugins/fixEventsPlugin.js b/src/components/CvDatePicker/plugins/fixEventsPlugin.js new file mode 100644 index 000000000..21523e707 --- /dev/null +++ b/src/components/CvDatePicker/plugins/fixEventsPlugin.js @@ -0,0 +1,79 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* + * COPIED FROM carbon-components-react - amended to remove keyboard code dependency + */ + +/** + * @param {object} config Plugin configuration. + * @returns {Plugin} A Flatpickr plugin to fix Flatpickr's behavior of certain events. + */ +export default config => fp => { + /** + * Handles `keydown` event. + */ + const handleKeydown = event => { + const { inputFrom, inputTo } = config; + const { target } = event; + if (inputFrom === target || inputTo === target) { + if (event.key === 'Enter') { + // Makes sure the hitting enter key picks up pending values of both `` + // Workaround for: https://github.com/flatpickr/flatpickr/issues/1942 + fp.setDate( + [inputFrom.value, inputTo && inputTo.value], + true, + fp.config.dateFormat + ); + event.stopPropagation(); + } else if (event.key === 'Left' || event.key === 'Right') { + // Prevents Flatpickr code from canceling the event if left/right arrow keys are hit on ``, + // so user can move the keyboard cursor for editing dates + // Workaround for: https://github.com/flatpickr/flatpickr/issues/1943 + event.stopPropagation(); + } else if (event.key === 'Down') { + event.preventDefault(); + fp.open(); + } + } + }; + + /** + * Releases event listeners used in this Flatpickr plugin. + */ + const release = () => { + const { inputFrom, inputTo } = config; + if (inputTo) { + inputTo.removeEventListener('keydown', handleKeydown, true); + } + inputFrom.removeEventListener('keydown', handleKeydown, true); + }; + + /** + * Sets up event listeners used for this Flatpickr plugin. + */ + const init = () => { + release(); + const { inputFrom, inputTo } = config; + inputFrom.addEventListener('keydown', handleKeydown, true); + if (inputTo) { + inputTo.addEventListener('keydown', handleKeydown, true); + } + }; + + /** + * Registers this Flatpickr plugin. + */ + const register = () => { + fp.loadedPlugins.push('carbonFlatpickrFixEventsPlugin'); + }; + + return { + onReady: [register, init], + onDestroy: [release], + }; +}; diff --git a/src/components/CvDatePicker/plugins/monthSelectPlugin.js b/src/components/CvDatePicker/plugins/monthSelectPlugin.js new file mode 100644 index 000000000..7e4af465a --- /dev/null +++ b/src/components/CvDatePicker/plugins/monthSelectPlugin.js @@ -0,0 +1,86 @@ +/* + * COPIED FROM carbon-components-react + */ + +/** + * @param {number} monthNumber The month number. + * @param {boolean} shorthand `true` to use shorthand month. + * @param {Locale} locale The Flatpickr locale data. + * @returns {string} The month string. + */ +const monthToStr = (monthNumber, shorthand, locale) => + locale.months[shorthand ? 'shorthand' : 'longhand'][monthNumber]; + +/** + * @param {object} config Plugin configuration. + * @param {boolean} [config.shorthand] `true` to use shorthand month. + * @param {string} config.selectorFlatpickrMonthYearContainer The CSS selector for the container of month/year selection UI. + * @param {string} config.selectorFlatpickrYearContainer The CSS selector for the container of year selection UI. + * @param {string} config.selectorFlatpickrCurrentMonth The CSS selector for the text-based month selection UI. + * @param {string} config.classFlatpickrCurrentMonth The CSS class for the text-based month selection UI. + * @returns {Plugin} A Flatpickr plugin to use text instead of `` for the starting date. + * Workaround for: https://github.com/flatpickr/flatpickr/issues/1944 + * * A logic to ensure `fp.setDate()` call won't end up with "startDate to endDate" set to the first `` + */ +export default config => { + const factory = rangePlugin(Object.assign({ position: 'left' }, config)); + + return fp => { + const origSetDate = fp.setDate; + + const init = () => { + fp.setDate = function setDate(dates, triggerChange, format) { + origSetDate.call(this, dates, triggerChange, format); + // If `triggerChange` is `true`, `onValueUpdate` Flatpickr event is fired + // where Flatpickr's range plugin takes care of fixing the first `` + if (!triggerChange) { + const { _input: inputFrom } = fp; + const { input: inputTo } = config; + [inputFrom, inputTo].forEach((input, i) => { + if (input) { + input.value = !dates[i] + ? '' + : fp.formatDate(new Date(dates[i]), fp.config.dateFormat); + } + }); + } + }; + }; + + const origRangePlugin = factory(fp); + const { onReady: origOnReady } = origRangePlugin; + + return Object.assign(origRangePlugin, { + onReady: [init, origOnReady], + onPreCalendarPosition() {}, + }); + }; +};