当前位置: 首页 > news >正文

前端​​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 = falseif (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) returnconst 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: 10pxreturn 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.innerHTMLconsole.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) returnconst 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(/<div><br><\/div>/g, '<br>').replace(/<p><br><\/p>/g, '<br>');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_=truetry {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>
// 会议待办事项组件222
export 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) returnconst 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 = falseif (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>
效果展示

在这里插入图片描述

http://www.lqws.cn/news/104419.html

相关文章:

  • windows安装多个版本composer
  • 燃尽图和甘特图
  • “刹车思维”:慢,是为了更快
  • 超声波粒度仪市场报告:行业现状、竞争格局与未来趋势分析
  • 【基础】Unity中Camera组件知识点
  • 49套夏日小清新计划总结日系卡通ppt模板
  • 《Effective Python》第六章 推导式和生成器——使用类替代生成器的 `throw` 方法管理迭代状态转换
  • 香橙派3B学习笔记6:基本的Bash脚本学习_UTF-8格式问题
  • 使用 Python + ExecJS 获取网易云音乐歌曲歌词
  • 红黑树与红黑树的插入——用C++实现
  • 关于list集合排序的常见方法
  • JAVA获取ES连接并查询所有数据
  • Eigen实现非线性最小二乘拟合 + Gauss-Newton算法
  • Server2003 B-1 Windows操作系统渗透
  • Java项目OOM排查
  • 华为云Flexus+DeepSeek征文|基于华为云Flexus X实例的小说转语音助手应用构建实录
  • JS对数据类型的检测
  • CppCon 2014 学习:Lightning Talk: Writing a Python Interpreter for Fun and Profit
  • Java 调用第三方接口注意事项
  • Axure设计案例:滑动拼图解锁
  • 电子电路:全面深入了解晶振的定义、作用及应用
  • WordPress 6.5版本带来的新功能
  • 接口重试的7种常用方案!
  • Eureka 高可用集群搭建实战:服务注册与发现的底层原理与避坑指南
  • C++:优先级队列
  • SOC-ESP32S3部分:28-BLE低功耗蓝牙
  • 【数学】高斯积分+伽马函数公式自用背诵笔记
  • Rust 学习笔记:Cargo 工作区
  • CppCon 2014 学习:Rolling Your Own Circuit Simulator
  • 应用智能化转型—MCP原理分析