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