javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript快捷键库

JavaScript手写实现一个简单的快捷键库

作者:玛尔斯通

前端开发中,有时项目会遇到一些快捷键需求,比如绑定快捷键,展示快捷键,编辑快捷键等需求,所以本文就来用JavaScript手写一个简单的快捷键库吧

背景

前端开发中,有时项目会遇到一些快捷键需求,比如绑定快捷键,展示快捷键,编辑快捷键等需求,特别是工具类的项目。如果只是简单的绑定几个快捷键之类的需求,我们一般会通过监听键盘事件(如keydown 事件)来实现,如果是稍微复杂点的需求,我们一般都会通过引入第三方快捷键库来实现,比如常用的几个快捷键库mousetrap, hotkey-js等。

接下来,我将会通过对快捷键库mousetrap第一次提交的源码进行简单分析,然后实现一个简单的快捷键库。

前置知识

首先,我们需要了解一些快捷键相关的基础知识。比如,如何监听键盘事件?如何监听用户按下的按键?键盘上的按键有哪些?是如何分类的?只有知道这些,才能更好的理解mousetrap这种快捷键库实现的思路,才能更好地实现我们自己的快捷键库。

如何监听键盘事件

实现快捷键需要监听用户按下键盘按键的行为,那就需要使用到键盘事件API

常用的键盘事件有keydown, keyup,keypress事件。一般来说,我们会通过监听用户按下按键的行为,来判断是否要触发对应的快捷键行为。通常来说,在用户按下按键时,就会判断是否有匹配的绑定过的快捷键,即通过监听keydown事件来实现快捷键。

如何监听键盘上按下的键

我们可以通过键盘事件来监听用户按键行为。那如何知道用户具体按下了哪个/哪些按键呢?

比如,用户绑定的快捷键是s,那如何知道当前按下的按键是s?我们可以通过键盘事件对象keyboardEvent上的code, keyCode, key这些属性来判断用户当前按下的按键。

键盘按键分类

有些按键会影响其他按键按下后产生的字符。比如,用户同时按下了shift/按键,此时产生的字符是?,然而实际上如果只按shift按键不会产生任何字符,只按/按键产生的字符本应该是/,最终产生的字符?就是因为同时按下了shift按键导致的。这里的shift按键就是影响其他按键按下后产生字符的按键,这种按键被称为修饰键。类似的修饰键还有ctrl, alt(option), command(meta)。

除了这几个修饰键以外,其他的按键称为非修饰键

快捷键分类

常用的快捷键有单个键,键组合。有的还会用到键序列。

单个键

故名思义,单个键是只需要按下一个键就会触发的快捷键。比如常用的音视频切换播放/暂停快捷键Space,游戏中控制移动方向快捷键w,a,s,d等等。

键组合

键组合通常是一个或多个修饰键和一个非修饰键组合而成的快捷键。比如常用的复制粘贴快捷键ctrl+c,ctrl+v,保存文件快捷键ctrl+s,新建(浏览器或其他app)窗口快捷键ctrl+shift+n(command+shift+n)。

键序列

依次按下的按键称为键序列。比如键序列h e l l o,需要依次按下h,e,l,l,o按键才会触发。

mousetrap源码分析

以下将以mousetrap第一次提交的源码为基础进行简单分析,源码链接如下:bit.ly/3TdcK8u

简单来说,代码只做了两件事,即绑定快捷键监听键盘事件

代码设计和初始化

首先,给window对象添加了一个全局属性Mousetrap,使用的是IIFE(立即执行函数表达式)对代码进行封装。

该函数对外暴露了几个公共方法:

最后当window加载后立即执行init()函数,即执行初始化逻辑:添加键盘事件监听等。

// 以下为简化后的代码
window['Mousetrap'] = (function () {
  return {
    /**
     * 绑定快捷键
     * @param keys 快捷键,支持一次绑定多个快捷键。
     * @param callback 快捷键触发后的回调函数
     * @param action 行为
     */
    bind: function (keys, callback, action) {
      action = action || '';
      _bindMultiple(keys.split(','), callback, action);
      _direct_map[keys + ':' + action] = callback;
    },

    /**
     * 手动触发快捷键对应的回调函数
     * @param keys 绑定时的快捷键
     * @param action 行为
     */
    trigger: function (keys, action) {
      _direct_map[keys + ':' + (action || '')]();
    },

    /**
     * 给DOM对象添加事件,针对浏览器兼容性的写法
     * @param object
     * @param type
     * @param callback
     */
    addEvent: function (object, type, callback) {
      _addEvent(object, type, callback);
    },

    init: function () {
      _addEvent(document, 'keydown', _handleKeyDown);
      _addEvent(document, 'keyup', _handleKeyUp);
      _addEvent(window, 'focus', _resetModifiers);
    },
  };
})();

