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

爲啥要寫假鍵盤?html

仍是輸入框、光標全假的假鍵盤?前端

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

裝逼?炫技?node

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

問題產生背景

移動端H5項目需求點:ios

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

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

實現方案擬定

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

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

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

必須手動點擊輸入框鍵盤纔會彈出; 因而在進入頁面的時候用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/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。

輸入框和鍵盤的交互

交互圖以下:

圖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。

相關文章
相關標籤/搜索