feat: textarea高亮词语组件调整
This commit is contained in:
@ -1,24 +1,11 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<a-textarea
|
||||
v-model="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
:max-length="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@focus="() => (focus = true)"
|
||||
@blur="() => (focus = false)"
|
||||
/>
|
||||
<a-textarea ref="textareaWrapRef" v-model="inputValue" placeholder="请输入作品描述" :disabled="disabled" show-word-limit
|
||||
:max-length="1000" size="large" class="textarea-input h-full w-full" @input="handleInput"
|
||||
@focus="() => (focus = true)" @blur="() => (focus = false)" />
|
||||
|
||||
<div
|
||||
class="textarea-highlight"
|
||||
:class="{ focus: focus }"
|
||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml"
|
||||
/>
|
||||
<div class="textarea-highlight" :class="{ focus: focus }" :style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -47,9 +34,11 @@ const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
// 内部状态管理
|
||||
const inputValue = ref(props.modelValue || '');
|
||||
const scrollTop = ref(0);
|
||||
const focus = ref(false);
|
||||
const textareaWrapRef = ref();
|
||||
let nativeTextarea: HTMLTextAreaElement | null = null;
|
||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||
|
||||
// 监听外部modelValue变化
|
||||
@ -67,12 +56,6 @@ const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
};
|
||||
|
||||
// 同步滚动位置
|
||||
const syncScroll = (e: Event) => {
|
||||
console.log('syncScroll');
|
||||
scrollTop.value = (e.target as HTMLTextAreaElement).scrollTop;
|
||||
};
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
if (!isString(str)) return '';
|
||||
return str
|
||||
@ -114,11 +97,24 @@ const generateHighlightedHtml = (): string => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelector('.textarea-input .arco-textarea')?.addEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea = (textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
||||
document.querySelector('.textarea-input .arco-textarea');
|
||||
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.addEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('.textarea-input .arco-textarea')?.removeEventListener('scroll', handleTextareaScroll);
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.removeEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.removeEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTextareaScroll = (e: Event) => {
|
||||
@ -129,6 +125,18 @@ const handleTextareaScroll = (e: Event) => {
|
||||
highlightElement.scrollTop = _scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompositionUpdate = () => {
|
||||
if (!nativeTextarea) return;
|
||||
// 使用 rAF 等待浏览器把最新字符写入 textarea.value 再读取
|
||||
requestAnimationFrame(() => {
|
||||
if (!nativeTextarea) return;
|
||||
const latest = nativeTextarea.value.slice(0, 1000);
|
||||
if (latest !== inputValue.value) {
|
||||
inputValue.value = latest;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -136,9 +144,11 @@ const handleTextareaScroll = (e: Event) => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@mixin textarea-padding {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@mixin textarea-style {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@ -153,6 +163,7 @@ const handleTextareaScroll = (e: Event) => {
|
||||
word-wrap: break-word;
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
.textarea-highlight {
|
||||
@include textarea-style;
|
||||
position: absolute;
|
||||
@ -163,13 +174,16 @@ const handleTextareaScroll = (e: Event) => {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
@include textarea-padding;
|
||||
|
||||
&.focus {
|
||||
border-color: rgb(var(--primary-6)) !important;
|
||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
@include textarea-style;
|
||||
|
||||
.arco-textarea {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@ -181,17 +195,25 @@ const handleTextareaScroll = (e: Event) => {
|
||||
caret-color: #211f24 !important;
|
||||
@include textarea-padding;
|
||||
|
||||
// -webkit-text-fill-color: transparent !important;
|
||||
// -webkit-text-fill-color: transparent !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.arco-textarea-word-limit {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.arco-textarea-disabled {
|
||||
.arco-textarea {
|
||||
background: #f2f3f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
||||
:deep(.textarea-input.composing .arco-textarea) {
|
||||
color: #211f24 !important;
|
||||
-webkit-text-fill-color: #211f24 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,24 +1,11 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<a-textarea
|
||||
v-model="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
:max-length="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@focus="() => (focus = true)"
|
||||
@blur="() => (focus = false)"
|
||||
/>
|
||||
<a-textarea ref="textareaWrapRef" v-model="inputValue" placeholder="请输入作品描述" :disabled="disabled" show-word-limit
|
||||
:max-length="1000" size="large" class="textarea-input h-full w-full" @input="handleInput"
|
||||
@focus="() => (focus = true)" @blur="() => (focus = false)" />
|
||||
|
||||
<div
|
||||
class="textarea-highlight"
|
||||
:class="{ focus: focus }"
|
||||
:style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml"
|
||||
/>
|
||||
<div class="textarea-highlight" :class="{ focus: focus }" :style="{ visibility: inputValue ? 'visible' : 'hidden' }"
|
||||
v-html="highlightedHtml" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -51,6 +38,8 @@ const emit = defineEmits<{
|
||||
const inputValue = ref(props.modelValue || '');
|
||||
const scrollTop = ref(0);
|
||||
const focus = ref(false);
|
||||
const textareaWrapRef = ref();
|
||||
let nativeTextarea: HTMLTextAreaElement | null = null;
|
||||
const highlightedHtml = computed(() => generateHighlightedHtml());
|
||||
|
||||
// 监听外部modelValue变化
|
||||
@ -68,12 +57,6 @@ const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
};
|
||||
|
||||
// 同步滚动位置
|
||||
const syncScroll = (e: Event) => {
|
||||
console.log('syncScroll');
|
||||
scrollTop.value = (e.target as HTMLTextAreaElement).scrollTop;
|
||||
};
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
if (!isString(str)) return '';
|
||||
return str
|
||||
@ -115,11 +98,24 @@ const generateHighlightedHtml = (): string => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelector('.textarea-input .arco-textarea')?.addEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea = (textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
||||
document.querySelector('.textarea-input .arco-textarea');
|
||||
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.addEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.addEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('.textarea-input .arco-textarea')?.removeEventListener('scroll', handleTextareaScroll);
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.removeEventListener('scroll', handleTextareaScroll);
|
||||
nativeTextarea.removeEventListener('compositionstart', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionupdate', handleCompositionUpdate);
|
||||
nativeTextarea.removeEventListener('compositionend', handleCompositionUpdate);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTextareaScroll = (e: Event) => {
|
||||
@ -130,6 +126,18 @@ const handleTextareaScroll = (e: Event) => {
|
||||
highlightElement.scrollTop = _scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompositionUpdate = () => {
|
||||
if (!nativeTextarea) return;
|
||||
// 使用 rAF 等待浏览器把最新字符写入 textarea.value 再读取
|
||||
requestAnimationFrame(() => {
|
||||
if (!nativeTextarea) return;
|
||||
const latest = nativeTextarea.value.slice(0, 1000);
|
||||
if (latest !== inputValue.value) {
|
||||
inputValue.value = latest;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -137,9 +145,11 @@ const handleTextareaScroll = (e: Event) => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@mixin textarea-padding {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@mixin textarea-style {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@ -154,6 +164,7 @@ const handleTextareaScroll = (e: Event) => {
|
||||
word-wrap: break-word;
|
||||
z-index: inherit;
|
||||
}
|
||||
|
||||
.textarea-highlight {
|
||||
@include textarea-style;
|
||||
position: absolute;
|
||||
@ -164,13 +175,16 @@ const handleTextareaScroll = (e: Event) => {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
@include textarea-padding;
|
||||
|
||||
&.focus {
|
||||
border-color: rgb(var(--primary-6)) !important;
|
||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
@include textarea-style;
|
||||
|
||||
.arco-textarea {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
@ -185,14 +199,22 @@ const handleTextareaScroll = (e: Event) => {
|
||||
// -webkit-text-fill-color: transparent !important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.arco-textarea-word-limit {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.arco-textarea-disabled {
|
||||
.arco-textarea {
|
||||
background: #f2f3f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
||||
:deep(.textarea-input.composing .arco-textarea) {
|
||||
color: #211f24 !important;
|
||||
-webkit-text-fill-color: #211f24 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user