一個數字鍵盤引發的血案——移動端H5輸入框、游標、數字鍵盤全假套件實現

為啥要寫假鍵盤?

還是輸入框、游標全假的假鍵盤?

手機自帶的不用非得寫個假的,吃飽沒事幹吧?

裝逼?炫技?

寶寶也是被逼的,寶寶也很委屈~.~

問題產生背景

移動端H5項目需求點:

進入某頁面自動彈出帶小數點的數字鍵盤,並且自帶輸入驗證,比如金額——只能輸入數字和小數點,並且只能輸入一位小數點、小數位不超過2位,且輸入前驗證不合法就不讓輸入、(UE特加功能——定製游標顏色>.<簡直是反人類的需求)。細分如下:

  • 進入相關頁面,輸入框自動獲取焦點
  • 鍵盤自動彈出
  • 彈出帶小數點的數字鍵盤
  • 數字輸入前自動驗證,只能輸入一個小數點,小數位數不超過2位,超過就不能繼續輸入
  • 如果游標在第一位,此時鍵入的是.,則自動放入0再插入.

實現方案擬定

1. 基於input + 手機自帶鍵盤實現方案

(1)針對功能點1,可以給 input 設置屬性 autofocus , 輸入框就能自動聚焦。 輕鬆搞定

(2)針對功能點2 ,給input設置屬性 autofocus 會自動聚焦但是鍵盤並不會自動彈出;

必須手動點擊輸入框鍵盤才會彈出; 於是在進入頁面的時候用js觸發click或者foucus,發現鍵

盤也不會自動彈出,延時click、focus也沒能彈出;那麼只有最後一種方案——就是讓NA端提

供讓鍵盤彈出的方法。 純前端無法搞定,需要NA端協助/,或者找PM砍掉自動彈鍵盤的需求

>.<(勉強能夠接受)

(3)針對功能點3,彈數字鍵盤的方法可以設置 type = "number" 或者type = "tel"; 前者

在Andriod可以彈出數字鍵盤在ios端只能彈全鍵盤,後者在Android和ios彈出的都是數字鍵

盤,但是!!坑爹的,彈出的數字鍵盤沒有小數點!(我的華為榮耀9倒是很給力的給我彈了

個帶小數點的數字鍵盤,不容易啊啊) 只能選擇type = "number",勉強能接受ios彈全鍵盤吧

(4)針對功能點4, 設置type = "number",發現可以不停的輸入小數點啊啊

啊啊看著真的要瘋了,第一次輸入小數點也不能自動變成0.

圖1 原生input type=number 效果

這時候聰明的你一定想到要使用事件監聽鍵入的字元,在輸入之前進行判斷,然後決定

是否放入輸入框。

你肯定又會開心的想到一堆可能有用的事件:onkeydown,onkeyup,onchange,oninput,

onpropertychange,textInput。

路漫漫其修遠兮啊~經過不斷嘗試之後仍然發現很多問題。

  • onkeyup——雖然每增加刪除字元都會觸發,但增加字元的時候是值輸入之後才觸發,無法做到輸入前驗證;
  • onchange——是在內容改變(兩次內容有可能相等)且失去焦點時觸發,也無法做到輸入前驗證。
  • onpropertychange——onchange事件在內容改變(兩次內容有可能還是相等的)且失去焦點時觸發;即每增加或刪除一個字元就會觸發,通過js改變也會觸發該事件,但是該事件IE專有。
  • oninput——移動端很多手機不支持。

(只剩下onkeyup/textInput,還有一線希望剛芭蕾>.<。)

  • onkeyup——其事件有兩個相關屬性event.key和event.keyCode。event.key在我的華為榮耀9手機上都不生效(其他低版本手機可想而知)。但其還有一個屬性event.keyCode其在PC端的值是鍵入字元的ascii碼。但在手機端輸入任何數字或者小數點其值均為229(華為榮耀9測試),所以onkeyup也不能用。
  • ontextInput——在pc和移動端都支持!!!(功夫不負有心人)其event.data可以獲取到輸入的值。歡天喜地,舉國歡慶,啊哈哈~~

終於鬆了一口氣,只要能在輸入前獲取值就能驗證了呀。

自信滿滿的一口氣寫完驗證過程:

html

<input id="amount-input" autofocus type="number" @textInput="checkNumber" v-model="amount" require/>

js

