Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at upstream/master 218 lines 7.7 kB view raw
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}