diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index 00148418..5963c03b 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -44,7 +44,7 @@ class DisplaySearch extends Display {
});
this.autoPlayAudioDelay = 0;
- this.registerActions([
+ this.hotkeyHandler.registerActions([
['focusSearchBox', this._onActionFocusSearchBox.bind(this)]
]);
}
@@ -73,6 +73,7 @@ class DisplaySearch extends Display {
window.addEventListener('copy', this._onCopy.bind(this));
this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this));
this._clipboardMonitorEnableCheckbox.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this));
+ this.hotkeyHandler.on('keydownNonHotkey', this._onKeyDown.bind(this));
this._onModeChange();
@@ -81,19 +82,6 @@ class DisplaySearch extends Display {
this._isPrepared = true;
}
- onKeyDown(e) {
- if (
- !super.onKeyDown(e) &&
- document.activeElement !== this._queryInput &&
- !e.ctrlKey &&
- !e.metaKey &&
- !e.altKey &&
- e.key.length === 1
- ) {
- this._queryInput.focus({preventScroll: true});
- }
- }
-
postProcessQuery(query) {
if (this._wanakanaEnabled) {
try {
@@ -115,6 +103,18 @@ class DisplaySearch extends Display {
// Private
+ _onKeyDown(e) {
+ if (
+ document.activeElement !== this._queryInput &&
+ !e.ctrlKey &&
+ !e.metaKey &&
+ !e.altKey &&
+ e.key.length === 1
+ ) {
+ this._queryInput.focus({preventScroll: true});
+ }
+ }
+
async _onOptionsUpdated() {
await this.updateOptions();
const query = this._queryInput.value;
diff --git a/ext/bg/search.html b/ext/bg/search.html
index 43f9f7eb..dae657e8 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -92,6 +92,7 @@
+
diff --git a/ext/fg/float.html b/ext/fg/float.html
index ccf9b99a..e10659f2 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -108,6 +108,7 @@
+
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 0924a5fe..eca4dda5 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -24,6 +24,7 @@
* DocumentUtil
* FrameEndpoint
* Frontend
+ * HotkeyHandler
* MediaLoader
* PopupFactory
* QueryParser
@@ -59,8 +60,7 @@ class Display extends EventDispatcher {
japaneseUtil,
mediaLoader: this._mediaLoader
});
- this._hotkeys = new Map();
- this._actions = new Map();
+ this._hotkeyHandler = new HotkeyHandler(this._pageType);
this._messageHandlers = new Map();
this._directMessageHandlers = new Map();
this._windowMessageHandlers = new Map();
@@ -115,7 +115,7 @@ class Display extends EventDispatcher {
this._tagNotification = null;
this._tagNotificationContainer = document.querySelector('#content-footer');
- this.registerActions([
+ this._hotkeyHandler.registerActions([
['close', () => { this.close(); }],
['nextEntry', () => { this._focusEntry(this._index + 1, true); }],
['nextEntry3', () => { this._focusEntry(this._index + 3, true); }],
@@ -183,6 +183,10 @@ class Display extends EventDispatcher {
return this._depth;
}
+ get hotkeyHandler() {
+ return this._hotkeyHandler;
+ }
+
async prepare() {
// State setup
const {documentElement} = document;
@@ -195,6 +199,7 @@ class Display extends EventDispatcher {
this._audioSystem.prepare();
this._queryParser.prepare();
this._history.prepare();
+ this._hotkeyHandler.prepare();
// Event setup
this._history.on('stateChanged', this._onStateChanged.bind(this));
@@ -213,7 +218,6 @@ class Display extends EventDispatcher {
documentElement.addEventListener('auxclick', this._onDocumentElementClick.bind(this), false);
}
- document.addEventListener('keydown', this.onKeyDown.bind(this), false);
document.addEventListener('wheel', this._onWheel.bind(this), {passive: false});
if (this._closeButton !== null) {
this._closeButton.addEventListener('click', this._onCloseButtonClick.bind(this), false);
@@ -251,27 +255,6 @@ class Display extends EventDispatcher {
yomichan.logError(error);
}
- onKeyDown(e) {
- const key = e.code;
- const handlers = this._hotkeys.get(key);
- if (typeof handlers === 'undefined') { return false; }
-
- const eventModifiers = DocumentUtil.getActiveModifiers(e);
- for (const {modifiers, action} of handlers) {
- if (!this._areSame(modifiers, eventModifiers)) { continue; }
-
- const actionHandler = this._actions.get(action);
- if (typeof actionHandler === 'undefined') { continue; }
-
- const result = actionHandler(e);
- if (result !== false) {
- e.preventDefault();
- return true;
- }
- }
- return false;
- }
-
getOptions() {
return this._options;
}
@@ -375,12 +358,6 @@ class Display extends EventDispatcher {
}
}
- registerActions(actions) {
- for (const [name, handler] of actions) {
- this._actions.set(name, handler);
- }
- }
-
registerMessageHandlers(handlers) {
for (const [name, handlerInfo] of handlers) {
this._messageHandlers.set(name, handlerInfo);
@@ -1595,16 +1572,6 @@ class Display extends EventDispatcher {
return await api.getDefinitionAudioInfo(source, expression, reading, details);
}
- _areSame(set, array) {
- if (set.size !== array.length) { return false; }
- for (const value of array) {
- if (!set.has(value)) {
- return false;
- }
- }
- return true;
- }
-
async _setOptionsContextIfDifferent(optionsContext) {
if (deepEqual(this._optionsContext, optionsContext)) { return; }
await this.setOptionsContext(optionsContext);
@@ -1911,25 +1878,9 @@ class Display extends EventDispatcher {
await this._invokeOwner('setFrameSize', {width, height});
}
- _registerHotkey(key, modifiers, action) {
- if (!this._actions.has(action)) { return false; }
-
- let handlers = this._hotkeys.get(key);
- if (typeof handlers === 'undefined') {
- handlers = [];
- this._hotkeys.set(key, handlers);
- }
- handlers.push({modifiers: new Set(modifiers), action});
- return true;
- }
-
_updateHotkeys(options) {
- this._hotkeys.clear();
-
- for (const {action, key, modifiers, scopes, enabled} of options.inputs.hotkeys) {
- if (!enabled || action === '' || !scopes.includes(this._pageType)) { continue; }
- this._registerHotkey(key, modifiers, action);
- }
+ this._hotkeyHandler.clearHotkeys();
+ this._hotkeyHandler.registerHotkeys(options.inputs.hotkeys);
}
async _closeTab() {
diff --git a/ext/mixed/js/hotkey-handler.js b/ext/mixed/js/hotkey-handler.js
new file mode 100644
index 00000000..01c33f5d
--- /dev/null
+++ b/ext/mixed/js/hotkey-handler.js
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2021 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+/* global
+ * DocumentUtil
+ */
+
+/**
+ * Class which handles hotkey events and actions.
+ */
+class HotkeyHandler extends EventDispatcher {
+ /**
+ * Creates a new instance of the class.
+ * @param scope The scope required for hotkey definitions.
+ */
+ constructor(scope) {
+ super();
+ this._scope = scope;
+ this._hotkeys = new Map();
+ this._actions = new Map();
+ this._eventListeners = new EventListenerCollection();
+ }
+
+ /**
+ * Gets the scope required for the hotkey definitions.
+ */
+ get scope() {
+ return this._scope;
+ }
+
+ /**
+ * Sets the scope required for the hotkey definitions.
+ */
+ set scope(value) {
+ this._scope = value;
+ }
+
+ /**
+ * Begins listening to key press events in order to detect hotkeys.
+ */
+ prepare() {
+ this._eventListeners.addEventListener(document, 'keydown', this._onKeyDown.bind(this), false);
+ }
+
+ /**
+ * Stops listening to key press events.
+ */
+ cleanup() {
+ this._eventListeners.removeAllEventListeners();
+ }
+
+ /**
+ * Registers a set of actions that this hotkey handler supports.
+ * @param actions An array of `[name, handler]` entries, where `name` is a string and `handler` is a function.
+ */
+ registerActions(actions) {
+ for (const [name, handler] of actions) {
+ this._actions.set(name, handler);
+ }
+ }
+
+ /**
+ * Registers a set of hotkeys
+ * @param hotkeys An array of hotkey definitions of the format `{action, key, modifiers, scopes, enabled}`.
+ * * `action` - a string indicating which action to perform.
+ * * `key` - a keyboard key code indicating which key needs to be pressed.
+ * * `modifiers` - an array of keyboard modifiers which also need to be pressed. Supports: `'alt', 'ctrl', 'shift', 'meta'`.
+ * * `scopes` - an array of scopes for which the hotkey is valid. If this array does not contain `this.scope`, the hotkey will not be registered.
+ * * `enabled` - a boolean indicating whether the hotkey is currently enabled.
+ */
+ registerHotkeys(hotkeys) {
+ for (const {action, key, modifiers, scopes, enabled} of hotkeys) {
+ if (
+ enabled &&
+ action !== '' &&
+ scopes.includes(this._scope)
+ ) {
+ this._registerHotkey(key, modifiers, action);
+ }
+ }
+ }
+
+ /**
+ * Removes all registered hotkeys.
+ */
+ clearHotkeys() {
+ this._hotkeys.clear();
+ }
+
+ // Private
+
+ _onKeyDown(e) {
+ const key = e.code;
+ const handlers = this._hotkeys.get(key);
+ if (typeof handlers !== 'undefined') {
+ const eventModifiers = DocumentUtil.getActiveModifiers(e);
+ for (const {modifiers, action} of handlers) {
+ if (!this._areSame(modifiers, eventModifiers)) { continue; }
+
+ const actionHandler = this._actions.get(action);
+ if (typeof actionHandler === 'undefined') { continue; }
+
+ const result = actionHandler(e);
+ if (result !== false) {
+ e.preventDefault();
+ return true;
+ }
+ }
+ }
+ this.trigger('keydownNonHotkey', e);
+ return false;
+ }
+
+ _registerHotkey(key, modifiers, action) {
+ let handlers = this._hotkeys.get(key);
+ if (typeof handlers === 'undefined') {
+ handlers = [];
+ this._hotkeys.set(key, handlers);
+ }
+ handlers.push({modifiers: new Set(modifiers), action});
+ }
+
+ _areSame(set, array) {
+ if (set.size !== array.length) { return false; }
+ for (const value of array) {
+ if (!set.has(value)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}