js控制input框内光标位置(setSelectionRange详解)

问题描述

前段时间碰到一个需求:在表单中有一个字段叫金额,用户希望点击该输入框后(focus),能够自动为其金额数字后加上“万元”两个字。

虽然这个需求可以通过其他的设计方式规避(例如在文本框后加入“万元”等),但是,既然碰到了问题,肯定还是希望能够研究一下技术解决方式。

对这个需求进行抽象,其实需要完成的任务就是:通过js来控制输入框内光标的位置。要完成这个任务,需要介绍一个input元素的方法:
HTMLInputElement.setSelectionRange()

setSelectionRange

介绍

在MDN上可以找到setSelectionRange()的官方介绍。其官方解释如下:

The HTMLInputElement.setSelectionRange() method sets the start and end positions of the current text selection in an <input> element.

翻译过来就是:首先,setSelectionRange()方法是作用在input元素上的,其次,这个方法可以为当前元素内的文本设置备选中范围(selection)。简单来说,就是可以通过设置起始于终止位置,来选中一段文本中的一部分。值得一提的是,在新版中,该方法还接受一个可选参数,这个参数指定的选择的方向。

使用方式

其使用方式如下:

inputElement.setSelectionRange(selectionStart, selectionEnd, [optional] selectionDirection);
  • selectionStart:第一个被选中的字符的序号(index),从0开始。
  • selectionEnd:被选中的最后一个字符的前一个。换句换说,不包括index为selectionEnd的字符。
  • selectionDirection:选择的方向。可选值为forward、backward或none。

来做一个简单的实例(在线浏览),当选中一个文本框时,文本框内的文字将会被选中。可以使用点击(click)输入框的方式,也可以使用tab切换焦点的方式,其效果如下:

效果图

实现代码如下
备注:本文中事件监听未使用兼容写法,读者可自行补全

<form>
  <label>这个input在focus时,内部文本会被选中</label>
  <input id="test" placeholder="万元" value="这是一段测试文本">
</form>
body {
  background-color: #f6f6f6;
}

form {
  padding: 30px;
}

input {
  display: block;
  margin-top: 10px;
  padding: 10px;
  font-size: 15px;
  color: #333333;
  border: 1px solid #555555;
  border-radius: 5px;
}
document.getElementById('test').addEventListener('focus', function() {
  changeCursorPos('test');
});

function changeCursorPos(inputId, pos) {
  var inpObj = document.getElementById(inputId);
  if (inpObj.setSelectionRange) {
    inpObj.setSelectionRange(0, inpObj.value.length);
  }
}

其中最重要的部分是

inpObj.setSelectionRange(0, inpObj.value.length);

这段代码会从第一个字符开始,到最后一个字符结束,选中输入框内的所有内容。

兼容性

那么,这个方法的兼容性怎么样呢?

Feature Chrome Edge Firefox(Gecko) Internet Explorer Opera Safari
Basic support 1.0 (Yes) 1.0 (1.7 or earlier) 9 8.0 (Yes)
selectionDirection 15 (Yes) 8.0 (8.0) ? ? (Yes)

可以看到,对于基本的功能,主流浏览器的常用版本(及以上)都有着较好的支持,而selectionDirection作为附加功能,虽然兼容性一般,但是不会影响对于该方法的使用。因此,可以在一定的场景下可以放心使用setSelectionRange()

控制光标位置

初步测试

以上介绍了inputElement.setSelectionRange()的含义与使用方式,然而,我的目标需求是控制输入框内的光标位置,而不是选中输入框内的部分文本。所以,上面介绍的这些和我的目标需求有关系么?
有。
回到上一部分的实例代码,我们可以将关键代码进行一定的修改

inpObj.setSelectionRange(0, inpObj.value.length-1);//修改了selectionEnd的值

修改之后的结果如下

这里写图片描述

可以明显看到,选中文本区域改变了。那么更进一步问自己一个问题,此时的光标实际停留的位置在哪——“文”字后面。因此,不难发现,代码成功将本该处于文本末端("本"字后面)的光标,移动至了其前一个字符“文”的后方。通过设置不同的selection,js能够成功将光标设置在不同的字符后方(其实是selection选区的后方)。
有了这个结论,可以进一步猜想,如果将选区的范围设置为0,那么则不会有文字被选中,同时,还可以控制光标的所处位置。显然,这个就是能够满足我需求的点。
为了测试这个功能,我写了一个简单的例子

代码如下

<form>
  <label>这个input在focus时,光标会移动至文本的开头处</label><input id="test" placeholder="万元" value="这是一段测试文本">
</form>

body {
  background-color: #f6f6f6;
}

