中文輸入法中游標跟隨能力觸發的瀏覽器事件探究

来源:https://www.cnblogs.com/zakum/archive/2022/10/13/16788790.html
-Advertisement-
Play Games

:::tip 最近在著手騰訊文檔的輸入體驗優化,在其中有一個不起眼的小需求引起了我的註意,並順便研究了一些事件監聽機制相結合的特點,特此記錄一下填坑過程。 ::: 模擬游標跟隨 大部分的主流輸入法都有這樣一個特性,在輸入中文時,可以通過左右方向鍵控制游標,移動至輸入區中任意兩個字元之間的位置,用戶接 ...



:::tip
最近在著手騰訊文檔的輸入體驗優化,在其中有一個不起眼的小需求引起了我的註意,並順便研究了一些事件監聽機制相結合的特點,特此記錄一下填坑過程。
:::

模擬游標跟隨

大部分的主流輸入法都有這樣一個特性,在輸入中文時,可以通過左右方向鍵控制游標,移動至輸入區中任意兩個字元之間的位置,用戶接下來的字元輸入將在游標處直接插入。

由於騰訊文檔的渲染的畫布是完全自主實現的,為了在體驗上與普通可編輯畫布保持一致,我們需要自己來模擬這一游標的移動行為。

首先,我們需要確定的是輸入法中的模擬游標進行更新的時機。經試驗,用戶在進行中文輸入時,若使用了方向鍵移動游標,將會觸發游標的移動行為。因此,首先要解決的是使用合適的事件監聽來捕獲這一行為,從而進行更新。既然是對輸入框的行為進行模擬,自然而然的,我們首先想到的是輸入框觸發的監聽器。

瀏覽器輸入框對輸入的監聽機制

在瀏覽器對鍵盤的輸入規範中,將鍵盤輸入分為了直接輸入與間接輸入兩種。直接輸入將會觸發輸入框的 onInput 事件 (IE9 之前不支持該事件,只能用 onKeyUp 等鍵盤事件作為降級選擇)。而對於間接輸入,規範將事件監聽分為了 onCompositionStart, onCompositionUpdate, onCompositionEnd 三個部分。

而間接輸入的同時,中間態的寫入也會導致輸入框內容的變化,從而也會觸發 onInput 事件。因此在間接輸入中,事件的觸發次序為:onCompositionStart, onCompositionUpdate, onInput, onCompositionEnd

20210203173000

需要註意的是,若輸入完成時,輸入框的內容沒有發生變化,則 onChange 事件與 onCompositionEnd 事件都將不會被觸發。

中文輸入法在鍵入選詞的過程屬於間接輸入情況,此時中間文本不會直接落盤在輸入框內。而通過回車等按鍵退出中文輸入選詞後,中文文字將會落盤到輸入框,此時屬於直接輸入情況。

而我們需要關註的游標事件顯然是在間接輸入中獲取到的。在輸入法選詞游標左右移動時,由於內容不變,此時並不會觸發 onInput 事件,但是會觸發一次 onCompositionUpdate 事件,我們可以通過這個事件來判斷游標位置,重置畫布的游標位置。但最終我們並未使用這個事件做判斷器,原因在下麵會講到。

判斷當前游標的位置

解決了了游標的重置時機,接下來就該解決游標的位置判定了。由於 DOM 標準中並沒有直接獲取游標位置的方法,因此這一塊也需要我們自主實現。我的思路是,通過選取游標到輸入起始位置的字元串,判斷選中的字元串長度,即可知道游標當前位置相對於起始位置的偏移量,從而確定游標位置。

對於普通的 input 輸入框來說起始比較簡單,輸入框提供了 inputElement.selectionStart 屬性作為當前游標位置距離輸入起始點的偏移量,我們直接使用就可以了。但是對於 contentEditable=true 的 div 節點來說是沒有這一屬性的,我們得另想辦法。

根據之前寫 E2E 測試得來的靈感,我們可以模擬創建一個從當前游標位置到輸入起始位置的選區,通過判斷該選區的字元串長度即游標所在位置的偏移量。通過 window.getSelection() 方法能夠得到 Selection 對象,這是一個表示當前文本選區的對象,由於我們正處在輸入狀態中,因此該選區位置就在當前的輸入框中,從而能獲取到上面所需的偏移量。

