欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript手寫實(shí)現(xiàn)一個(gè)簡單的快捷鍵庫

 更新時(shí)間:2024年02月29日 08:53:00   作者:瑪爾斯通  
前端開發(fā)中,有時(shí)項(xiàng)目會(huì)遇到一些快捷鍵需求,比如綁定快捷鍵,展示快捷鍵,編輯快捷鍵等需求,所以本文就來用JavaScript手寫一個(gè)簡單的快捷鍵庫吧

背景

前端開發(fā)中,有時(shí)項(xiàng)目會(huì)遇到一些快捷鍵需求,比如綁定快捷鍵,展示快捷鍵,編輯快捷鍵等需求,特別是工具類的項(xiàng)目。如果只是簡單的綁定幾個(gè)快捷鍵之類的需求,我們一般會(huì)通過監(jiān)聽鍵盤事件(如keydown 事件)來實(shí)現(xiàn),如果是稍微復(fù)雜點(diǎn)的需求,我們一般都會(huì)通過引入第三方快捷鍵庫來實(shí)現(xiàn),比如常用的幾個(gè)快捷鍵庫mousetrap, hotkey-js等。

接下來,我將會(huì)通過對快捷鍵庫mousetrap第一次提交的源碼進(jìn)行簡單分析,然后實(shí)現(xiàn)一個(gè)簡單的快捷鍵庫。

前置知識

首先,我們需要了解一些快捷鍵相關(guān)的基礎(chǔ)知識。比如,如何監(jiān)聽鍵盤事件?如何監(jiān)聽用戶按下的按鍵?鍵盤上的按鍵有哪些?是如何分類的?只有知道這些,才能更好的理解mousetrap這種快捷鍵庫實(shí)現(xiàn)的思路,才能更好地實(shí)現(xiàn)我們自己的快捷鍵庫。

如何監(jiān)聽鍵盤事件

實(shí)現(xiàn)快捷鍵需要監(jiān)聽用戶按下鍵盤按鍵的行為,那就需要使用到鍵盤事件API

常用的鍵盤事件有keydown, keyup,keypress事件。一般來說,我們會(huì)通過監(jiān)聽用戶按下按鍵的行為,來判斷是否要觸發(fā)對應(yīng)的快捷鍵行為。通常來說,在用戶按下按鍵時(shí),就會(huì)判斷是否有匹配的綁定過的快捷鍵,即通過監(jiān)聽keydown事件來實(shí)現(xiàn)快捷鍵。

如何監(jiān)聽鍵盤上按下的鍵

我們可以通過鍵盤事件來監(jiān)聽用戶按鍵行為。那如何知道用戶具體按下了哪個(gè)/哪些按鍵呢?

比如,用戶綁定的快捷鍵是s,那如何知道當(dāng)前按下的按鍵是s?我們可以通過鍵盤事件對象keyboardEvent上的code, keyCode, key這些屬性來判斷用戶當(dāng)前按下的按鍵。

鍵盤按鍵分類

有些按鍵會(huì)影響其他按鍵按下后產(chǎn)生的字符。比如,用戶同時(shí)按下了shift/按鍵,此時(shí)產(chǎn)生的字符是?,然而實(shí)際上如果只按shift按鍵不會(huì)產(chǎn)生任何字符,只按/按鍵產(chǎn)生的字符本應(yīng)該是/,最終產(chǎn)生的字符?就是因?yàn)橥瑫r(shí)按下了shift按鍵導(dǎo)致的。這里的shift按鍵就是影響其他按鍵按下后產(chǎn)生字符的按鍵,這種按鍵被稱為修飾鍵。類似的修飾鍵還有ctrl, alt(option), command(meta)。

除了這幾個(gè)修飾鍵以外,其他的按鍵稱為非修飾鍵。

快捷鍵分類

常用的快捷鍵有單個(gè)鍵,鍵組合。有的還會(huì)用到鍵序列。

單個(gè)鍵