form {
  padding: 30px;
}

input {
  display: block;
  margin-top: 10px;
  padding: 10px;
  font-size: 15px;
  color: #333333;
  border: 1px solid #555555;
  border-radius: 5px;
}
document.getElementById('test').addEventListener('focus', function() {
  changeCursorPos('test')
});

function changeCursorPos(inputId, pos) {
  var inpObj = document.getElementById(inputId);
  if (inpObj.setSelectionRange) {
    inpObj.setSelectionRange(0, 0);
  }
}

测试浏览器firefox(chrome请使用tab来切换焦点,具体原因后半部分会进一步解释)。实现结果与预期一致,光标被定位在了文本的最前方。

实现

<form>
  <label>自动添加“万元”单位的input</label>
  <input id="money" name="money" placeholder="万元">
</form>
body {
  background-color: #f6f6f6;
}

form {
  padding: 30px;
}

input {
  display: block;
  margin-top: 10px;
  padding: 10px;
  font-size: 15px;
  color: #333333;
  border: 1px solid #555555;
  border-radius: 5px;
}
document.getElementById('money').addEventListener('focus', function(e) {
  e.preventDefault();
  var val = this.value,
    len = val.length;
  if (val.indexOf('万元') !== -1) {
    pos = len - 2;
    changeCursorPos('money', pos);
  } else {
    $(this).val(val + '万元');
    pos = len;
    changeCursorPos('money', pos);
  }
});

function changeCursorPos(inputId, pos) {
  var inpObj = document.getElementById(inputId);
  if (inpObj.setSelectionRange) {
    inpObj.setSelectionRange(pos, pos);
  } else {
    console.log('不兼容该方法');
  }
}

BUG fix

理论上来说,我已经完成了目标需求,在firefox下点击输入框或用tab、chrome下使用tab都可以实现功能。但是,我在上面也提到了,chrome中,只能使用tab,如果你用点击输入框的方式进行测试,会发现,这个方法失效了,光标仍然处于文本的末尾。
造成这个问题的原因是chrome存在的一个bug:setSelectionRange() for input/textarea during onFocus fails when mouse clicks
这个bug似乎是由于chrome中默认的事件处理顺序引起的,有人提到

WebKit and Blink handle tasks for mousedown in the following order:

  1. Focus
  2. Selection
    The order looks reversed in other browsers

chrome默认的selection操作将会覆盖focus中的js操作代码。为了解决这个问题,第一个想到的就是阻止浏览器默认行为

e.prevenDefault();
//return false;

然而,尝试之后发现,阻止浏览器默认行为在这个问题上并不生效。需要寻求其他方法。

最初的解决方法

解决这个问题的第一个思路就是,将changeCursorPos()这个方法的启动时间延迟,最好能够在浏览器默认行为之后。这个实现非阻塞有异曲同工之处,因此,可以使用定时器setTimeout来改变其在队列中的顺序

setTimeout(function(){
    changeCursorPos('money', pos);
}, 0);

针对这个改动,只需要将原js代码中的

changeCursorPos('money', pos);

全部替换为

setTimeout(function(){
    changeCursorPos('money', pos);
}, 0);

其效果可以在线观看

然而,我在测试时发现,这个方法存在以下两个重要问题:

  • 效果较差。光标会先处于文本尾部,在跳至文本开头,对用户显示不友好。
  • 失效。更重要的问题是,即使使用setTimeout也不能保证将changeCursorPos操作最后执行,可以发现,在测试中,时常会出现其失效的情况。

解决失效问题

要解决失效问题,其实就是要保证将changeCursorPos的执行顺序添加至最后。需要了解,在鼠标点击与tab切换时,这两个操作之间的区别。

在tab切换时,相当于调用了inputElement.focus(),或者准确地说,在使用setSelectionRange()时两者的操作结果相同。而当使用鼠标点击选择输入框时,不仅会触发focus监听,还会触发一个click监听,而且通过测试可以发现,click事件触发晚于focus事件。

因此,如果在click监听中也添加changeCursorPos操作,就可以保证该操作不会被chrome的默认行为覆盖掉。

html与css不变,js代码如下

//为input添加一个click监听,保证changeCursorPos在chrome默认focus事件之后执行
document.getElementById('money').addEventListener('click', function(e) {
  var val = this.value;
  var len = val.length;
  if (val.indexOf('万元') !== -1) {
    pos = len - 2;
    setTimeout(function() {
      changeCursorPos('money', pos);
    }, 0);
  }
  else {
    $(this).val(val + '万元');
    pos = len;
    setTimeout(function() {
      changeCursorPos('money', pos);
    }, 0);
  }
});

