前言
在輸入編輯的業務場景中,可能會需要在光標當前的位置或附近顯示提示選項。
比如社交評論中的@user
功能,要確保提示的用戶列表總是出現在@
字符右下方,又或者是在自定義編輯器中 autocomplete 語法提示,都需要獲取光標當前的位置作為參照點。
兩種位置
對於 WEB 開發來講,當我們提到某某元素的位置,通常是指這個元素相對於父級或文檔的像素單位坐標。而對於輸入框中光標,就有了額外的區分。
相對於內容
相對於內容,光標位於第幾個字符之后,姑且稱之為字符位置吧。
相對於UI
相對於UI,也就是跟普通頁面元素一樣的像素位置了。
插入或替換內容
在前言提到的場景中,也有在光標位置處插入內容的需求,比如對選取文字加粗text => <strong>text</strong>
等。
textarea
textarea
元素可以很容易獲取到選擇的一段文字的起止位置。如果當前沒有選擇文字,則兩個位置值都為光標右側字符的索引,從 0 開始。
// 開始位置
textarea.selectionStart
// 結束位置
textarea.selectionEnd
|
對於加粗功能,有了起止位置,就能獲取到選擇的文字內容,然后對內容進行替換。
由於textarea
不能包含子元素,只有純文本,所以基於textarea
實現加粗只能像用 Markdown 標記語法實現。
var selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd)
textarea.setRangeText(
'**'+ selectedText +'**')
|
textarea.setRangeText(text: String)
把選中的文字替換為其他內容。
contenteditable
也可能我們會使用contenteditable
屬性把一個元素變為可編輯元素。而上面所用的屬性和函數都是普通元素所沒有的,所以要換一種姿勢實現。
還是以加粗功能為例。
// 獲取文檔中選中區域
var range = window.getSelection().getRangeAt(0)
var strongNode = document.createElement('strong')
// 選中區域文本
strongNode.innerHTML = range.toString()
// 刪除選中區
range.deleteContents()
// 在光標處插入新節點
range.insertNode(strongNode)
|
基於contenteditable
的可編輯元素,其中的內容均為子元素,文本為textNode
,加粗使用 HTML 元素,插入或替換是對元素的操作。
如果想使用操作內容的思路實現會比較麻煩,因為可以獲取到的起止位置是基於子元素的。
<div contenteditable>hello<strong>你好</strong><big>w</big>orld</div>
|
假如選中的文字是你好wor
,調用相關 API 的輸出如下。
// 當前在文檔中選擇的文本,document 和 window 都有這個函數
// var selection = document.getSelection()
var selection = window.getSelection()
selection.anchorNode
// 你好
selection.anchorOffset
// 0
selection.focusNode
// orld
selection.focusOffset
// 2
// 或者使用 Range
var range = selection.getRangeAt(0)
range.startContainer
// 你好
range.startOffset
// 0
range.endContainer
// orld
range.endOffset
// 2
|
最終可以獲取到起止元素以及選中區域在開始元素內容中的字符位置和在結束元素內容中的字符位置。
其中的起止元素均為textNode
類型,通過parentNode
獲取到包裹元素。
range.startContainer.parentNode
// <strong>你好</strong>
range.endContainer.parentNode
// <div contenteditable>...</div>
|
需要注意的是通過
Selection
和Rang
獲取到起止位置是有方向之分的,從左向右選擇和從右向左選擇得到的值是正好相反的。
基於光標像素位置創建內容
這里就要開始用像素位置,同樣分為兩種實現來講。
contenteditable
可編輯元素獲取光標像素位置就像textarea
獲取光標的字符位置一樣簡單。
var range = window.getSelection().getRangeAt(0)
range.getBoundingClientRect()
// { width, height, top, right, bottom, right }
|
這么具體的尺寸值,實現自動完成真是 So easy!
textarea
textarea
其中的內容都是純文本,在 DOM 中不存在相關的對象,對於像素位置就得另作他想了。
基於行高和字體大小計算
// 1.獲取光標結束位置
var end = textarea.selectionEnd
// 2.通過匹配光標之前文本中的換行符計算所在行
var row = textarea.value.substring(0, end).match(/\r\n|\r|\n/).length
// 3.計算 top,行高 * 行數 + 上填充 + 邊框寬度
var top = lineHeight * (row + 1) + paddingTop + borderWidth
// 4.獲取光標左側的文本
var leftText = textarea.value.split(/\r\n|\r|\n/)[row]
// 5.影響一段文字所占寬度的因素太多,除字體大小、中英文、符號、字符間距等,還有字體、瀏覽器、系統等客觀因素
// var left = ...
|
這個方案的思路是沒問題的,但是考慮所有問題的成本太高。
雖然可以創建測試元素去計算文本寬度,但這個方案本身是從嚴謹的角度出發的。與其混在一塊,直接用取巧的辦法更簡單.
鏡像元素
文本不支持定位?那我創建 DOM 好了。
// 光標位置
var end = textarea.selectionEnd
// 光標前的內容
var beforeText = textarea.value.slice(0, end)
// 光標后的內容
var afterText = textarea.value.slice(end)
// 對影響 UI 的特殊元素編碼
var escape = function(text) {
return text.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, '<br>')
}
// 創建鏡像內容,復制樣式
var mirror = '<div class="'+ textarea.className +'">'
+
escape(beforeText)
+
'<span id="cursor">|</span>'
+
escape(afterText)
+
'</div>'
// 添加到 textarea 同級,注意設置定位及 zIndex,使兩個元素重合
textarea.insertAdjacentHTML(
'afterend', mirror)
// 通過鏡像元素中的假光標占位元素獲取像素位置
var cursor = document.getElementById('cursor')
cursor.getBoundingClientRect()
// { width, height, top, right, bottom, right }
|
End
最后悄悄說一句,以上內容不兼容低版本 IE,但是 IE 畢竟主場運行,有些 API 反而是其他瀏覽器所沒有的。就上面提到的案例來說,低版本 IE 也有對應的 API 可用。
真是不想在 IE 上去浪費精力了,索性不提。
原文路徑:https://imys.net/20161125/cursor-offset-at-input.html