feat(temporal-editor): 支持手动输入 datetime 字段值
* ✨ feat(temporal-editor): 数据库修改datatime字段时支持手动输入数据 * 🔧 fix(temporal-editor): 修复 picker 回到输入框时误提交的问题
This commit is contained in:
3 files changed
+78
-24
No files matched your search
@@ -3,8 +3,8 @@ import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
|
||||
import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { formatTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor";
|
||||
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { formatTemporalInputValue, parseTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGrid/dataGridTemporalEditor";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -27,19 +27,26 @@ const emit = defineEmits<{
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
const open = ref(true);
|
||||
const triggerRef = ref<HTMLButtonElement | null>(null);
|
||||
const open = ref(false);
|
||||
const editorRootRef = ref<HTMLDivElement | null>(null);
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const localValue = ref(props.modelValue);
|
||||
let closeHandled = false;
|
||||
let isCommitting = false;
|
||||
let skipCommitOnClose = false;
|
||||
|
||||
const hasDate = computed(() => props.kind !== "time");
|
||||
const hasTime = computed(() => props.kind !== "date");
|
||||
const displayValue = computed(() => localValue.value || "NULL");
|
||||
const triggerClass = computed(() =>
|
||||
const editorRootClass = computed(() => (props.variant === "inline" ? "relative h-9 w-full" : "absolute inset-0 z-10"));
|
||||
const inputClass = computed(() =>
|
||||
props.variant === "inline"
|
||||
? "cell-edit-input flex h-9 w-full items-center gap-2 rounded border bg-background px-2 text-left text-xs outline-none hover:border-primary/60 focus:border-primary"
|
||||
: ["cell-edit-input absolute inset-0 z-10 flex items-center gap-1 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "px-1.5" : "px-2.5"],
|
||||
? "cell-edit-input h-9 w-full rounded border bg-background py-0 pl-2 pr-8 text-left text-xs outline-none hover:border-primary/60 focus:border-primary"
|
||||
: ["cell-edit-input absolute inset-0 z-10 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "pl-1.5 pr-6" : "pl-2.5 pr-7"],
|
||||
);
|
||||
const triggerButtonClass = computed(() =>
|
||||
props.variant === "inline"
|
||||
? "absolute right-1 top-1/2 z-20 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
: "absolute right-0.5 top-1/2 z-20 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
);
|
||||
const dateParts = computed(() => {
|
||||
const text = formatTemporalInputValue(localValue.value, "date");
|
||||
@@ -62,7 +69,10 @@ const timeParts = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => triggerRef.value?.focus());
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus();
|
||||
inputRef.value?.select();
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -74,7 +84,7 @@ watch(
|
||||
|
||||
function setOpen(value: boolean) {
|
||||
open.value = value;
|
||||
if (!value && props.commitOnClose && !closeHandled) finishCommit();
|
||||
if (!value && props.commitOnClose && !closeHandled && !skipCommitOnClose) finishCommit();
|
||||
}
|
||||
|
||||
function setModelValue(value: string) {
|
||||
@@ -82,6 +92,10 @@ function setModelValue(value: string) {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
|
||||
function updateTextInput(event: Event) {
|
||||
setModelValue((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function updateDate(part: "day" | "month" | "year", rawValue: string | number) {
|
||||
const numberValue = Number(rawValue);
|
||||
const next = { ...dateParts.value };
|
||||
@@ -130,6 +144,10 @@ function flushInputValue(target: EventTarget | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTextInputValue() {
|
||||
setModelValue(parseTemporalInputValue(localValue.value, props.kind) ?? "");
|
||||
}
|
||||
|
||||
function setNull() {
|
||||
setModelValue("NULL");
|
||||
}
|
||||
@@ -145,6 +163,7 @@ function setNow() {
|
||||
|
||||
function finishCommit() {
|
||||
if (isCommitting) return;
|
||||
normalizeTextInputValue();
|
||||
closeHandled = true;
|
||||
isCommitting = true;
|
||||
emit("commit");
|
||||
@@ -169,13 +188,36 @@ function onKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function onInputBlur() {
|
||||
if (!props.commitOnClose || open.value || closeHandled || skipCommitOnClose) return;
|
||||
finishCommit();
|
||||
}
|
||||
|
||||
function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideEvent) {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
if (originalEvent instanceof FocusEvent || isSelectInteractionTarget(originalEvent.target)) {
|
||||
if (isEditorInteractionTarget(originalEvent.target)) {
|
||||
event.preventDefault();
|
||||
closePickerOnly();
|
||||
return;
|
||||
}
|
||||
if (isSelectInteractionTarget(originalEvent.target)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function closePickerOnly() {
|
||||
if (!open.value) return;
|
||||
skipCommitOnClose = true;
|
||||
setOpen(false);
|
||||
nextTick(() => {
|
||||
skipCommitOnClose = false;
|
||||
});
|
||||
}
|
||||
|
||||
function isEditorInteractionTarget(target: EventTarget | null): boolean {
|
||||
return target instanceof Node && !!editorRootRef.value?.contains(target);
|
||||
}
|
||||
|
||||
function isSelectInteractionTarget(target: EventTarget | null): boolean {
|
||||
return target instanceof Element && !!target.closest("[data-slot='select-content'], [data-slot='select-trigger']");
|
||||
}
|
||||
@@ -202,14 +244,19 @@ function twoDigit(value: string | number): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover :open="open" @update:open="setOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<button ref="triggerRef" type="button" :class="triggerClass" @keydown.stop="onKeydown" @click.stop>
|
||||
<CalendarClock class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ displayValue }}</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" side="bottom" class="w-auto gap-1.5 rounded-md p-1.5" @click.stop @keydown.stop="onKeydown" @interact-outside="onPopoverInteractOutside">
|
||||
<Popover :open="open" :modal="false" @update:open="setOpen">
|
||||
<div ref="editorRootRef" :class="editorRootClass" @click.stop>
|
||||
<PopoverAnchor as-child>
|
||||
<input ref="inputRef" :value="localValue" :class="inputClass" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" placeholder="NULL" @blur="onInputBlur" @click.stop @input="updateTextInput" @keydown.stop="onKeydown" />
|
||||
</PopoverAnchor>
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" :class="triggerButtonClass" @mousedown.prevent @click.stop>
|
||||
<CalendarClock class="h-3.5 w-3.5" />
|
||||
<span class="sr-only">Open temporal picker</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<PopoverContent align="start" side="bottom" class="w-auto gap-1.5 rounded-md p-1.5" @click.stop @keydown.stop="onKeydown" @open-auto-focus.prevent @close-auto-focus.prevent @interact-outside="onPopoverInteractOutside">
|
||||
<div v-if="hasDate" class="grid grid-cols-[4.5rem_4.5rem_4.5rem] gap-1.5">
|
||||
<div class="grid h-7 min-w-0 grid-cols-[1fr_1.35rem] overflow-hidden rounded-md border border-input bg-background">
|
||||
<input :value="dateParts.year" data-temporal-part="year" inputmode="numeric" class="min-w-0 bg-transparent px-1 text-center text-[13px] tabular-nums outline-none" @input="updateDateFromInput('year', $event)" />
|
||||
|
||||
Reference in new issue
Block a user