vue中實現(xiàn)div可編輯并插入指定元素與樣式
前言:
vue中實現(xiàn)一個既可以編輯內容,有可以動態(tài)編輯內容插入一個帶有樣式的內容,改變默認內容后,這個樣式消失的效果,這里來整理下調研與解決實現(xiàn)問題之路。
實現(xiàn)最終效果:圖2為默認內容
1、可以光標點擊任意地方,然后點擊按鈕,插入帶有span的內容
2、默認span是有樣式,但是一旦內容與我們的默認內容不同就取消樣式
3、獲取的時候,可以拿到帶有標簽的內容,也可以拿到純文本內容
4、默認內容,支持帶標簽內容


探索之路:
1、剛開始從網上查找了很多資料,但是都有不同的缺陷,下面是我根據(jù)網上資料實現(xiàn)的效果
1)封裝一個 inputDiv.vue
<template>
<div
ref="divInput"
class="edit-div"
style="-webkit-user-select: auto"
:contenteditable="canEdit"
@blur="onBlur"
@input="changeText($event)"
@keyup.space="changeSpace($event)"
@keyup.enter="changeEnter($event)"
@paste="onPaste"
@focus="onFocus"
placeholder="請輸入消息內容"
slot="title"
v-html="innerText"
></div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: "",
},
canEdit: {
type: Boolean,
default: true,
},
},
data() {
return {
innerText: this.value,
isLocked: false,
}
},
watch: {
value() {
if (!this.isLocked && !this.innerHTML) {
this.innerText = this.value
}
},
},
methods: {
getRefBlur() {
this.$refs.divInput.blur()
},
getRefFocus() {
this.$refs.divInput.focus()
},
onBlur() {
this.$emit("onblurChange", this.$el.innerHTML)
this.isLocked = false
},
// 主要用于處理光標得定位問題
keepLastIndex(obj) {
console.log(obj);
if (window.getSelection) {
obj.focus()
let range = window.getSelection()
range.selectAllChildren(obj)
range.collapseToEnd()
} else if (document.selection) {
let range = document.selection.createRange() //創(chuàng)建選擇對象
range.moveToElementText(obj) //range定位到obj
range.collapse(false) //光標移至最后
range.select()
}
},
changeSpace(e) {
setTimeout(() => {
this.keepLastIndex(e.target)
}, 30)
this.$emit("keyupSpace", this.$el.innerHTML)
},
changeEnter(e) {
setTimeout(() => {
this.keepLastIndex(e.target)
}, 30)
this.$emit("keyupEnter", this.$el.innerHTML)
},
// 實時監(jiān)聽當前內容
changeText(e) {
console.log(e)
if(e.data){
console.log('數(shù)據(jù):'+this.$el.innerHTML)
setTimeout(() => {
this.keepLastIndex(e.target)
}, 30)
this.$emit("input", this.$el.innerHTML)
}
},
// 輸入框粘貼事件
onPaste(event) {
this.$emit("onPaste", event)
},
//
onFocus(event) {
this.$emit("onFocus", event)
},
}
}
</script>
<style scoped>
</style>2)父級中使用:
結果:{{editInputVal}}-{{mrEditInputVal}}
<el-button @click = 'addStrBtnFun'>{{addBtnText}}</el-button>
<inputDiv
ref="imitate"
id="imitate"
class="imitate-input imitate-placeholder js-imitate-input"
@onblurChange="onblurChange($event)"
@onFocus="getFocus"
@onPaste="onPaste"
v-html="mrEditInputVal"
@input="contentedInput($event)"
@keyupSpace="contentedKeySpace($event)"
@keyupEnter="contentedKeyEnter($event)"
@changeKeyup="changeKeyup($event)"
></inputDiv>js部分:
import inputDiv from './components/inputDiv'
components: {
inputDiv,
},
data() {
return {
mrEditInputVal:'',
editInputVal:'',
addBtnText:'插入員工昵稱',
abc:'',
teamContent: "<p>幸福 人和 測試增加數(shù)據(jù) 在加1111222</p>"
}
},
methods: {
onblurChange(val){
// debugger
},
getFocus(val){
// debugger
},
onPaste(val){
// debugger
},
contentedInput(val){
if(val.indexOf('<span')!= -1){
let arr1 = val.split('<span')
let arr2 = arr1[1].split('/span>')
let arr3 = arr2[0].split('>')
let arr4 = arr3[1].split('<')
if(arr4[0] != this.addBtnText){
let val = this.editInputVal = arr1[0]+arr4[0]+arr2[1]
this.mrEditInputVal = JSON.parse(JSON.stringify(val))
return
}
}
this.editInputVal = val
},
contentedKeySpace(val){
// debugger
},
contentedKeyEnter(val){
// debugger
},
changeKeyup(val){
// debugger
},
addStrBtnFun(){
let index = this.getCursorPosition()
let insertStr = (soure,start, newStr) => {
return soure.slice(0, start) + newStr + soure.slice(start)
}
let newStr = insertStr(this.editInputVal,index,'<span class="selColor">'+this.addBtnText+'</span>')
this.mrEditInputVal = newStr
},
getCursorPosition(){
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const cursorPos = range.startOffset;
return cursorPos
},3)最終實現(xiàn)效果
1、實現(xiàn)了不同內容的時候可以正常監(jiān)聽
2、點擊按鈕,可以正常插入并修改樣式
缺點:
1、點擊多個插入,位置有問題
2、每次輸入第二個字符的時候,光標都移動到了最后一位