Mousetrap.addEvent(window, 'load', Mousetrap.init);

绑定快捷键

一般来说,快捷键库都会提供一个绑定快捷键的函数,比如bind(key, callback)。在mousetrap中,我们可以通过调用Mousetrap.bind()函数来实现快捷键绑定。

我们可以结合调用时的写法对Mousetrap.bind()函数进行分析。比如,我们绑定了快捷键ctrl+scommand+s,如下:Mousetrap.bind('ctrl+s, command+s', () => {console.log('保存成功')} )

bind(keys, callback, action)

由于bind()函数支持一次绑定多个快捷键(绑定时多个快捷键用逗号分隔),因此内部封装了_bindMultiple()函数用于处理一次绑定多个快捷键的用法。

window['Mousetrap'] = (function () {
  return {
    bind: function (keys, callback, action) {
      action = action || '';
      _bindMultiple(keys.split(','), callback, action);
      _direct_map[keys + ':' + action] = callback;
    },
  };
})();

_bindMultiple(combinations, callback, action)

该函数只是对绑定时传入的多个快捷键进行遍历,然后调用_bindSingle()函数依次绑定。

/**
 * binds multiple combinations to the same callback
 */
function _bindMultiple(combinations, callback, action) {
  for (var i = 0; i < combinations.length; ++i) {
    _bindSingle(combinations[i], callback, action);
  }
}

_bindSingle(combination, callback, action)

该函数是实现绑定快捷键的核心代码。

主要分为以下几部分:

/**
 * binds a single event
 */
function _bindSingle(combination, callback, action) {
  var i,
      key,
      keys = combination.split('+'),
      // 修饰键列表
      modifiers = [];

  // 收集修饰键到修饰键数组中
  for (i = 0; i < keys.length; ++i) {
    if (keys[i] in _MODIFIERS) {
      modifiers.push(_MODIFIERS[keys[i]]);
    }

    // 获取当前按键(修饰键 || 特殊键 || 普通按键(a-z, 0-9))的 key code,注意这里charCodeAt()的用法
    key = _MODIFIERS[keys[i]] || _MAP[keys[i]] || keys[i].toUpperCase().charCodeAt(0);
  }

  // 以 key code 为属性名,保存回调函数
  if (!_callbacks[key]) {
    _callbacks[key] = [];
  }

  // 如果之前有绑定过相同的快捷键,则移除之前绑定的快捷键
  _getMatch(key, modifiers, action, true);

  // 保存当前绑定的快捷键的回调函数/修饰键等数据到回调函数数组中
  _callbacks[key].push({callback: callback, modifiers: modifiers, action: action});
}

注意这里的_callbacks数据结构。假设绑定了以下快捷键:

Mousetrap.bind('s', e => {
  console.log('sss')
})
Mousetrap.bind('ctrl+s', e => {
  console.log('ctrl+s')
})

_callbacks值如下:

{
  // key code 作为属性名,属性值为数组,用于保存当前绑定的修饰键和回调函数等数据
  "83": [ // 83对应的是字符s的key code
    {
      modifiers: [],
      callback: e => { console.log('sss') }
      action: ""
    },
    {
      modifiers: [17], // 17对应的是修饰键ctrl的key code
      callback: e => { console.log('ctrl+s') }
      action: ""
    }
  ]
}

_getMatch(code, modifiers, action, remove)

从快捷键回调函数集合_callbacks中获取/删除已经绑定的快捷键对应的回调函数callback

function _getMatch(code, modifiers, action, remove) {
  if (!_callbacks[code]) {
    return;
  }

  var i,
      callback;

  // loop through all callbacks for the key that was pressed
  // and see if any of them match
  for (i = 0; i < _callbacks[code].length; ++i) {
    callback = _callbacks[code][i];

    if (action == callback.action && _modifiersMatch(modifiers, callback.modifiers)) {
      if (remove) {
        _callbacks[code].splice(i, 1);
      }
      return callback;
    }
  }
}

监听键盘事件

在初始化逻辑init()函数中给document对象注册了keydown事件监听。

⚠: 这里只分析keydown事件,keyup事件类似。

_addEvent(document, 'keydown', _handleKeyDown);

_handleKeyDown(e)

首先,会调用_stop(e)函数判断是否需要停止执行后续操作。如果需要则直接return。

其次,根据键盘事件对象event获取当前按下的按键对应的key code,并收集当前按下的所有修饰键的key code到修饰键列表_active_modifiers中。

