Blackbing Playground

react-taggable-input

最近碰到一個有趣的 UI,不曉得該怎麼命名,類似像 facebook 的 @ 或是 twitter 可以在文中 tag 標籤。我就稱它為 TaggableInput

文長,若您想直接看結論請 電梯往下

因為平常在 facebook, twitter 用的很習慣,看起來跟一般的 input 行為也沒有什麼不同,介面也很好學習。不過前端工程師一看到就知道案情不單純。首先 inputtextarea element 都只能接受純文字,如果用這兩個 element 要做到這個功能一定是不可能。以下是幾個類似的 library。

react-tagsinput


inspect 看一下就可以知道他的實作方式是在後面多一個 input element,輸入完就 append tag。但這只適合處理 tag,無法結合文字輸入,而且他的 trigger 目的其實是要將文字斷開,所以 trigger 點會是像 Space, , 這樣的字元。

react-mention

react-mention 剛看到時覺得有點訝異,這套竟然能在 inputtextarea 裡頭呈現不同的樣式,於是我很好奇的偷看他怎麼做的。

沒想到他竟然隨著編輯 textarea 動態產生一個 div,並且隨著不同的 tag 可以 apply 不同的樣式,稍微 trace 他的 source 發現他是利用 radium 這套 lib 動態算出樣式然後把 textarea 疊在一起。像是這樣。

這真是讓我太驚訝了,前端工程師真是創意無極限誒。原本我想要很快速地用這一個套件來實作這個 UI 功能,但這套的缺點是他跟 recommand list 的 UI 綁的太緊(我們有自己的 recommand list 與邏輯),而且這種功能通常都需要透過 ajax 來 query list 的部分,要客製化不太容易,而且用了一下覺得 style 的部分還是沒有很輕鬆。

draft-js mention

這套是 facebook 出品的,UI 做的很細膩,很多細節都處理完了,但我覺得不需要為了這個功能把 draft-js 整包拉進來用,所以先擱著。

contenteditable

最後我偷看了一下 facebook 怎麼做的(早就應該偷看了),結果我發現一絲曙光,contenteditable。這樣一切都 make sense 了。既可以 input,又可以 apply style。

can I use?

IE 11 以下都支援,那就開始實作吧。

需求

整理一下這個 input 所需要的功能:

  1. placeholder:placeholder。
  2. maxLength:字串長度。
  3. trigger: 按下某個符號會 trigger recommand list。
  4. Esc, Space: 要可以取消 trigger recommand list。
  5. onSubmit: 送出輸入資料。
  6. 要可以從 recommand list callback 回 input 並取代使用者輸入的字串。

總之,一般 <input /> element 的功能都要有,並且要可以 trigger 字元。

jsx

接下來定義一下 jsx 的介面:

1
2
3
4
5
6
7
8
9
10
11
<TaggableInput
className="input"
placeHolder="請輸入文字"
trigger="#"
maxLength={ 100 }
onKeyDown={ this.props.onKeyDown }
handleSubmit={ this.handleSubmit }
onTrigger={ this.handleTrigger }
onChange={ this.handleChange }
onSubmit={ this.handleSubmit }
/>

問題

原本以為 trigger 某一個字元應該不難,沒想到實作之後才發現問題一拖拉庫,這篇的心得其實重點在記錄遇到的問題們。

Key Event

首先 DOM Level 3 有一個 Input Event 可以處理當值改變的時候會觸發這個事件,而且查了一下 can i use input event 看起來也都沒問題。

那麼這跟 KeyPress, KeyUp 等 event 有什麼不同呢? 主要是 Keyboard 相關的 event 都是處理跟鍵盤有關的事件,但內容物的修改不只從 Keyboard 來,還有滑鼠,觸控,等等。Input Event 的優點就是可以處理任何 input value 的改變。

看起來很美好,於是我就一路寫下去感覺沒什麼太大的問題,沒想到開始測試 IE 之後才發現問題大條了,IE 系列竟然不支援 contenteditable 的 Input Event。查了一下還蠻多災情的:

回頭重新看 MDN Input Event 才發現他真的有提到 IE 不支援 contenteditable 的 input Event。

DOMCharacterDataModified

胡亂查資料的時候看到 DOMCharacterDataModified ,覺得好像有一絲曙光,仔細一查才知道他屬於 Mutation Event,但已經 Deprecated,取而代之的是 Mutation Observer。但 Mutation Observer 依然不支援 IE10(含以下),相關討論可以參考 Stack overflow 的討論 contenteditable change events