//保留focus监听,确保tab的正确使用
document.getElementById('money').addEventListener('focus', function(e) {
  var val = this.value;
  var len = val.length;
  if (val.indexOf('万元') !== -1) {
    pos = len - 2;
    setTimeout(function() {
      changeCursorPos('money', pos);
    }, 0);
  } else {
    $(this).val(val + '万元');
    pos = len;
    setTimeout(function() {
      changeCursorPos('money', pos);
    }, 0);
  }
});

function changeCursorPos(inputId, pos) {
  var inpObj = document.getElementById(inputId);
  if (inpObj.setSelectionRange) {
    inpObj.setSelectionRange(pos, pos);
  } else {
    console.log('不兼容该方法');
  }
}

点击查看实例

然而,再来看看之前碰到的两个问题:

  • 效果较差。光标会先处于文本尾部,在跳至文本开头,对用户显示不友好。
  • 失效。更重要的问题是,即使使用setTimeout也不能保证将changeCursorPos操作最后执行,可以发现,在测试中,时常会出现其失效的情况。

可见,上一部分代码已经解决了失效的问题,保证了功能的实现。然而,这个方案还不完美,其效果差的问题仍然没有解决。因此,还需要找一个更完美的实现方案。

最终的解决方案

最终的解决方案的思路如下:

  1. 通过文档的按键监听来判断是使用tab操作还是鼠标点击操作,并设置标志位
  2. 当触发focus监听时,判断操作方式,如果focus事件的来源为tab操作则转执行changeCursorPos,否则使其失去焦点
  3. 在click监听中手动触发focus事件,并将设置标志,模拟tab行为

代码如下

<form>
  <label>解决chrome中点击input的bug的方案</label>
  <input id="exception" placeholder="exception">
  <input id="money" name="money" placeholder="万元">
</form>
body {
  background-color: #f6f6f6;
}

form {
  padding: 30px;
}

input {
    display: block;
    margin-top: 10px;
    padding: 10px;
    font-size: 15px;
    color: #333333;
    border: 1px solid #555555;
    border-radius: 5px;
  }
var tab = false;
document.addEventListener('keydown', function(e) {
  if (e.keyCode == 9) {
    tab = true;
  }
});
document.getElementById('exception').addEventListener('focus', function() {
  tab = false;
});
document.getElementById('money').addEventListener('click', function() {
  tab = true;
  this.focus();
});

document.getElementById('money').addEventListener('focus', function() {
  if (tab) {
    var val = this.value,
      len = val.length;
    if (val.indexOf('万元') !== -1) {
      pos = len - 2;
      setTimeout(function() {
        changeCursorPos('money', pos);
      }, 0);
    } else {
      $(this).val(val + '万元');
      pos = len;
      setTimeout(function() {
        changeCursorPos('money', pos);
      }, 0);
    }
  } else {
    this.blur();
  }
  tab = false;
});

function changeCursorPos(inputId, pos) {
  var inpObj = document.getElementById(inputId);
  if (inpObj.setSelectionRange) {
    inpObj.setSelectionRange(pos, pos);
  } else {
    console.log('不兼容该方法');
  }
}

在线演示

可以看到,在演示代码中,使用了tab变量作为标志,代码复用性较低,不太好,在实际项目中可以使用一些闭包或模块方式来进行处理,做成一个更加通用的功能。此处抛砖引玉,主要是展示实现的思路。

总结

setSelectionRange()方法可以帮助我们很容易的选中文本中的某一部分内容。同时,活用该方法也可以实现设置光标位置的功能。然而,chrome中存在的一个小bug导致该功能在鼠标点击时失效。文中研究了修复该bug的一些方法。然而作为抛砖引用,还是期待更多简便与解决方案。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,756评论 25 707
  • 表单基础知识 在HTML中,表单是由 元素来表示的,而在JS中,表单对应的则是HTMLFormElement类型。...
    oWSQo阅读 904评论 0 1
  • 随着互联网的日益发达,淘宝、京东、亚马逊、唯品会、聚美优品等等的平台崛起。同时也对传统的实体经济起到了不小的冲击,...
    68云购阅读 297评论 0 0
  • 近年来,塔罗占卜越来越流行,朋友圈里鱼龙混杂的占卜师,你们真的分辨的清吗? 第一 什么是塔罗牌 什么是塔罗牌? “...
    落落是个占卜师阅读 767评论 0 0
  • 我女儿当然并不叫女儿,我认识她的时候,她叫甜甜。现在依旧叫甜甜,她的名字很甜,人也长的甜。而我就一直喊她女儿。至于...
    巴黎在燃烧阅读 250评论 0 0