最后,调用_fireCallback(code, modifers, action, e)函数,获取当前匹配的快捷键对应的回调函数callback,并执行。

function _handleKeyDown(e) {
  if (_stop(e)) {
    return;
  }

  var code = _keyCodeFromEvent(e);

  if (_MODS[code]) {
    _active_modifiers.push(code);
  }

  return _fireCallback(code, _active_modifiers, '', e);
}

_stop(e)

如果当前keydown事件触发时所在的目标元素是input/select/textarea元素,则停止处理keydown事件。

function _stop(e) {
  var tag_name = (e.target || e.srcElement).tagName;

  // stop for input, select, and textarea
  return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA';
}

_keyCodeFromEvent(e)

根据键盘事件对象event获取对应按键的key code

注意,这里并没有直接使用event.keyCode。原因是有些按键在不同浏览器中的event.keyCode值不一致,需要进行特殊处理。

function _keyCodeFromEvent(e) {
  var code = e.keyCode;

  // right command on webkit, command on gecko
  if (code == 93 || code == 224) {
    code = 91;
  }

  return code;
}

_fireCallback(code, modifiers, action, e)

获取当前匹配的快捷键对应的回调函数callback,并执行。

function _fireCallback(code, modifiers, action, e) {
  var callback = _getMatch(code, modifiers, action);
  if (callback) {
    return callback.callback(e);
  }
}

_getMatch(code, modifiers, action)

获取当前匹配的快捷键对应的回调函数callback

function _getMatch(code, modifiers, action, remove) {
  if (!_callbacks[code]) {
    return;
  }

  var i,
      callback;

  // loop through all callbacks for the key that was pressed
  // and see if any of them match
  for (i = 0; i < _callbacks[code].length; ++i) {
    callback = _callbacks[code][i];

    if (action == callback.action && _modifiersMatch(modifiers, callback.modifiers)) {
      if (remove) {
        _callbacks[code].splice(i, 1);
      }
      return callback;
    }
  }
}

_modifiersMatch(modifiers1, modifiers2)

判断两个修饰键数组中的元素是否完全一致。eg: _modifiersMatch(['ctrl', 'shift'], ['shift', 'ctrl'])

function _modifiersMatch(group1, group2) {
  return group1.sort().join(',') === group2.sort().join(',');
}

实现一个简单的快捷键库

结合前置知识和对mousetrap的源码的分析,我们可以很容易实现一个简单的快捷键库。

思路

总体思路和mousetrap几乎完全一样,只做两件事。即1. 对外提供bind()函数用于绑定快捷键,2. 内部通过添加keydown事件,监听键盘输入,查找与对应快捷键匹配的回调函数callback并执行。

mousetrap不同的是,这次将使用event.key属性来判断用户按下的具体按键,该属性也是规范/标准推荐使用的属性(Authors SHOULD use the key attribute instead of the charCode and keyCode attributes.)。

代码将使用ES6 class 语法,对外提供bind()函数用于绑定快捷键。

功能

支持绑定快捷键(单个键,键组合)。

实现

由于实现思路前文已经分析过,因此这里就不详细解释了,以下直接给出完整的源代码。

不过,代码有几点需要注意下:

/**
 * this is a mapping of keys that converts characters generated by pressing shift key
 * at the same time to characters produced when the shift key is not pressed
 *
 * @type {Object}
 */
var _SHIFT_MAP = {
  '~': '`',
  '!': '1',
  '@': '2',
  '#': '3',
  $: '4',
  '%': '5',
  '^': '6',
  '&': '7',
  '*': '8',
  '(': '9',
  ')': '0',
  _: '-',
  '+': '=',
  ':': ';',
  '"': "'",
  '<': ',',
  '>': '.',
  '?': '/',
  '|': '\\',
};

/**
 * get modifer key list by keyboard event
 * @param {KeyboardEvent} event - keyboard event
 * @returns {Array}
 */
const getModifierKeysByKeyboardEvent = (event) => {
  const modifiers = [];

  if (event.shiftKey) {
    modifiers.push('shift');
  }

  if (event.altKey) {
    modifiers.push('alt');
  }

  if (event.ctrlKey) {
    modifiers.push('ctrl');
  }

  if (event.metaKey) {
    modifiers.push('command');
  }

  return modifiers;
};

/**
 * get non modifier key
 * @param {string} shortcut
 * @returns {string}
 */
function getNonModifierKeyByShortcut(shortcut) {
  if (typeof shortcut !== 'string') return '';
  if (!shortcut.trim()) return '';

  const validModifierKeys = ['shift', 'ctrl', 'alt', 'command'];
  return (
    shortcut.split('+').filter((key) => !validModifierKeys.includes(key))[0] ||
    ''
  );
}

