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('**' |
textarea.setRangeText(text: String)
把選中的文字替換為其他內容。
contenteditable
也可能我們會使用contenteditable
屬性把一個元素變為可編輯元素。而上面所用的屬性和函式都是普通元素所沒有的,所以要換一種姿勢實現。
還是以加粗功能為例。
// 獲取文件中選中區域var range = window.getSelection().getRangeAt(0)var strongNode = document.createElement('strong')// 選中區域文字strongNode.innerHTML = range.toString()// 刪除選中區 |
基於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 // 0selection.focusNode // orldselection.focusOffset // 2// 或者使用 Rangevar range = selection.getRangeAt(0)range.startContainer // 你好range.startOffset // 0range.endContainer // orldrange.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 } |