Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-2025 Yomitan Authors
3 * Copyright (C) 2021-2022 Yomichan Authors
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19import {parseJson} from '../core/json.js';
20import {isObjectNotArray} from '../core/object-utilities.js';
21import {HotkeyUtil} from './hotkey-util.js';
22
23export class HotkeyHelpController {
24 constructor() {
25 /** @type {HotkeyUtil} */
26 this._hotkeyUtil = new HotkeyUtil();
27 /** @type {Map<string, string>} */
28 this._localActionHotkeys = new Map();
29 /** @type {Map<string, string>} */
30 this._globalActionHotkeys = new Map();
31 /** @type {RegExp} */
32 this._replacementPattern = /\{0\}/g;
33 }
34
35 /**
36 * @param {import('../comm/api.js').API} api
37 */
38 async prepare(api) {
39 const {platform: {os}} = await api.getEnvironmentInfo();
40 this._hotkeyUtil.os = os;
41 await this._setupGlobalCommands(this._globalActionHotkeys);
42 }
43
44 /**
45 * @param {import('settings').ProfileOptions} options
46 */
47 setOptions(options) {
48 const hotkeys = options.inputs.hotkeys;
49 const hotkeyMap = this._localActionHotkeys;
50 hotkeyMap.clear();
51 for (const {enabled, action, key, modifiers} of hotkeys) {
52 if (!enabled || key === null || action === '' || hotkeyMap.has(action)) { continue; }
53 hotkeyMap.set(action, this._hotkeyUtil.getInputDisplayValue(key, modifiers));
54 }
55 }
56
57 /**
58 * @param {ParentNode} node
59 */
60 setupNode(node) {
61 const replacementPattern = this._replacementPattern;
62 for (const node2 of /** @type {NodeListOf<HTMLElement>} */ (node.querySelectorAll('[data-hotkey]'))) {
63 const info = this._getNodeInfo(node2);
64 if (info === null) { continue; }
65 const {action, global, attributes, values, defaultAttributeValues} = info;
66 const multipleValues = Array.isArray(values);
67 const hotkey = (global ? this._globalActionHotkeys : this._localActionHotkeys).get(action);
68 for (let i = 0, ii = attributes.length; i < ii; ++i) {
69 const attribute = attributes[i];
70 /** @type {unknown} */
71 let value;
72 if (typeof hotkey !== 'undefined') {
73 value = multipleValues ? values[i] : values;
74 if (typeof value === 'string') {
75 value = value.replace(replacementPattern, hotkey);
76 }
77 } else {
78 value = defaultAttributeValues[i];
79 }
80
81 if (typeof value === 'string') {
82 node2.setAttribute(attribute, value);
83 } else {
84 node2.removeAttribute(attribute);
85 }
86 }
87 }
88 }
89
90 // Private
91
92 /**
93 * @returns {Promise<chrome.commands.Command[]>}
94 */
95 _getAllCommands() {
96 return new Promise((resolve, reject) => {
97 if (!(isObjectNotArray(chrome.commands) && typeof chrome.commands.getAll === 'function')) {
98 resolve([]);
99 return;
100 }
101
102 chrome.commands.getAll((result) => {
103 const e = chrome.runtime.lastError;
104 if (e) {
105 reject(new Error(e.message));
106 } else {
107 resolve(result);
108 }
109 });
110 });
111 }
112
113 /**
114 * @param {Map<string, string>} commandMap
115 */
116 async _setupGlobalCommands(commandMap) {
117 const commands = await this._getAllCommands();
118
119 commandMap.clear();
120 for (const {name, shortcut} of commands) {
121 if (typeof name !== 'string' || typeof shortcut !== 'string' || shortcut.length === 0) { continue; }
122 const {key, modifiers} = this._hotkeyUtil.convertCommandToInput(shortcut);
123 commandMap.set(name, this._hotkeyUtil.getInputDisplayValue(key, modifiers));
124 }
125 }
126
127 /**
128 * @param {HTMLElement} node
129 * @param {unknown[]} data
130 * @param {string[]} attributes
131 * @returns {unknown[]}
132 */
133 _getDefaultAttributeValues(node, data, attributes) {
134 if (data.length > 3) {
135 const result = data[3];
136 if (Array.isArray(result)) {
137 return result;
138 }
139 }
140
141 /** @type {(?string)[]} */
142 const defaultAttributeValues = [];
143 for (let i = 0, ii = attributes.length; i < ii; ++i) {
144 const attribute = attributes[i];
145 const value = node.hasAttribute(attribute) ? node.getAttribute(attribute) : null;
146 defaultAttributeValues.push(value);
147 }
148 data[3] = defaultAttributeValues;
149 node.dataset.hotkey = JSON.stringify(data);
150 return defaultAttributeValues;
151 }
152
153 /**
154 * @param {HTMLElement} node
155 * @returns {?{action: string, global: boolean, attributes: string[], values: unknown, defaultAttributeValues: unknown[]}}
156 */
157 _getNodeInfo(node) {
158 const {hotkey} = node.dataset;
159 if (typeof hotkey !== 'string') { return null; }
160 const data = /** @type {unknown} */ (parseJson(hotkey));
161 if (!Array.isArray(data)) { return null; }
162 const dataArray = /** @type {unknown[]} */ (data);
163 const [action, attributes, values] = dataArray;
164 if (typeof action !== 'string') { return null; }
165 /** @type {string[]} */
166 const attributesArray = [];
167 if (Array.isArray(attributes)) {
168 for (const item of attributes) {
169 if (typeof item !== 'string') { continue; }
170 attributesArray.push(item);
171 }
172 } else if (typeof attributes === 'string') {
173 attributesArray.push(attributes);
174 }
175 const defaultAttributeValues = this._getDefaultAttributeValues(node, data, attributesArray);
176 const globalPrexix = 'global:';
177 const global = action.startsWith(globalPrexix);
178 return {
179 action: global ? action.substring(globalPrexix.length) : action,
180 global,
181 attributes: attributesArray,
182 values,
183 defaultAttributeValues,
184 };
185 }
186
187 /**
188 * @param {HTMLElement} node
189 * @returns {?string}
190 */
191 getHotkeyLabel(node) {
192 const {hotkey} = node.dataset;
193 if (typeof hotkey !== 'string') { return null; }
194
195 const data = /** @type {unknown} */ (parseJson(hotkey));
196 if (!Array.isArray(data)) { return null; }
197
198 const values = /** @type {unknown[]} */ (data)[2];
199 if (typeof values !== 'string') { return null; }
200
201 return values;
202 }
203
204 /**
205 * @param {HTMLElement} node
206 * @param {string} label
207 */
208 setHotkeyLabel(node, label) {
209 const {hotkey} = node.dataset;
210 if (typeof hotkey !== 'string') { return; }
211
212 const data = /** @type {unknown} */ (parseJson(hotkey));
213 if (!Array.isArray(data)) { return; }
214
215 data[2] = label;
216 node.dataset.hotkey = JSON.stringify(data);
217 }
218}