/**
 * check if two modifiers match
 * @param {Array} modifers1
 * @param {Array} modifers2
 * @returns {boolean}
 */
function checkModifiersMatch(modifers1, modifers2) {
  return modifers1.sort().join(',') === modifers2.sort().join(',');
}

/**
 * check if key match
 * @param {string} shortcutKey - shortcut key
 * @param {string} eventKey - event.key
 * @returns {boolean}
 */
function checkKeyMatch(shortcutKey, eventKey) {
  if (shortcutKey === 'space') {
    return eventKey === ' ';
  }

  return shortcutKey === (_SHIFT_MAP[eventKey] || eventKey);
}

/**
 * shortcut binder class
 */
class ShortcutBinder {
  constructor() {
    /**
     * shortcut list
     */
    this.shortcuts = [];

    this.init();
  }

  /**
   * init, add keyboard event listener
   */
  init() {
    this._addKeydownEvent();
  }

  /**
   * add keydown event
   */
  _addKeydownEvent() {
    document.addEventListener('keydown', (event) => {
      const modifers = getModifierKeysByKeyboardEvent(event);
      const matchedShortcut = this.shortcuts.find(
        (shortcut) =>
          checkKeyMatch(shortcut.key, event.key.toLowerCase()) &&
          checkModifiersMatch(shortcut.modifiers, modifers)
      );

      if (matchedShortcut) {
        matchedShortcut.callback(event);
      }
    });
  }

  /**
   * bind shortcut & callback
   * @param {string} shortcut
   * @param {Function} callback
   */
  bind(shortcut, callback) {
    this._addShortcut(shortcut, callback);
  }

  /**
   * add shortcut & callback to shortcut list
   * @param {string} shortcut
   * @param {Function} callback
   */
  _addShortcut(shortcut, callback) {
    this.shortcuts.push({
      shortcut,
      callback,
      key: this._getKeyByShortcut(shortcut),
      modifiers: this._getModifiersByShortcut(shortcut),
    });
  }

  /**
   * get key (character/name) by shortcut
   * @param {string} shortcut
   * @returns {string}
   */
  _getKeyByShortcut(shortcut) {
    const key = getNonModifierKeyByShortcut(shortcut);
    return key.toLowerCase();
  }

  /**
   * get modifier keys by shortcut
   * @param {string} shortcut
   * @returns {Array}
   */
  _getModifiersByShortcut(shortcut) {
    const keys = shortcut.split('+').map((key) => key.trim());
    const VALID_MODIFIERS = ['shift', 'ctrl', 'alt', 'command'];
    let modifiers = [];
    keys.forEach((key) => {
      if (VALID_MODIFIERS.includes(key)) {
        modifiers.push(key);
      }
    });

    return modifiers;
  }
}

调用

调用方法和mousetrap类似。以下仅列出部分测试代码,可以查看在线示例测试实际效果。

shortcutBinder.bind('ctrl+s', () => {
  console.log('ctrl+s');
});

shortcutBinder.bind('ctrl+shift+s', () => {
  console.log('ctrl+shift+s');
});

shortcutBinder.bind('space', (e) => {
  e.preventDefault();
  console.log('space');
});

shortcutBinder.bind('shift+5', (e) => {
  e.preventDefault();
  console.log('shift+5');
});

shortcutBinder.bind(`shift+\\`, (e) => {
  e.preventDefault();
  console.log('shift+\\');
});

shortcutBinder.bind(`f2`, (e) => {
  e.preventDefault();
  console.log('f2');
});

在线示例

CodePen: 手写一个简单的快捷键库

TODO

至此,我们已经实现了一个简单的快捷键库,可以满足常见的快捷键绑定相关的业务需求。当然,相对当前流行的几个快捷键库而言,我们实现的快捷键库比较简单,还有很多功能和细节有待实现和完善。以下列出待完成的几个事项,感兴趣的可以尝试实现下。

总结

通过学习mousetrap源码以及手写一个简单的快捷键库,我们可以学习到一些关于快捷键和键盘事件相关的知识。目的不是重复造轮子,而是通过日常业务需求,驱动我们去了解当前流行的常见快捷键库的实现思路,以便于我们更好地理解并实现相关业务需求。假如日后有展示、修改快捷键或者其他快捷键相关的需求,我们就可以做到胸有成竹,举一反三。

以上就是JavaScript手写实现一个简单的快捷键库的详细内容,更多关于JavaScript快捷键库的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文