https://juejin.im/post/5a44c5eef265da432d2868f6

为啥要写假键盘?

还是输入框、光标全假的假键盘?

手机自带的不用非得写个假的,吃饱没事干吧?

装逼?炫技?

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

问题产生背景

移动端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/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。

作者:百度外卖大前端技术团队
链接:https://juejin.im/post/5a44c5eef265da432d2868f6
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一个数字键盘引发的血案——移动端H5输入框、光标、数字键盘全假套件实现的更多相关文章

  1. 一个由正则表达式引发的血案 vs2017使用rdlc实现批量打印 vs2017使用rdlc [asp.net core 源码分析] 01 - Session SignalR sql for xml path用法 MemCahe C# 操作Excel图形——绘制、读取、隐藏、删除图形 IOC,DIP,DI,IoC容器

    1. 血案由来 近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:1. 英文字母大小写2. 数字3. 越南文4. 一些特殊字符,如“&”,“- ...

  2. 转:一个Sqrt函数引发的血案

    转自:http://www.cnblogs.com/pkuoliver/archive/2010/10/06/1844725.html 源码下载地址:http://diducoder.com/sotr ...

  3. 一个Sqrt函数引发的血案(转)

    作者: 码农1946  来源: 博客园  发布时间: 2013-10-09 11:37  阅读: 4556 次  推荐: 41   原文链接   [收藏]   好吧,我承认我标题党了,不过既然你来了, ...

  4. 【转载】一个Sqrt函数引发的血案

    转自:http://www.cnblogs.com/pkuoliver/archive/2010/10/06/sotry-about-sqrt.html 源码下载地址:http://diducoder ...

  5. 一个Sqrt函数引发的血案

    源码下载地址:http://diducoder.com/sotry-about-sqrt.html 好吧,我承认我标题党了,不过既然你来了,就认真看下去吧,保证你有收获. 我们平时经常会有一些数据运算 ...

  6. 移动端H5页面解决软件键盘把页面顶起

    在input失去焦点的时候加上强制页面归位 window.scroll(0,0); 上代码 <input data-component="SearchInput" type= ...

  7. Replication的犄角旮旯(六)-- 一个DDL引发的血案(上)(如何近似估算DDL操作进度)

    <Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...

  8. Replication的犄角旮旯(七)-- 一个DDL引发的血案(下)(聊聊logreader的延迟)

    <Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...

  9. 一个无锁消息队列引发的血案(五)——RingQueue(中) 休眠的艺术

    目录 (一)起因 (二)混合自旋锁 (三)q3.h 与 RingBuffer (四)RingQueue(上) 自旋锁 (五)RingQueue(中) 休眠的艺术 (六)RingQueue(中) 休眠的 ...

随机推荐

  1. 深入理解java虚拟机(十二) Java 语法糖背后的真相

    语法糖(Syntactic Sugar),也叫糖衣语法,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语.指的是,在计算机语言中添加某种语法,这些语法糖虽然不会对语言 ...

  2. Maven项目常见错误

    一.Cannot change version of project facet Dynamic Web Module to 3.0. 和 One or more constraints have n ...

  3. Spring学习(一)——环境准备

            以前做的项目都是用.net开发的,以后准备迁移到java平台上,近期正好有个新项目要上马,所以调研下java相关技术.Spring作为java平台下的一个全栈框架, 其简洁优雅的设计和 ...

  4. python版本selenium定位方式(不止八种哦)

    除了大家熟知的8种定位方式之外 1.id定位:find_element_by_id(self, id_)2.name定位:find_element_by_name(self, name)3.class ...

  5. robotframework+jenkins分布式执行自动化测试用例

    http://blog.sina.com.cn/s/blog_53f023270101sc3w.html http://www.cnblogs.com/2test/p/5336842.html

  6. HashMap源码解析 非原创

    Stack过时的类,使用Deque重新实现. HashCode和equals的关系 HashCode为hash码,用于散列数组中的存储时HashMap进行散列映射. equals方法适用于比较两个对象 ...

  7. better-scroll在vue中的应用

    在我们日常的移动端项目开发中,处理滚动列表是再常见不过的需求了,以滴滴为例,可以是这样竖向滚动的列表,如图所示: 微信 —> 钱包—>滴滴出行”体验效果. 什么是 better-scrol ...

  8. jquery源码解析:代码结构分析

    本系列是针对jquery2.0.3版本进行的讲解.此版本不支持IE8及以下版本. (function(){ (21, 94)     定义了一些变量和函数,   jQuery = function() ...

  9. iOS开发之-Debug、Release、Archive、Profile、Analyze

    1,Debug和Release版本区别? 众所周知,我们进行iOS开发,在Xcode调试程序时,分为两种方式, Debug 和 Release ,在Target的Setting中相信大家应该看到很多选 ...

  10. css中设置background属性

    属性解释 background属性是css中应用比较多,且比较重要的一个属性,它是负责给盒子设置背景图片和背景颜色的,background是一个复合属性,它可以分解成如下几个设置项: backgrou ...