故名思義,單個(gè)鍵是只需要按下一個(gè)鍵就會(huì)觸發(fā)的快捷鍵。比如常用的音視頻切換播放/暫??旖萱ISpace,游戲中控制移動(dòng)方向快捷鍵w,a,s,d等等。

鍵組合

鍵組合通常是一個(gè)或多個(gè)修飾鍵和一個(gè)非修飾鍵組合而成的快捷鍵。比如常用的復(fù)制粘貼快捷鍵ctrl+c,ctrl+v,保存文件快捷鍵ctrl+s,新建(瀏覽器或其他app)窗口快捷鍵ctrl+shift+n(command+shift+n)。

鍵序列

依次按下的按鍵稱為鍵序列。比如鍵序列h e l l o,需要依次按下h,e,l,l,o按鍵才會(huì)觸發(fā)。

mousetrap源碼分析

以下將以mousetrap第一次提交的源碼為基礎(chǔ)進(jìn)行簡單分析,源碼鏈接如下:bit.ly/3TdcK8u

簡單來說,代碼只做了兩件事,即綁定快捷鍵監(jiān)聽鍵盤事件。

代碼設(shè)計(jì)和初始化

首先,給window對象添加了一個(gè)全局屬性Mousetrap,使用的是IIFE(立即執(zhí)行函數(shù)表達(dá)式)對代碼進(jìn)行封裝。

該函數(shù)對外暴露了幾個(gè)公共方法:

  • bind(keys, callback, action): 綁定快捷鍵
  • trigger(): 手動(dòng)觸發(fā)綁定的快捷鍵對應(yīng)的回調(diào)函數(shù)。

最后當(dāng)window加載后立即執(zhí)行init()函數(shù),即執(zhí)行初始化邏輯:添加鍵盤事件監(jiān)聽等。

