前言:因项目需要,输入框内需要输入文字和带样式的文字,首先考虑使用现成的富文本编辑器,发现不满足需求,且富文本编辑器功能太多,项目只需要能输入带样式的文字即可,因此在此基础上修改太过冗余。通过搜索发现,可以将div标签设置contenteditable属性,将普通的标签变成输入框。因此,开启自定义输入框的旅程~
先看最终的成果:
上述输入框原始文本内容为
<span class="tag-pause" contenteditable="false">2秒</span>你好<span class="tag-liaison" contenteditable="false">连读</span><span class="tag-breath" contenteditable="false">换气</span><span class="tag-action" contenteditable="false">左手向左上介绍</span>
处理后
[p2000]你好,<动作:左手向左上介绍>
接下来是开发步骤:
首先,定义div标签
1)v-html 绑定富文本内容
2)contenteditable 将div标签置为可编辑标签
3)@input 输入监听
4)@click 点击输入框监听
<div
class="custom-input"
id="customInput"
ref="customInputRef"
v-html="textHtml"
placeholder="请输入需要虚拟主播播报的内容……"
contenteditable
@input="inputText"
@click="handleCustomInputClick">
</div>
然后,默认将光标设置到该标签上,并且记录当前光标
this.$nextTick(() => {
// 初始化光标
this.initSelection();
});
// 初始化光标
initSelection() {
let input = document.getElementById("customInput");
var selection = window.getSelection();
let range = document.createRange();
range.setStart(input, 0);
range.setEnd(input, 0);
selection.removeAllRanges();
selection.addRange(range);
this.lastRange = selection.getRangeAt(0);
}
点击输入区域,同样需要记录光标位置
// 点击输入区域,记录光标位置
handleCustomInputClick() {
let selection = window.getSelection();
this.lastRange = selection.getRangeAt(0);
}
输入普通文本,监听input回调(@input=“inputText”),记录输入时的光标
// 监听输入框内容
inputText() {
let selection = window.getSelection();
this.lastRange = selection.getRangeAt(0);
}
插入标签
// 插入标签
insertTag(text, className) {
// 不能连续添加动作
if (className == "tag-action") {
const node = this.getAdjacentNodeClasses(this.$refs.customInputRef);
if (node) {
if (node.before == "tag-action" || node.after == "tag-action") {
this.$message.warning("不能连续添加动作");
return;
}
}
}
//插入标签
setTimeout(() => {
// 输入框获取焦点
let input = document.getElementById("customInput");
input.focus();
// 获取光标位置
let selection = window.getSelection();
if (this.lastRange) {
selection.removeAllRanges();
selection.addRange(this.lastRange);
}
let range = selection.getRangeAt(0);
// 创建span标签,设置标签内部文本、class名称
const span = document.createElement("span");
span.innerHTML = text;
span.className = className;// 标签样式通过设置的class设置
span.setAttribute("contenteditable", false);
// 插入标签节点到输入框,并将光标设置到标签节点后
range.insertNode(span);
range.setStartAfter(span);
range.setEndAfter(span);
// 记录光标位置
selection.removeAllRanges();
selection.addRange(range);
this.lastRange = selection.getRangeAt(0);
}, 100);
}
其中,插入标签时要求不能连续插入动作标签,因此,当检测到标签class名称为"tag-action"时,需要判断光标前后的内容是否为动作标签。
// 获取光标前后紧邻的节点className
getAdjacentNodeClasses(editableElement) {
editableElement.focus();
let selection = window.getSelection();
if (this.lastRange) {
selection.removeAllRanges();
selection.addRange(this.lastRange);
}
let range = selection.getRangeAt(0);
if (
!selection.rangeCount ||
!selection.getRangeAt(0).intersectsNode(editableElement)
) {
return { before: null, after: null };
}
let beforeNode = null;
let afterNode = null;
// 尝试获取光标前的节点
if (range.startOffset > 0) {
let currentNode = range.startContainer;
let offset = range.startOffset;
// 如果当前节点是文本节点
if (currentNode.nodeType === Node.ELEMENT_NODE) {
let childNodes = currentNode.childNodes;
if (childNodes && childNodes.length >= offset) {
beforeNode = childNodes[offset - 1];
} else {
beforeNode = null;
}
}
}
// 尝试获取光标后的节点
if (
range.endOffset <
(range.endContainer.nodeType === Node.TEXT_NODE
? range.endContainer.nodeValue.length
: range.endContainer.childNodes.length)
) {
let currentNode = range.endContainer;
let offset = range.endOffset;
// 如果当前节点是文本节点
if (currentNode.nodeType === Node.ELEMENT_NODE) {
let childNodes = currentNode.childNodes;
if (childNodes && childNodes.length > offset) {
afterNode = childNodes[offset];
} else {
afterNode = null;
}
}
}
// 返回前后节点的class(如果存在)
return {
before:
beforeNode && beforeNode.nodeType === Node.ELEMENT_NODE
? beforeNode.className
: null,
after:
afterNode && afterNode.nodeType === Node.ELEMENT_NODE
? afterNode.className
: null,
};
}
当选择无动作时,需要将输入框内所有的动作标签都清除
// 清除动作标签
removeActionTag() {
// 选择所有具有类名tag-action的span元素
var elements = document.querySelectorAll("span.tag-action");
// 遍历并移除这些元素
elements.forEach(function (element) {
element.remove();
});
}
获取输入框内的内容
let text = this.$refs.customInputRef.innerHTML
通过上面这种方式获取的输入框内容是原生的带标签样式的,最终我们需要将其处理成想要的内容。在这个项目中,需要做如下处理:
1)连读标签所在的地方需删除它附近的标点符号。
2)换气标签处理成逗号。
3)停顿标签处理成对应的值。
extractTextFromHTML(htmlString) {
if (htmlString === undefined) {
return "";
}
// 创建一个临时的DOM元素来容纳HTML字符串
var tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlString;
// 使用TreeWalker遍历DOM树(可选,但这里我们可以简单地使用childNodes)
var textContent = "";
var nodes = tempDiv.childNodes;
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].nodeType === Node.TEXT_NODE) {
// 只处理文本节点
textContent += nodes[i].nodeValue;
} else if (nodes[i].nodeType === Node.ELEMENT_NODE) {
if (nodes[i].innerHTML == "连读") {
textContent += '<span class="tag-liaison" contenteditable="false"></span>';
} else if (nodes[i].innerHTML == "换气") {
textContent += '<span class="tag-breath" contenteditable="false"></span>';
} else if (nodes[i].className == "tag-pause") {
// 停顿处理为对应的value
textContent += this.convertPauseTime(nodes[i].innerHTML);
} else if (nodes[i].className == "tag-action") {
// 动作处理为<动作:动作名称>
textContent += "<动作:" + nodes[i].innerHTML + ">";
}
}
}
// 先删除连读标签附近的标点符号
textContent = this.removePunctuationAroundTagLiaison(textContent.trim());
// 再将换气标签处理为逗号
textContent = textContent.replace(
/<span class="tag-breath" contenteditable="false">(.*?)<\/span>/g,
","
);
console.log("删除处理后", textContent);
// 返回去除HTML标签后的文本内容
return textContent; // 使用trim()去除可能的首尾空白字符
},
convertPauseTime(label) {
const dict = this.pauseTimeOptions.find((item) => item.label === String(label));
return dict !== undefined ? dict.value : "";
},
removePunctuationAroundTagLiaison(text) {
// 正则表达式匹配 class="tag-liaison" 的 span 标签及其前后的标点符号
// 注意:这个正则表达式假设标点符号是直接与 span 标签相邻的,或者通过空白字符分隔
const regex = /([^\w\s<>]*)<span class="tag-liaison" contenteditable="false">(.*?)<\/span>([^\w\s<>]*)/g;
// 定义要删除的标点符号集合
const punctuation = /[, , 、。. ! !]/g;
// 替换匹配的文本,删除 span 标签前后的标点符号
let result = text.replace(regex, (match, prefix, content, suffix) => {
// 去除 prefix 和 suffix 中的标点符号
prefix = prefix.replace(punctuation, "");
suffix = suffix.replace(punctuation, "");
// 去除 content 两侧的空白字符(如果需要)
content = content.trim();
// 返回处理后的文本
return prefix + content + suffix;
});
return result;
}
const PAUSE_TIME_OPTIONS = [
{
label: "0.5秒",
value: "[p500]",
},
{
label: "1秒",
value: "[p1000]",
},
{
label: "2秒",
value: "[p2000]",
},
];
清空输入框内的内容
this.$refs.customInputRef.innerHTML = "";
以上,就是自定义输入框的全过程。