aboutsummaryrefslogtreecommitdiff
path: root/ext/js/app/popup-window.js
blob: 801afb3fa7994303bd97e2154fe66a7666a278ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/*
 * Copyright (C) 2023  Yomitan Authors
 * Copyright (C) 2020-2022  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 <https://www.gnu.org/licenses/>.
 */

import {EventDispatcher} from '../core.js';
import {yomitan} from '../yomitan.js';

/**
 * This class represents a popup that is hosted in a new native window.
 * @augments EventDispatcher<import('popup').Events>
 */
export class PopupWindow extends EventDispatcher {
    /**
     * Creates a new instance.
     * @param {import('popup').PopupWindowConstructorDetails} details Details about how to set up the instance.
     */
    constructor({
        id,
        depth,
        frameId
    }) {
        super();
        /** @type {string} */
        this._id = id;
        /** @type {number} */
        this._depth = depth;
        /** @type {number} */
        this._frameId = frameId;
        /** @type {?number} */
        this._popupTabId = null;
    }

    /**
     * The ID of the popup.
     * @type {string}
     */
    get id() {
        return this._id;
    }

    /**
     * @type {?import('./popup.js').Popup}
     */
    get parent() {
        return null;
    }

    /**
     * The parent of the popup, which is always `null` for `PopupWindow` instances,
     * since any potential parent popups are in a different frame.
     * @param {import('./popup.js').Popup} _value The parent to assign.
     * @throws {Error} Throws an error, since this class doesn't support children.
     */
    set parent(_value) {
        throw new Error('Not supported on PopupWindow');
    }

    /**
     * The popup child popup, which is always null for `PopupWindow` instances,
     * since any potential child popups are in a different frame.
     * @type {?import('./popup.js').Popup}
     */
    get child() {
        return null;
    }

    /**
     * Attempts to set the child popup.
     * @param {import('./popup.js').Popup} _value The child to assign.
     * @throws Throws an error, since this class doesn't support children.
     */
    set child(_value) {
        throw new Error('Not supported on PopupWindow');
    }

    /**
     * The depth of the popup.
     * @type {number}
     */
    get depth() {
        return this._depth;
    }

    /**
     * Gets the content window of the frame. This value is null,
     * since the window is hosted in a different frame.
     * @type {?Window}
     */
    get frameContentWindow() {
        return null;
    }

    /**
     * Gets the DOM node that contains the frame.
     * @type {?Element}
     */
    get container() {
        return null;
    }

    /**
     * Gets the ID of the frame.
     * @type {number}
     */
    get frameId() {
        return this._frameId;
    }

    /**
     * Sets the options context for the popup.
     * @param {import('settings').OptionsContext} optionsContext The options context object.
     * @returns {Promise<void>}
     */
    async setOptionsContext(optionsContext) {
        await this._invoke(false, 'displaySetOptionsContext', {id: this._id, optionsContext});
    }

    /**
     * Hides the popup. This does nothing for `PopupWindow`.
     * @param {boolean} _changeFocus Whether or not the parent popup or host frame should be focused.
     */
    hide(_changeFocus) {
        // NOP
    }

    /**
     * Returns whether or not the popup is currently visible.
     * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise.
     */
    async isVisible() {
        return (this._popupTabId !== null && await yomitan.api.isTabSearchPopup(this._popupTabId));
    }

    /**
     * Force assigns the visibility of the popup.
     * @param {boolean} _value Whether or not the popup should be visible.
     * @param {number} _priority The priority of the override.
     * @returns {Promise<?import('core').TokenString>} A token used which can be passed to `clearVisibleOverride`,
     *   or null if the override wasn't assigned.
     */
    async setVisibleOverride(_value, _priority) {
        return null;
    }

    /**
     * Clears a visibility override that was generated by `setVisibleOverride`.
     * @param {import('core').TokenString} _token The token returned from `setVisibleOverride`.
     * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise.
     */
    async clearVisibleOverride(_token) {
        return false;
    }

    /**
     * Checks whether a point is contained within the popup's rect.
     * @param {number} _x The x coordinate.
     * @param {number} _y The y coordinate.
     * @returns {Promise<boolean>} `true` if the point is contained within the popup's rect, `false` otherwise.
     */
    async containsPoint(_x, _y) {
        return false;
    }

    /**
     * Shows and updates the positioning and content of the popup.
     * @param {import('popup').ContentDetails} _details Settings for the outer popup.
     * @param {?import('display').ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
     * @returns {Promise<void>}
     */
    async showContent(_details, displayDetails) {
        if (displayDetails === null) { return; }
        await this._invoke(true, 'displaySetContent', {id: this._id, details: displayDetails});
    }

    /**
     * Sets the custom styles for the popup content.
     * @param {string} css The CSS rules.
     * @returns {Promise<void>}
     */
    async setCustomCss(css) {
        await this._invoke(false, 'displaySetCustomCss', {id: this._id, css});
    }

    /**
     * Stops the audio auto-play timer, if one has started.
     * @returns {Promise<void>}
     */
    async clearAutoPlayTimer() {
        await this._invoke(false, 'displayAudioClearAutoPlayTimer', {id: this._id});
    }

    /**
     * Sets the scaling factor of the popup content.
     * @param {number} _scale The scaling factor.
     */
    async setContentScale(_scale) {
        // NOP
    }

    /**
     * Returns whether or not the popup is currently visible, synchronously.
     * @throws An exception is thrown for `PopupWindow` since it cannot synchronously detect visibility.
     */
    isVisibleSync() {
        throw new Error('Not supported on PopupWindow');
    }

    /**
     * Updates the outer theme of the popup.
     */
    async updateTheme() {
        // NOP
    }

    /**
     * Sets the custom styles for the outer popup container.
     * This does nothing for `PopupWindow`.
     * @param {string} _css The CSS rules.
     * @param {boolean} _useWebExtensionApi Whether or not web extension APIs should be used to inject the rules.
     *   When web extension APIs are used, a DOM node is not generated, making it harder to detect the changes.
     */
    async setCustomOuterCss(_css, _useWebExtensionApi) {
        // NOP
    }

    /**
     * Gets the rectangle of the DOM frame, synchronously.
     * @returns {import('popup').ValidRect} The rect.
     *   `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
     */
    getFrameRect() {
        return {left: 0, top: 0, right: 0, bottom: 0, valid: false};
    }

    /**
     * Gets the size of the DOM frame.
     * @returns {Promise<import('popup').ValidSize>} The size and whether or not it is valid.
     */
    async getFrameSize() {
        return {width: 0, height: 0, valid: false};
    }

    /**
     * Sets the size of the DOM frame.
     * @param {number} _width The desired width of the popup.
     * @param {number} _height The desired height of the popup.
     * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise.
     */
    async setFrameSize(_width, _height) {
        return false;
    }

    // Private

    // TODO : Type safety
    /**
     * @template {import('core').SerializableObject} TParams
     * @template [TReturn=unknown]
     * @param {boolean} open
     * @param {string} action
     * @param {TParams} params
     * @returns {Promise<TReturn|undefined>}
     */
    async _invoke(open, action, params) {
        if (yomitan.isExtensionUnloaded) {
            return void 0;
        }

        const frameId = 0;
        if (this._popupTabId !== null) {
            try {
                return await yomitan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params});
            } catch (e) {
                if (yomitan.isExtensionUnloaded) {
                    open = false;
                }
            }
            this._popupTabId = null;
        }

        if (!open) {
            return void 0;
        }

        const {tabId} = await yomitan.api.getOrCreateSearchPopup({focus: 'ifCreated'});
        this._popupTabId = tabId;

        return await yomitan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params});
    }
}