2、繼續(xù)努力實現(xiàn)效果,效果圖實現(xiàn)步驟:
注意:
不能實時獲取它的數(shù)據(jù),它的光標位置會出現(xiàn)問題,目前官方提供的方法,默認是回到第一個,可以修改到回到最后一個,但是我們如果是中間輸入內容,光標會異常,故而不建議實時獲取數(shù)據(jù)
1、div+contenteditable="true" 來實現(xiàn)div的可編輯,v-html來實現(xiàn)對他默認值的修改
<div
class="edit"
contenteditable="true"
@input="editInput"
placeholder="請輸入消息內容"
v-html="innerHtml"
>
</div>2、初始化方法
// 這一步是保留住edit編輯框中的選區(qū)的范圍對象。否則失焦后,getSelection()方法返回的選區(qū)對象已經不再是編輯框了,已經獲取不到編輯框中的范圍對象了。
initEditFun(){
editDiv = document.getElementsByClassName("edit")[0]
editDiv.addEventListener("blur", () => {
// 這一步是保留住edit編輯框中的選區(qū)的范圍對象。否則失焦后,getSelection()方法返回的選區(qū)對象已經不再是編輯框了,已經獲取不到編輯框中的范圍對象了。
range = window.getSelection().getRangeAt(0)
})
},3、點擊按鈕時候,給可編輯區(qū)域中增加一個帶有顏色的span
記得在你的功能樣式中添加,可在app.vue,也可以在你項目其他公共區(qū)域
.selColor{
color: red;
}點擊事件:
window.getSelection() 獲取他的光標選取
selection.selectAllChildren(editDiv) // selectAllChildren把指定元素的所有子元素設為選中區(qū)域,并取消之前的選中區(qū)域。不包括node節(jié)點本身。
selection.collapseToEnd() //Selection.collapseToEnd() 方法的作用是取消當前選區(qū),并把光標定位在原選區(qū)的最末尾處,如果此時光標所處的位置是可編輯的,且它獲得了焦點,則光標會在原地閃爍。(想查看更多屬性,點擊鏈接,去看官方api)
range = window.getSelection().getRangeAt(0) // 保存當前編輯框的選區(qū)對象
let sel = window.getSelection()range.insertNode(span) // insertNode方法,在range選區(qū)開頭插入一個節(jié)點
sel.removeAllRanges() //removeAllRanges方法:刪除之前的所有選區(qū)。sel.addRange(range) // 這一步就是添加當前區(qū)域對象到選區(qū)對象中
addStrBtnFun(){
const span = document.createElement("span")
span.innerText = this.addBtnText
span.className = 'selColor'
// 如果在頁面刷新再點擊編輯框之前就點擊了按鈕,此時range中并沒有選區(qū)范圍對象
if (range === "") {
let selection = window.getSelection()
selection.selectAllChildren(editDiv) // selectAllChildren把指定元素的所有子元素設為選中區(qū)域,并取消之前的選中區(qū)域。不包括node節(jié)點本身。
/*
Selection.collapseToEnd() 方法的作用是取消當前選區(qū),并把光標定位在原選區(qū)的最末尾處,如果此時光標所處的位置是可編輯的,且它獲得了焦點,則光標會在原地閃爍。
以上selectAllChildren方法,將div中子節(jié)點全部選中,collapseToEnd方法將選中區(qū)域取消,并且將光標定位到原區(qū)的末尾。
*/
selection.collapseToEnd()
range = window.getSelection().getRangeAt(0) // 無論哪一步都需要保存當前編輯框的選區(qū)對象
}
let sel = window.getSelection()
range.insertNode(span) // insertNode方法,在range選區(qū)開頭插入一個節(jié)點
/*
removeAllRanges方法:刪除之前的所有選區(qū)。
這一步的意義:因為當我們點擊其他區(qū)域時,選區(qū)對象已經改變,不再是編輯框中的選區(qū)對象,這時候我們接下來的操作都不會符合我們想象中的樣子
*/
sel.removeAllRanges()
sel.addRange(range) // 這一步就是添加當前區(qū)域對象到選區(qū)對象中,所以選區(qū)對象會再次指向編輯框中的選區(qū),不會出現(xiàn)別的錯誤操作。
sel.collapseToEnd()
},4、我們修改span內容,讓他的樣式消失方法
1)首先要用到 他的@input事件
@input="editInput"
editInput(e) {
console.log(e);
console.log(e.target.children);
},這是它的默認拿到的數(shù)據(jù)