checkNumber(event) { var key = event.data || ; if (key.search(/[0-9.]/) > -1) { var value = document.getElementById(amount-input).value; if (key === . && value.search(/./) > -1) { event.preventDefault(); } if (value.search(/.d{2}/) > -1) { event.preventDefault(); } } else { event.preventDefault(); }},

杯具再次發生了~~~~~我所期望的效果仍然沒有達到。

通過value獲取輸入框內所有字元失敗

發現 input type = number 取到的value只能是數值無法獲取輸入框里的所有字元。

也就是說如果輸入12.,通過value獲取到是12,只輸入.,value獲取到的是 空字元串,

獲取不到小數點。

這樣就無法判斷是否輸入小數點,因而不能判斷是否還能輸入小數點,那就還是能輸入無數個

小數點,問題依然得不到解決。

嘗試:

  • 使用VUE中雙向綁定的this.amount來獲取輸入的所有字元,發現this.amount獲取到的和value獲取值的情況相同。嘗試失敗。
  • 通過textInput獲取到的輸入值,自己維護一個字元數組。但是textInput在刪除時不會觸發,因而不能實時獲取input輸入框裡面的所有準確字元;而且由於無法獲取游標在input輸入框的具體位置而無法確定刪除的是哪個字元,因而字元數組無法準確維護。嘗試失敗。

(5)針對功能點5,功能4解決了,功能5是小case。。。

所以基於input + 手機自帶鍵盤實現方案要滿足以上需求難以實現。

2. 基於input + 假數字鍵盤實現方案

若是用假鍵盤加原生input輸入框,需要做到:

  • 禁用手機自帶鍵盤
  • 獲取Input輸入框中的內容

禁用手機自帶鍵盤,在沒有NA暴露的方法支持的情況下,可以設置Input的readonly屬性。這

樣的話輸入框也不能添加刪除字元了。

若在可以要NA端提供禁用手機自帶鍵盤的方法的前提下,要實現點擊假鍵盤輸入框能添加刪

除字元。

若是只從後面添加刪除,很容易實現,只需要將點擊鍵盤對應的字元拼接到 Input type=text

獲取到的value的後面,刪除同理。

但是要是游標不在最後一位,而是在中間

圖2 游標在數字中間示例圖

那麼當我們點擊假鍵盤添加或刪除字元的時候,如何能知道添加或刪除字元的位置呢。

也許需要獲取游標位置。

目前只有IE和火狐支持的document.selection,selectionStart可以獲取游標位置。

// 獲取游標位置function getCursortPosition (textDom) { var cursorPos = 0; if (document.selection) { // IE Support textDom.focus (); var selectRange = document.selection.createRange(); selectRange.moveStart (character, -textDom.value.length); cursorPos = selectRange.text.length; }else if (textDom.selectionStart || textDom.selectionStart == 0) { // Firefox support cursorPos = textDom.selectionStart; } return cursorPos;}

由於我們的是移動端H5開發項目,考慮兼容性,顯然以上方法不能兼容大部分的機型。

3. 輸入框、游標、數字鍵盤全假實現方案

以上兩種方案均難以實現,因此我只能大膽想像,要實現滿足以上需求的假鍵盤就得實現假輸

入框、假游標、假keyboard的一套裝備。這樣所有的元素我都能控制,上面的那些問題全

部可以解決。

雛形

若是實現只能從最後面增加刪除沒有游標的假鍵盤非常容易,只需要給每個鍵綁定一個click事

件,維護一個數組,每次從後面push或者pop就能維護輸入框中的內容。

圖3 只能從最後添加、刪除且沒有游標的效果圖

但是這樣跟正真的輸入框效果比體驗太差了。

難點

要實現體驗跟原生鍵盤一樣並且自帶輸入驗證的假鍵盤,難點主要在於:

  • 有游標,且游標閃動
  • 游標定位,點擊數字中間游標自動移過去
  • 根據游標的位置實現插入刪除
  • 失去焦點游標隱藏,點擊輸入框游標顯示並且彈出鍵盤

原生js實現

對於游標實現,創造一個元素設置背景色,可以控制它隱藏和出現。

對於「點擊數字中間游標自動移過去 」,可以每添加一個數字或者小數點就先加一個帶點擊事件的空元素space,再添加要輸入的字元。space是為了綁定一個點擊事件,告訴游標要移動到的位置。

