Files
workspace/code/fms/fms-vue/src/components/FmsMemoInput.vue
T
2026-07-28 22:54:43 +08:00

170 lines
4.2 KiB
Vue

<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: { type: String, default: "" },
disabled: { type: Boolean, default: false },
readonly: { type: Boolean, default: false },
maxlength: { type: Number, default: undefined },
popupWidth: { type: [Number, String], default: 420 },
popupHeight: { type: [Number, String], default: 180 },
placement: { type: String, default: "bottomCenter" },
});
const emit = defineEmits(["update:modelValue", "change", "focus", "blur", "openChange"]);
const open = ref(false);
const inputRef = ref(null);
const textareaRef = ref(null);
const editorRef = ref(null);
const triggerWidth = ref(0);
const popoverPlacement = computed(() =>
props.placement === "bottomCenter" ? "bottom" : props.placement,
);
const popupWidth = computed(() =>
typeof props.popupWidth === "number" ? `${props.popupWidth}px` : props.popupWidth,
);
const popupHeight = computed(() =>
typeof props.popupHeight === "number" ? `${props.popupHeight}px` : props.popupHeight,
);
const popupStyles = computed(() => ({
container: {
width: popupWidth.value,
minWidth: triggerWidth.value ? `${triggerWidth.value}px` : undefined,
maxWidth: "calc(100vw - 24px)",
padding: "8px",
},
}));
function updateValue(value) {
emit("update:modelValue", value ?? "");
}
function onInputChange(event) {
emit("change", event);
}
async function showEditor() {
if (props.disabled || open.value) return;
const inputElement = inputRef.value?.$el ?? inputRef.value;
triggerWidth.value = inputElement?.getBoundingClientRect?.().width ?? 0;
open.value = true;
emit("openChange", true);
await nextTick();
textareaRef.value?.focus?.({ cursor: "end" });
}
function onInputFocus(event) {
emit("focus", event);
showEditor();
}
function onOpenChange(value) {
if (value && props.disabled) return;
open.value = value;
emit("openChange", value);
}
function closeEditor() {
if (!open.value) return;
open.value = false;
emit("openChange", false);
}
function onDocumentPointerDown(event) {
if (!open.value) return;
const inputElement = inputRef.value?.$el ?? inputRef.value;
const path = event.composedPath?.() ?? [];
if (path.includes(inputElement) || path.includes(editorRef.value)) return;
closeEditor();
}
function onTextareaKeydown(event) {
event.stopPropagation();
if (event.key === "Escape") {
event.preventDefault();
closeEditor();
}
}
function onInputBlur(event) {
if (!open.value) emit("blur", event);
}
function onTextareaBlur(event) {
emit("blur", event);
}
onMounted(() => document.addEventListener("pointerdown", onDocumentPointerDown, true));
onBeforeUnmount(() => document.removeEventListener("pointerdown", onDocumentPointerDown, true));
</script>
<template>
<a-popover
:open="open"
:trigger="[]"
:placement="popoverPlacement"
:arrow="false"
:styles="popupStyles"
@open-change="onOpenChange"
>
<template #content>
<div ref="editorRef" class="fms-memo-editor" @mousedown.stop @click.stop>
<a-textarea
ref="textareaRef"
:value="modelValue"
:maxlength="maxlength"
:placeholder="$attrs.placeholder"
:disabled="disabled"
:readonly="readonly"
:style="{ height: popupHeight, resize: 'both' }"
@update:value="updateValue"
@change="onInputChange"
@keydown="onTextareaKeydown"
@blur="onTextareaBlur"
/>
</div>
</template>
<a-input
ref="inputRef"
v-bind="$attrs"
class="fms-memo-input"
:value="modelValue"
:disabled="disabled"
:readonly="readonly"
:maxlength="maxlength"
@update:value="updateValue"
@change="onInputChange"
@focus="onInputFocus"
@blur="onInputBlur"
@click.stop="showEditor"
@keydown.stop
>
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
<slot :name="name" v-bind="slotProps || {}" />
</template>
</a-input>
</a-popover>
</template>
<style scoped>
.fms-memo-input {
width: 100%;
}
.fms-memo-editor {
width: 100%;
}
.fms-memo-editor :deep(textarea) {
min-width: 180px;
min-height: 96px;
}
</style>