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

article/2025/6/7 23:10:42

​​什么是 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.hkcw.cn/article/QFjWuhBRfB.shtml

相关文章

极智项目 | 基于PyQT实现的YOLOv12行人目标检测软件设计

基于YOLOv12的专业级行人目标检测软件应用 开发者: 极智视界 软件下载&#xff1a;链接 &#x1f31f; 项目特色 专业检测: 基于最新YOLOv12模型&#xff0c;专门针对行人检测优化现代界面: 采用PyQt5构建的美观、直观的图形用户界面高性能: 支持GPU加速&#xff0c;检测速…

[yolov11改进系列]基于yolov11引入可变形注意力DAttention的python源码+训练源码

【DAttention介绍】 DAT全称为Deformable Attention Transformer&#xff0c;其作用在于通过可变形注意力机制&#xff0c;同时包含了数据依赖的注意力模式&#xff0c;克服了常见注意力方法存在的内存计算成本高、受无关区域影响以及数据不可知等问题。相比一些只提供固定注意…

谷歌地图2022高清卫星地图手机版v10.38.2 安卓版 - 前端工具导航

谷歌地图2022高清卫星地图手机版是由谷歌公司推出的一款非常好用的手机地图服务软件&#xff0c;用户能够通过精准的导航和定位来查看地图&#xff0c;周边的商店等生活服务都会在地图上显示&#xff0c;用起来超级方便。 谷歌卫星高清地图 下载链接&#xff1a;夸克网盘分享 …

优化 Spring Boot API 性能:利用 GZIP 压缩处理大型有效载荷

引言 在构建需要处理和传输大量数据的API服务时&#xff0c;响应时间是一个关键的性能指标。一个常见的场景是&#xff0c;即使后端逻辑和数据库查询已得到充分优化&#xff0c;当API端点返回大型数据集&#xff08;例如&#xff0c;数千条记录的列表&#xff09;时&#xff0…

对接系统外部服务组件技术方案

概述 当前系统需与多个外部系统对接,然而外部系统稳定性存在不确定性。对接过程中若出现异常,需依靠双方的日志信息来定位问题,但若日志信息不够完整,会极大降低问题定位效率。此外,问题发生后,很大程度上依赖第三方的重试机制,若第三方缺乏完善的重试机制,就需要手动…

sudo docker exec -it backend bash 以交互方式(interactive)进入正在运行的 Docker 容器的命令行环境

sudo docker exec -it backend bash&#x1f50d; 总体作用 这条命令的作用是&#xff1a; 以交互方式&#xff08;interactive&#xff09;进入名为 backend 的正在运行的 Docker 容器的命令行环境。 你会进入容器的“终端”&#xff0c;就像登录到一个 Linux 系统一样&#…

深度思考:摆脱无效忙碌的核心策略

很多人在日复一日地努力工作&#xff0c;感到非常忙碌&#xff0c;但却始终看不到明显的进展&#xff0c;甚至陷入了一种“原地踏步”的感觉。 这背后&#xff0c;很可能是缺乏**深度思考&#xff08;Deep Thinking&#xff09;**所导致的。 为什么你每天都很忙却没有进展&…

JavaScript性能优化实战:从核心原理到工程实践的全流程解析

下面我给出一个较为系统和深入的解析&#xff0c;帮助你理解和实践“JavaScript 性能优化实战&#xff1a;从核心原理到工程实践的全流程解析”。下面的内容不仅解释了底层原理&#xff0c;也结合实际工程中的最佳模式和工具&#xff0c;帮助你在项目中贯彻性能优化理念&#x…

无需巨魔越狱也能使用的自定义emoji触摸轨迹,更新了!

新版本不会再有闪退问题&#x1f44c; 粒子效果体现出来还可以。自定义emoji轨迹小尾巴当然还可以自定义文本&#xff0c;非常有意思全版本支持&#xff0c;越狱包括无根&#xff0c;巨魔&#xff0c;自签都支持&#xff0c;所有应用都支持注入建议越狱可以用别的&#xff0c;毕…

【设计模式-3.7】结构型——组合模式

