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