//字元插入,在游標前插入字元function insert(value) { var span = document.createElement("span"); //創建包含值的元素 span.className = val; span.innerText = value; var space = document.createElement("span"); space.className = space; space.addEventListener(click, moveCursor); var cursor = document.getElementsByClassName(cursor)[0]; inputArea.insertBefore(space, cursor);//插入空列 inputArea.insertBefore(span, cursor);//插入值}

刪除時也是先刪除游標之前的數字字元,再刪除space元素。

//刪除元素function deleteElement() { setCursorFlash(); var cursor = document.getElementsByClassName(cursor)[0]; var n = 2; //兩個刪除動作 while(cursor.previousSibling && n > 0) { inputArea.removeChild(cursor.previousSibling ); n--; } if(getInputStr().search(/^.d*/) > -1) { insert(0); } if(getInputStr() === ){ //元素為空placeholder顯示 var placeHolder = document.getElementsByClassName(holder)[0]; placeHolder.className = holder; }}

通過chrome裡面元素審查可以看到添加刪除的過程。

圖4 添加、刪除、游標移動元素變化圖

每一個space元素都綁定一個click事件,用來移動游標,最右邊有個right-space可以用來放placeholder,也可以添加click事件,點擊時游標總是移到最後一位。

//移動游標位置function moveCursor(event) { var cursor = document.getElementsByClassName(cursor)[0];//獲取游標 if(event.currentTarget.className == right-space){ if(!cursor.nextSibling || cursor.nextSibling.nodeName == #text){ return; } else { var ele = cursor.nextSibling; inputArea.insertBefore(inputArea.lastElementChild, ele); inputArea.appendChild(cursor); } }else { var tempEle = event.currentTarget.nextSibling; // var nodeName = event.currentTarget.nextSibling.nodeName; // var cursor = document.getElementsByClassName(cursor)[0]; if(!tempEle || tempEle.nodeName == #text) { var temp = event.currentTarget.previousSibling; var ele = inputArea.replaceChild( event.currentTarget, cursor);//把游標替換成當前元素 inputArea.appendChild(ele); } else { var temp = event.currentTarget.nextSibling; var ele = inputArea.replaceChild( event.currentTarget, cursor);//把游標替換成當前元素 inputArea.insertBefore(ele, temp); } }}

從上面的GIF圖可以看出,游標始終只有一個而且有個定時任務。游標的閃動設置如下,使用原生的setInterval實現。

//設置游標定時任務function setCursorFlash() { //placeholder 隱藏 var placeHolder = document.getElementsByClassName(holder)[0]; placeHolder.className = holder hidden; var cursor = document.getElementsByClassName(cursor)[0]; var inputContainer = document.getElementsByClassName(input-container)[0]; cursor.className = "cursor"; var isShowCursor = true; inputContainer.focus(); showKeyBoard(); if (intervalId) { clearInterval(intervalId); } intervalId = setInterval(function() { isShowCursor = !isShowCursor; if (isShowCursor) { cursor.className = cursor; } else { cursor.className = cursor hidden; } }, 1000);}

最終使用原生js實現的帶輸入框、游標,keyboard的假數字鍵盤。

除了完成以上功能,還實現了輸入前驗證功能,為了跟接近真實輸入框表現,同時實現了點擊

輸入框獲取焦點、游標閃動、彈出鍵盤;失去焦點游標消失。

為什麼不使用jQuery?

一是因為,當前的H5項目沒有使用jQuery。

二是因為使用VUE之後很少需要直接操作DOM,少數方法自己實現更輕量,若是只為了使用

其一兩個方法而引入jQuery,會使得項目更重。

原生js實現效果

圖5 原生js實現輸入框、游標、鍵盤全假套件效果圖

源碼github.com/DaisyWang88/

手機掃碼驗證:

sandbox.runjs.cn/show/m (chrome插件url二維碼生成器GetCrx.cn)

由於移動端click事件有300毫秒延時,因此原生js實現的效果,有點不是很流暢。若使用原生

JS實現版的需要使fastclick或zepto的tap事件解決延時問題。

PS之前說『VUE本身解決300毫秒延時問題』,考證之後發現不對,給大家帶來困擾實在抱歉。

考證之後發現VUE的click事件都是原生的click並沒有處理這個延時。

為了不讓大家困擾,github上的demo已經使用fastClick解決了延時問題,(之前太懶了>.<)。現在原生的js實現效果也很順暢了。


