什么是 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>