實現的效果圖以下,主要實現的功能有css
代碼地址傳送門 代碼實現Vue
html
「你想作的必定有人作了,你必定不是第一個遇到這個問題的人」——這句話對80%(二八分佈)的人是有效的,我也從中獲益很多。html5
個人第一份參考案例是qq空間的說說發佈框&&webqq的消息發送框,從中的收穫有如下幾點node
div+contenteditable
實現消息發送框button
標籤來高亮被@的用戶,在Firefox中使用img
標籤來高亮被@的用戶(絕妙)這裏不一樣標籤的使用很講究,考慮了瀏覽器兼容性。placeholder
的實現
我在拜讀張鑫旭老師的文章翻譯-你必須知道的28個HTML5特徵、竅門和技術的時候第一次接觸到能夠經過div+contenteditable
替代textarea
實現一個編輯框。以後再閱讀了div模擬textarea文本域輕鬆實現高度自適應發現了這個屬性的強大之處。 另外想要網頁上的元素可以高亮,首先你須要一個能夠被賦予CSS的HTML標籤選中它,而後去改變這個標籤纔可以完成這個任務,傳統的發佈框使用textarea
,內嵌標籤極其困難,能夠說是不行,可是div
不一樣,內嵌標籤是屢見不鮮,掘金和qq空間的成功案例就不用多說。git
關於contenteditable
屬性的特性能夠去上面的兩個連接中查閱,總之是會把設置了這個屬性的標籤和裏面的子標籤都設置爲可編輯屬性。github
像input
和textarea
這些標籤自帶placeholder
屬性,可是div
沒有,要實現就須要經過JS和CSS來模擬。 經過兩層div
實現,在外層經過監聽編輯框中是否存在文字來選擇是否展現placeholder
,placeholder
經過僞元素和絕對定位實現,脫離標準文檔流浮於編輯框之上web
<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
就行app
插入表情的時候不能想固然的使用dom操做插入一個img
標籤,這裏對比一下掘金實現的功能和qq空間實現的功能。我發現掘金插入表情的時候輸入框會閃爍一下,而qq空間的不會。我合理的猜測掘金是經過dom操做來插入表情的,而後是記錄了插入以前的range
對象,在插入以後還原range
這樣就不會丟失光標位置,range
對象是用來控制和獲取當前光標選取的內容的。詳細參考MDN——Range。qq空間則是經過其餘的方式來插入表情,相似於其餘的富文本編輯器,在插入的時候不會閃爍一下。我這裏去探索了一下,使用下面的這個方法插入,經過建立一個dom片斷
,而後用range
對象插入到編輯框中,這樣也不會丟失光標。dom
這裏須要注意,只要操做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
}
複製代碼
range
對象操做的不一樣,contenteditable
屬性表現出來的問題我能完成這個功能要感謝@炒飯君的幫助。實習期間給力很大的幫助。