172 lines
4.3 KiB
Vue
172 lines
4.3 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 },
|
|
allowClear: { type: Boolean, default: true },
|
|
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"
|
|
:allow-clear="allowClear"
|
|
: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>
|