JavaScript手寫實現(xiàn)一個簡單的快捷鍵庫
背景
前端開發(fā)中,有時項目會遇到一些快捷鍵需求,比如綁定快捷鍵,展示快捷鍵,編輯快捷鍵等需求,特別是工具類的項目。如果只是簡單的綁定幾個快捷鍵之類的需求,我們一般會通過監(jiān)聽鍵盤事件(如keydown 事件)來實現(xiàn),如果是稍微復(fù)雜點的需求,我們一般都會通過引入第三方快捷鍵庫來實現(xiàn),比如常用的幾個快捷鍵庫mousetrap, hotkey-js等。
接下來,我將會通過對快捷鍵庫mousetrap第一次提交的源碼進(jìn)行簡單分析,然后實現(xiàn)一個簡單的快捷鍵庫。
前置知識
首先,我們需要了解一些快捷鍵相關(guān)的基礎(chǔ)知識。比如,如何監(jiān)聽鍵盤事件?如何監(jiān)聽用戶按下的按鍵?鍵盤上的按鍵有哪些?是如何分類的?只有知道這些,才能更好的理解mousetrap這種快捷鍵庫實現(xiàn)的思路,才能更好地實現(xiàn)我們自己的快捷鍵庫。
如何監(jiān)聽鍵盤事件
實現(xiàn)快捷鍵需要監(jiān)聽用戶按下鍵盤按鍵的行為,那就需要使用到鍵盤事件API。
常用的鍵盤事件有keydown, keyup,keypress事件。一般來說,我們會通過監(jiān)聽用戶按下按鍵的行為,來判斷是否要觸發(fā)對應(yīng)的快捷鍵行為。通常來說,在用戶按下按鍵時,就會判斷是否有匹配的綁定過的快捷鍵,即通過監(jiān)聽keydown事件來實現(xiàn)快捷鍵。
如何監(jiān)聽鍵盤上按下的鍵
我們可以通過鍵盤事件來監(jiān)聽用戶按鍵行為。那如何知道用戶具體按下了哪個/哪些按鍵呢?
比如,用戶綁定的快捷鍵是s,那如何知道當(dāng)前按下的按鍵是s?我們可以通過鍵盤事件對象keyboardEvent上的code, keyCode, key這些屬性來判斷用戶當(dāng)前按下的按鍵。
鍵盤按鍵分類
有些按鍵會影響其他按鍵按下后產(chǎn)生的字符。比如,用戶同時按下了shift和/按鍵,此時產(chǎn)生的字符是?,然而實際上如果只按shift按鍵不會產(chǎn)生任何字符,只按/按鍵產(chǎn)生的字符本應(yīng)該是/,最終產(chǎn)生的字符?就是因為同時按下了shift按鍵導(dǎo)致的。這里的shift按鍵就是影響其他按鍵按下后產(chǎn)生字符的按鍵,這種按鍵被稱為修飾鍵。類似的修飾鍵還有ctrl, alt(option), command(meta)。
除了這幾個修飾鍵以外,其他的按鍵稱為非修飾鍵。
快捷鍵分類
常用的快捷鍵有單個鍵,鍵組合。有的還會用到鍵序列。
單個鍵
故名思義,單個鍵是只需要按下一個鍵就會觸發(fā)的快捷鍵。比如常用的音視頻切換播放/暫??旖萱ISpace,游戲中控制移動方向快捷鍵w,a,s,d等等。
鍵組合
鍵組合通常是一個或多個修飾鍵和一個非修飾鍵組合而成的快捷鍵。比如常用的復(fù)制粘貼快捷鍵ctrl+c,ctrl+v,保存文件快捷鍵ctrl+s,新建(瀏覽器或其他app)窗口快捷鍵ctrl+shift+n(command+shift+n)。
鍵序列
依次按下的按鍵稱為鍵序列。比如鍵序列h e l l o,需要依次按下h,e,l,l,o按鍵才會觸發(fā)。
mousetrap源碼分析
以下將以mousetrap第一次提交的源碼為基礎(chǔ)進(jìn)行簡單分析,源碼鏈接如下:bit.ly/3TdcK8u
簡單來說,代碼只做了兩件事,即綁定快捷鍵和監(jiān)聽鍵盤事件。
代碼設(shè)計和初始化
首先,給window對象添加了一個全局屬性Mousetrap,使用的是IIFE(立即執(zhí)行函數(shù)表達(dá)式)對代碼進(jìn)行封裝。
該函數(shù)對外暴露了幾個公共方法:
bind(keys, callback, action): 綁定快捷鍵trigger(): 手動觸發(fā)綁定的快捷鍵對應(yīng)的回調(diào)函數(shù)。
最后當(dāng)window加載后立即執(zhí)行init()函數(shù),即執(zhí)行初始化邏輯:添加鍵盤事件監(jiān)聽等。
// 以下為簡化后的代碼
window['Mousetrap'] = (function () {
return {
/**
* 綁定快捷鍵
* @param keys 快捷鍵,支持一次綁定多個快捷鍵。
* @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;
},
/**
* 手動觸發(fā)快捷鍵對應(yīng)的回調(diào)函數(shù)
* @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);
綁定快捷鍵
一般來說,快捷鍵庫都會提供一個綁定快捷鍵的函數(shù),比如bind(key, callback)。在mousetrap中,我們可以通過調(diào)用Mousetrap.bind()函數(shù)來實現(xiàn)快捷鍵綁定。
我們可以結(jié)合調(diào)用時的寫法對Mousetrap.bind()函數(shù)進(jìn)行分析。比如,我們綁定了快捷鍵ctrl+s和command+s,如下:Mousetrap.bind('ctrl+s, command+s', () => {console.log('保存成功')} )
bind(keys, callback, action)
由于bind()函數(shù)支持一次綁定多個快捷鍵(綁定時多個快捷鍵用逗號分隔),因此內(nèi)部封裝了_bindMultiple()函數(shù)用于處理一次綁定多個快捷鍵的用法。
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ù)只是對綁定時傳入的多個快捷鍵進(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ù)是實現(xiàn)綁定快捷鍵的核心代碼。
主要分為以下幾部分:
- 將綁定的快捷鍵
combination拆分為單個鍵數(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)
首先,會調(diào)用_stop(e)函數(shù)判斷是否需要停止執(zhí)行后續(xù)操作。如果需要則直接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ā)時所在的目標(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)
判斷兩個修飾鍵數(shù)組中的元素是否完全一致。eg: _modifiersMatch(['ctrl', 'shift'], ['shift', 'ctrl'])
function _modifiersMatch(group1, group2) {
return group1.sort().join(',') === group2.sort().join(',');
}
實現(xiàn)一個簡單的快捷鍵庫
結(jié)合前置知識和對mousetrap的源碼的分析,我們可以很容易實現(xiàn)一個簡單的快捷鍵庫。
思路
總體思路和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ù)用于綁定快捷鍵。
功能
支持綁定快捷鍵(單個鍵,鍵組合)。
實現(xiàn)
由于實現(xiàn)思路前文已經(jīng)分析過,因此這里就不詳細(xì)解釋了,以下直接給出完整的源代碼。
不過,代碼有幾點需要注意下:
event.key受shift按鍵影響。比如,綁定的快捷鍵是shift+/,實際上在keydown事件對象event中event.key的值是?,因此代碼里維護(hù)了這種特殊字符的映射_SHIFT_MAP,用于判斷用戶是否按下了這類特殊字符。- 有些特殊字符按鍵產(chǎn)生的字符(
event.key)需要特殊處理,比如空格按鍵Space,按下后實際產(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類似。以下僅列出部分測試代碼,可以查看在線示例測試實際效果。
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');
});
在線示例
TODO
至此,我們已經(jīng)實現(xiàn)了一個簡單的快捷鍵庫,可以滿足常見的快捷鍵綁定相關(guān)的業(yè)務(wù)需求。當(dāng)然,相對當(dāng)前流行的幾個快捷鍵庫而言,我們實現(xiàn)的快捷鍵庫比較簡單,還有很多功能和細(xì)節(jié)有待實現(xiàn)和完善。以下列出待完成的幾個事項,感興趣的可以嘗試實現(xiàn)下。
- 支持設(shè)置鍵序列快捷鍵
- 支持設(shè)置快捷鍵作用域
- 支持解綁單個快捷鍵
- 支持重置所有綁定的快捷鍵
- 支持獲取所有綁定的快捷鍵信息
總結(jié)
通過學(xué)習(xí)mousetrap源碼以及手寫一個簡單的快捷鍵庫,我們可以學(xué)習(xí)到一些關(guān)于快捷鍵和鍵盤事件相關(guān)的知識。目的不是重復(fù)造輪子,而是通過日常業(yè)務(wù)需求,驅(qū)動我們?nèi)チ私猱?dāng)前流行的常見快捷鍵庫的實現(xiàn)思路,以便于我們更好地理解并實現(xiàn)相關(guān)業(yè)務(wù)需求。假如日后有展示、修改快捷鍵或者其他快捷鍵相關(guān)的需求,我們就可以做到胸有成竹,舉一反三。
以上就是JavaScript手寫實現(xiàn)一個簡單的快捷鍵庫的詳細(xì)內(nèi)容,更多關(guān)于JavaScript快捷鍵庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用javascript實現(xiàn)一些常用軟件的下載導(dǎo)航
利用javascript實現(xiàn)一些常用軟件的下載導(dǎo)航,非常不錯的應(yīng)用,思路值得借鑒,沒看過的朋友可以看下。2009-08-08
js中scrollTop()方法和scroll()方法用法示例
這篇文章主要介紹了js中scrollTop()方法和scroll()方法用法,結(jié)合實例形式分析了scrollTop()方法和scroll()方法滾動操作的用法與相關(guān)操作技巧,需要的朋友可以參考下2016-10-10
element?UI中在?el-select?與?el-tree?結(jié)合組件實現(xiàn)過程
項目上實現(xiàn)某個功能,使用到了?el-select?和?el-tree?組合實現(xiàn),記錄下兩者結(jié)合的實現(xiàn)過程,對?el-select?與?el-tree?結(jié)合組件實現(xiàn)過程感興趣的朋友跟隨小編一起看看吧2023-02-02
JavaScript判斷數(shù)字是否為質(zhì)數(shù)的方法匯總
這篇文章主要介紹了JavaScript判斷數(shù)字是否為質(zhì)數(shù)的方法匯總的相關(guān)資料,非常不錯具有參考借鑒價值,需要的朋友可以參考下2016-06-06
Bootstrap基本組件學(xué)習(xí)筆記之進(jìn)度條(15)
這篇文章主要為大家詳細(xì)介紹了Bootstrap基本組件學(xué)習(xí)筆記之進(jìn)度條,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12

