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

百度外賣大前端技術團隊發表於2017-12-28

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

為啥要寫假鍵盤?

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

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

裝逼?炫技?

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

一個數字鍵盤引發的血案——移動端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.'

一個數字鍵盤引發的血案——移動端H5輸入框、游標、數字鍵盤全假套件實現
圖1 原生input type=number 效果

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

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

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

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

  • 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的後面,刪除同理。但是要是游標不在最後一位,而是在中間

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

 圖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就能維護輸入框中的內容。

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

 圖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裡面元素審查可以看到新增刪除的過程。

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

圖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/mvjrca…(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。

輸入框和鍵盤的互動

互動圖如下:

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

 圖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());
複製程式碼

效果如下:

原始碼參見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。

相關文章