// 以下為簡化后的代碼
window['Mousetrap'] = (function () {
  return {
    /**
     * 綁定快捷鍵
     * @param keys 快捷鍵,支持一次綁定多個(gè)快捷鍵。
     * @param callback 快捷鍵觸發(fā)后的回調(diào)函數(shù)
     * @param action 行為
     */
    bind: function (keys, callback, action) {
      action = action || '';
      _bindMultiple(keys.split(','), callback, action);
      _direct_map[keys + ':' + action] = callback;
    },

    /**
     * 手動(dòng)觸發(fā)快捷鍵對應(yīng)的回調(diào)函數(shù)
     * @param keys 綁定時(shí)的快捷鍵
     * @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);

綁定快捷鍵

一般來說,快捷鍵庫都會(huì)提供一個(gè)綁定快捷鍵的函數(shù),比如bind(key, callback)。在mousetrap中,我們可以通過調(diào)用Mousetrap.bind()函數(shù)來實(shí)現(xiàn)快捷鍵綁定。

我們可以結(jié)合調(diào)用時(shí)的寫法對Mousetrap.bind()函數(shù)進(jìn)行分析。比如,我們綁定了快捷鍵ctrl+scommand+s,如下:Mousetrap.bind('ctrl+s, command+s', () => {console.log('保存成功')} )

bind(keys, callback, action)

由于bind()函數(shù)支持一次綁定多個(gè)快捷鍵(綁定時(shí)多個(gè)快捷鍵用逗號分隔),因此內(nèi)部封裝了_bindMultiple()函數(shù)用于處理一次綁定多個(gè)快捷鍵的用法。

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)

該函數(shù)只是對綁定時(shí)傳入的多個(gè)快捷鍵進(jìn)行遍歷,然后調(diào)用_bindSingle()函數(shù)依次綁定。

/**
 * 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)

該函數(shù)是實(shí)現(xiàn)綁定快捷鍵的核心代碼。

主要分為以下幾部分:

  • 將綁定的快捷鍵combination拆分為單個(gè)鍵數(shù)組,然后收集修飾鍵到修飾鍵數(shù)組modifiers中。
  • key(key code)為屬性名,將當(dāng)前綁定的快捷鍵及其對應(yīng)的回調(diào)函數(shù)等數(shù)據(jù)保存到回調(diào)函數(shù)集合_callbacks中。
  • 如果之前有綁定過相同的快捷鍵,則調(diào)用_getMatch()函數(shù)移除之前綁定的快捷鍵。
/**
 * binds a single event
 */
function _bindSingle(combination, callback, action) {
  var i,
      key,
      keys = combination.split('+'),
      // 修飾鍵列表
      modifiers = [];

  // 收集修飾鍵到修飾鍵數(shù)組中
  for (i = 0; i < keys.length; ++i) {
    if (keys[i] in _MODIFIERS) {
      modifiers.push(_MODIFIERS[keys[i]]);
    }

    // 獲取當(dāng)前按鍵(修飾鍵 || 特殊鍵 || 普通按鍵(a-z, 0-9))的 key code,注意這里charCodeAt()的用法
    key = _MODIFIERS[keys[i]] || _MAP[keys[i]] || keys[i].toUpperCase().charCodeAt(0);
  }

  // 以 key code 為屬性名,保存回調(diào)函數(shù)
  if (!_callbacks[key]) {
    _callbacks[key] = [];
  }

  // 如果之前有綁定過相同的快捷鍵,則移除之前綁定的快捷鍵
  _getMatch(key, modifiers, action, true);

  // 保存當(dāng)前綁定的快捷鍵的回調(diào)函數(shù)/修飾鍵等數(shù)據(jù)到回調(diào)函數(shù)數(shù)組中
  _callbacks[key].push({callback: callback, modifiers: modifiers, action: action});
}

注意這里的_callbacks數(shù)據(jù)結(jié)構(gòu)。假設(shè)綁定了以下快捷鍵:

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

_callbacks值如下:

{
  // key code 作為屬性名,屬性值為數(shù)組,用于保存當(dāng)前綁定的修飾鍵和回調(diào)函數(shù)等數(shù)據(jù)
  "83": [ // 83對應(yīng)的是字符s的key code
    {
      modifiers: [],
      callback: e => { console.log('sss') }
      action: ""
    },
    {
      modifiers: [17], // 17對應(yīng)的是修飾鍵ctrl的key code
      callback: e => { console.log('ctrl+s') }
      action: ""
    }
  ]
}

_getMatch(code, modifiers, action, remove)

從快捷鍵回調(diào)函數(shù)集合_callbacks中獲取/刪除已經(jīng)綁定的快捷鍵對應(yīng)的回調(diào)函數(shù)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;
    }
  }
}

監(jiān)聽鍵盤事件

在初始化邏輯init()函數(shù)中給document對象注冊了keydown事件監(jiān)聽。

?: 這里只分析keydown事件,keyup事件類似。

_addEvent(document, 'keydown', _handleKeyDown);

_handleKeyDown(e)

首先,會(huì)調(diào)用_stop(e)函數(shù)判斷是否需要停止執(zhí)行后續(xù)操作。如果需要?jiǎng)t直接return。

其次,根據(jù)鍵盤事件對象event獲取當(dāng)前按下的按鍵對應(yīng)的key code,并收集當(dāng)前按下的所有修飾鍵的key code到修飾鍵列表_active_modifiers中。

最后,調(diào)用_fireCallback(code, modifers, action, e)函數(shù),獲取當(dāng)前匹配的快捷鍵對應(yīng)的回調(diào)函數(shù)callback,并執(zhí)行。

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)

如果當(dāng)前keydown事件觸發(fā)時(shí)所在的目標(biāo)元素是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)

根據(jù)鍵盤事件對象event獲取對應(yīng)按鍵的key code。

注意,這里并沒有直接使用event.keyCode。原因是有些按鍵在不同瀏覽器中的event.keyCode值不一致,需要進(jìn)行特殊處理。

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)

獲取當(dāng)前匹配的快捷鍵對應(yīng)的回調(diào)函數(shù)callback,并執(zhí)行。

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

_getMatch(code, modifiers, action)

獲取當(dāng)前匹配的快捷鍵對應(yīng)的回調(diào)函數(shù)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)

