背景
最近在做一个项目,有一个功能是为了发送短信。要求是,用户可以在文本输入框内自定义短信模板,其中短信模板的 变量以【】包括起来。
产品:为了提升可读性,要求高亮展示
【】变量的内容,以提示用户自定义了变量。 像这样:
当时觉得没什么大不了,不就是高亮展示输入文本么,用正则表达式过一遍模板就可以。可实践起来发现textarea标签内的文字是不能自定义样式的。此外,一些富文本好像是不能自定义高亮的规则的,魔改起来还要花时间,于是开动脑筋。
可编辑的div
当时想到的一个例子就是飞书文档,或者说网页端markdown在线编辑工具,可以在输入的一行内容里高亮一部分的文字,比如:
可以发现
contenteditable属性可以让div内的内容变得可以编辑。
如果我对其中高亮的文字用行内块元素 包裹起来 并 设置样式,那就可以高亮出来了。
然而,实践起来发现存在问题:
- 可编辑的div内部文字改变,可能会影响内部的html元素组成形式。
- 需要考虑光标行为和元素展示方式(短信模板粘贴到文本框就能高亮?还是光标编辑到
】的那一刻高亮元素?)如果改变了内容,还得考虑光标放在哪(这可能要接触 Range 和 Selection 类了,开发可没有这么多时间) - 换行了以后,第一行文字又是纯文本,而
<div><br></div>表示换行的内容,还要合理考虑高亮文本的话,这个坑就特别大了。
于是放弃
透明的textarea和文本展示区div
这个想法是,外层用隐藏的textarea编辑文本,内层用div展示内容。
这让编辑和展示分离。不用考虑编辑区域所有光标选择的情况,并且能更好地排布展示区内容。
需要用到子绝父相,同时需要对其textarea和div。
但是又失败了:textarea和div就是不能对齐,哪怕调整letter-spacing还是不对。
于是放弃
透明且可编辑的div和文本展示区div
div和div总是可以对齐吧?确实可以
直接展示代码(Vue3):
<template>
<div class="my-textarea" :class="{ editale: editable }" :style="{ fontSize, lineHeight }">
<!-- 文本展示区 -->
<div class="my-textarea-result">
<p v-for="(pItem, pIndex) in templateTextList" :key="pIndex" :style="{minHeight: lineHeight}">
<span
style="display: inline-block;"
v-for="(textItem, tIndex) in pItem"
:key="tIndex"
:class="{ highlight: textItem.highlight }"
>
{{ textItem.value }}
</span>
</p>
</div>
<!-- 文本编辑区 -->
<div
v-show="editable"
class="my-textarea-input"
contenteditable="true"
@input="handleTextAreaInput"
>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, computed } from "vue";
const props = defineProps({
editable: {
type: Boolean,
default: true,
},
fontSize: {
type: Number,
default: 16,
},
lineHeight: {
type: Number,
default: 1.5,
}
});
const editable = computed(() => props.editable);
const fontSize = computed(() => props.fontSize);
const lineHeight = computed(() => `${props.lineHeight}em`);
// 文本列表
const templateTextList = ref([]);
// 划分高亮区域
function _highlightparagaph(p) {
const regex = /【(.*?)】/g;
let match;
const extractedStrings = [];
let currentIndex = 0;
while ((match = regex.exec(p)) !== null) {
const matchIndex = match.index;
const matchedString = match[0];
// 将【】之前的字符串加入数组
if (matchIndex > currentIndex) {
extractedStrings.push({value: p.substring(currentIndex, matchIndex), highlight: false});
}
// 将带有【】的字符串加入数组
extractedStrings.push({ value: matchedString, highlight: true});
currentIndex = matchIndex + matchedString.length;
}
// 加入最后一个【】之后的字符串
if (currentIndex < p.length) {
extractedStrings.push({value: p.substring(currentIndex), highlight: false});
}
return extractedStrings;
}
const handleTextAreaInput = (e) => {
const innerText = e.target.innerText;
const paragaphList = innerText.replaceAll("\n\n","\n").split("\n");
console.log(paragaphList)
templateTextList.value = paragaphList.map(_highlightparagaph);
};
</script>
<style scoped>
.my-textarea {
width: 80%;
height: 350px;
position: relative;
overflow: auto;
border-radius: 4px;
background-color: #fff;
}
.editale {
transition: all 0.3s ease-in-out;
}
.editale:hover {
outline: 2px solid #68c8ff;
}
.editale:focus {
outline: 2px solid #2faff9;
}
.my-textarea > div {
width: 100%;
height: 100%;
padding: 4px;
box-sizing: border-box;
}
.my-textarea-result span {
word-break: break-all;
white-space: normal;
}
.my-textarea-result span.highlight {
border-radius: 4px;
background-color: #68c8ff;
}
.my-textarea-input {
position: absolute;
left: 0;
top: 0;
background-color: transparent;
color: transparent;
caret-color: #000;
outline: none;
}
</style>
其中template标签内就处理显示样式,可编辑div处理input事件即可
(其实还是有点问题,变量前输入空格会造成内容不对齐,以后再看看)
父组件简单调用一下:
<template>
<div class="wrapper">
<div class="section">
<h1 style="width: fit-content">高亮文本框测试</h1>
<MyTextArea :editable="true"></MyTextArea>
</div>
</div>
</template>
<script setup>
import MyTextArea from "./components/MyTextArea.vue";
</script>
<style>
* {
margin: 0;
padding: 0;
}
html,
body,
#app,
.wrapper {
width: 100%;
height: 100%;
}
.wrapper {
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
}
.section {
width: 600px;
height: 400px;
background-color: #999;
display: flex;
flex-flow: column nowrap;
align-items: center;
}
</style>