说明&#xff1a;本文介绍结构型设计模式之一的组合模式 定义 组合模式&#xff08;Composite Pattern&#xff09;又叫作整体-部分&#xff08;Part-Whole&#xff09;模式&#xff0c;它的宗旨是通过将单个对象&#xff08;叶子节点&#xff09;和组合对象&#xff08;树枝…

如何做好一份技术文档?—— 以 LM358 运算放大器手册为例谈技术文档的核心要素

在科技高速发展的当下&#xff0c;技术文档作为知识传递与技术交流的关键载体&#xff0c;其重要性不言而喻。一份优质的技术文档不仅能精准传达技术信息&#xff0c;还能助力读者快速理解复杂内容、推动技术落地。本文将结合《LM358 运算放大器手册》这一典型技术文档&#xf…

20250603在荣品的PRO-RK3566开发板的Android13下的命令行查看RK3566的温度

20250603在荣品的PRO-RK3566开发板的Android13下的命令行查看RK3566的温度 2025/6/3 11:58 RK3566的cpu运行效率 top rk3566_t:/ # rk3566_t:/ # rk3566_t:/ # cd /sys/class/thermal/ rk3566_t:/sys/class/thermal # ls -l rk3566_t:/sys/class/thermal # cd thermal_zone0/ r…

leetcode hot100(两数之和、字母异位词分组、最长连续序列)

两数之和 题目链接 参考链接&#xff1a; 题目描述&#xff1a; 暴力法 双重循环查找目标值 class Solution {public int[] twoSum(int[] nums, int target) {int[] res new int[2];for(int i 0 ; i < nums.length ; i){boolean isFind false;for(int j i 1 ; j …

JWTの求生记录

Token 三巨头通常指的是三种主流的令牌&#xff08;Token&#xff09;技术&#xff0c;它们各自解决了不同场景下的身份验证和授权问题 Token 验证是现代 Web 和移动应用中常用的身份验证方式&#xff0c;它比传统的 session-cookie 机制更适用于分布式系统和 RESTful API。 …

个人博客系统自动化测试报告

个人博客系统自动化测试报告 文章目录 个人博客系统自动化测试报告1. 项目背景2. 测试内容2.1 编写测试用例2.2 执行测试用例 1. 项目背景 个人博客系统由四个界面组成&#xff1a;博客登录页、博客列表页、博客详情页、博客发布页。通过使用Python Selenium实现web自动测试&a…

2025年人文发展与文化传播国际会议(ICHDCC 2025)

2025年人文发展与文化传播国际会议&#xff08;ICHDCC 2025&#xff09; 2025 International Conference on Humanistic Development and Cultural Communication 一、大会信息 会议简称&#xff1a;ICHDCC 2025 大会地点&#xff1a;中国绵阳 审稿通知&#xff1a;投稿后2-3…

MySQL - Windows 中 MySQL 禁用开机自启,并在需要时手动启动

Windows 中 MySQL 禁用开机自启&#xff0c;并在需要时手动启动 打开服务管理器&#xff1a;在底部搜索栏输入【services.msc】 -> 点击【服务】 打开 MySQL 服务的属性管理&#xff1a;找到并右击 MySQL 服务 -> 点击【属性】 此时的 MySQL 服务&#xff1a;正在运行&a…

「EN 18031」访问控制机制(ACM - 1):智能路由器的安全守卫

家用路由器要是出口欧洲&#xff0c;可得留意欧盟EN18031标准里的访问控制机制。以路由器为例&#xff0c;访问控制机制&#xff08;ACM&#xff09;能决定谁能连入网络、访问哪些网站。比如通过设置不同的用户角色和权限&#xff0c;家长可以限制孩子设备的上网时间和可访问的…

线性动态规划

具有「线性」阶段划分的动态规划方法统称为线性动态规划&#xff08;简称为「线性 DP」&#xff09;&#xff0c;如下图所示。 一、概念 如果状态包含多个维度&#xff0c;但是每个维度上都是线性划分的阶段&#xff0c;也属于线性 DP。比如背包问题、区间 DP、数位 DP 等都属…

如何做接口测试?

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 01、通用的项目架构 02、什么是接口 接口&#xff1a;服务端程序对外提供的一种统一的访问方式&#xff0c;通常采用HTTP协议&#xff0c;通过不同的url&#xff…