Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
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}