完善的输入框监听方案:兼容、高效和组合输入友好

完善的输入框监听方案

keyup

监听输入框的输入,最原始的方法是使用keyup事件。
不使用change事件,它只会在输入框失去焦点后被触发。
此方式兼容性广,但效率较低,毕竟任意的按键都会触发该事件。

<input id="input" type="text" />

<script>
  document.querySelector('#input')
  .addEventListener('keyup', function() {
    console.log('value:', this.value);
  });
</script>

input

我们只希望当值发生变化后再触发监听,这样,input事件出现了。
它只会在输入框的值发生变化后被触发,不过IE8及以下不支持该事件。

网上很多人使用IE独有的propertychange事件,作为替代input的方案,这里不推荐。
一方面它会在任意属性值变化后被触发,没有专一性,不够语义,比较浪费。
二方面网上都是用jQuery等工具库操作,比较简单,而我们的目的是用原生代码实现。
三方面是不支持input事件的浏览器已经很少了,硬碰上了就用keyup对付。

<input id="input" type="text" />

<script>
  document.querySelector('#input')
  .addEventListener('input', function() {
    console.log('value:', this.value);
  });
</script>

接着上步,如何在不支持input事件时使用keyup事件呢?
直接检测事件不太靠谱,可以利用input在keyup之前发生的性质,巧妙的实现此功能。

<input id="input" type="text" />

<script>
  let inInputEvent = false;
  let input = document.querySelector('#input');
  
  input.addEventListener('keyup', function() {
    if (inInputEvent) {
      // You can remove keyup listener.
    } else {
      console.log('keyup:', this.value);
    }
  });
  input.addEventListener('input', function() {
    if (!inInputEvent) inInputEvent = true;
    console.log('input:', this.value);
  });
</script>

延迟函数

在搜索功能中,理想化的情景是当用户全部输入后,再立即执行搜索。
那么问题来了,如何在不需要用户点击搜索按钮的情况下,得知其过程的完成呢?没有办法。
虽然没有办法,但有优化的方式:假定用户每个单词的输入间隔,以此时间延迟执行搜索功能。

英文一般为 300ms ,中文可设置成 500ms 。

<input id="input" type="text" />

<script>
  let input = document.querySelector('#input');
  let trigger = createDelayFunction(console.log);
  
  input.addEventListener('input', function() {
    trigger(this.value);
  });

  function createDelayFunction(fn, timeout = 300) {
    let timeoutId = -1;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        fn.apply(null, args);
      }, timeout);
    }
  }
</script>

组合

中文、日文等需要借助输入法组合输入,即便是英文,现在也可借助组合输入进行选词等。
实际中,我们希望将用户组合输入完的一段文字,而不是每输入一个字母,算做一次输入的完成。

组合输入事件应运而生,常用的是compositionstart(组输开始)和compositionend(组输结束)事件。
结合组合事件不监听普通的输入,以及compositionstart发生在input事件之前,可以如此优化中文输入

<input id="input" type="text" />

<script>
  let inCompositionEvent = false;
  let input = document.querySelector('#input');
  
  input.addEventListener('input', function() {
    !inCompositionEvent && console.log('input', this.value);
  });
  input.addEventListener('compositionstart', function() {
    inCompositionEvent = true;
  });
  input.addEventListener('compositionend', function() {
    inCompositionEvent = false;
    console.log('composition', this.value);
  });
</script>

结合

最后是结合以上几步生成一个融合方法,代码加示例:

里面还做了些增强:
比如监听函数返回的是一个,移除这一步所加的所有事件的方法。
比如配置是否监听组合输入事件,因为好的搜索框会直接根据拼音开始搜索,无需等到汉字的形成。

代码使用ES6语法,需使用支持ES6的浏览器(Chrome最新版)或转码后才能使用,谅解

function listenInput(dom, callback, {
  timeout = 300,
  useCompositionEvent = true
} = {}) {
  let value = '';
  let inInputEvent = false;
  let inCompositionEvent = false;
  let trigger = createDelayFunction(valueChanged, timeout);

  // Return a function that can remove listeners added here.
  return enabledEvent(dom);

  function valueChanged(val) {
    if (val === value) {
      return ;
    } else {
      value = val;
    }

    callback(value, { dom: dom });
  }

  function enabledEvent(dom) {
    dom.addEventListener('keyup', keyup);
    dom.addEventListener('input', input);
    useCompositionEvent && dom.addEventListener('compositionstart', compositionstart);
    useCompositionEvent && dom.addEventListener('compositionend', compositionend);

    return function() {
      dom.removeEventListener('keyup', keyup);
      dom.removeEventListener('input', input);
      useCompositionEvent && dom.removeEventListener('compositionstart', compositionstart);
      useCompositionEvent && dom.removeEventListener('compositionend', compositionend);
    };

    function keyup() {
      if (inInputEvent) {
        dom.removeEventListener('keyup', keyup);
      } else {
        trigger(this.value);
      }
    }
    
    function input() {
      if (!inInputEvent) inInputEvent = true;
      if (!inCompositionEvent) trigger(this.value);
    }
    
    function compositionstart() {
      inCompositionEvent = true;
    }
    
    function compositionend() {
      inCompositionEvent = false;
      trigger(this.value);
    }
  }
}

function createDelayFunction(fn, timeout = 300) {
  let timeoutId = -1;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn.apply(null, args);
    }, timeout);
  }
}
阅读 875

推荐阅读
大罗成长笔记
用户专栏

大罗从正式入职的第一天开始的成长笔记...

0 人关注
4 篇文章
专栏主页