Skip to content

Commit

Permalink
fix(datepicker): update focused date on editing directly (#941)
Browse files Browse the repository at this point in the history
* Update Datepicker focusedDate on modelValue changed after edit directly
* Update Datepicker examples
* Fix Datepicker dateparsing with multiple on directly edit
* Fix Datepiceker error date formatting
  • Loading branch information
mlmoravek authored Jun 5, 2024
1 parent 20ceedd commit 1af13cf
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 87 deletions.
2 changes: 1 addition & 1 deletion packages/docs/components/Datepicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ title: Datepicker
| closeOnClick | Close dropdown on click | boolean | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;closeOnClick: true<br>}</code> |
| dateCreator | Date creator function, default is `new Date()` | () =&gt; Date | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;dateCreator: () => new Date()<br>}</code> |
| dateFormatter | Custom function to format a date into a string | (date: Date \| Date[]) =&gt; string | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;dateFormatter: defaultFunction<br>}</code> |
| dateParser | Custom function to parse a string into a date | (date: string) =&gt; Date | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;dateParser: defaultFunction<br>}</code> |
| dateParser | Custom function to parse a string into a date | (date: string) =&gt; Date \| Date[] | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;dateParser: defaultFunction<br>}</code> |
| dayNames | Set custom day names, else use names based on locale | string[] | - | <div><small>From <b>config</b>:</small></div><code style='white-space: nowrap; padding: 0;'>datepicker: {<br>&nbsp;&nbsp;dayNames: undefined<br>}</code> |
| disabled | Same as native disabled | boolean | - | <code style='white-space: nowrap; padding: 0;'>false</code> |
| events | Events to display on picker | DatepickerEvent[] | - | |
Expand Down
63 changes: 29 additions & 34 deletions packages/oruga/src/components/datepicker/Datepicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,11 @@ const props = defineProps({
},
/** Custom function to parse a string into a date */
dateParser: {
type: Function as PropType<(date: string) => Date>,
default: (date: string, defaultFunction: (date: string) => Date) =>
getOption("datepicker.dateParser", defaultFunction)(date),
type: Function as PropType<(date: string) => Date | Date[]>,
default: (
date: string,
defaultFunction: (date: string) => Date | Date[],
) => getOption("datepicker.dateParser", defaultFunction)(date),
},
/** Date creator function, default is `new Date()` */
dateCreator: {
Expand Down Expand Up @@ -616,34 +618,30 @@ const isTypeMonth = computed(() => props.type === "month");
/**
* When v-model is changed:
* 1. Update internal value.
* 2. If it's invalid, validate again.
*/
watch(
() => props.modelValue,
(value) => {
// updateInternalState
if (vmodel.value !== value) {
const isArray = Array.isArray(value);
const currentDate = isArray
? !value.length
? props.dateCreator()
: value[value.length - 1]
: !value
? props.dateCreator()
: value;
if (
!isArray ||
(isArray &&
Array.isArray(vmodel.value) &&
value.length > vmodel.value.length)
) {
focusedDateData.value = {
day: currentDate.getDate(),
month: currentDate.getMonth(),
year: currentDate.getFullYear(),
};
}
}
const isArray = Array.isArray(value);
const currentDate = isArray
? value.length
? value[value.length - 1]
: props.dateCreator()
: value
? value
: props.dateCreator();
if (
!isArray ||
(isArray &&
Array.isArray(vmodel.value) &&
value.length > vmodel.value.length)
)
// updateInternalState
focusedDateData.value = {
day: currentDate.getDate(),
month: currentDate.getMonth(),
year: currentDate.getFullYear(),
};
},
);
Expand Down Expand Up @@ -854,17 +852,14 @@ function formatNative(value: Date | Date[]): string {
function onChange(value: string): void {
const date = (props.dateParser as any)(value, defaultDateParser);
if (
const isValid =
isDate(date) ||
(Array.isArray(date) &&
date.length === 2 &&
isDate(date[0]) &&
isDate(date[1]))
) {
vmodel.value = date;
} else {
vmodel.value = null;
}
isDate(date[1]));
vmodel.value = isValid ? date : null;
}
/** Parse date from string */
Expand Down
99 changes: 53 additions & 46 deletions packages/oruga/src/components/datepicker/useDatepickerMixins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export function useDatepickerMixins(props: DatepickerProps) {
const defaultDateFormatter = (date: Date | Date[]): string => {
if (!date) return "";
const targetDates = Array.isArray(date) ? date : [date];
if (!targetDates.length) return "";
const dates = targetDates.map((date) => {
const d = new Date(
date.getFullYear(),
Expand All @@ -112,57 +113,63 @@ export function useDatepickerMixins(props: DatepickerProps) {
};

/** Parse a string into a date */
const defaultDateParser = (date: string): Date => {
const defaultDateParser = (date: string): Date[] | Date => {
if (!date) return null;
if (
dtf.value.formatToParts &&
typeof dtf.value.formatToParts === "function"
) {
const formatRegex = (isTypeMonth.value ? dtfMonth.value : dtf.value)
.formatToParts(sampleTime.value)
.map((part) => {
if (part.type === "literal") return part.value;
return `((?!=<${part.type}>)\\d+)`;
})
.join("");
const dateGroups = matchWithGroups(formatRegex, date);

// We do a simple validation for the group.
// If it is not valid, it will fallback to Date.parse below
const targetDates = !props.multiple ? [date] : date.split(", ");
const dates = targetDates.map((date) => {
if (
dateGroups.year &&
dateGroups.year.length === 4 &&
dateGroups.month &&
dateGroups.month <= 12
dtf.value.formatToParts &&
typeof dtf.value.formatToParts === "function"
) {
if (isTypeMonth.value)
return new Date(dateGroups.year, dateGroups.month - 1);
else if (dateGroups.day && dateGroups.day <= 31) {
return new Date(
dateGroups.year,
dateGroups.month - 1,
dateGroups.day,
12,
);
const formatRegex = (
isTypeMonth.value ? dtfMonth.value : dtf.value
)
.formatToParts(sampleTime.value)
.map((part) => {
if (part.type === "literal") return part.value;
return `((?!=<${part.type}>)\\d+)`;
})
.join("");
const dateGroups = matchWithGroups(formatRegex, date);

// We do a simple validation for the group.
// If it is not valid, it will fallback to Date.parse below
if (
dateGroups.year &&
dateGroups.year.length === 4 &&
dateGroups.month &&
dateGroups.month <= 12
) {
if (isTypeMonth.value)
return new Date(dateGroups.year, dateGroups.month - 1);
else if (dateGroups.day && dateGroups.day <= 31) {
return new Date(
dateGroups.year,
dateGroups.month - 1,
dateGroups.day,
12,
);
}
}
}
}
// Fallback if formatToParts is not supported or if we were not able to parse a valid date
if (!isTypeMonth.value) return new Date(Date.parse(date));
const s = date.split("/");
const year = s[0].length === 4 ? s[0] : s[1];
const month = s[0].length === 2 ? s[0] : s[1];
if (year && month) {
return new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
1,
0,
0,
0,
0,
);
}
// Fallback if formatToParts is not supported or if we were not able to parse a valid date
if (!isTypeMonth.value) return new Date(Date.parse(date));
const s = date.split("/");
const year = s[0].length === 4 ? s[0] : s[1];
const month = s[0].length === 2 ? s[0] : s[1];
if (year && month) {
return new Date(
parseInt(year, 10),
parseInt(month, 10) - 1,
1,
0,
0,
0,
0,
);
}
});
return props.multiple ? dates : dates[0];
};

return { isDateSelectable, defaultDateParser, defaultDateFormatter };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ const locale = ref(); // Browser locale
<o-field label="Select datetime">
<o-datetimepicker
v-model="selected"
rounded
placeholder="Click to select..."
icon="calendar"
:locale="locale"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const datetime = ref(new Date());
<o-field label="Select datetime">
<o-datetimepicker
v-model="datetime"
rounded
placeholder="Click to select...">
<template #footer>
<div class="footer-container">
Expand Down
16 changes: 11 additions & 5 deletions packages/oruga/src/components/utils/PickerWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
getActiveClasses,
useEventListener,
useInputHandler,
usePropBinding,
} from "@/composables";
import type { ClassBind, ComponentClass } from "@/types";
Expand Down Expand Up @@ -121,16 +120,23 @@ const computedNativeType = computed(() =>
watch(
() => props.value,
() => {
// reset input value if they not match
if (vmodel.value !== props.formattedValue)
vmodel.value = props.formattedValue;
// toggle picker if not stay open
if (!props.stayOpen) togglePicker(false);
if (!isValid.value) checkHtml5Validity();
},
{ flush: "post" },
);
const isActive = usePropBinding<boolean>("active", props, emits, {
passive: true,
});
const isActive = defineModel<boolean>("active", { default: false });
const vmodel = ref(props.formattedValue);
watch(
() => props.formattedValue,
(value) => (vmodel.value = value),
);
watch(isActive, onActiveChange);
Expand Down Expand Up @@ -211,8 +217,8 @@ defineExpose({ focus: setFocus });
<o-input
ref="inputRef"
v-bind="inputBind"
v-model="vmodel"
autocomplete="off"
:model-value="formattedValue"
:placeholder="picker.placeholder"
:size="picker.size"
:icon-pack="picker.iconPack"
Expand Down

0 comments on commit 1af13cf

Please sign in to comment.