判斷兩個(gè)修飾鍵數(shù)組中的元素是否完全一致。eg: _modifiersMatch(['ctrl', 'shift'], ['shift', 'ctrl'])

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

實(shí)現(xiàn)一個(gè)簡單的快捷鍵庫

結(jié)合前置知識和對mousetrap的源碼的分析,我們可以很容易實(shí)現(xiàn)一個(gè)簡單的快捷鍵庫。

思路

總體思路和mousetrap幾乎完全一樣,只做兩件事。即1. 對外提供bind()函數(shù)用于綁定快捷鍵,2. 內(nèi)部通過添加keydown事件,監(jiān)聽鍵盤輸入,查找與對應(yīng)快捷鍵匹配的回調(diào)函數(shù)callback并執(zhí)行。

mousetrap不同的是,這次將使用event.key屬性來判斷用戶按下的具體按鍵,該屬性也是規(guī)范/標(biāo)準(zhǔn)推薦使用的屬性(Authors SHOULD use the key attribute instead of the charCode and keyCode attributes.)。

代碼將使用ES6 class 語法,對外提供bind()函數(shù)用于綁定快捷鍵。

功能

支持綁定快捷鍵(單個(gè)鍵,鍵組合)。

實(shí)現(xiàn)

由于實(shí)現(xiàn)思路前文已經(jīng)分析過,因此這里就不詳細(xì)解釋了,以下直接給出完整的源代碼。

不過,代碼有幾點(diǎn)需要注意下:

  • event.keyshift按鍵影響。比如,綁定的快捷鍵是shift+/,實(shí)際上在keydown事件對象eventevent.key的值是?,因此代碼里維護(hù)了這種特殊字符的映射_SHIFT_MAP,用于判斷用戶是否按下了這類特殊字符。
  • 有些特殊字符按鍵產(chǎn)生的字符(event.key)需要特殊處理,比如空格按鍵Space,按下后實(shí)際產(chǎn)生的字符(event.key)是' ',詳情見代碼中的checkKeyMatch()函數(shù)。
/**
 * 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;
  }
}

調(diào)用

調(diào)用方法和mousetrap類似。以下僅列出部分測試代碼,可以查看在線示例測試實(shí)際效果。

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: 手寫一個(gè)簡單的快捷鍵庫

TODO

至此,我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡單的快捷鍵庫,可以滿足常見的快捷鍵綁定相關(guān)的業(yè)務(wù)需求。當(dāng)然,相對當(dāng)前流行的幾個(gè)快捷鍵庫而言,我們實(shí)現(xiàn)的快捷鍵庫比較簡單,還有很多功能和細(xì)節(jié)有待實(shí)現(xiàn)和完善。以下列出待完成的幾個(gè)事項(xiàng),感興趣的可以嘗試實(shí)現(xiàn)下。

  • 支持設(shè)置鍵序列快捷鍵
  • 支持設(shè)置快捷鍵作用域
  • 支持解綁單個(gè)快捷鍵
  • 支持重置所有綁定的快捷鍵
  • 支持獲取所有綁定的快捷鍵信息

總結(jié)

通過學(xué)習(xí)mousetrap源碼以及手寫一個(gè)簡單的快捷鍵庫,我們可以學(xué)習(xí)到一些關(guān)于快捷鍵和鍵盤事件相關(guān)的知識。目的不是重復(fù)造輪子,而是通過日常業(yè)務(wù)需求,驅(qū)動(dòng)我們?nèi)チ私猱?dāng)前流行的常見快捷鍵庫的實(shí)現(xiàn)思路,以便于我們更好地理解并實(shí)現(xiàn)相關(guān)業(yè)務(wù)需求。假如日后有展示、修改快捷鍵或者其他快捷鍵相關(guān)的需求,我們就可以做到胸有成竹,舉一反三。

以上就是JavaScript手寫實(shí)現(xiàn)一個(gè)簡單的快捷鍵庫的詳細(xì)內(nèi)容,更多關(guān)于JavaScript快捷鍵庫的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論