|
|
@@ -5,10 +5,14 @@ const isEqual = (a, b) => { |
|
|
return false; |
|
|
} |
|
|
|
|
|
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]); |
|
|
return aKeys.every( |
|
|
(k) => Object.prototype.hasOwnProperty.call(b, k) |
|
|
&& a[k] === b[k], |
|
|
); |
|
|
}; |
|
|
|
|
|
const isArrayEqual = (a, b) => a.length === b.length && a.every((v, i) => isEqual(v, b[i])); |
|
|
const isArrayEqual = (a, b) => a.length === b.length |
|
|
&& a.every((v, i) => isEqual(v, b[i])); |
|
|
|
|
|
export const matchHotkey = (buffer, hotkey) => { |
|
|
if (buffer.length < hotkey.length) { |
|
|
@@ -37,22 +41,74 @@ const isHotkeyValid = (hotkey) => Object.keys(hotkey) |
|
|
.filter((k) => !indexedModifiers[k]) |
|
|
.length === 1; |
|
|
|
|
|
const validate = (value, message) => { |
|
|
if (!value) { |
|
|
throw new Error(message); |
|
|
} |
|
|
}; |
|
|
|
|
|
const validateType = (value, name, type) => { |
|
|
validate( |
|
|
typeof value === type, |
|
|
`The ${name} must be a ${type}; given ${typeof value}`, |
|
|
); |
|
|
}; |
|
|
|
|
|
export const normalizeHotkey = (hotkey) => hotkey.split(/ +/g).map( |
|
|
(part) => { |
|
|
const arr = part.split('+').filter(Boolean); |
|
|
const result = arrayToObject(arr); |
|
|
|
|
|
if (Object.keys(result).length < arr.length) { |
|
|
throw new Error(`Hotkey combination must not contain duplicates "${hotkey}"`); |
|
|
} |
|
|
validate( |
|
|
Object.keys(result).length >= arr.length, |
|
|
`Hotkey combination has duplicates "${hotkey}"`, |
|
|
); |
|
|
|
|
|
validate( |
|
|
isHotkeyValid(result), |
|
|
`Invalid hotkey combination: "${hotkey}"`, |
|
|
); |
|
|
|
|
|
if (!isHotkeyValid(result)) { |
|
|
throw new Error(`Invalid hotkey combination: "${hotkey}"`); |
|
|
} |
|
|
return result; |
|
|
}, |
|
|
); |
|
|
|
|
|
const validateListenerArgs = (hotkey, callback) => { |
|
|
validateType(hotkey, 'hotkey', 'string'); |
|
|
validateType(callback, 'callback', 'function'); |
|
|
}; |
|
|
|
|
|
const createListenersFn = (listeners, fn) => (hotkey, callback) => { |
|
|
validateListenerArgs(hotkey, callback); |
|
|
fn(listeners, hotkey, callback); |
|
|
}; |
|
|
|
|
|
const registerListener = (listeners, hotkey, callback) => { |
|
|
listeners.push({ hotkey: normalizeHotkey(hotkey), callback }); |
|
|
}; |
|
|
|
|
|
const unregisterListener = (listeners, hotkey, callback) => { |
|
|
const normalized = normalizeHotkey(hotkey); |
|
|
|
|
|
const index = listeners.findIndex( |
|
|
(l) => l.callback === callback |
|
|
&& isArrayEqual(normalized, l.hotkey), |
|
|
); |
|
|
|
|
|
if (index !== -1) { |
|
|
listeners.splice(index, 1); |
|
|
} |
|
|
}; |
|
|
|
|
|
const debounce = (fn, time) => { |
|
|
let timeoutId = null; |
|
|
|
|
|
return () => { |
|
|
clearTimeout(timeoutId); |
|
|
timeoutId = setTimeout(fn, time); |
|
|
}; |
|
|
}; |
|
|
|
|
|
const getKey = (key) => { |
|
|
switch (key) { |
|
|
case '+': |
|
|
@@ -64,21 +120,15 @@ const getKey = (key) => { |
|
|
} |
|
|
}; |
|
|
|
|
|
export const createContext = ({ debounceTime = 500 } = {}) => { |
|
|
if (typeof debounceTime !== 'number') { |
|
|
throw new Error('debounceTime must be a number'); |
|
|
} |
|
|
|
|
|
if (debounceTime <= 0) { |
|
|
throw new Error('debounceTime must be a > 0'); |
|
|
} |
|
|
|
|
|
const createKeyDownListener = (listeners, debounceTime) => { |
|
|
let buffer = []; |
|
|
const clearBuffer = () => { buffer = []; }; |
|
|
let bufferClearTimeout = null; |
|
|
const listeners = []; |
|
|
|
|
|
const keyDownListener = (event) => { |
|
|
const clearBufferDebounced = debounce( |
|
|
() => { buffer = []; }, |
|
|
debounceTime, |
|
|
); |
|
|
|
|
|
return (event) => { |
|
|
if (event.repeat) { |
|
|
return; |
|
|
} |
|
|
@@ -87,8 +137,7 @@ export const createContext = ({ debounceTime = 500 } = {}) => { |
|
|
return; |
|
|
} |
|
|
|
|
|
clearTimeout(bufferClearTimeout); |
|
|
bufferClearTimeout = setTimeout((clearBuffer), debounceTime); |
|
|
clearBufferDebounced(); |
|
|
|
|
|
const description = { |
|
|
[getKey(event.key)]: true, |
|
|
@@ -108,40 +157,43 @@ export const createContext = ({ debounceTime = 500 } = {}) => { |
|
|
} |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
const register = (hotkey, callback) => { |
|
|
if (typeof hotkey !== 'string') { |
|
|
throw new Error( |
|
|
`The hotkey must be a string; given ${typeof hotkey}`, |
|
|
); |
|
|
} |
|
|
const validateContext = (options) => { |
|
|
const { debounceTime = 500, autoEnable = true } = (options || {}); |
|
|
|
|
|
if (typeof callback !== 'function') { |
|
|
throw new Error( |
|
|
`The callback must be a function; given ${typeof callback}`, |
|
|
); |
|
|
} |
|
|
console.log(normalizeHotkey(hotkey)); |
|
|
listeners.push({ hotkey: normalizeHotkey(hotkey), callback }); |
|
|
}; |
|
|
validateType(debounceTime, 'debounceTime', 'number'); |
|
|
validate(debounceTime > 0, 'debounceTime must be > 0'); |
|
|
validateType(autoEnable, 'autoEnable', 'boolean'); |
|
|
|
|
|
const unregister = (hotkeyString, callback) => { |
|
|
const normalized = normalizeHotkey(hotkeyString); |
|
|
const index = listeners.findIndex( |
|
|
(l) => l.callback === callback |
|
|
&& isArrayEqual(normalized, l.hotkey), |
|
|
); |
|
|
if (index !== -1) { |
|
|
listeners.splice(index, 1); |
|
|
} |
|
|
}; |
|
|
return { debounceTime, autoEnable }; |
|
|
}; |
|
|
|
|
|
export const createContext = (options) => { |
|
|
const { debounceTime, autoEnable } = validateContext(options); |
|
|
|
|
|
const enable = () => document.addEventListener('keydown', keyDownListener); |
|
|
const disable = () => document.removeEventListener('keydown', keyDownListener); |
|
|
const listeners = []; |
|
|
const keyDownListener = createKeyDownListener( |
|
|
listeners, |
|
|
debounceTime, |
|
|
); |
|
|
|
|
|
const enable = () => document.addEventListener( |
|
|
'keydown', |
|
|
keyDownListener, |
|
|
); |
|
|
const disable = () => document.removeEventListener( |
|
|
'keydown', |
|
|
keyDownListener, |
|
|
); |
|
|
|
|
|
if (autoEnable) { |
|
|
enable(); |
|
|
} |
|
|
|
|
|
return { |
|
|
register, |
|
|
unregister, |
|
|
register: createListenersFn(listeners, registerListener), |
|
|
unregister: createListenersFn(listeners, unregisterListener), |
|
|
enable, |
|
|
disable, |
|
|
}; |
|
|
|