Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 320 lines 10 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2020-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 {EventDispatcher} from '../core/event-dispatcher.js'; 20 21/** 22 * This class represents a popup that is hosted in a new native window. 23 * @augments EventDispatcher<import('popup').Events> 24 */ 25export class PopupWindow extends EventDispatcher { 26 /** 27 * @param {import('../application.js').Application} application The main application instance. 28 * @param {string} id The identifier of the popup. 29 * @param {number} depth The depth of the popup. 30 * @param {number} frameId The frameId of the host frame. 31 */ 32 constructor(application, id, depth, frameId) { 33 super(); 34 /** @type {import('../application.js').Application} */ 35 this._application = application; 36 /** @type {string} */ 37 this._id = id; 38 /** @type {number} */ 39 this._depth = depth; 40 /** @type {number} */ 41 this._frameId = frameId; 42 /** @type {?number} */ 43 this._popupTabId = null; 44 } 45 46 /** 47 * The ID of the popup. 48 * @type {string} 49 */ 50 get id() { 51 return this._id; 52 } 53 54 /** 55 * @type {?import('./popup.js').Popup} 56 */ 57 get parent() { 58 return null; 59 } 60 61 /** 62 * The parent of the popup, which is always `null` for `PopupWindow` instances, 63 * since any potential parent popups are in a different frame. 64 * @param {import('./popup.js').Popup} _value The parent to assign. 65 * @throws {Error} Throws an error, since this class doesn't support children. 66 */ 67 set parent(_value) { 68 throw new Error('Not supported on PopupWindow'); 69 } 70 71 /** 72 * The popup child popup, which is always null for `PopupWindow` instances, 73 * since any potential child popups are in a different frame. 74 * @type {?import('./popup.js').Popup} 75 */ 76 get child() { 77 return null; 78 } 79 80 /** 81 * Attempts to set the child popup. 82 * @param {import('./popup.js').Popup} _value The child to assign. 83 * @throws Throws an error, since this class doesn't support children. 84 */ 85 set child(_value) { 86 throw new Error('Not supported on PopupWindow'); 87 } 88 89 /** 90 * The depth of the popup. 91 * @type {number} 92 */ 93 get depth() { 94 return this._depth; 95 } 96 97 /** 98 * Gets the content window of the frame. This value is null, 99 * since the window is hosted in a different frame. 100 * @type {?Window} 101 */ 102 get frameContentWindow() { 103 return null; 104 } 105 106 /** 107 * Gets the DOM node that contains the frame. 108 * @type {?Element} 109 */ 110 get container() { 111 return null; 112 } 113 114 /** 115 * Gets the ID of the frame. 116 * @type {number} 117 */ 118 get frameId() { 119 return this._frameId; 120 } 121 122 /** 123 * Sets the options context for the popup. 124 * @param {import('settings').OptionsContext} optionsContext The options context object. 125 * @returns {Promise<void>} 126 */ 127 async setOptionsContext(optionsContext) { 128 await this._invoke(false, 'displaySetOptionsContext', {optionsContext}); 129 } 130 131 /** 132 * Hides the popup. This does nothing for `PopupWindow`. 133 * @param {boolean} _changeFocus Whether or not the parent popup or host frame should be focused. 134 */ 135 hide(_changeFocus) { 136 // NOP 137 } 138 139 /** 140 * Returns whether or not the popup is currently visible. 141 * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise. 142 */ 143 async isVisible() { 144 return (this._popupTabId !== null && await this._application.api.isTabSearchPopup(this._popupTabId)); 145 } 146 147 /** 148 * Force assigns the visibility of the popup. 149 * @param {boolean} _value Whether or not the popup should be visible. 150 * @param {number} _priority The priority of the override. 151 * @returns {Promise<?import('core').TokenString>} A token used which can be passed to `clearVisibleOverride`, 152 * or null if the override wasn't assigned. 153 */ 154 async setVisibleOverride(_value, _priority) { 155 return null; 156 } 157 158 /** 159 * Clears a visibility override that was generated by `setVisibleOverride`. 160 * @param {import('core').TokenString} _token The token returned from `setVisibleOverride`. 161 * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise. 162 */ 163 async clearVisibleOverride(_token) { 164 return false; 165 } 166 167 /** 168 * Checks whether a point is contained within the popup's rect. 169 * @param {number} _x The x coordinate. 170 * @param {number} _y The y coordinate. 171 * @returns {Promise<boolean>} `true` if the point is contained within the popup's rect, `false` otherwise. 172 */ 173 async containsPoint(_x, _y) { 174 return false; 175 } 176 177 /** 178 * Shows and updates the positioning and content of the popup. 179 * @param {import('popup').ContentDetails} _details Settings for the outer popup. 180 * @param {?import('display').ContentDetails} displayDetails The details parameter passed to `Display.setContent`. 181 * @returns {Promise<void>} 182 */ 183 async showContent(_details, displayDetails) { 184 if (displayDetails === null) { return; } 185 await this._invoke(true, 'displaySetContent', {details: displayDetails}); 186 } 187 188 /** 189 * Sets the custom styles for the popup content. 190 * @param {string} css The CSS rules. 191 * @returns {Promise<void>} 192 */ 193 async setCustomCss(css) { 194 await this._invoke(false, 'displaySetCustomCss', {css}); 195 } 196 197 /** 198 * Stops the audio auto-play timer, if one has started. 199 * @returns {Promise<void>} 200 */ 201 async clearAutoPlayTimer() { 202 await this._invoke(false, 'displayAudioClearAutoPlayTimer', void 0); 203 } 204 205 /** 206 * Sets the scaling factor of the popup content. 207 * @param {number} _scale The scaling factor. 208 */ 209 async setContentScale(_scale) { 210 // NOP 211 } 212 213 /** 214 * Returns whether or not the popup is currently visible, synchronously. 215 * @throws An exception is thrown for `PopupWindow` since it cannot synchronously detect visibility. 216 */ 217 isVisibleSync() { 218 throw new Error('Not supported on PopupWindow'); 219 } 220 221 /** 222 * Updates the outer theme of the popup. 223 */ 224 async updateTheme() { 225 // NOP 226 } 227 228 /** 229 * Sets the custom styles for the outer popup container. 230 * This does nothing for `PopupWindow`. 231 * @param {string} _css The CSS rules. 232 * @param {boolean} _useWebExtensionApi Whether or not web extension APIs should be used to inject the rules. 233 * When web extension APIs are used, a DOM node is not generated, making it harder to detect the changes. 234 */ 235 async setCustomOuterCss(_css, _useWebExtensionApi) { 236 // NOP 237 } 238 239 /** 240 * Gets the rectangle of the DOM frame, synchronously. 241 * @returns {import('popup').ValidRect} The rect. 242 * `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. 243 */ 244 getFrameRect() { 245 return {left: 0, top: 0, right: 0, bottom: 0, valid: false}; 246 } 247 248 /** 249 * Gets the size of the DOM frame. 250 * @returns {Promise<import('popup').ValidSize>} The size and whether or not it is valid. 251 */ 252 async getFrameSize() { 253 return {width: 0, height: 0, valid: false}; 254 } 255 256 /** 257 * Sets the size of the DOM frame. 258 * @param {number} _width The desired width of the popup. 259 * @param {number} _height The desired height of the popup. 260 * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise. 261 */ 262 async setFrameSize(_width, _height) { 263 return false; 264 } 265 266 /** 267 * @returns {Promise<boolean>} 268 */ 269 async isPointerOver() { 270 return false; 271 } 272 273 // Private 274 275 /** 276 * @template {import('display').DirectApiNames} TName 277 * @param {boolean} open 278 * @param {TName} action 279 * @param {import('display').DirectApiParams<TName>} params 280 * @returns {Promise<import('display').DirectApiReturn<TName>|undefined>} 281 */ 282 async _invoke(open, action, params) { 283 if (this._application.webExtension.unloaded) { 284 return void 0; 285 } 286 287 const message = /** @type {import('display').DirectApiMessageAny} */ ({action, params}); 288 289 const frameId = 0; 290 if (this._popupTabId !== null) { 291 try { 292 return /** @type {import('display').DirectApiReturn<TName>} */ (await this._application.crossFrame.invokeTab( 293 this._popupTabId, 294 frameId, 295 'displayPopupMessage2', 296 message, 297 )); 298 } catch (e) { 299 if (this._application.webExtension.unloaded) { 300 open = false; 301 } 302 } 303 this._popupTabId = null; 304 } 305 306 if (!open) { 307 return void 0; 308 } 309 310 const {tabId} = await this._application.api.getOrCreateSearchPopup({focus: 'ifCreated'}); 311 this._popupTabId = tabId; 312 313 return /** @type {import('display').DirectApiReturn<TName>} */ (await this._application.crossFrame.invokeTab( 314 this._popupTabId, 315 frameId, 316 'displayPopupMessage2', 317 message, 318 )); 319 } 320}