效果圖和實現的功能
實現的效果圖如下,主要實現的功能有
- 表情的插入
- 插入話題之後部分文字選中
- 文字框高度自適應
- 傳送訊息,獲取傳送框中的純文字內容
- placeholder的實現
- 輸入文字的計數皮膚
程式碼地址傳送門 程式碼實現Vue
靈感來源
“你想做的一定有人做了,你一定不是第一個遇到這個問題的人”——這句話對80%(二八分佈)的人是有效的,我也從中獲益不少。
我的第一份參考案例是qq空間的說說釋出框&&webqq的訊息傳送框,從中的收穫有以下幾點
- 可以使用
div+contenteditable
實現訊息傳送框 - 在Chrome中使用
button
標籤來高亮被@的使用者,在Firefox中使用img
標籤來高亮被@的使用者(絕妙)這裡不同標籤的使用很講究,考慮了瀏覽器相容性。 - 插入話題之後部分文字選中,提高使用者體驗,這裡還參考了張鑫旭老師的部落格_新浪微博插入話題後部分文字選中的js實現 第二份參考案例是掘金的動態釋出框,收穫如下
- 右下角顯示還能夠輸入的字數
placeholder
的實現
實現的一些細節
contenteditable
我在拜讀張鑫旭老師的文章翻譯-你必須知道的28個HTML5特徵、竅門和技術的時候第一次接觸到可以通過div+contenteditable
替代textarea
實現一個編輯框。之後再閱讀了div模擬textarea文字域輕鬆實現高度自適應發現了這個屬性的強大之處。
另外想要網頁上的元素能夠高亮,首先你需要一個可以被賦予CSS的HTML標籤選中它,然後去改變這個標籤才能夠完成這個任務,傳統的釋出框使用textarea
,內嵌標籤極其困難,可以說是不行,但是div
不同,內嵌標籤是家常便飯,掘金和qq空間的成功案例就不用多說。
關於contenteditable
屬性的特性可以去上面的兩個連結中查閱,總之是會把設定了這個屬性的標籤和裡面的子標籤都設定為可編輯屬性。
palceholder的實現
像input
和textarea
這些標籤自帶placeholder
屬性,但是div
沒有,要實現就需要通過JS和CSS來模擬。
通過兩層div
實現,在外層通過監聽編輯框中是否存在文字來選擇是否展示placeholder
,placeholder
通過偽元素和絕對定位實現,脫離標準文件流浮於編輯框之上
<div
class="edit-panel"
:class="{'show-placeholder' : showPlaceholder}"
:placeholder="placeholder"
>
<div contenteditable="true" ref="editor" class="editor"></div>
<span class="count" :class="{'font-red':textCount < 0}">{{ textCount }}</span>
</div>
複製程式碼
.edit-panel {
position: relative;
width: 100%;
height: auto;
font-size: 14px;
line-height: 20px;
border: 1px solid;
}
.show-placeholder::before {
content: attr(placeholder);
position: absolute;
top: 4px;
left: 8px;
color: #555;
pointer-events: none;
}
複製程式碼
皮膚計數
於上面placeholder
實現異曲同工
.edit-panel .count {
position: absolute;
color: #555;
right: 1rem;
bottom: 0.5rem;
user-select: none;
pointer-events: none;
}
複製程式碼
文字框高度自適應
這裡只需要設定min-height
和max-height
就行
插入表情
插入表情的時候不能想當然的使用dom操作插入一個img
標籤,這裡對比一下掘金實現的功能和qq空間實現的功能。我發現掘金插入表情的時候輸入框會閃爍一下,而qq空間的不會。我合理的猜想掘金是通過dom操作來插入表情的,然後是記錄了插入之前的range
物件,在插入之後還原range
這樣就不會丟失游標位置,range
物件是用來控制和獲取當前游標選取的內容的。詳細參考MDN——Range。qq空間則是通過其他的方式來插入表情,類似於其他的富文字編輯器,在插入的時候不會閃爍一下。我這裡去探索了一下,使用下面的這個方法插入,通過建立一個dom片段
,然後用range
物件插入到編輯框中,這樣也不會丟失游標。
這裡需要注意,只要操作DOM的方式不對,游標位置就會錯位,良好的使用者體驗就是游標位置保持不變
function insertHtmlAtCaret (html) {
var sel, range, frag
if (window.getSelection) {
sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0)
range.deleteContents()
var el = document.createElement('div')
el.innerHTML = html
frag = document.createDocumentFragment()
var node
var lastNode
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node)
}
range.insertNode(frag)
if (lastNode) {
range = range.cloneRange()
range.setStartAfter(lastNode)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
}
}
}
複製程式碼
插入話題之後部分文字選中
這裡需要去理解一下range
物件中四個重要的屬性startContainer
、startOffset
、endContainer
、endOffset
。在不同情況下指代的意思是不一樣的,我這裡就是輕描淡寫的提一下,我理解的不是很透徹就不誤導大家了。
addTopic (event) {
this.$refs.editor.focus()
insertHtmlAtCaret('#')
insertHtmlAtCaret('請輸入一個話題')
insertHtmlAtCaret('#')
var range = window.getSelection().getRangeAt(0)
console.log(range)
range.selectNodeContents(range.startContainer.childNodes[range.startOffset - 2])
}
複製程式碼
獲取純文字內容
直接使用textContent
是不行的,這樣獲取不到img
標籤中的內容,加上之後會用button
或者input[type=button]
去實現一些高亮功能,這裡需要自己去定義一個獲取純文字內容的方法。我實現的比較簡單
function getDomValue (elem) {
var res = ''
Array.from(elem.childNodes).forEach((child) => {
if (child.nodeName === '#text') {
res += child.nodeValue
} else if (child.nodeName === 'BR') {
res += '\n'
} else if (child.nodeName === 'BUTTON') {
res += getDomValue(child)
} else if (child.nodeName === 'IMG') {
res += child.alt
} else if (child.nodeName === 'DIV') {
res += '\n' + getDomValue(child)
}
})
return res
}
複製程式碼
注意點和展望
- 富文字編輯框需要考慮很多XSS的問題,賦值貼上時標籤的過濾等
- 瀏覽器相容性的問題,
range
物件操作的不同,contenteditable
屬性表現出來的問題 - 統一插入文字的樣式,否則輸入文字的時候會沿用前面的樣式
- @使用者高亮顯示的瀏覽器相容問題
感謝
我能完成這個功能要感謝@炒飯君的幫助。實習期間給力很大的幫助。