const selection = window.getSelection();
// 確定輸入框在輸入態,存在選區
if (selection.rangeCount > 0) {
    const range = selection.getRangeAt(0);
    return range.endOffset;
}

獲取完游標位置,還需要在我們的畫布上重新設置回去。設置的思路其實是類似的,通過使用document.createRange方法新建一個選區範圍,其起始位置設置為需要移動的目標位置,然後移除選區,即可使游標落在目標位置了。

性能優化

之前說到在游標移動時的確會觸發一次onCompositionUpdate 事件。但是,onCompositionUpdate 事件是一個高頻的操作,每一次間接輸入時都會觸發,這會導致游標不斷地重置位置,帶來不必要的性能損失。

並且,onCompositionUpdate 事件的入參只有更新的中間字元串值,只能用來判斷輸入中間字元串是否發生變化。移動游標行為本身並不會導致字元串發生改變,但反過來,使字元串不發生改變的操作一定是移動游標操作這一說法並不成立。因此,儘管移動游標會觸發該事件,但我們仍然沒有有效的手段去判斷是輸入法中的游標移動導致的事件觸發。

那麼,之前用很大篇幅講過游標變動的本質實際上是選區變化,那麼,輸入法觸發的游標移動會不會給輸入框發出選區變更通知呢?很不幸,目前絕大多數的輸入法都是不支持的。並且由於游標移動被視為輸入法內部的行為,因此在輸入框中游標所進行的移動,不會有事件主動拋出。因此,輸入框中的選區變更事件 onSelectionChange 事件也無法被觸發。

既然輸入框中的事件監聽無法準確判斷游標的移動,我們只能退而求其次,從更低層次的邏輯,通過監聽鍵盤的按鍵輸入來嘗試還原這一行為了。優化思路是這樣的,觸發游標跟隨的時機規則為:用戶輸入時,若使用了左方向鍵移動游標,將會開啟游標跟隨的能力,隨著輸入不斷更新的游標位置,直到游標再次被移動到末尾位置結束。由於中文輸入時按下左方向鍵的行為是一個低頻操作,這樣一來,大部分的輸入操作都不需要執行判斷並重置游標,提高普通輸入下的性能表現。

附上最終的判斷邏輯吧:

20210207174544

那麼,如何獲取並判斷用戶輸入時的按鍵信息呢?當然是使用更第一層級的事件介面 KeyboardEvent 了。

鍵盤輸入事件對中文輸入法的支持

KeyboardEvent 在低層級下提示用戶與一個鍵盤按鍵的交互是什麼,不涉及這個交互的上下文含義。一般來說當你需要處理文本輸入的時候,應當使用上節所說的輸入框監聽事件代替。例如當用戶使用其他方式輸入文本時,如平板電腦的手寫系統等,鍵盤事件可能不會觸發。

KeyboardEvent 對象描述了用戶與鍵盤的交互。 每個事件都描述了用戶與一個按鍵(或一個按鍵和修飾鍵的組合)的單個交互;事件類型 keydown,keypress 與 keyup 用於識別不同的鍵盤活動類型。

鍵盤輸入事件的設計思路與間接輸入的鉤子類似,瀏覽器中對於鍵盤輸入同樣分為 onKeyDown, onKeyPress, onKeyUp 三個階段的事件觸發,分別對應按鍵不同的行為觸發時機。(註:onKeyPress 事件高度依賴設備支持,所以儘量不要使用該鉤子)

這三個事件都傳入了 KeyboardEvent 入參,幫助我們瞭解當前執行該事件時觸發的按鍵信息。MDN 上該入參具有如下屬性支持:

20210203204752

在文檔規範中,我們可以發現許多對問題的解決十分有用的新屬性,例如 event.isComposing 屬性用於判斷當前是否會觸發 onCompositionUpdate 事件,event.code 用於判斷與鍵盤佈局與輸入狀態無關的當前按鍵輸入,獲取中文輸入中的按鍵輕而易舉。我們可以利用這兩個狀態幫助我們完成按鍵監聽與事件觸發。

兜底方案支持

之前說過, KeyboardEvent 是一個十分依賴軟硬體支持的事件,不僅需要瀏覽器的能力支持,與輸入法甚至鍵盤類型都有關係。經試驗後發現,這些新屬性在許多瀏覽器與輸入法的組合中都無法通過onKeyDown正確獲取,在 Windows 下部分中文輸入法甚至都無法支持 event.key 屬性。為了達到最大的相容性,在兜底的方法下,僅能用 event.keyCode 這種已經被 deprecated 的方法來勉強替代使用了。

