JavaScript 獲取輸入時的光標位置及場景問題

前言

在輸入編輯的業務場景中,可能會需要在光標當前的位置或附近顯示提示選項。
比如社交評論中的@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>

需要注意的是通過SelectionRang獲取到起止位置是有方向之分的,從左向右選擇和從右向左選擇得到的值是正好相反的。

基於光標像素位置創建內容

這裏就要開始用像素位置,同樣分爲兩種實現來講。

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 }
原文地址:http://imys.net/20161125/cursor-offset-at-input.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章