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