mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
323 lines
12 KiB
JavaScript
323 lines
12 KiB
JavaScript
/*
|
|
* Copyright (C) 2023-2025 Yomitan Authors
|
|
* Copyright (C) 2019-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 {Frontend} from '../../app/frontend.js';
|
|
import {ThemeController} from '../../app/theme-controller.js';
|
|
import {createApiMap, invokeApiMapHandler} from '../../core/api-map.js';
|
|
import {EventListenerCollection} from '../../core/event-listener-collection.js';
|
|
import {querySelectorNotNull} from '../../dom/query-selector.js';
|
|
import {TextSourceRange} from '../../dom/text-source-range.js';
|
|
import {isComposing} from '../../language/ime-utilities.js';
|
|
import {convertToKanaIME} from '../../language/ja/japanese-wanakana.js';
|
|
|
|
export class PopupPreviewFrame {
|
|
/**
|
|
* @param {import('../../application.js').Application} application
|
|
* @param {import('../../app/popup-factory.js').PopupFactory} popupFactory
|
|
* @param {import('../../input/hotkey-handler.js').HotkeyHandler} hotkeyHandler
|
|
*/
|
|
constructor(application, popupFactory, hotkeyHandler) {
|
|
/** @type {import('../../application.js').Application} */
|
|
this._application = application;
|
|
/** @type {import('../../app/popup-factory.js').PopupFactory} */
|
|
this._popupFactory = popupFactory;
|
|
/** @type {import('../../input/hotkey-handler.js').HotkeyHandler} */
|
|
this._hotkeyHandler = hotkeyHandler;
|
|
/** @type {?Frontend} */
|
|
this._frontend = null;
|
|
/** @type {?(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */
|
|
this._apiOptionsGetOld = null;
|
|
/** @type {boolean} */
|
|
this._popupShown = false;
|
|
/** @type {?import('core').Timeout} */
|
|
this._themeChangeTimeout = null;
|
|
/** @type {?import('text-source').TextSource} */
|
|
this._textSource = null;
|
|
/** @type {?import('settings').OptionsContext} */
|
|
this._optionsContext = null;
|
|
/** @type {HTMLElement} */
|
|
this._exampleText = querySelectorNotNull(document, '#example-text');
|
|
/** @type {HTMLInputElement} */
|
|
this._exampleTextInput = querySelectorNotNull(document, '#example-text-input');
|
|
/** @type {EventListenerCollection} */
|
|
this._exampleTextInputEvents = new EventListenerCollection();
|
|
/** @type {string} */
|
|
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
|
|
/** @type {import('language').LanguageSummary[]} */
|
|
this._languageSummaries = [];
|
|
/** @type {ThemeController} */
|
|
this._themeController = new ThemeController(document.documentElement);
|
|
|
|
/* eslint-disable @stylistic/no-multi-spaces */
|
|
/** @type {import('popup-preview-frame').ApiMap} */
|
|
this._windowMessageHandlers = createApiMap([
|
|
['setText', this._onSetText.bind(this)],
|
|
['setCustomCss', this._setCustomCss.bind(this)],
|
|
['setCustomOuterCss', this._setCustomOuterCss.bind(this)],
|
|
['updateOptionsContext', this._updateOptionsContext.bind(this)],
|
|
['setLanguageExampleText', this._setLanguageExampleText.bind(this)],
|
|
['updateSearch', this._updateSearch.bind(this)],
|
|
]);
|
|
/* eslint-enable @stylistic/no-multi-spaces */
|
|
}
|
|
|
|
/** */
|
|
async prepare() {
|
|
window.addEventListener('message', this._onMessage.bind(this), false);
|
|
|
|
this._themeController.prepare();
|
|
|
|
// Setup events
|
|
this._exampleText.addEventListener('click', this._onExampleTextClick.bind(this), false);
|
|
this._exampleTextInput.addEventListener('blur', this._onExampleTextInputBlur.bind(this), false);
|
|
this._exampleTextInput.addEventListener('input', this._onExampleTextInputInput.bind(this), false);
|
|
|
|
// Overwrite API functions
|
|
/** @type {?(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */
|
|
this._apiOptionsGetOld = this._application.api.optionsGet.bind(this._application.api);
|
|
this._application.api.optionsGet = this._apiOptionsGet.bind(this);
|
|
|
|
this._languageSummaries = await this._application.api.getLanguageSummaries();
|
|
const options = await this._application.api.optionsGet({current: true});
|
|
void this._setLanguageExampleText({language: options.general.language});
|
|
|
|
// Overwrite frontend
|
|
this._frontend = new Frontend({
|
|
application: this._application,
|
|
popupFactory: this._popupFactory,
|
|
depth: 0,
|
|
parentPopupId: null,
|
|
parentFrameId: null,
|
|
useProxyPopup: false,
|
|
canUseWindowPopup: false,
|
|
pageType: 'web',
|
|
allowRootFramePopupProxy: false,
|
|
childrenSupported: false,
|
|
hotkeyHandler: this._hotkeyHandler,
|
|
});
|
|
this._frontend.setOptionsContextOverride(this._optionsContext);
|
|
await this._frontend.prepare();
|
|
this._frontend.setDisabledOverride(true);
|
|
this._frontend.canClearSelection = false;
|
|
const {popup} = this._frontend;
|
|
if (popup !== null) {
|
|
popup.on('customOuterCssChanged', this._onCustomOuterCssChanged.bind(this));
|
|
}
|
|
|
|
// Update search
|
|
void this._updateSearch();
|
|
}
|
|
|
|
// Private
|
|
|
|
/**
|
|
* @param {import('settings').OptionsContext} optionsContext
|
|
* @returns {Promise<import('settings').ProfileOptions>}
|
|
*/
|
|
async _apiOptionsGet(optionsContext) {
|
|
const options = await /** @type {(optionsContext: import('settings').OptionsContext) => Promise<import('settings').ProfileOptions>} */ (this._apiOptionsGetOld)(optionsContext);
|
|
options.general.enable = true;
|
|
options.general.debugInfo = false;
|
|
options.general.popupWidth = 400;
|
|
options.general.popupHeight = 250;
|
|
options.general.popupHorizontalOffset = 0;
|
|
options.general.popupVerticalOffset = 10;
|
|
options.general.popupHorizontalOffset2 = 10;
|
|
options.general.popupVerticalOffset2 = 0;
|
|
options.general.popupHorizontalTextPosition = 'below';
|
|
options.general.popupVerticalTextPosition = 'before';
|
|
options.scanning.selectText = false;
|
|
this._themeController.theme = options.general.popupTheme;
|
|
this._themeController.siteOverride = true;
|
|
this._themeController.updateTheme();
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* @param {import('popup').EventArgument<'customOuterCssChanged'>} details
|
|
*/
|
|
_onCustomOuterCssChanged({node, inShadow}) {
|
|
if (node === null || inShadow) { return; }
|
|
|
|
const node2 = document.querySelector('#popup-outer-css');
|
|
if (node2 === null) { return; }
|
|
const {parentNode} = node2;
|
|
if (parentNode === null) { return; }
|
|
|
|
// This simulates the stylesheet priorities when injecting using the web extension API.
|
|
parentNode.insertBefore(node, node2);
|
|
}
|
|
|
|
/**
|
|
* @param {MessageEvent<import('popup-preview-frame.js').ApiMessageAny>} event
|
|
*/
|
|
_onMessage(event) {
|
|
if (event.origin !== this._targetOrigin) { return; }
|
|
const {action, params} = event.data;
|
|
const callback = () => {}; // NOP
|
|
invokeApiMapHandler(this._windowMessageHandlers, action, params, [], callback);
|
|
}
|
|
|
|
/** */
|
|
_onExampleTextClick() {
|
|
if (this._exampleTextInput === null) { return; }
|
|
const visible = this._exampleTextInput.hidden;
|
|
this._exampleTextInput.hidden = !visible;
|
|
if (!visible) { return; }
|
|
this._exampleTextInput.focus();
|
|
this._exampleTextInput.select();
|
|
}
|
|
|
|
/** */
|
|
_onExampleTextInputBlur() {
|
|
if (this._exampleTextInput === null) { return; }
|
|
this._exampleTextInput.hidden = true;
|
|
}
|
|
|
|
/**
|
|
* @param {Event} e
|
|
*/
|
|
_onExampleTextInputInput(e) {
|
|
const element = /** @type {HTMLInputElement} */ (e.currentTarget);
|
|
this._setText(element.value, false);
|
|
}
|
|
|
|
/** @type {import('popup-preview-frame').ApiHandler<'setText'>} */
|
|
_onSetText({text}) {
|
|
this._setText(text, true);
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @param {boolean} setInput
|
|
*/
|
|
_setText(text, setInput) {
|
|
if (setInput && this._exampleTextInput !== null) {
|
|
this._exampleTextInput.value = text;
|
|
}
|
|
|
|
if (this._exampleText === null) { return; }
|
|
|
|
this._exampleText.textContent = text;
|
|
if (this._frontend === null) { return; }
|
|
void this._updateSearch();
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} visible
|
|
*/
|
|
_setInfoVisible(visible) {
|
|
const node = document.querySelector('.placeholder-info');
|
|
if (node === null) { return; }
|
|
|
|
node.classList.toggle('placeholder-info-visible', visible);
|
|
}
|
|
|
|
/** @type {import('popup-preview-frame').ApiHandler<'setCustomCss'>} */
|
|
_setCustomCss({css}) {
|
|
if (this._frontend === null) { return; }
|
|
const popup = this._frontend.popup;
|
|
if (popup === null) { return; }
|
|
void popup.setCustomCss(css);
|
|
}
|
|
|
|
/** @type {import('popup-preview-frame').ApiHandler<'setCustomOuterCss'>} */
|
|
_setCustomOuterCss({css}) {
|
|
if (this._frontend === null) { return; }
|
|
const popup = this._frontend.popup;
|
|
if (popup === null) { return; }
|
|
void popup.setCustomOuterCss(css, false);
|
|
}
|
|
|
|
/** @type {import('popup-preview-frame').ApiHandler<'updateOptionsContext'>} */
|
|
async _updateOptionsContext(details) {
|
|
const {optionsContext} = details;
|
|
this._optionsContext = optionsContext;
|
|
if (this._frontend === null) { return; }
|
|
this._frontend.setOptionsContextOverride(optionsContext);
|
|
await this._frontend.updateOptions();
|
|
await this._updateSearch();
|
|
}
|
|
|
|
/** @type {import('popup-preview-frame').ApiHandler<'setLanguageExampleText'>} */
|
|
_setLanguageExampleText({language}) {
|
|
const activeLanguage = /** @type {import('language').LanguageSummary} */ (this._languageSummaries.find(({iso}) => iso === language));
|
|
|
|
this._exampleTextInputEvents.removeAllEventListeners();
|
|
if (this._exampleTextInput !== null && language === 'ja') {
|
|
this._exampleTextInputEvents.addEventListener(this._exampleTextInput, 'input', this._onSearchInput.bind(this), false);
|
|
}
|
|
|
|
this._exampleTextInput.lang = language;
|
|
this._exampleTextInput.value = activeLanguage.exampleText;
|
|
this._exampleTextInput.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
/**
|
|
* @param {InputEvent} e
|
|
*/
|
|
_onSearchInput(e) {
|
|
const element = /** @type {HTMLTextAreaElement} */ (e.currentTarget);
|
|
this._searchTextKanaConversion(element, e);
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLTextAreaElement} element
|
|
* @param {InputEvent} event
|
|
*/
|
|
_searchTextKanaConversion(element, event) {
|
|
const platform = document.documentElement.dataset.platform ?? 'unknown';
|
|
const browser = document.documentElement.dataset.browser ?? 'unknown';
|
|
if (isComposing(event, platform, browser)) { return; }
|
|
|
|
const {kanaString, newSelectionStart} = convertToKanaIME(element.value, element.selectionStart);
|
|
element.value = kanaString;
|
|
element.setSelectionRange(newSelectionStart, newSelectionStart);
|
|
}
|
|
|
|
/** */
|
|
async _updateSearch() {
|
|
if (this._exampleText === null) { return; }
|
|
|
|
const textNode = this._exampleText.firstChild;
|
|
if (textNode === null) { return; }
|
|
|
|
const range = document.createRange();
|
|
range.selectNodeContents(textNode);
|
|
const source = TextSourceRange.create(range);
|
|
const frontend = /** @type {Frontend} */ (this._frontend);
|
|
|
|
try {
|
|
await frontend.setTextSource(source);
|
|
} finally {
|
|
source.cleanup();
|
|
}
|
|
this._textSource = source;
|
|
await frontend.showContentCompleted();
|
|
|
|
const popup = frontend.popup;
|
|
if (popup !== null && popup.isVisibleSync()) {
|
|
this._popupShown = true;
|
|
}
|
|
|
|
this._setInfoVisible(!this._popupShown);
|
|
|
|
this._themeController.updateTheme();
|
|
}
|
|
}
|