VUE組件化

考慮到項目里有的應用場景有多個輸入框,當然輸入的時候只需要一個鍵盤,因此組件化的時

候將輸入框作為一個組件v-input,鍵盤作為一個組件v-keyboard。

輸入框和鍵盤的交互

交互圖如下:

圖6 VUE組件交互圖

考慮到本項目裡面存在一個頁面多個輸入框的場景,因此需要控制鍵盤與哪個輸入框配合使

用。

為了達到這樣的目的,採用「當點擊輸入框獲取焦點的時候,將當前v-input輸入框組件的實

例傳給v-keyboard鍵盤組件」的方式。

this.$refs.virtualKeyBoard.$emit(getInputVm, this.$refs.virtualInput);

如圖6 ,v-keyboard組件會監聽getInputVm事件,獲取v-input的實例。

鍵盤組件v-keyboard獲取到輸入框組件v-input的實例之後就可以根據鍵盤的點擊事件——添

加或刪除,操作輸入框組件v-input來放入或者刪除字元了。

這樣即使有多個輸入框,也方便控制鍵盤和輸入框之間的操作。

輸入框自動獲取焦點,鍵盤自動彈出

需求里要求進入某個頁面輸入框自動獲取焦點,鍵盤自動彈出。

  • 輸入框自動獲取焦點可以通過設置is-auto-focus來控制是否自動獲取焦點。

<v-input ref="virtualInput" v-model="amount" :placeholder="placeText" :is-auto-focus="true" @show-key-board="showKeyBoard"></v-input>

  • 要自動彈出鍵盤如圖6,需要在頁面實例化完成之後將相應的輸入框組件v-input的實例傳給鍵盤組件v-keyboard。

this.$refs.virtualKeyBoard.$emit(getInputVm, this.$refs.virtualInput);

鍵盤組間捕獲getInputVm事件之後獲取了相應輸入框的實例,同時自動彈出。

this.$on(getInputVm, function(obj) { this.refObject = obj; this.isShow = true;});

v-model支持

vue支持自定義v-model,子組件設置一個value 的 props。

props: { value: { type: String, default: , },}

在value改變的時候$emit一個input事件並把相應的值傳出去就可以實現v-model的雙向綁定了。this.getInputStr()是用來獲取輸入框中字元串的函數。

this.$emit(input, this.getInputStr());

效果如下:

圖7 vue版效果圖

源碼參見github.com/DaisyWang88/

總結

原生的input 設置type = number,想要做輸入前驗證控制小數點個數和小數位數等功能基本很

難實現,要在輸入前取到值也是存在各種兼容性問題,目前只有ontextInput在移動端能在輸入

前準確取到值,還有個關鍵的問題type = number的時候取到的value不包含小數點,導致輸入

前使用正則驗證幾乎無法實現;若是設置type= text 雖然能取到輸入框中所有字元,但是就無

法彈出數字鍵盤。要想使用原生input輸入小數,就必須有所取捨。

  • 要麼不做輸入前驗證,使用type = number ,可以輸入多個小數點,只在數值數值不合法的時候提示輸入不合法,但是只有android可以彈出數字鍵盤,IOS仍然彈出全鍵盤。用戶體驗可能差些。
  • 要麼使用type = text,雖然可以做到輸入前驗證(因為可以取到全部字元),但是所有機型都只會彈全鍵盤了,用戶體驗也一般。
  • 以上兩種都無法實現進入頁面鍵盤自動彈出,只能藉助NA提供的方法實現。
  • 如果你是強迫症癌晚期患者,用戶體驗之上者,那麼你就可以跟我一樣做個假鍵盤,這樣以上問題都不是問題。還可以添加附加功能,比如輸入的時候若在第一位輸入小數點的時候,前面自動補0;刪除的時候,若小數點在第一位前面自動補0;還可以定製游標顏色、鍵盤樣式等等。

很不幸,我就是一個強迫症癌晚期患者,目前實現的鍵盤套件改造成VUE組件已經成功在項目

中使用,有單輸入框的頁面,也有多輸入框的頁面,支持placeholder 和v-model。


推薦閱讀:

opengl/webgl 可以部分重繪嗎?
推動HTML5生態發展,Gospel還能做什麼?
手把手教你擼一個跑男動畫 順便抽絲剝繭CSS3動畫奧秘

TAG:vue | HTML5 | 前端开发 |