可编辑标签高亮
效果演示
Vue3 源码
vue
<template>
<div class="editable-tags">
<h3>可编辑标签高亮</h3>
<p>
在下面的文本框中输入内容,支持
<code>#</code>, <code>@</code>, 和 <code>*</code> 开头的标签。 "#官方",
"#推荐", "#热点" 为官方标签。
</p>
<div
ref="editableDiv"
class="editable"
contenteditable="true"
@input="handleInput"
@keydown="handleKeyDown"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
></div>
<button @click="getEditableData">获取数据</button>
<button @click="setEditableData">回显数据</button>
</div>
</template>
<script setup>
import { ref, nextTick, onMounted } from "vue";
// 可编辑区域的引用
const editableDiv = ref(null);
// 判断是否为官方标签
const isOfficialTag = (tag) => {
const officialTags = ["#官方", "#推荐", "#热点"]; // 官方标签定义
return officialTags.includes(tag);
};
// 高亮标签
const highlightTags = (text) => {
const parts = text.split(/(\s+)/); // 保留空格
return parts
.map((part) => {
if (part.startsWith("#")) {
if (isOfficialTag(part)) {
return `<span class="tag prefix-hash-official">${part}</span>`;
} else {
return `<span class="tag prefix-hash-custom">${part}</span>`;
}
} else if (part.startsWith("@")) {
return `<span class="tag prefix-at">${part}</span>`;
} else if (part.startsWith("*")) {
return `<span class="tag prefix-star">${part}</span>`;
} else {
return part.replace(/\n/g, ""); // 保留换行
}
})
.join("");
};
// 保存光标位置
const saveCaretPosition = (context) => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(context);
preCaretRange.setEnd(range.startContainer, range.startOffset);
return preCaretRange.toString().length; // 返回光标位置
};
// 恢复光标位置
const restoreCaretPosition = (context, position) => {
const selection = window.getSelection();
const range = document.createRange();
let charCount = 0;
function traverseNodes(currentNode) {
if (currentNode.nodeType === Node.TEXT_NODE) {
const nextCharCount = charCount + currentNode.textContent.length;
if (position <= nextCharCount) {
range.setStart(currentNode, position - charCount);
range.collapse(true);
return true;
}
charCount = nextCharCount;
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
for (let i = 0; i < currentNode.childNodes.length; i++) {
if (traverseNodes(currentNode.childNodes[i])) return true;
}
}
return false;
}
traverseNodes(context);
selection.removeAllRanges();
selection.addRange(range);
};
// 获取编辑区域中的数据
const getEditableData = () => {
if (editableDiv.value) {
const text = editableDiv.value.innerText; // 提取纯文本
alert(`获取的数据:\n${text}`);
}
};
// 设置编辑区域的数据并回显高亮
const setEditableData = () => {
const sampleData = "#官方 @演示 *自定义 #热点 #测试 ";
if (editableDiv.value) {
const highlightedHTML = highlightTags(sampleData); // 高亮处理
editableDiv.value.innerHTML = highlightedHTML;
nextTick(() => {
restoreCaretPosition(
editableDiv.value,
editableDiv.value.innerText.length
); // 将光标移到末尾
});
}
};
// 检测是否清空并恢复焦点
const ensureCaretPosition = () => {
if (editableDiv.value && editableDiv.value.innerHTML.trim() === "") {
editableDiv.value.innerHTML = ""; // 清空内容
nextTick(() => {
const range = document.createRange();
range.selectNodeContents(editableDiv.value);
range.collapse(true);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
}
};
let isComposing = false; // 标记是否正在拼音输入状态
const handleCompositionStart = () => {
isComposing = true; // 拼音联想开始
// console.log('拼音联想开始');
};
const handleCompositionEnd = (event) => {
isComposing = false; // 拼音联想结束
// 在拼音输入完成时处理最终输入
console.log("Composition ended:", event.target.innerHTML);
handleInput();
};
// 监听用户输入事件
const handleInput = () => {
if (!editableDiv.value) {
return;
}
if (isComposing) {
// 拼音输入联想状态中,不处理输入
return;
}
const caretPosition = saveCaretPosition(editableDiv.value); // 保存光标位置
const text = editableDiv.value.innerText;
editableDiv.value.innerHTML = highlightTags(text); // 更新内容
nextTick(() => {
restoreCaretPosition(editableDiv.value, caretPosition); // 恢复光标位置
});
};
// 监听删除键事件
const handleKeyDown = (event) => {
if (event.key === "Backspace") {
setTimeout(() => {
ensureCaretPosition(); // 确保内容清空后光标仍存在
});
}
};
onMounted(() => {
setEditableData();
});
</script>
<style lang="scss">
.editable-tags {
button {
padding: 2px 8px;
border-radius: 5px;
background-color: #eee;
}
button + button {
margin-left: 8px;
}
.editable {
width: 100%;
min-height: 100px;
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
font-size: 16px;
line-height: 1.5;
outline: none;
white-space: pre-wrap;
margin-bottom: 12px;
}
.tag {
display: inline-block;
margin: 2px;
border-radius: 3px;
font-weight: bold;
}
.prefix-hash-official {
color: #108ee9;
}
.prefix-hash-custom {
color: #f59a23;
}
.prefix-at {
color: #d9001b;
}
.prefix-star {
color: #4b7902;
}
}
</style>
html 源码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>可编辑标签高亮</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.editable {
width: 100%;
min-height: 100px;
border: 1px solid #ccc;
padding: 10px;
border-radius: 5px;
font-size: 16px;
line-height: 1.5;
outline: none;
white-space: pre-wrap;
}
.tag {
display: inline-block;
margin: 2px;
border-radius: 3px;
font-weight: bold;
}
.prefix-hash-official {
color: #108ee9;
}
.prefix-hash-custom {
color: #f59a23;
}
.prefix-at {
color: #d9001b;
}
.prefix-star {
color: #4b7902;
}
</style>
</head>
<body>
<h1>可编辑标签高亮</h1>
<p>
在下面的文本框中输入内容,支持 <code>#</code>, <code>@</code>, 和
<code>*</code> 开头的标签。 "#官方", "#推荐", "#热点" 为官方标签
</p>
<div id="editable" class="editable" contenteditable="true"></div>
<button id="get-data-btn">获取数据</button>
<button id="set-data-btn">回显数据</button>
<script>
const editableDiv = document.getElementById("editable");
const getDataBtn = document.getElementById("get-data-btn");
const setDataBtn = document.getElementById("set-data-btn");
// 判断是否为官方标签
function isOfficialTag(tag) {
const officialTags = ["#官方", "#推荐", "#热点"]; // 官方标签定义
return officialTags.includes(tag);
}
// 高亮标签
function highlightTags(text) {
const parts = text.split(/(\s+)/); // 保留空格
return parts
.map((part) => {
if (part.startsWith("#")) {
if (isOfficialTag(part)) {
return `<span class="tag prefix-hash-official">${part}</span>`;
} else {
return `<span class="tag prefix-hash-custom">${part}</span>`;
}
} else if (part.startsWith("@")) {
return `<span class="tag prefix-at">${part}</span>`;
} else if (part.startsWith("*")) {
return `<span class="tag prefix-star">${part}</span>`;
} else {
return part.replace(/\n/g, "<br>"); // 保留换行
}
})
.join("");
}
// 获取光标位置
function saveCaretPosition(context) {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(context);
preCaretRange.setEnd(range.startContainer, range.startOffset);
return preCaretRange.toString().length; // 返回光标位置
}
// 恢复光标位置
function restoreCaretPosition(context, position) {
const selection = window.getSelection();
const range = document.createRange();
let charCount = 0;
let node;
function traverseNodes(currentNode) {
if (currentNode.nodeType === Node.TEXT_NODE) {
const nextCharCount = charCount + currentNode.textContent.length;
if (position <= nextCharCount) {
range.setStart(currentNode, position - charCount);
range.collapse(true);
return true;
}
charCount = nextCharCount;
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
for (let i = 0; i < currentNode.childNodes.length; i++) {
if (traverseNodes(currentNode.childNodes[i])) return true;
}
}
return false;
}
traverseNodes(context);
selection.removeAllRanges();
selection.addRange(range);
}
// 获取编辑区域中的数据
function getEditableData() {
const text = editableDiv.innerText; // 提取纯文本
console.log("获取的纯文本数据:", text);
return text;
}
// 设置编辑区域的数据并回显高亮
function setEditableData(data) {
const highlightedHTML = highlightTags(data); // 高亮处理
editableDiv.innerHTML = highlightedHTML;
}
// 监听用户输入
editableDiv.addEventListener("input", () => {
const caretPosition = saveCaretPosition(editableDiv); // 保存光标位置
const text = editableDiv.innerText;
editableDiv.innerHTML = highlightTags(text); // 更新内容
restoreCaretPosition(editableDiv, caretPosition); // 恢复光标位置
});
// 按钮事件
getDataBtn.addEventListener("click", () => {
const data = getEditableData();
alert(`获取的数据:\n${data}`);
});
setDataBtn.addEventListener("click", () => {
const sampleData = "#官方 @演示 *自定义 #热点 #测试";
setEditableData(sampleData);
});
// 初始化
editableDiv.innerHTML = highlightTags("");
</script>
</body>
</html>