以下節錄討論的內容:

Consider using MutationObserver. These observers are designed to react to changes in the DOM, and as a performant replacement to Mutation Events.

Pros:

Fires when any change occurs, which is difficult to achieve by listening to key events as suggested by other answers. For example, all of these work well: drag & drop, italicizing, copy/cut/paste through context menu.
Designed with performance in mind.
Simple, straightforward code. It’s a lot easier to understand and debug code that listens to one event rather than code that listens to 10 events.
Google has an excellent mutation summary library which makes using MutationObservers very easy.
Cons:

Requires a very recent version of Firefox (14.0+), Chrome (18+), or IE (11+).
New API to understand
Not a lot of information available yet on best practices or case studies

總之 MutationObserver 是未來的曙光,想要解決所有可能的 Input 問題,包含拖拉,複製貼上,滑鼠操作,觸控,右鍵選單等等等等。但這位成員有點太新了,API 也有很大部分的改變,IE 的 support 也不完整,部分瀏覽器還需要 prefix,blablabla….所以先擱著備用。

Keydown/KeyUp/KeyPress

那麼我用老方法總可以吧?KeyDown/Keyup event 可以拿到 KeyCode,感覺土法煉鋼還是有可能辦得到的?於是我就土法煉鋼也是刻出了一個版本。結果問題又來了…

KeyDown

首先 KeyDown 會在按下任何按鍵時觸發,但 KeyDown 時還不會拿到 change 之後的結果,因此 KeyDown 的處理時機無法達到目的。

KeyUp

KeyUp 的問題在於每個按鍵都會被觸發,包括 MetaCtrlShift,但當使用者按下 Shift+# 之後的行為有兩種可能:

  1. KeyUp # -> key 拿到 3
  2. Keyup Shift -> key 拿到 Shift

這在實作上就會遇到很大的問題,因為 KeyUp 的順序會造成取到的 key 值不同。

KeyCode 229

另外在 windows 上開啟中文輸入法之後按下 # 會拿到 KeyCode 229,查了一下也是災情慘重:

有興趣的話可以在 windows 打開測試 keycode 的網頁,切換英數輸入法之後輸入 #,會印出

keydown keyCode=16 which=16 charCode=0
keydown keyCode=51 (3) which=51 (3) charCode=0
keypress keyCode=35 (#) which=35 (#) charCode=35 (#)
keyup keyCode=51 (3) which=51 (3) charCode=0

開啟注音輸入法之後輸入 #,會印出

keydown keyCode=16 which=16 charCode=0

#keydown keyCode=229 which=229 charCode=0
textInput data=#
keyup keyCode=16 which=16 charCode=0

主要原因是因為在 windows 上用輸入中英文的習慣是按下 shift+按鍵,按照 spec 來說,輸入法開啟狀態時,回應 229 其實是沒錯的。在 Mac 反而沒這個問題,因為你必須切換輸入法才能按下半形的 #

react-taggable-input

最後終於推出 react-taggable-input,如此一來我們就能在 input 時塞任何的 html 了(劇情急轉直下)。

那麼最後怎麼處理 trigger 的問題?

value change

主要概念就是「在 keyup 之後從目前游標往前找最近的 trigger 字元」,而不是想辦法在 keydown keyup 時檢查是否是有 trigger 字元。

嗯,概念看起來很簡單,但我可是拐了好多彎才想到啊。

當然其中還有一些比較特別的處理:

  • 利用 CompositionEvent API來偵測使用者正在開啟輸入法模式。
  • 利用 Selection Api 偵測和操作使用者選取範圍的內容修改。
  • 取得 contenteditable 的游標位置。
  • Mac 可以直接按下全形的 # 來輸入,不用切換輸入法。
  • 由於在 KeyUp 處理,因此理論上支援 IE 系列瀏覽器。

其他細節有興趣的人請直接看 source code 吧,

DEMO


http://blog.blackbing.net/react-taggable-input/

工商服務時間

鉅亨網推出 股市 Talk 可以設定自己常用的自選股,讓你可以邊看股票邊討論股票,還有新聞,影音等,還有超強大的線圖,堪稱是目前網頁版最強的看盤軟體。

看看我們一個小功能都可以執著成這樣你就知道我們有多細膩了。

Reference