> 技术文档 > 前端​​HTML contenteditable 属性使用指南

前端​​HTML contenteditable 属性使用指南


​​什么是 contenteditable?

  • HTML5 提供的全局属性,使元素内容可编辑
  • 类似于简易富文本编辑器
  • 兼容性​​
    支持所有现代浏览器(Chrome、Firefox、Safari、Edge)
    移动端(iOS/Android)部分键盘行为需测试
<p contenteditable=\"true\">可编辑的段落</p>

属性值说明
contenteditable 的三种值:
true:元素可编辑
false:元素不可编辑
inherit:继承父元素的可编辑状态

<p contenteditable=\"false\">不可编辑的段落</p><div contenteditable=\"true\">点击编辑此内容</div><p contenteditable=\"inherit\">继承父元素的可编辑状态</p>

核心功能实现​

保存编辑内容​
 <div style=\"margin-left: 36px;\" v-html=\"newData\" contenteditable=\"true\" ref=\"ediPending2Div\" class=\"editable\" @blur=\"updateContent\" @input=\"handleInput\" @focus=\"saveCursorPosition\" @keydown.enter.prevent=\"handleEnterKey\"></div>
 // 更新内容 updateContent() { this.isEditing = false if (this.rawData !== this.editContent) { this.submitChanges() this.editContent = this.rawData } },
编辑时光标位置的设置
 <div style=\"margin-left: 36px;\" v-html=\"newData\" contenteditable=\"true\" ref=\"ediPending2Div\" class=\"editable\" @blur=\"updateContent\" @input=\"handleInput\" @focus=\"saveCursorPosition\" @keydown.enter.prevent=\"handleEnterKey\"></div>
 // 保存光标位置 saveCursorPosition() { const selection = window.getSelection() if (selection.rangeCount > 0) { const range = selection.getRangeAt(0) this.lastCursorPos = { startContainer: range.startContainer, startOffset: range.startOffset, endOffset: range.endOffset } } }, // 恢复光标位置 restoreCursorPosition() { if (!this.lastCursorPos || !this.isEditing) return const selection = window.getSelection() const range = document.createRange() try { range.setStart( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.startOffset, this.lastCursorPos.startContainer.length) ) range.setEnd( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.endOffset, this.lastCursorPos.startContainer.length) ) selection.removeAllRanges() selection.addRange(range) } catch (e) { // 出错时定位到末尾 range.selectNodeContents(this.$refs.ediPending2Div) range.collapse(false) selection.removeAllRanges() selection.addRange(range) } }, // 处理输入 handleInput() { this.saveCursorPosition() this.rawData = this.$refs.ediPending2Div.innerHTML },
处理换行失败的问题(需要回车两次触发)
 // 给数组添加回车事件 handleEnterKey(e) { // 阻止默认回车行为(创建新div) e.preventDefault(); // 获取当前选区 const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const br = document.createElement(\'br\'); // 插入换行 range.deleteContents(); range.insertNode(br); // 移动光标到新行 range.setStartAfter(br); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); // 触发输入更新 this.handleInput(); },

踩坑案例

  • 数组遍历标签上不能够使用此事件contenteditable

完整代码展示

  • 带数组的处理
  • 不带数组的处理

带数组代码

