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

news2025/6/6 17:43:43

​​什么是 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(/<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_=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>
// 会议待办事项组件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) 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>

效果展示

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2399174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自动化采集脚本与隧道IP防封设计

最近群里讨论问如何编写一个自动化采集脚本&#xff0c;要求使用隧道IP&#xff08;代理IP池&#xff09;来防止IP被封。这样的脚本通常用于爬虫或数据采集任务&#xff0c;其中目标网站可能会因为频繁的请求而封禁IP。对于这些我还是有些经验的。 核心思路&#xff1a; 1、使…

【设计模式-4.7】行为型——备忘录模式

说明&#xff1a;本文介绍行为型设计模式之一的备忘录模式 定义 备忘录模式&#xff08;Memento Pattern&#xff09;又叫作快照模式&#xff08;Snapshot Pattern&#xff09;或令牌模式&#xff08;Token Pattern&#xff09;指在不破坏封装的前提下&#xff0c;捕获一个对…

docker离线镜像下载

背景介绍 在某些网络受限的环境中&#xff0c;直接从Docker Hub或其他在线仓库拉取镜像可能会遇到困难。为了在这种情况下也能顺利使用Docker镜像&#xff0c;我们可以提前下载好所需的镜像&#xff0c;并通过离线方式分发和使用。 当前镜像有&#xff1a;python-3.8-slim.ta…

Vert.x学习笔记-Verticle原理解析

Vert.x学习笔记 一、设计理念&#xff1a;事件驱动的组件化模型二、生命周期管理三、部署方式与策略四、通信机制&#xff1a;事件总线&#xff08;Event Bus&#xff09;五、底层实现原理六、典型应用场景七、Verticle与EventLoop的关系1、核心关系&#xff1a;一对一绑定与线…

jQuery和CSS3卡片列表布局特效

这是一款jQuery和CSS3卡片列表布局特效。该卡片布局使用owl.carousel.js来制作轮播效果&#xff0c;使用简单的css代码来制作卡片布局&#xff0c;整体效果时尚大方。 预览 下载 使用方法 在页面最后引入jquery和owl.carousel.js相关文件。 <link rel"stylesheet&qu…

不连网也能跑大模型?

一、这是个什么 App&#xff1f; 你有没有想过&#xff0c;不用连网&#xff0c;你的手机也能像 ChatGPT 那样生成文字、识别图片、甚至回答复杂问题&#xff1f;Google 最近悄悄发布了一个实验性 Android 应用——AI Edge Gallery&#xff0c;就是为此而生的。 这个应用不在…

强化学习鱼书(10)——更多深度强化学习的算法

&#xff1a;是否使用环境模型&#xff08;状态迁移函数P(s’|s,a)和奖 励函数r(s&#xff0c;a&#xff0c;V)&#xff09;。不使用环境模型的方法叫作无模型&#xff08;model-free&#xff09;的方法&#xff0c;使用环境模型的方法叫作有模型&#xff08;model-based&#…

K8S上使用helm部署 Prometheus + Grafana

一、使用 Helm 安装 Prometheus 1. 配置源 地址&#xff1a;prometheus 27.19.0 prometheus/prometheus-community # 添加repo $ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts "prometheus-community" has been added…

Java面试八股--07-项目篇

致谢:2025年 Java 面试八股文(20w字)_java面试八股文-CSDN博客 目录 1、介绍一下最近做的项目 1.1 项目背景: 1.2 项目功能 1.3 技术栈 1.4自己负责的功能模块 1.5项目介绍参考: 1.6整体业务介绍: 1.8后台管理系统功能: 1.8.1后台主页: 1.8.2 商品模块: 1.8…

MCP架构全解析:从核心原理到企业级实践

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storms…

从0到1认识EFK

一、ES集群部署 操作系统Ubuntu22.04LTS/主机名IP地址主机配置elk9110.0.0.91/244Core8GB100GB磁盘elk9210.0.0.92/244Core8GB100GB磁盘elk9310.0.0.93/244Core8GB100GB磁盘 1. 什么是ElasticStack? # 官网 https://www.elastic.co/ ElasticStack早期名称为elk。 elk分别…

定制开发开源AI智能名片驱动下的海报工厂S2B2C商城小程序运营策略——基于社群口碑传播与子市场细分的实证研究

摘要 本文聚焦“定制开发开源AI智能名片S2B2C商城小程序”技术与海报工厂业务的融合实践&#xff0c;探讨其如何通过风格化海报矩阵的精细化开发、AI技术驱动的用户体验升级&#xff0c;以及S2B2C模式下的社群裂变机制&#xff0c;实现“工具功能-社交传播-商业变现”的生态…

【Unity开发】控制手机移动端的震动

&#x1f43e; 个人主页 &#x1f43e; 阿松爱睡觉&#xff0c;横竖醒不来 &#x1f3c5;你可以不屠龙&#xff0c;但不能不磨剑&#x1f5e1; 目录 一、前言二、Unity的Handheld.Vibrate()三、调用Android原生代码四、NiceVibrations插件五、DeviceVibration插件六、控制游戏手…

Cesium快速入门到精通系列教程二:添加地形与添加自定义地形、相机控制

一、添加地形与添加自定义地形 在 Cesium 1.93 中添加地形可以通过配置terrainProvider实现。Cesium 支持多种地形数据源&#xff0c;包括 Cesium Ion 提供的全球地形、自定义地形服务以及开源地形数据。下面介绍几种常见的添加地形的方法&#xff1a; 使用 Cesium Ion 全球地…

python学习打卡day43

DAY 43 复习日 作业&#xff1a; kaggle找到一个图像数据集&#xff0c;用cnn网络进行训练并且用grad-cam做可视化 浙大疏锦行 数据集使用猫狗数据集&#xff0c;训练集中包含猫图像4000张、狗图像4005张。测试集包含猫图像1012张&#xff0c;狗图像1013张。以下是数据集的下…

Microsoft Word使用技巧分享(本科毕业论文版)

小铃铛最近终于完成了毕业答辩后空闲下来了&#xff0c;但是由于学校没有给出准确地参考模板&#xff0c;相信诸位朋友们也在调整排版时感到头疼&#xff0c;接下来小铃铛就自己使用到的一些排版技巧分享给大家。 注&#xff1a;以下某些设置是根据哈尔滨工业大学&#xff08;威…

windows安装多个版本composer

一、需求场景 公司存在多个项目&#xff0c;有的项目比较老&#xff0c;需要composer 1.X版本才能使用 新的项目又需要composer 2.X版本才能使用 所以需要同时安装多个版本的composer二、下载多个版本composer #composer官网 https://getcomposer.org/download/三、放到指定目…

【办公类-22-05】20250601Python模拟点击鼠标上传CSDN12篇

、 背景需求: 每周为了获取流量券,每天上传2篇,获得1500流量券,每周共上传12篇,才能获得3000和500的券。之前我用UIBOT模拟上传12篇。 【办公类-22-04】20240418 UIBOT模拟上传每天两篇,获取流量券,并删除内容_csdn 每日任务流量券-CSDN博客文章浏览阅读863次,点赞18…

贪心算法应用:边着色问题详解

贪心算法应用&#xff1a;边着色问题详解 贪心算法是一种在每一步选择中都采取当前状态下最优的选择&#xff0c;从而希望导致结果是全局最优的算法策略。边着色问题是图论中的一个经典问题&#xff0c;贪心算法可以有效地解决它。下面我将从基础概念到具体实现&#xff0c;全…

ck-editor5的研究 (2):对 CKEditor5 进行设计,并封装成一个可用的 vue 组件

前言 在上一篇文章中—— ck-editor5的研究&#xff08;1&#xff09;&#xff1a;快速把 CKEditor5 集成到 nuxt 中 &#xff0c;我仅仅是把 ckeditor5 引入到了 nuxt 中&#xff0c;功能还不算通用。 这一篇内容将会对其进行设计&#xff0c;并封装成可复用的 vue 组件&…