需求就是用js模擬word的有序列表鍵盤操作,那種全選加序號的還沒實現(xiàn)。
提取幾個比較明顯的特征:
- 列表項回車生成新列表項
- 數(shù)字前刪除只會刪除數(shù)字,而不會換到上一行
- 刪除中間列表項會導(dǎo)致后面的重排序
還有很多細節(jié),暫時不討論,光是上面這些,隱藏的東西就夠做了。
思路
- 可編輯區(qū)域可以用contenteditable處理
- 序號用屬性+偽類的方式實現(xiàn)
- 在數(shù)字前刪除只刪除序號,有兩種方法,一是重寫delete的keydown,阻止默認行為,對所有情形進行判斷;二是在列表項最前面加一個隱式字符
​,這個charCodeAt()輸出的是8203,而編輯的空格是 ,code是160,后面會用到。
我是用第二種思路寫的,但我感覺第一種更好,原因是隱式字符對刪除、左右鍵的影響較大,這個實現(xiàn)的版本還沒有對左右鍵進行重寫。 - enter的keydown要加stop和prevent,完全重寫。主要考慮:
- 字符中間截斷
- 序號行回車,如有文本內(nèi)容,新增序號行;否則刪除當(dāng)前序號
- 非序號行回車新增非序號行
- enter的keyup不用修飾,只做序號重排處理
- delete的keydown比較復(fù)雜,prevent需要在內(nèi)部分情況主動調(diào)用。
- 前面是占位符
- 如果是序號行,刪除序號,序號重排
- 非序號行要看上一行的狀態(tài),如果有文本內(nèi)容,刪除當(dāng)前行,光標移到上一行末尾;如果沒有文本內(nèi)容,把當(dāng)前行的內(nèi)容移至上一行,并把光標移至上一行開頭,刪除當(dāng)前行,且都需要阻止默認行為。
- 還有一種情況,如果通過鍵盤操作越過了占位符,那么前面就什么都沒有了,非序號行默認行為即可,序號行則與前面是占位符的邏輯一樣
- 前面是占位符
Range
這里面多次用到了光標的判斷和設(shè)置,用到的是Range API,不做多介紹,詳細可以參考文檔。
值得一提的是,如果序號行用的是<li>some text</li>的結(jié)構(gòu),設(shè)置光標時需要將textNode作為參數(shù)而不是li。
這點坑了我好久,關(guān)系到光標的位置設(shè)置,li作為rangeContent,其offset范圍只有0~1,而textNode的范圍是0~text.length。
code
只上js部分,html思路里提到過,結(jié)構(gòu):ol > <li class="number" index="1、">​some text</li>
const isFst = () => {
return getSelection().getRangeAt(0).startOffset == 1
}
const isZero = () => {
return getSelection().getRangeAt(0).startOffset == 0
}
export default {
methods: {
// 獲取當(dāng)前行
getCurLi() {
return $(getSelection().getRangeAt(0).commonAncestorContainer).closest('li')
},
// 設(shè)置光標
setRange(el, isStart = false) {
let textNode = el.childNodes[0]
let range = document.createRange()
range.selectNodeContents(textNode)
range.collapse()
// isStart用來處理是否設(shè)置到首部,1是因為前面還有占位符
if (isStart) {
range.setStart(textNode, 1)
range.setEnd(textNode, 1)
}
let sel = getSelection()
sel.removeAllRanges()
sel.addRange(range)
},
enterKeydown() {
let li = this.getCurLi()
let offset = getSelection().getRangeAt(0).startOffset
let text = li.text()
let slice
// 中間截斷
if (isZero) {
slice = text.substr(1)
li.text(text.substr(0, 1))
} else {
slice = text.substr(offset)
li.text(text.substr(0, offset))
}
// 有文本
if (text.length > 1 && li.hasClass('number')) {
let newLi = $(`<li class="number" index="">​${slice}</li>`).get(0)
li.after(newLi)
this.setRange(newLi, true)
} else {
// 空文本
if (li.hasClass('number')) {
let preLi = li.prev()
li.remove()
let newLi = $(`<li>​</li>`).get(0)
preLi.after(newLi)
this.setRange(newLi)
} else {
// 非序號行
let newLi = $(`<li>​${slice}</li>`).get(0)
li.after(newLi)
this.setRange(newLi, true)
}
}
},
// 設(shè)置序號
enterKeyup() {
let lis = $('ol > li.number')
for (let i = 0; i < lis.length; i++) {
lis.eq(i).attr('index', `${j + 1}、`)
}
},
deleteKeydown(e) {
let li = this.getCurLi()
// 刪到占位 防止第一位是空格
if (isFst() && li.text()[0].charCodeAt() !== 160) {
// 序號行刪序號 重排序
if (li.hasClass('number')) {
li.removeClass('number')
li.attr('index', '')
this.enterKeyup()
} else {
let pre = li.prev()
// 前一行有文本
if (pre.text().length != 1) {
this.setRange(pre.get(0))
li.remove()
e.preventDefault()
} else {
// 無文本
pre.text(li.text())
this.setRange(pre.get(0), true)
li.remove()
e.preventDefault()
}
}
} else if (isZero()) {
if (li.hasClass('number')) {
li.removeClass('number')
li.attr('index', '')
this.enterKeyup()
li.text(li.text().substr(1))
e.preventDefault()
}
}
}
}
}