如果插入span以后,可以看到e.target.children多了span信息

拿我們頁面的edit div下面所有的span,如果哪個內容被改變,就去掉它的class
editInput(e) {
console.log(e);
console.log(e.target.children);
if(e.target.children.length>0){
this.editSpanClass()
}
},
editSpanClass(){
let spanAll = document.querySelectorAll('.edit span')
spanAll.forEach(item=>{
if(item.innerHTML != this.addBtnText){
item.className = ''
}
})
},源碼分享:tinymce.vue
<template>
<div class="chitchat">
<div style="width:32px;height:32px;">
<img
src="../../../../assets/images/microcode/touxiang.png"
alt=""
width="100%"
height="100%"
>
</div>
<div id="editDivBody" :class="isInspection?'Inspection':''">
<div style="margin-bottom:12px">
<el-button class="nickname" @click ="addStrBtnFun(addBtnText)">{{addBtnText}}</el-button>
<el-button class="nickname" @click = "addStrBtnFun(serviceConsultant)">{{serviceConsultant}}</el-button>
<el-button class="nickname" @click = "addStrBtnFun(departments)">{{ departments }}</el-button>
<!-- <el-button @click="getSpecialContent">點我</el-button>-->
<!-- <el-button @click="checkIsNonData">是否空</el-button>-->
</div>
<div class="headPortrait" v-if="uploadHtml">
<div
:disabled="textSize >= maxNum"
ref="editInput"
class="edit"
id="editSelDiv"
contenteditable="true"
@input="editInput"
placeholder="請輸入消息內容"
v-html="innerHtml"
@paste="handlePaste"
>
</div>
<!-- 實時數(shù)據(jù)1:-->
<!-- {{nowHtmlT}}-->
<!-- <hr>-->
<!-- 實時數(shù)據(jù)2:{{nowText}}-->
</div>
</div>
<div class="limit">{{textSize}}/{{maxNum}}</div>
<p v-if="isInspection" class="Contentfilling">請?zhí)顚懴热?lt;/p>
</div>
</template>
<script>
let editDiv = null //編輯元素
let range = "" //選區(qū)
export default {
data () {
return {
addBtnText:'客戶昵稱',
serviceConsultant:'所屬顧問',
departments:'所屬部門',
// 賦值
innerHtml:'',// 初始化賦值
innerHtmlBf:'',//備份數(shù)據(jù)
nowHtmlT:'',// 實時html-動態(tài)變量
nowText:'',// 實時文本
// 獲取當前最新數(shù)據(jù)
getDataTime:1000,
getDataTimeInter:null,
textSize:0,
maxNum:500,
isInspection:false,
uploadHtml:true,
}
},
mounted() {
this.initEditFun()
},
methods:{
initEditFun(){
editDiv = document.getElementById("editSelDiv")
editDiv.addEventListener("blur", () => {
// 這一步是保留住edit編輯框中的選區(qū)的范圍對象。
// 否則失焦后,getSelection()方法返回的選區(qū)對象已經不再是編輯框了,已經獲取不到編輯框中的范圍對象了。
range = window.getSelection()?window.getSelection().getRangeAt(0):''
})
document.addEventListener('selectionchange', (e) => {
let selection = document.getSelection();
let rangeVal = selection && selection.rangCount>0?selection.getRangeAt(0):''
if (
(rangeVal && this.hasAncestorWithId(rangeVal.commonAncestorContainer,'editSelDiv'))
) {
range = window.getSelection()?window.getSelection().getRangeAt(0):''
}
});
},
// 遞歸判斷是否有父級id = editSelDiv
hasAncestorWithId(element, id) {
if (!element) return false;
if (element.id === id) return true;
if (element.parentNode) {
return this.hasAncestorWithId(element.parentNode, id);
}
return false;
},
editInput(e) {
// console.log(e);
this.getNowContent() //獲取最新內容
if(e.target.children.length>0){
this.editSpanClass()
}
if(e.target.innerText == ''){
range = ''
e.target.innerHtml = ''
this.innerHtml = ''
this.innerHtmlBf = ''
}
},
editSpanClass(){
let spanAll = document.querySelectorAll('.edit span')
spanAll.forEach(item=>{
let nowVal = item.childNodes[0].data
if(
nowVal &&
nowVal.trim() != this.addBtnText &&
nowVal.trim() != this.serviceConsultant &&
nowVal.trim() != this.departments
){
item.className = 'mrClass'
}
})
},
// 粘貼
handlePaste(e){
e.preventDefault(); // 阻止默認粘貼操作
const clipboardData = e.clipboardData || window.clipboardData;
const text = clipboardData.getData('text/plain'); // 獲取純文本內容
document.execCommand('insertHTML', false, text); // 將文本插入 contenteditable 元素中
},
addStrBtnFun(Text){
const innerText = this.$refs.editInput.innerText
const now_length = innerText.split('\u200B').join('').length
const length = now_length + Text.length //當前長度+按鈕文字長度
if(length > this.maxNum){
return
}
const span = document.createElement("span")
span.innerText = Text
span.className = 'selColor'
// 如果在頁面刷新再點擊編輯框之前就點擊了按鈕,此時range中并沒有選區(qū)范圍對象
if (range === "") {
let selection = window.getSelection()
selection.selectAllChildren(editDiv) // selectAllChildren把指定元素的所有子元素設為選中區(qū)域,并取消之前的選中區(qū)域。不包括node節(jié)點本身。
/*
Selection.collapseToEnd() 方法的作用是取消當前選區(qū),并把光標定位在原選區(qū)的最末尾處,如果此時光標所處的位置是可編輯的,且它獲得了焦點,則光標會在原地閃爍。
以上selectAllChildren方法,將div中子節(jié)點全部選中,collapseToEnd方法將選中區(qū)域取消,并且將光標定位到原區(qū)的末尾。
*/
selection.collapseToEnd()
range = window.getSelection()?window.getSelection().getRangeAt(0):''// 無論哪一步都需要保存當前編輯框的選區(qū)對象
}
let sel = window.getSelection()
let space = document.createTextNode('\u200B'); // 創(chuàng)建 ​ 實體字符節(jié)點
range.insertNode(space)
range.insertNode(span) // insertNode方法,在range選區(qū)開頭插入一個節(jié)點
/*
removeAllRanges方法:刪除之前的所有選區(qū)。
這一步的意義:因為當我們點擊其他區(qū)域時,選區(qū)對象已經改變,不再是編輯框中的選區(qū)對象,這時候我們接下來的操作都不會符合我們想象中的樣子
*/
sel.removeAllRanges()
sel.addRange(range) // 這一步就是添加當前區(qū)域對象到選區(qū)對象中,所以選區(qū)對象會再次指向編輯框中的選區(qū),不會出現(xiàn)別的錯誤操作。
sel.collapseToEnd()
this.editSpanClass()
this.getNowContent()
},
resevedSelDiv(){
range = document.createRange();
range.selectNode(document.getElementById("editSelDiv").lastChild);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
let selection = window.getSelection()
selection.selectAllChildren(editDiv)
selection.collapseToEnd()
range = window.getSelection()?window.getSelection().getRangeAt(0):''
console.log(333);
},
// 獲取最新的內容
getNowContent(){
const innerText = this.$refs.editInput.innerText
const length = innerText.split('\u200B').join('').length
if(length<= this.maxNum){
this.innerHtmlBf = this.$refs.editInput.innerHTML
this.nowText = this.$refs.editInput.innerText
this.textSize = length
this.isInspection=false
}else{
this.uploadHtml = false
this.$nextTick(() => {
this.innerHtml = this.innerHtmlBf
this.uploadHtml = true
setTimeout(()=>{
this.resevedSelDiv()
},1)
});
}
},
// 獲取帶符號的內容
getSpecialContent(){
let spanAll = document.querySelectorAll('.edit span')
let mrArr = []
spanAll.forEach(item=>{
let nowVal = item.childNodes[0].data
mrArr.push(nowVal)
if(item.className == 'selColor'){
if(nowVal == this.addBtnText){
item.childNodes[0].data = '${customerNickname}' //客戶昵稱
}else if(nowVal == this.serviceConsultant){
item.childNodes[0].data = '${affiliatedConsultant}'//所屬顧問
}else if(nowVal == this.departments){
item.childNodes[0].data = '${department}' //所屬部門
}
}
})
this.nowHtmlT = this.$refs.editInput.innerText
this.nowHtmlT = this.replaceText(this.nowHtmlT,this.addBtnText,"${customerNickname}")
this.nowHtmlT = this.replaceText(this.nowHtmlT,this.serviceConsultant,"${affiliatedConsultant}")
this.nowHtmlT = this.replaceText(this.nowHtmlT,this.departments,"${department}")
spanAll.forEach((item,i)=>{
item.childNodes[0].data = mrArr[i]
})
this.$emit('nowHtmlT',this.nowHtmlT)
},
replaceText(text, oldStr, newStr) {
// 檢查是否為字符串類型
if (typeof text !== 'string') {
text = String(text);
}
// 替換字符
text = text.replace(new RegExp(oldStr, "g"), newStr);
// 處理子級
if (text.includes('<') && text.includes('>')) {
const start = text.indexOf('<');
const end = text.indexOf('>') + 1;
let subtext = text.substring(start, end);
while (start >= 0 && end >= 0 && end > start) {
const subtextNew = replaceText(subtext, oldStr, newStr);
text = text.substring(0, start) + subtextNew + text.substring(end);
subtext = text.substring(start, end);
}
}
return text;
},
checkIsNonData(){
const innerText = this.$refs.editInput.innerText
const length = innerText.split('\u200B').join('').length
if(innerText == '' || innerText == '\n' || length == 0){
this.isInspection = true
}else{
this.isInspection = false
}
},
},
beforeDestroy() {
editDiv = null //編輯元素
range = "" //選區(qū)
}
}
</script>
<style lang="scss" scoped>
.edit{
width: 100%;
height:auto;
min-height: 130px;
max-height: 135px;
overflow-y: auto;
}
.edit:focus {
outline: 0;
border-color: #409EFF;
}
#editDivBody{
width: 100%;
//height:203px;
overflow-y: auto;
background: #FAFBFC;
border-radius: 4px;
border: 1px solid #DCDEE6;
padding: 12px 12px 20px 13px;
margin-left: 8px;
position: relative;
}
::-webkit-scrollbar {
/*滾動條整體樣式*/
width: 5px; /*高寬分別對應橫豎滾動條的尺寸*/
height: 1px;
}
::-webkit-scrollbar-thumb {
/*滾動條里面小方塊*/
border-radius: 5px;
background-color: #D8D8D8;
}
::-webkit-scrollbar-track {
background-color: #f6f6f6;
}
::-webkit-scrollbar-thumb,
::-webkit-scrollbar-track {
border: 0;
}
.Inspection{
border: 1px solid #FF4949!important;
}
.nickname{
background: #FFFFFF;
border-radius: 4px;
border: 1px solid #DCDFE6;
font-size: 12px;
font-weight: 400;
color: #1890FF;
line-height: 17px;
padding: 4px 8px;
}
.chitchat{
display: flex;
position: relative;
width: 79%;
}
.Contentfilling{
width: 84px;
height: 17px;
font-size:12px;
line-height: 17px;
color: #FF4949;
position: absolute;
bottom: -18px;
left: 41px;
margin: 0;
}
/deep/.selColor{
color: #1890FF;
font-size: 14px;
}
/deep/.mrClass{
color: #000;
font-size: 14px;
}
.limit{
position: absolute;
bottom: 0;
right:0;
padding: 0 8px;
line-height: 22px;
font-size: 14px;
color: #86909C;
}
.headPortrait{
position: relative;
font-size: 14px;
}
</style>總結
到此這篇關于vue中實現(xiàn)div可編輯并插入指定元素與樣式的文章就介紹到這了,更多相關vue實現(xiàn)div可編輯內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue.js如何在網頁中實現(xiàn)一個金屬拋光質感的按鈕
這篇文章主要給大家介紹了關于vue.js如何在網頁中實現(xiàn)一個金屬拋光質感的按鈕的相關資料,文中給出了詳細的實例代碼以及圖文將實現(xiàn)的方法介紹的非常詳細,需要的朋友可以參考下2023-04-04
vue-cli 腳手架基于Nightwatch的端到端測試環(huán)境的過程
這篇文章主要介紹了vue-cli 腳手架基于Nightwatch的端到端測試環(huán)境的過程,需要的朋友可以參考下2018-09-09

