aboutsummaryrefslogtreecommitdiff
path: root/ext/js/app/frontend.js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2023-11-27 12:48:14 -0500
committertoasted-nutbread <toasted-nutbread@users.noreply.github.com>2023-11-27 12:48:14 -0500
commit4da4827bcbcdd1ef163f635d9b29416ff272b0bb (patch)
treea8a0f1a8befdb78a554e1be91f2c6059ca3ad5f9 /ext/js/app/frontend.js
parentfd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff)
Add JSDoc type annotations to project (rebased)
Diffstat (limited to 'ext/js/app/frontend.js')
-rw-r--r--ext/js/app/frontend.js308
1 files changed, 246 insertions, 62 deletions
diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js
index 4c13eac2..fec933f8 100644
--- a/ext/js/app/frontend.js
+++ b/ext/js/app/frontend.js
@@ -17,7 +17,7 @@
*/
import {GoogleDocsUtil} from '../accessibility/google-docs-util.js';
-import {EventListenerCollection, invokeMessageHandler, isObject, log, promiseAnimationFrame} from '../core.js';
+import {EventListenerCollection, invokeMessageHandler, log, promiseAnimationFrame} from '../core.js';
import {DocumentUtil} from '../dom/document-util.js';
import {TextSourceElement} from '../dom/text-source-element.js';
import {TextSourceRange} from '../dom/text-source-range.js';
@@ -25,7 +25,6 @@ import {HotkeyHandler} from '../input/hotkey-handler.js';
import {TextScanner} from '../language/text-scanner.js';
import {yomitan} from '../yomitan.js';
import {PopupFactory} from './popup-factory.js';
-import {Popup} from './popup.js';
/**
* This is the main class responsible for scanning and handling webpage content.
@@ -33,19 +32,7 @@ import {Popup} from './popup.js';
export class Frontend {
/**
* Creates a new instance.
- * @param {object} details Details about how to set up the instance.
- * @param {string} details.pageType The type of page, one of 'web', 'popup', or 'search'.
- * @param {PopupFactory} details.popupFactory A PopupFactory instance to use for generating popups.
- * @param {number} details.depth The nesting depth value of the popup.
- * @param {number} details.tabId The tab ID of the host tab.
- * @param {number} details.frameId The frame ID of the host frame.
- * @param {?string} details.parentPopupId The popup ID of the parent popup if one exists, otherwise null.
- * @param {?number} details.parentFrameId The frame ID of the parent popup if one exists, otherwise null.
- * @param {boolean} details.useProxyPopup Whether or not proxy popups should be used.
- * @param {boolean} details.canUseWindowPopup Whether or not window popups can be used.
- * @param {boolean} details.allowRootFramePopupProxy Whether or not popups can be hosted in the root frame.
- * @param {boolean} details.childrenSupported Whether popups can create child popups or not.
- * @param {HotkeyHandler} details.hotkeyHandler A HotkeyHandler instance.
+ * @param {import('frontend').ConstructorDetails} details Details about how to set up the instance.
*/
constructor({
pageType,
@@ -61,24 +48,43 @@ export class Frontend {
childrenSupported=true,
hotkeyHandler
}) {
+ /** @type {import('frontend').PageType} */
this._pageType = pageType;
+ /** @type {PopupFactory} */
this._popupFactory = popupFactory;
+ /** @type {number} */
this._depth = depth;
+ /** @type {number|undefined} */
this._tabId = tabId;
+ /** @type {number} */
this._frameId = frameId;
+ /** @type {?string} */
this._parentPopupId = parentPopupId;
+ /** @type {?number} */
this._parentFrameId = parentFrameId;
+ /** @type {boolean} */
this._useProxyPopup = useProxyPopup;
+ /** @type {boolean} */
this._canUseWindowPopup = canUseWindowPopup;
+ /** @type {boolean} */
this._allowRootFramePopupProxy = allowRootFramePopupProxy;
+ /** @type {boolean} */
this._childrenSupported = childrenSupported;
+ /** @type {HotkeyHandler} */
this._hotkeyHandler = hotkeyHandler;
+ /** @type {?import('popup').PopupAny} */
this._popup = null;
+ /** @type {boolean} */
this._disabledOverride = false;
+ /** @type {?import('settings').ProfileOptions} */
this._options = null;
+ /** @type {number} */
this._pageZoomFactor = 1.0;
+ /** @type {number} */
this._contentScale = 1.0;
+ /** @type {Promise<void>} */
this._lastShowPromise = Promise.resolve();
+ /** @type {TextScanner} */
this._textScanner = new TextScanner({
node: window,
ignoreElements: this._ignoreElements.bind(this),
@@ -87,19 +93,27 @@ export class Frontend {
searchTerms: true,
searchKanji: true
});
+ /** @type {boolean} */
this._textScannerHasBeenEnabled = false;
+ /** @type {Map<'default'|'window'|'iframe'|'proxy', Promise<?import('popup').PopupAny>>} */
this._popupCache = new Map();
+ /** @type {EventListenerCollection} */
this._popupEventListeners = new EventListenerCollection();
+ /** @type {?import('core').TokenObject} */
this._updatePopupToken = null;
+ /** @type {?number} */
this._clearSelectionTimer = null;
+ /** @type {boolean} */
this._isPointerOverPopup = false;
+ /** @type {?import('settings').OptionsContext} */
this._optionsContextOverride = null;
- this._runtimeMessageHandlers = new Map([
+ /** @type {import('core').MessageHandlerMap} */
+ this._runtimeMessageHandlers = new Map(/** @type {import('core').MessageHandlerArray} */ ([
['Frontend.requestReadyBroadcast', {async: false, handler: this._onMessageRequestFrontendReadyBroadcast.bind(this)}],
['Frontend.setAllVisibleOverride', {async: true, handler: this._onApiSetAllVisibleOverride.bind(this)}],
['Frontend.clearAllVisibleOverride', {async: true, handler: this._onApiClearAllVisibleOverride.bind(this)}]
- ]);
+ ]));
this._hotkeyHandler.registerActions([
['scanSelectedText', this._onActionScanSelectedText.bind(this)],
@@ -125,7 +139,7 @@ export class Frontend {
/**
* Gets the popup instance.
- * @type {Popup}
+ * @type {?import('popup').PopupAny}
*/
get popup() {
return this._popup;
@@ -148,8 +162,8 @@ export class Frontend {
window.addEventListener('resize', this._onResize.bind(this), false);
DocumentUtil.addFullscreenChangeEventListener(this._updatePopup.bind(this));
- const visualViewport = window.visualViewport;
- if (visualViewport !== null && typeof visualViewport === 'object') {
+ const {visualViewport} = window;
+ if (typeof visualViewport !== 'undefined' && visualViewport !== null) {
visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this));
visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this));
}
@@ -172,7 +186,7 @@ export class Frontend {
this._prepareSiteSpecific();
this._updateContentScale();
- this._signalFrontendReady();
+ this._signalFrontendReady(null);
}
/**
@@ -186,7 +200,7 @@ export class Frontend {
/**
* Set or clear an override options context object.
- * @param {?object} optionsContext An options context object to use as the override, or `null` to clear the override.
+ * @param {?import('settings').OptionsContext} optionsContext An options context object to use as the override, or `null` to clear the override.
*/
setOptionsContextOverride(optionsContext) {
this._optionsContextOverride = optionsContext;
@@ -194,7 +208,7 @@ export class Frontend {
/**
* Performs a new search on a specific source.
- * @param {TextSourceRange|TextSourceElement} textSource The text source to search.
+ * @param {import('text-source').TextSource} textSource The text source to search.
*/
async setTextSource(textSource) {
this._textScanner.setCurrentTextSource(null);
@@ -216,7 +230,7 @@ export class Frontend {
/**
* Waits for the previous `showContent` call to be completed.
- * @returns {Promise} A promise which is resolved when the previous `showContent` call has completed.
+ * @returns {Promise<void>} A promise which is resolved when the previous `showContent` call has completed.
*/
showContentCompleted() {
return this._lastShowPromise;
@@ -224,45 +238,73 @@ export class Frontend {
// Message handlers
+ /**
+ * @param {import('frontend').FrontendRequestReadyBroadcastParams} params
+ */
_onMessageRequestFrontendReadyBroadcast({frameId}) {
this._signalFrontendReady(frameId);
}
// Action handlers
+ /**
+ * @returns {void}
+ */
_onActionScanSelectedText() {
this._scanSelectedText(false);
}
+ /**
+ * @returns {void}
+ */
_onActionScanTextAtCaret() {
this._scanSelectedText(true);
}
// API message handlers
+ /**
+ * @returns {string}
+ */
_onApiGetUrl() {
return window.location.href;
}
+ /**
+ * @returns {void}
+ */
_onApiClosePopup() {
this._clearSelection(false);
}
+ /**
+ * @returns {void}
+ */
_onApiCopySelection() {
// This will not work on Firefox if a popup has focus, which is usually the case when this function is called.
document.execCommand('copy');
}
+ /**
+ * @returns {string}
+ */
_onApiGetSelectionText() {
- return document.getSelection().toString();
+ const selection = document.getSelection();
+ return selection !== null ? selection.toString() : '';
}
+ /**
+ * @returns {import('frontend').GetPopupInfoResult}
+ */
_onApiGetPopupInfo() {
return {
popupId: (this._popup !== null ? this._popup.id : null)
};
}
+ /**
+ * @returns {{url: string, documentTitle: string}}
+ */
_onApiGetPageInfo() {
return {
url: window.location.href,
@@ -270,6 +312,10 @@ export class Frontend {
};
}
+ /**
+ * @param {{value: boolean, priority: number, awaitFrame: boolean}} params
+ * @returns {Promise<import('core').TokenString>}
+ */
async _onApiSetAllVisibleOverride({value, priority, awaitFrame}) {
const result = await this._popupFactory.setAllVisibleOverride(value, priority);
if (awaitFrame) {
@@ -278,45 +324,71 @@ export class Frontend {
return result;
}
+ /**
+ * @param {{token: import('core').TokenString}} params
+ * @returns {Promise<boolean>}
+ */
async _onApiClearAllVisibleOverride({token}) {
return await this._popupFactory.clearAllVisibleOverride(token);
}
// Private
+ /**
+ * @returns {void}
+ */
_onResize() {
this._updatePopupPosition();
}
+ /** @type {import('extension').ChromeRuntimeOnMessageCallback} */
_onRuntimeMessage({action, params}, sender, callback) {
const messageHandler = this._runtimeMessageHandlers.get(action);
if (typeof messageHandler === 'undefined') { return false; }
return invokeMessageHandler(messageHandler, params, callback, sender);
}
+ /**
+ * @param {{newZoomFactor: number}} params
+ */
_onZoomChanged({newZoomFactor}) {
this._pageZoomFactor = newZoomFactor;
this._updateContentScale();
}
+ /**
+ * @returns {void}
+ */
_onClosePopups() {
this._clearSelection(true);
}
+ /**
+ * @returns {void}
+ */
_onVisualViewportScroll() {
this._updatePopupPosition();
}
+ /**
+ * @returns {void}
+ */
_onVisualViewportResize() {
this._updateContentScale();
}
+ /**
+ * @returns {void}
+ */
_onTextScannerClear() {
this._clearSelection(false);
}
- _onSearched({type, dictionaryEntries, sentence, inputInfo: {eventType, passive, detail}, textSource, optionsContext, detail: {documentTitle}, error}) {
- const scanningOptions = this._options.scanning;
+ /**
+ * @param {import('text-scanner').SearchedEventDetails} details
+ */
+ _onSearched({type, dictionaryEntries, sentence, inputInfo: {eventType, passive, detail: inputInfoDetail}, textSource, optionsContext, detail, error}) {
+ const scanningOptions = /** @type {import('settings').ProfileOptions} */ (this._options).scanning;
if (error !== null) {
if (yomitan.isExtensionUnloaded) {
@@ -326,34 +398,43 @@ export class Frontend {
} else {
log.error(error);
}
- } if (type !== null) {
+ } if (type !== null && optionsContext !== null) {
this._stopClearSelectionDelayed();
let focus = (eventType === 'mouseMove');
- if (isObject(detail)) {
- const focus2 = detail.focus;
+ if (typeof inputInfoDetail === 'object' && inputInfoDetail !== null) {
+ const focus2 = inputInfoDetail.focus;
if (typeof focus2 === 'boolean') { focus = focus2; }
}
- this._showContent(textSource, focus, dictionaryEntries, type, sentence, documentTitle, optionsContext);
+ this._showContent(textSource, focus, dictionaryEntries, type, sentence, detail !== null ? detail.documentTitle : null, optionsContext);
} else {
if (scanningOptions.autoHideResults) {
- this._clearSelectionDelayed(scanningOptions.hideDelay, false);
+ this._clearSelectionDelayed(scanningOptions.hideDelay, false, false);
}
}
}
+ /**
+ * @returns {void}
+ */
_onPopupFramePointerOver() {
this._isPointerOverPopup = true;
this._stopClearSelectionDelayed();
}
+ /**
+ * @returns {void}
+ */
_onPopupFramePointerOut() {
this._isPointerOverPopup = false;
- const scanningOptions = this._options.scanning;
+ const scanningOptions = /** @type {import('settings').ProfileOptions} */ (this._options).scanning;
if (scanningOptions.hidePopupOnCursorExit) {
- this._clearSelectionDelayed(scanningOptions.hidePopupOnCursorExitDelay, false);
+ this._clearSelectionDelayed(scanningOptions.hidePopupOnCursorExitDelay, false, false);
}
}
+ /**
+ * @param {boolean} passive
+ */
_clearSelection(passive) {
this._stopClearSelectionDelayed();
if (this._popup !== null) {
@@ -364,6 +445,11 @@ export class Frontend {
this._textScanner.clearSelection();
}
+ /**
+ * @param {number} delay
+ * @param {boolean} restart
+ * @param {boolean} passive
+ */
_clearSelectionDelayed(delay, restart, passive) {
if (!this._textScanner.hasSelection()) { return; }
if (delay > 0) {
@@ -379,6 +465,9 @@ export class Frontend {
}
}
+ /**
+ * @returns {void}
+ */
_stopClearSelectionDelayed() {
if (this._clearSelectionTimer !== null) {
clearTimeout(this._clearSelectionTimer);
@@ -386,6 +475,9 @@ export class Frontend {
}
}
+ /**
+ * @returns {Promise<void>}
+ */
async _updateOptionsInternal() {
const optionsContext = await this._getOptionsContext();
const options = await yomitan.api.optionsGet(optionsContext);
@@ -426,12 +518,16 @@ export class Frontend {
await this._textScanner.searchLast();
}
+ /**
+ * @returns {Promise<void>}
+ */
async _updatePopup() {
- const {usePopupWindow, showIframePopupsInRootFrame} = this._options.general;
+ const {usePopupWindow, showIframePopupsInRootFrame} = /** @type {import('settings').ProfileOptions} */ (this._options).general;
const isIframe = !this._useProxyPopup && (window !== window.parent);
const currentPopup = this._popup;
+ /** @type {Promise<?import('popup').PopupAny>|undefined} */
let popupPromise;
if (usePopupWindow && this._canUseWindowPopup) {
popupPromise = this._popupCache.get('window');
@@ -466,6 +562,7 @@ export class Frontend {
// The token below is used as a unique identifier to ensure that a new _updatePopup call
// hasn't been started during the await.
+ /** @type {?import('core').TokenObject} */
const token = {};
this._updatePopupToken = token;
const popup = await popupPromise;
@@ -489,6 +586,9 @@ export class Frontend {
this._isPointerOverPopup = false;
}
+ /**
+ * @returns {Promise<?import('popup').PopupAny>}
+ */
async _getDefaultPopup() {
const isXmlDocument = (typeof XMLDocument !== 'undefined' && document instanceof XMLDocument);
if (isXmlDocument) {
@@ -502,6 +602,9 @@ export class Frontend {
});
}
+ /**
+ * @returns {Promise<import('popup').PopupAny>}
+ */
async _getProxyPopup() {
return await this._popupFactory.getOrCreatePopup({
frameId: this._parentFrameId,
@@ -511,6 +614,9 @@ export class Frontend {
});
}
+ /**
+ * @returns {Promise<?import('popup').PopupAny>}
+ */
async _getIframeProxyPopup() {
const targetFrameId = 0; // Root frameId
try {
@@ -520,7 +626,8 @@ export class Frontend {
return await this._getDefaultPopup();
}
- const {popupId} = await yomitan.crossFrame.invoke(targetFrameId, 'Frontend.getPopupInfo');
+ /** @type {import('frontend').GetPopupInfoResult} */
+ const {popupId} = await yomitan.crossFrame.invoke(targetFrameId, 'Frontend.getPopupInfo', {});
if (popupId === null) {
return null;
}
@@ -537,6 +644,9 @@ export class Frontend {
return popup;
}
+ /**
+ * @returns {Promise<import('popup').PopupAny>}
+ */
async _getPopupWindow() {
return await this._popupFactory.getOrCreatePopup({
depth: this._depth,
@@ -545,6 +655,9 @@ export class Frontend {
});
}
+ /**
+ * @returns {Element[]}
+ */
_ignoreElements() {
if (this._popup !== null) {
const container = this._popup.container;
@@ -555,6 +668,11 @@ export class Frontend {
return [];
}
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @returns {Promise<boolean>}
+ */
async _ignorePoint(x, y) {
try {
return this._popup !== null && await this._popup.containsPoint(x, y);
@@ -566,17 +684,44 @@ export class Frontend {
}
}
+ /**
+ * @param {import('text-source').TextSource} textSource
+ */
_showExtensionUnloaded(textSource) {
- if (textSource === null) {
- textSource = this._textScanner.getCurrentTextSource();
- if (textSource === null) { return; }
- }
this._showPopupContent(textSource, null, null);
}
+ /**
+ * @param {import('text-source').TextSource} textSource
+ * @param {boolean} focus
+ * @param {?import('dictionary').DictionaryEntry[]} dictionaryEntries
+ * @param {import('display').PageType} type
+ * @param {?import('display').HistoryStateSentence} sentence
+ * @param {?string} documentTitle
+ * @param {import('settings').OptionsContext} optionsContext
+ */
_showContent(textSource, focus, dictionaryEntries, type, sentence, documentTitle, optionsContext) {
const query = textSource.text();
const {url} = optionsContext;
+ /** @type {import('display').HistoryState} */
+ const detailsState = {
+ focusEntry: 0,
+ optionsContext,
+ url
+ };
+ if (sentence !== null) { detailsState.sentence = sentence; }
+ if (documentTitle !== null) { detailsState.documentTitle = documentTitle; }
+ /** @type {import('display').HistoryContent} */
+ const detailsContent = {
+ contentOrigin: {
+ tabId: this._tabId,
+ frameId: this._frameId
+ }
+ };
+ if (dictionaryEntries !== null) {
+ detailsContent.dictionaryEntries = dictionaryEntries;
+ }
+ /** @type {import('display').ContentDetails} */
const details = {
focus,
historyMode: 'clear',
@@ -585,28 +730,22 @@ export class Frontend {
query,
wildcards: 'off'
},
- state: {
- focusEntry: 0,
- optionsContext,
- url,
- sentence,
- documentTitle
- },
- content: {
- dictionaryEntries,
- contentOrigin: {
- tabId: this._tabId,
- frameId: this._frameId
- }
- }
+ state: detailsState,
+ content: detailsContent
};
- if (textSource.type === 'element' && textSource.fullContent !== query) {
+ if (textSource instanceof TextSourceElement && textSource.fullContent !== query) {
details.params.full = textSource.fullContent;
details.params['full-visible'] = 'true';
}
this._showPopupContent(textSource, optionsContext, details);
}
+ /**
+ * @param {import('text-source').TextSource} textSource
+ * @param {?import('settings').OptionsContext} optionsContext
+ * @param {?import('display').ContentDetails} details
+ * @returns {Promise<void>}
+ */
_showPopupContent(textSource, optionsContext, details) {
const sourceRects = [];
for (const {left, top, right, bottom} of textSource.getRects()) {
@@ -631,6 +770,9 @@ export class Frontend {
return this._lastShowPromise;
}
+ /**
+ * @returns {void}
+ */
_updateTextScannerEnabled() {
const enabled = (this._options !== null && this._options.general.enable && !this._disabledOverride);
if (enabled === this._textScanner.isEnabled()) { return; }
@@ -643,15 +785,18 @@ export class Frontend {
}
}
+ /**
+ * @returns {void}
+ */
_updateContentScale() {
- const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general;
+ const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = /** @type {import('settings').ProfileOptions} */ (this._options).general;
let contentScale = popupScalingFactor;
if (popupScaleRelativeToPageZoom) {
contentScale /= this._pageZoomFactor;
}
if (popupScaleRelativeToVisualViewport) {
- const visualViewport = window.visualViewport;
- const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0);
+ const {visualViewport} = window;
+ const visualViewportScale = (typeof visualViewport !== 'undefined' && visualViewport !== null ? visualViewport.scale : 1.0);
contentScale /= visualViewportScale;
}
if (contentScale === this._contentScale) { return; }
@@ -663,6 +808,9 @@ export class Frontend {
this._updatePopupPosition();
}
+ /**
+ * @returns {Promise<void>}
+ */
async _updatePopupPosition() {
const textSource = this._textScanner.getCurrentTextSource();
if (
@@ -674,7 +822,11 @@ export class Frontend {
}
}
- _signalFrontendReady(targetFrameId=null) {
+ /**
+ * @param {?number} targetFrameId
+ */
+ _signalFrontendReady(targetFrameId) {
+ /** @type {import('frontend').FrontendReadyDetails} */
const params = {frameId: this._frameId};
if (targetFrameId === null) {
yomitan.api.broadcastTab('frontendReady', params);
@@ -683,8 +835,14 @@ export class Frontend {
}
}
+ /**
+ * @param {number} frameId
+ * @param {?number} timeout
+ * @returns {Promise<void>}
+ */
async _waitForFrontendReady(frameId, timeout) {
return new Promise((resolve, reject) => {
+ /** @type {?number} */
let timeoutId = null;
const cleanup = () => {
@@ -694,10 +852,11 @@ export class Frontend {
}
chrome.runtime.onMessage.removeListener(onMessage);
};
- const onMessage = (message, sender, sendResponse) => {
+ /** @type {import('extension').ChromeRuntimeOnMessageCallback} */
+ const onMessage = (message, _sender, sendResponse) => {
try {
const {action, params} = message;
- if (action === 'frontendReady' && params.frameId === frameId) {
+ if (action === 'frontendReady' && /** @type {import('frontend').FrontendReadyDetails} */ (params).frameId === frameId) {
cleanup();
resolve();
sendResponse();
@@ -720,6 +879,10 @@ export class Frontend {
});
}
+ /**
+ * @param {import('settings').PreventMiddleMouseOptions} preventMiddleMouseOptions
+ * @returns {boolean}
+ */
_getPreventMiddleMouseValueForPageType(preventMiddleMouseOptions) {
switch (this._pageType) {
case 'web': return preventMiddleMouseOptions.onWebPages;
@@ -729,6 +892,9 @@ export class Frontend {
}
}
+ /**
+ * @returns {Promise<import('settings').OptionsContext>}
+ */
async _getOptionsContext() {
let optionsContext = this._optionsContextOverride;
if (optionsContext === null) {
@@ -737,10 +903,13 @@ export class Frontend {
return optionsContext;
}
+ /**
+ * @returns {Promise<{optionsContext: import('settings').OptionsContext, detail?: import('text-scanner').SearchResultDetail}>}
+ */
async _getSearchContext() {
let url = window.location.href;
let documentTitle = document.title;
- if (this._useProxyPopup) {
+ if (this._useProxyPopup && this._parentFrameId !== null) {
try {
({url, documentTitle} = await yomitan.crossFrame.invoke(this._parentFrameId, 'Frontend.getPageInfo', {}));
} catch (e) {
@@ -759,6 +928,10 @@ export class Frontend {
};
}
+ /**
+ * @param {boolean} allowEmptyRange
+ * @returns {Promise<boolean>}
+ */
async _scanSelectedText(allowEmptyRange) {
const range = this._getFirstSelectionRange(allowEmptyRange);
if (range === null) { return false; }
@@ -767,8 +940,13 @@ export class Frontend {
return true;
}
+ /**
+ * @param {boolean} allowEmptyRange
+ * @returns {?Range}
+ */
_getFirstSelectionRange(allowEmptyRange) {
const selection = window.getSelection();
+ if (selection === null) { return null; }
for (let i = 0, ii = selection.rangeCount; i < ii; ++i) {
const range = selection.getRangeAt(i);
if (range.toString().length > 0 || allowEmptyRange) {
@@ -778,6 +956,9 @@ export class Frontend {
return null;
}
+ /**
+ * @returns {void}
+ */
_prepareSiteSpecific() {
switch (location.hostname.toLowerCase()) {
case 'docs.google.com':
@@ -786,6 +967,9 @@ export class Frontend {
}
}
+ /**
+ * @returns {Promise<void>}
+ */
async _prepareGoogleDocs() {
if (typeof GoogleDocsUtil !== 'undefined') { return; }
await yomitan.api.loadExtensionScripts([