<template> <div style=\"margin-left: 36px;\" v-loading=\"loading_\" contenteditable=\"true\" ref=\"editPendingDiv\" class=\'editable\' @blur=\"updateContent\" @input=\"handleInput\" @focus=\"saveCursorPosition\" @keydown.enter.prevent=\"handleEnterKey\"> <p class=\"pending_title\">会议待办</p> <p>提炼待办事项如下:</p> <div v-for=\"(item, index) in newData\" :key=\"index\" class=\"todo-item\"> <div class=\"text_container\"> <!-- <img src=\"@/assets/404.png\" alt=\"icon\" class=\"icon-img\"> --> <p><span class=\"icon-span\">AI</span> {{ item }}</p> </div> </div> </div></template><script>// 会议待办事项组件import { todoList } from \'@/api/audio\';import router from \'@/router\';export default { name: \'pendingResult\', props: { // items: { // type: Array, // required: true // } }, data() { return { rawData:null, editContent: \'\', // 编辑内容缓存 lastCursorPos: null, // 光标位置记录 isEditing: false, loading_:false, dataList: [] , routerId: this.$route.params.id }; }, computed: { newData () { // 在合格换行后下面添加margin-botton: 10px return this.dataList } }, watch: { newData() { this.$nextTick(this.restoreCursorPosition) this.$nextTick(this.sendHemlToParent) } }, mounted() { this.$refs.editPendingDiv.addEventListener(\'focus\', () => { this.isEditing = true }) }, created() { this.getDataList(); }, methods: { // 给数组添加回车事件 handleEnterKey(e) { // 阻止默认回车行为(创建新div) e.preventDefault(); // 获取当前选区 const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const br = document.createElement(\'br\'); // 插入换行 range.deleteContents(); range.insertNode(br); // 移动光标到新行 range.setStartAfter(br); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); // 触发输入更新 this.handleInput(); }, // 发送生成数据 sendHemlToParent(){ this.$nextTick(()=>{ const htmlString = this.$refs.editPendingDiv.innerHTML console.log(\'获取修改\',htmlString) this.$emit(\'editList\',htmlString) }) }, // 保存光标位置 saveCursorPosition() { const selection = window.getSelection() if (selection.rangeCount > 0) { const range = selection.getRangeAt(0) this.lastCursorPos = { startContainer: range.startContainer, startOffset: range.startOffset, endOffset: range.endOffset } } }, // 恢复光标位置 restoreCursorPosition() { if (!this.lastCursorPos || !this.isEditing) return const selection = window.getSelection() const range = document.createRange() try { range.setStart( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.startOffset, this.lastCursorPos.startContainer.length) ) range.setEnd( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.endOffset, this.lastCursorPos.startContainer.length) ) selection.removeAllRanges() selection.addRange(range) } catch (e) { // 出错时定位到末尾 range.selectNodeContents(this.$refs.editPendingDiv) range.collapse(false) selection.removeAllRanges() selection.addRange(range) } }, // 处理输入 handleInput() { this.saveCursorPosition() this.rawData = this.$refs.editPendingDiv.innerHTML }, // 更新内容 // updateContent() { // this.isEditing = false // if (this.rawData !== this.editContent) { // this.submitChanges() // this.editContent = this.rawData // } // }, updateContent() { this.isEditing = false; // 清理HTML格式 const cleanedHTML = this.rawData .replace(/

/g, \'
\'
) .replace(/


/g, \'
\'
); if (cleanedHTML !== this.editContent) { this.submitChanges(cleanedHTML); }}, // 提交修改 submitChanges() { // 这里添加API调用逻辑 console.log(\'提交内容:\', this.rawData) this.$emit(\'editList\',this.rawData) }, async getDataList() { const id = { translate_task_id: this.routerId }; this.loading_=true try { const res=await todoList(id) if (res.code === 0) { if (res.data.todo_text == [] || res.data.todo_text === null) { this.$message.warning(\"暂无待办事项\"); return; } // console.log(\"会议纪要数据:\", res.data); this.dataList=res.data.todo_text } } finally { this.loading_=false } // const normalizedText = res.data.todo_text.replace(/\\/n/g, \'\\n\'); // // 分割文本并过滤空行 // this.dataList = normalizedText.split(\'\\n\') // .filter(line => line.trim().length > 0) // .map(line => line.trim()); } }}</script><style scoped>.pending_title { /* font-size: 20px; */ /* font-family: \"宋体\"; */ /* font-weight: bold; */ margin-bottom: 20px;}.text_container { display: flex; align-items: center;}.icon-img { width: 20px; height: 20px; margin-right: 10px;}.editable { /* 确保可编辑区域行为正常 */ user-select: text; white-space: pre-wrap; outline: none;}.todo-item { display: flex; align-items: center; margin: 4px 0;}/* 防止图片被选中 */.icon-span { pointer-events: none; user-select: none; margin-right: 6px; font-weight: 700; color: #409EFF;}</style>