兜底方案的使用問題就此解決了嗎?並沒有。中文拼音的輸入中間字元是系統無法識別的。在 Windows 桌面應用程式對鍵盤輸入規範中,我們發現 Windows 將所有未識別的設備輸入都設置為 VK_PROCESSKEY 229,瀏覽器的 event.keyCode 復用了這一規範,因此在中文輸入過程中,無論按下什麼按鍵,返回的 event.keyCode 永遠是 229。

網上對於該問題的解決方案都是建議使用 onKeyUp 代替 onKeyDown。但首先,這不滿足對於一個要求實時體現輸入的游標移動操作要求。第二,使用 onKeyUp 會有更多的問題,在 Windows 下進行中文輸入時,由於不同的輸入法回調 onKeyUp 的實現不同,該事件可能會被觸發一次或兩次,要麼全為 229,要麼一次為 229,另一次為正確的 key(對,說的就是你,搜狗)。為了避免我們去不斷去填五花八門的第三方輸入法實現的坑,兜底方案採用了當檢測到輸入了未識別的按鍵時,也啟用游標跟隨能力。

結語

一套操作下來,這套中文輸入法下游標跟隨的功能算是完美實現了。回顧一下我們解決這個問題所趟過的坑,實際上也反映著瀏覽器 JS DOM 標準在不斷進化,不斷補足歷史遺留的坑點。當然,它還遠遠稱不上完美,仍然存在大量的能力缺失,如我們在這個問題中遇到的判斷游標偏移量的解決方案,本質上還是一種 hack。而擴展 JS 的能力邊界,使其變得更強大,更好用,這正是我們作為前端開發人員需要努力的方向。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:高性能、大容量、低成本、強穩定性,廣告業務需要的Ta都有 本文分享自華為雲社區《廣告業務存儲神器:華為雲GaussDB for Redis》,作者: GaussDB 資料庫。 一、從需求場景說起,什麼是RTA廣告業務? 在互聯網時代,媒體平臺逐漸成為廣告業務的主體,而作為廣告主的企業往往每年需 ...
  • Mysql雙機主從搭建 一、規劃說明 主節點: IP:192.168.1.146 系統:Centos7.6 版本:MySQL-5.7.38 mysql賬戶密碼:root/Admin_2022 同步賬戶:mysync/Admin_2022 主機名:m1 已關閉防火牆,配置阿裡源,連接互聯網 從節點: ...
  • 本文介紹瞭如何從Mac OS X Catalina將IPA文件上傳到App Store的處理方法,對大家解決問題具有一定的參考價值,需要的朋友們下麵隨著小編來一起學習吧! 問題描述 我剛剛在Apple Developer門戶中創建了一個應用程式,現在我想將從Phonegap創建的IPA文件上傳到Ap ...
  • 在上一篇文章Flutter(六):Flutter_Boost接入現有原生工程(iOS+Android)中介紹了Flutter_Boost的接入方法,這一篇將介紹Flutter自帶的接入方法。 新建工程 1.新建工程 1.使用Xcode新建flutter_demo_ios(模擬已有工程) 2.使用An ...
  • vue實現功能 單選 取消單選 全選 取消全選 代碼部分 <template> <div class=""> <h1>全選框</h1> <center> <button @click="checkAnti">反選</button> <table border="1px"> <tr> <!-- 全選框 ...
  • 本人水平有限,如有疏漏或者不正確部分,請大佬指正。 一.Vuex概述 1.1組件之間共用數據的方式 父向子傳值:v-bind 屬性綁定子向父傳值:v-on 事件綁定兄弟組件之間共用數據:eventBUs* $on 接收數據的那個組件* $emit 發送數據的那個組件 特點:適合小範圍使用 1.2Vu ...
  • mounted() { document.body.onresize = function() { // ctrl+滑鼠中鍵滾輪 禁止伸縮頁面 document.body.style.zoom = 1 / window.devicePixelRatio // document.documentEle ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 最近接了個項目,採用uniapp的nvue開發安卓和ios端+小程式端,第一次開發nvue,對於css佈局這塊,還是踩了很多坑。以及一些uniapp的Api在nvue中也無法使用。文章中也收錄了一些我在項目中使用的一些方法,比如富文本解析 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...