​​什么是 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>
效果展示

在这里插入图片描述

posted on 2025-10-07 16:52  lxjshuju  阅读(4)  评论(0)    收藏  举报