今天公司有个需求,要求多行文本输入框内的文字,#号开头,空格结尾的部分,颜色要变成蓝色,其他部分的文字颜色不变
实现下图的效果

image.png
最终代码-vue3可直接使用
<!-- 调用的地方 -->
<template>
<div>
<color-input v-model="str" />
<div>{{ str }}</div>
</div>
</template>
<script setup>
import colorInput from "@/components/colorInput.vue";
import { ref } from "vue";
const str = ref("111 #112 333");
</script>
<!-- 组件 -->
<template>
<div
class="colorInput"
ref="htmlDom"
@compositionstart="isComposing = true"
@compositionend="handleCompositionEnd"
@input="textChange"
v-html="htmlStr"
contenteditable="true"
:style="{ width, height }"
></div>
</template>
<script setup>
import { ref, nextTick, defineEmits, defineProps } from "vue";
const htmlStr = ref("");
const htmlDom = ref(null);
const isComposing = ref(false);
const props = defineProps({
modelValue: {
type: String,
default: "",
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "32px",
},
});
const emit = defineEmits(["update:modelValue"]);
console.log(props);
if (props.modelValue) setText(props.modelValue);
function textChange(e) {
if (!isComposing.value) {
setText(e.target.innerText);
}
}
function setText(str) {
htmlStr.value = setTxtColor(str);
nextTick(() => {
emit("update:modelValue", str);
placeCaretAtEnd(htmlDom.value);
});
}
// 正则匹配需要变色的文案
function setTxtColor(text) {
return text.replace(
/#[^\s#]+/g,
(match) => `<span style="color:#5290ff">${match}</span>`
);
}
// 这里是输入中文的时候会触发两次input,在这里进行限制
function handleCompositionEnd(event) {
isComposing.value = false;
// 手动触发一次处理,因为compositionend后可能不会触发input
textChange(event);
}
// 辅助光标到输入框的最后一位
function placeCaretAtEnd(el) {
el.focus();
if (
typeof window.getSelection !== "undefined" &&
typeof document.createRange !== "undefined"
) {
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
}
</script>
<style lang="less" scoped>
.colorInput {
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
margin: 0;
font-variant: tabular-nums;
list-style: none;
font-feature-settings: "tnum";
position: relative;
display: inline-block;
padding: 4px 11px;
font-size: 14px;
line-height: 1.5;
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
</style>
具体实现思路
- 因为textarea内部是纯文本,没有办法变色,所以最初的想法是给div添加contenteditable="true",这样可以让div变成可以输入的盒子,模拟一个textarea,然后我想着在上面放上v-model
<div contenteditable="true" v-model='str'></div>
<!-- 但是这样失败了,因为v-model只能绑定到input这些表单的dom上 -->
- 然后我想着把v-model拆开,分成一个input事件,和一个v-html(因为是div不是input。所以不能:value)
<div
class="colorInput"
@input="textChange"
v-html="htmlStr"
contenteditable="true"
></div>
const htmlStr = ref("");
function textChange(e) {
setText(e.target.innerText);
}
function setText(str) {
htmlStr.value = setTxtColor(str);
}
function setTxtColor(text) {
return text.replace(
/#[^\s#]+/g,
(match) => `<span style="color:#5290ff">${match}</span>`
);
}
在触发input的时候,对文字进行处理,用setTxtColor这个正则匹配,然后,修改文字颜色,再通过v-html展示到页面上
- 又遇到问题,每次修改之后会通过v-html更新到div里面,这时候鼠标会自动聚焦到输入内容的句首,而不是句尾
function placeCaretAtEnd(el) {
el.focus();
if (
typeof window.getSelection !== "undefined" &&
typeof document.createRange !== "undefined"
) {
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
}
所以就有这个函数,来将光标定位到元素末尾的函数
- 但是后来又发现问题了,输入中文部分符号(,。《》?¥)的时候,会触发两次input事件
这个和输入法有关,不同的输入法,对于标点符号输入事件序列也不太一样
// 这里是输入中文的时候会触发两次input,在这里进行限制
function handleCompositionEnd(event) {
isComposing.value = false;
// 手动触发一次处理,因为compositionend后可能不会触发input
textChange(event);
}
这里通过一个变量来处理,只触发一次修改
然后对代码封装一下,就结束了,功能完成