一個數字鍵盤引發的血案——移動端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.
這時候聰明的你一定想到要使用事件監聽鍵入的字元,在輸入之前進行判斷,然後決定
是否放入輸入框。
你肯定又會開心的想到一堆可能有用的事件: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的後面,刪除同理。
但是要是游標不在最後一位,而是在中間
那麼當我們點擊假鍵盤添加或刪除字元的時候,如何能知道添加或刪除字元的位置呢。
也許需要獲取游標位置。
目前只有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就能維護輸入框中的內容。
但是這樣跟正真的輸入框效果比體驗太差了。
難點
要實現體驗跟原生鍵盤一樣並且自帶輸入驗證的假鍵盤,難點主要在於:
- 有游標,且游標閃動
- 游標定位,點擊數字中間游標自動移過去
- 根據游標的位置實現插入刪除
- 失去焦點游標隱藏,點擊輸入框游標顯示並且彈出鍵盤
原生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裡面元素審查可以看到添加刪除的過程。
每一個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實現效果
源碼https://github.com/DaisyWang88/js-keyboard
手機掃碼驗證:
http://sandbox.runjs.cn/show/mvjrcagy (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。
輸入框和鍵盤的交互
交互圖如下:
考慮到本項目裡面存在一個頁面多個輸入框的場景,因此需要控制鍵盤與哪個輸入框配合使
用。
為了達到這樣的目的,採用「當點擊輸入框獲取焦點的時候,將當前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());
效果如下:
源碼參見https://github.com/DaisyWang88/VUE-keyboard。
總結
原生的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動畫奧秘