不带数组代码

<template> <div> <div style=\"margin-left: 36px;\" v-html=\"newData\" contenteditable=\"true\" ref=\"ediPending2Div\" class=\"editable\" @blur=\"updateContent\" @input=\"handleInput\" @focus=\"saveCursorPosition\" @keydown.enter.prevent=\"handleEnterKey\"></div> </div></template><script>// 会议待办事项组件222export default { name: \'pendingResult2\', props: { dataList: { type: Object, required: true } }, data() { return { rawData:null, editContent: \'\', // 编辑内容缓存 lastCursorPos: null, // 光标位置记录 isEditing: false, }; }, computed: { newData () { return this.dataList.todo_text } }, watch: { newData() { this.$nextTick(this.restoreCursorPosition) } }, mounted() { this.$refs.ediPending2Div.addEventListener(\'focus\', () => { this.isEditing = true }) }, created() { // console.log(\":\", this.dataList); }, methods: { // 给数组添加回车事件 handleEnterKey(e) { // 阻止默认回车行为(创建新div) e.preventDefault(); // 获取当前选区 const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const br = document.createElement(\'br\'); // 插入换行 range.deleteContents(); range.insertNode(br); // 移动光标到新行 range.setStartAfter(br); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); // 触发输入更新 this.handleInput(); }, // 保存光标位置 saveCursorPosition() { const selection = window.getSelection() if (selection.rangeCount > 0) { const range = selection.getRangeAt(0) this.lastCursorPos = { startContainer: range.startContainer, startOffset: range.startOffset, endOffset: range.endOffset } } }, // 恢复光标位置 restoreCursorPosition() { if (!this.lastCursorPos || !this.isEditing) return const selection = window.getSelection() const range = document.createRange() try { range.setStart( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.startOffset, this.lastCursorPos.startContainer.length) ) range.setEnd( this.lastCursorPos.startContainer, Math.min(this.lastCursorPos.endOffset, this.lastCursorPos.startContainer.length) ) selection.removeAllRanges() selection.addRange(range) } catch (e) { // 出错时定位到末尾 range.selectNodeContents(this.$refs.ediPending2Div) range.collapse(false) selection.removeAllRanges() selection.addRange(range) } }, // 处理输入 handleInput() { this.saveCursorPosition() this.rawData = this.$refs.ediPending2Div.innerHTML }, // 更新内容 updateContent() { this.isEditing = false if (this.rawData !== this.editContent) { this.submitChanges() this.editContent = this.rawData } }, // 提交修改 submitChanges() { // 这里添加API调用逻辑 console.log(\'提交内容:\', this.rawData) this.$emit(\'editList\',this.rawData) }, getDataList() { }, },}</script><style scoped>::v-deep .el-loading-mask{ display: none !important;}p { /* margin: 0.5em 0; */ /* font-family: \"思源黑体 CN Regular\"; */ /* font-size: 18px; */}img { width: 20px; height: 20px; margin-right: 10px;}.indent_paragraph { text-indent: 2em; /* 默认缩进 */}.pending_title { /* font-size: 20px; */ /* font-family: \"宋体\"; */ /* font-weight: bold; */ margin-bottom: 20px;}.text_container { display: flex; align-items: center;}.icon-img { width: 20px; height: 20px; margin-right: 10px;}.editable { /* 确保可编辑区域行为正常 */ user-select: text; white-space: pre-wrap; outline: none;}.todo-item { display: flex; align-items: center; margin: 4px 0;}/* 防止图片被选中 */.icon-span { pointer-events: none; user-select: none; margin-right: 6px; font-weight: 700; color: #409EFF;}</style>
效果展示

前端​​HTML contenteditable 属性使用指南