summaryrefslogtreecommitdiff
path: root/ext/fg
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fg')
-rw-r--r--ext/fg/float.html3
-rw-r--r--ext/fg/js/document.js8
-rw-r--r--ext/fg/js/float.js85
-rw-r--r--ext/fg/js/frontend-api-sender.js10
-rw-r--r--ext/fg/js/frontend-initialize.js12
-rw-r--r--ext/fg/js/frontend.js3
-rw-r--r--ext/fg/js/popup-nested.js1
-rw-r--r--ext/fg/js/popup-proxy-host.js65
-rw-r--r--ext/fg/js/popup-proxy.js9
-rw-r--r--ext/fg/js/popup.js262
-rw-r--r--ext/fg/js/source.js16
11 files changed, 323 insertions, 151 deletions
diff --git a/ext/fg/float.html b/ext/fg/float.html
index bec5ae68..352a866a 100644
--- a/ext/fg/float.html
+++ b/ext/fg/float.html
@@ -35,7 +35,7 @@
<h1>Yomichan Updated!</h1>
<p>
The Yomichan extension has been updated to a new version! In order to continue
- viewing definitions on this page you must reload this tab or restart your browser.
+ viewing definitions on this page, you must reload this tab or restart your browser.
</p>
</div>
</div>
@@ -51,6 +51,7 @@
<script src="/mixed/js/display.js"></script>
<script src="/mixed/js/display-generator.js"></script>
<script src="/mixed/js/scroll.js"></script>
+ <script src="/mixed/js/template-handler.js"></script>
<script src="/fg/js/float.js"></script>
diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js
index 71654b29..35861475 100644
--- a/ext/fg/js/document.js
+++ b/ext/fg/js/document.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global TextSourceElement, TextSourceRange, DOM*/
const REGEX_TRANSPARENT_COLOR = /rgba\s*\([^)]*,\s*0(?:\.0+)?\s*\)/;
@@ -49,7 +50,9 @@ function docImposterCreate(element, isTextarea) {
const imposter = document.createElement('div');
const imposterStyle = imposter.style;
- imposter.innerText = element.value;
+ let value = element.value;
+ if (value.endsWith('\n')) { value += '\n'; }
+ imposter.textContent = value;
for (let i = 0, ii = elementStyle.length; i < ii; ++i) {
const property = elementStyle[i];
@@ -191,8 +194,7 @@ function docSentenceExtract(source, extent) {
if (terminators.includes(c)) {
endPos = i + 1;
break;
- }
- else if (c in quotesBwd) {
+ } else if (c in quotesBwd) {
endPos = i;
break;
}
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js
index 8d61d8f6..8f21a9c5 100644
--- a/ext/fg/js/float.js
+++ b/ext/fg/js/float.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global popupNestedInitialize, apiForward, apiGetMessageToken, Display*/
class DisplayFloat extends Display {
constructor() {
@@ -28,11 +29,33 @@ class DisplayFloat extends Display {
};
this._orphaned = false;
+ this._prepareInvoked = false;
+ this._messageToken = null;
+ this._messageTokenPromise = null;
yomichan.on('orphaned', () => this.onOrphaned());
window.addEventListener('message', (e) => this.onMessage(e), false);
}
+ async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) {
+ if (this._prepareInvoked) { return; }
+ this._prepareInvoked = true;
+
+ await super.prepare(options);
+
+ const {id, depth, parentFrameId} = popupInfo;
+ this.optionsContext.depth = depth;
+ this.optionsContext.url = url;
+
+ if (childrenSupported) {
+ popupNestedInitialize(id, depth, parentFrameId, url);
+ }
+
+ this.setContentScale(scale);
+
+ apiForward('popupPrepareCompleted', {uniqueId});
+ }
+
onError(error) {
if (this._orphaned) {
this.setContent('orphaned');
@@ -54,11 +77,23 @@ class DisplayFloat extends Display {
}
onMessage(e) {
- const {action, params} = e.data;
- const handler = DisplayFloat._messageHandlers.get(action);
- if (typeof handler !== 'function') { return; }
-
- handler(this, params);
+ const data = e.data;
+ if (typeof data !== 'object' || data === null) { return; } // Invalid data
+
+ const token = data.token;
+ if (typeof token !== 'string') { return; } // Invalid data
+
+ if (this._messageToken === null) {
+ // Async
+ this.getMessageToken()
+ .then(
+ () => { this.handleAction(token, data); },
+ () => {}
+ );
+ } else {
+ // Sync
+ this.handleAction(token, data);
+ }
}
onKeyDown(e) {
@@ -73,6 +108,30 @@ class DisplayFloat extends Display {
return super.onKeyDown(e);
}
+ async getMessageToken() {
+ // this._messageTokenPromise is used to ensure that only one call to apiGetMessageToken is made.
+ if (this._messageTokenPromise === null) {
+ this._messageTokenPromise = apiGetMessageToken();
+ }
+ const messageToken = await this._messageTokenPromise;
+ if (this._messageToken === null) {
+ this._messageToken = messageToken;
+ }
+ this._messageTokenPromise = null;
+ }
+
+ handleAction(token, {action, params}) {
+ if (token !== this._messageToken) {
+ // Invalid token
+ return;
+ }
+
+ const handler = DisplayFloat._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+
+ handler(this, params);
+ }
+
getOptionsContext() {
return this.optionsContext;
}
@@ -92,20 +151,6 @@ class DisplayFloat extends Display {
setContentScale(scale) {
document.body.style.fontSize = `${scale}em`;
}
-
- async initialize(options, popupInfo, url, childrenSupported, scale) {
- await super.initialize(options);
-
- const {id, depth, parentFrameId} = popupInfo;
- this.optionsContext.depth = depth;
- this.optionsContext.url = url;
-
- if (childrenSupported) {
- popupNestedInitialize(id, depth, parentFrameId, url);
- }
-
- this.setContentScale(scale);
- }
}
DisplayFloat._onKeyDownHandlers = new Map([
@@ -122,7 +167,7 @@ DisplayFloat._messageHandlers = new Map([
['setContent', (self, {type, details}) => self.setContent(type, details)],
['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()],
['setCustomCss', (self, {css}) => self.setCustomCss(css)],
- ['initialize', (self, {options, popupInfo, url, childrenSupported, scale}) => self.initialize(options, popupInfo, url, childrenSupported, scale)],
+ ['prepare', (self, {options, popupInfo, url, childrenSupported, scale, uniqueId}) => self.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)],
['setContentScale', (self, {scale}) => self.setContentScale(scale)]
]);
diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js
index 93c2e593..8dc6aaf3 100644
--- a/ext/fg/js/frontend-api-sender.js
+++ b/ext/fg/js/frontend-api-sender.js
@@ -19,7 +19,7 @@
class FrontendApiSender {
constructor() {
- this.senderId = FrontendApiSender.generateId(16);
+ this.senderId = yomichan.generateId(16);
this.ackTimeout = 3000; // 3 seconds
this.responseTimeout = 10000; // 10 seconds
this.callbacks = new Map();
@@ -123,12 +123,4 @@ class FrontendApiSender {
info.timer = null;
info.reject(new Error(reason));
}
-
- static generateId(length) {
- let id = '';
- for (let i = 0; i < length; ++i) {
- id += Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
- }
- return id;
- }
}
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
index 9c923fea..54b874f2 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -16,18 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global PopupProxyHost, PopupProxy, Frontend*/
async function main() {
const data = window.frontendInitializationData || {};
const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data;
- let popupHost = null;
- if (!proxy) {
- popupHost = new PopupProxyHost();
+ let popup;
+ if (proxy) {
+ popup = new PopupProxy(null, depth + 1, id, parentFrameId, url);
+ } else {
+ const popupHost = new PopupProxyHost();
await popupHost.prepare();
+
+ popup = popupHost.getOrCreatePopup();
}
- const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : popupHost.createPopup(null, depth);
const frontend = new Frontend(popup, ignoreNodes);
await frontend.prepare();
}
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 2286bf19..67045241 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiGetZoom, apiOptionsGet, apiTermsFind, apiKanjiFind, docSentenceExtract, TextScanner*/
class Frontend extends TextScanner {
constructor(popup, ignoreNodes) {
@@ -55,7 +56,7 @@ class Frontend extends TextScanner {
}
yomichan.on('orphaned', () => this.onOrphaned());
- yomichan.on('optionsUpdate', () => this.updateOptions());
+ yomichan.on('optionsUpdated', () => this.updateOptions());
yomichan.on('zoomChanged', (e) => this.onZoomChanged(e));
chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this));
diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js
index 3f3c945e..3e5f5b80 100644
--- a/ext/fg/js/popup-nested.js
+++ b/ext/fg/js/popup-nested.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiOptionsGet*/
let popupNestedInitialized = false;
diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js
index 427172c6..e55801ff 100644
--- a/ext/fg/js/popup-proxy-host.js
+++ b/ext/fg/js/popup-proxy-host.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiFrameInformationGet, FrontendApiReceiver, Popup*/
class PopupProxyHost {
constructor() {
@@ -33,7 +34,7 @@ class PopupProxyHost {
if (typeof frameId !== 'number') { return; }
this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([
- ['createNestedPopup', ({parentId}) => this._onApiCreateNestedPopup(parentId)],
+ ['getOrCreatePopup', ({id, parentId}) => this._onApiGetOrCreatePopup(id, parentId)],
['setOptions', ({id, options}) => this._onApiSetOptions(id, options)],
['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)],
['isVisible', ({id}) => this._onApiIsVisibleAsync(id)],
@@ -46,14 +47,51 @@ class PopupProxyHost {
]));
}
- createPopup(parentId, depth) {
- return this._createPopupInternal(parentId, depth).popup;
+ getOrCreatePopup(id=null, parentId=null) {
+ // Find by existing id
+ if (id !== null) {
+ const popup = this._popups.get(id);
+ if (typeof popup !== 'undefined') {
+ return popup;
+ }
+ }
+
+ // Find by existing parent id
+ let parent = null;
+ if (parentId !== null) {
+ parent = this._popups.get(parentId);
+ if (typeof parent !== 'undefined') {
+ const popup = parent.child;
+ if (popup !== null) {
+ return popup;
+ }
+ } else {
+ parent = null;
+ }
+ }
+
+ // New unique id
+ if (id === null) {
+ id = this._nextId++;
+ }
+
+ // Create new popup
+ const depth = (parent !== null ? parent.depth + 1 : 0);
+ const popup = new Popup(id, depth, this._frameIdPromise);
+ if (parent !== null) {
+ popup.setParent(parent);
+ }
+ this._popups.set(id, popup);
+ return popup;
}
// Message handlers
- async _onApiCreateNestedPopup(parentId) {
- return this._createPopupInternal(parentId, 0).id;
+ async _onApiGetOrCreatePopup(id, parentId) {
+ const popup = this.getOrCreatePopup(id, parentId);
+ return {
+ id: popup.id
+ };
}
async _onApiSetOptions(id, options) {
@@ -105,25 +143,10 @@ class PopupProxyHost {
// Private functions
- _createPopupInternal(parentId, depth) {
- const parent = (typeof parentId === 'string' && this._popups.has(parentId) ? this._popups.get(parentId) : null);
- const id = `${this._nextId}`;
- if (parent !== null) {
- depth = parent.depth + 1;
- }
- ++this._nextId;
- const popup = new Popup(id, depth, this._frameIdPromise);
- if (parent !== null) {
- popup.setParent(parent);
- }
- this._popups.set(id, popup);
- return {popup, id};
- }
-
_getPopup(id) {
const popup = this._popups.get(id);
if (typeof popup === 'undefined') {
- throw new Error('Invalid popup ID');
+ throw new Error(`Invalid popup ID ${id}`);
}
return popup;
}
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 4cacee53..093cdd2e 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -16,12 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global FrontendApiSender*/
class PopupProxy {
- constructor(depth, parentId, parentFrameId, url) {
+ constructor(id, depth, parentId, parentFrameId, url) {
this._parentId = parentId;
this._parentFrameId = parentFrameId;
- this._id = null;
+ this._id = id;
this._idPromise = null;
this._depth = depth;
this._url = url;
@@ -69,7 +70,7 @@ class PopupProxy {
if (this._id === null) {
return;
}
- this._invokeHostApi('setVisibleOverride', {id, visible});
+ this._invokeHostApi('setVisibleOverride', {id: this._id, visible});
}
async containsPoint(x, y) {
@@ -112,7 +113,7 @@ class PopupProxy {
}
async _getPopupIdAsync() {
- const id = await this._invokeHostApi('createNestedPopup', {parentId: this._parentId});
+ const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId});
this._id = id;
return id;
}
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index e7dae93e..4927f4bd 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiInjectStylesheet, apiGetMessageToken*/
class Popup {
constructor(id, depth, frameIdPromise) {
@@ -27,32 +28,40 @@ class Popup {
this._child = null;
this._childrenSupported = true;
this._injectPromise = null;
- this._isInjected = false;
- this._isInjectedAndLoaded = false;
this._visible = false;
this._visibleOverride = null;
this._options = null;
- this._stylesheetInjectedViaApi = false;
this._contentScale = 1.0;
this._containerSizeContentScale = null;
+ this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
+ this._messageToken = null;
this._container = document.createElement('iframe');
this._container.className = 'yomichan-float';
this._container.addEventListener('mousedown', (e) => e.stopPropagation());
this._container.addEventListener('scroll', (e) => e.stopPropagation());
- this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
this._container.style.width = '0px';
this._container.style.height = '0px';
+ this._fullscreenEventListeners = new EventListenerCollection();
+
this._updateVisibility();
}
// Public properties
+ get id() {
+ return this._id;
+ }
+
get parent() {
return this._parent;
}
+ get child() {
+ return this._child;
+ }
+
get depth() {
return this._depth;
}
@@ -117,16 +126,12 @@ class Popup {
}
clearAutoPlayTimer() {
- if (this._isInjectedAndLoaded) {
- this._invokeApi('clearAutoPlayTimer');
- }
+ this._invokeApi('clearAutoPlayTimer');
}
setContentScale(scale) {
this._contentScale = scale;
- if (this._isInjectedAndLoaded) {
- this._invokeApi('setContentScale', {scale});
- }
+ this._invokeApi('setContentScale', {scale});
}
// Popup-only public functions
@@ -146,7 +151,7 @@ class Popup {
}
isVisibleSync() {
- return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible);
+ return (this._visibleOverride !== null ? this._visibleOverride : this._visible);
}
updateTheme() {
@@ -154,21 +159,13 @@ class Popup {
this._container.dataset.yomichanSiteColor = this._getSiteColor();
}
- async setCustomOuterCss(css, injectDirectly) {
- // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them.
- if (this._stylesheetInjectedViaApi) { return; }
-
- if (injectDirectly || Popup._isOnExtensionPage()) {
- Popup.injectOuterStylesheet(css);
- } else {
- if (!css) { return; }
- try {
- await apiInjectStylesheet(css);
- this._stylesheetInjectedViaApi = true;
- } catch (e) {
- // NOP
- }
- }
+ async setCustomOuterCss(css, useWebExtensionApi) {
+ return await Popup._injectStylesheet(
+ 'yomichan-popup-outer-user-stylesheet',
+ 'code',
+ css,
+ useWebExtensionApi
+ );
}
setChildrenSupported(value) {
@@ -183,26 +180,6 @@ class Popup {
return this._container.getBoundingClientRect();
}
- static injectOuterStylesheet(css) {
- if (Popup.outerStylesheet === null) {
- if (!css) { return; }
- Popup.outerStylesheet = document.createElement('style');
- Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet';
- }
-
- const outerStylesheet = Popup.outerStylesheet;
- if (css) {
- outerStylesheet.textContent = css;
-
- const par = document.head;
- if (par && outerStylesheet.parentNode !== par) {
- par.appendChild(outerStylesheet);
- }
- } else {
- outerStylesheet.textContent = '';
- }
- }
-
// Private functions
_inject() {
@@ -222,11 +199,18 @@ class Popup {
// NOP
}
+ if (this._messageToken === null) {
+ this._messageToken = await apiGetMessageToken();
+ }
+
return new Promise((resolve) => {
const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null);
+ this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html'));
this._container.addEventListener('load', () => {
- this._isInjectedAndLoaded = true;
- this._invokeApi('initialize', {
+ const uniqueId = yomichan.generateId(32);
+ Popup._listenForDisplayPrepareCompleted(uniqueId, resolve);
+
+ this._invokeApi('prepare', {
options: this._options,
popupInfo: {
id: this._id,
@@ -235,17 +219,60 @@ class Popup {
},
url: this.url,
childrenSupported: this._childrenSupported,
- scale: this._contentScale
+ scale: this._contentScale,
+ uniqueId
});
- resolve();
});
- this._observeFullscreen();
+ this._observeFullscreen(true);
this._onFullscreenChanged();
- this.setCustomOuterCss(this._options.general.customPopupOuterCss, false);
- this._isInjected = true;
+ this._injectStyles();
});
}
+ async _injectStyles() {
+ try {
+ await Popup._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true);
+ } catch (e) {
+ // NOP
+ }
+
+ try {
+ await this.setCustomOuterCss(this._options.general.customPopupOuterCss, true);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ _observeFullscreen(observe) {
+ if (!observe) {
+ this._fullscreenEventListeners.removeAllEventListeners();
+ return;
+ }
+
+ if (this._fullscreenEventListeners.size > 0) {
+ // Already observing
+ return;
+ }
+
+ const fullscreenEvents = [
+ 'fullscreenchange',
+ 'MSFullscreenChange',
+ 'mozfullscreenchange',
+ 'webkitfullscreenchange'
+ ];
+ const onFullscreenChanged = () => this._onFullscreenChanged();
+ for (const eventName of fullscreenEvents) {
+ this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false);
+ }
+ }
+
+ _onFullscreenChanged() {
+ const parent = (Popup._getFullscreenElement() || document.body || null);
+ if (parent !== null && this._container.parentNode !== parent) {
+ parent.appendChild(this._container);
+ }
+ }
+
async _show(elementRect, writingMode) {
await this._inject();
@@ -327,38 +354,38 @@ class Popup {
}
_invokeApi(action, params={}) {
- if (!this._isInjectedAndLoaded) {
- throw new Error('Frame not loaded');
- }
- this._container.contentWindow.postMessage({action, params}, '*');
- }
+ const token = this._messageToken;
+ const contentWindow = this._container.contentWindow;
+ if (token === null || contentWindow === null) { return; }
- _observeFullscreen() {
- const fullscreenEvents = [
- 'fullscreenchange',
- 'MSFullscreenChange',
- 'mozfullscreenchange',
- 'webkitfullscreenchange'
- ];
- for (const eventName of fullscreenEvents) {
- document.addEventListener(eventName, () => this._onFullscreenChanged(), false);
- }
+ contentWindow.postMessage({action, params, token}, this._targetOrigin);
}
- _getFullscreenElement() {
+ static _getFullscreenElement() {
return (
document.fullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
- document.webkitFullscreenElement
+ document.webkitFullscreenElement ||
+ null
);
}
- _onFullscreenChanged() {
- const parent = (this._getFullscreenElement() || document.body || null);
- if (parent !== null && this._container.parentNode !== parent) {
- parent.appendChild(this._container);
- }
+ static _listenForDisplayPrepareCompleted(uniqueId, resolve) {
+ const runtimeMessageCallback = ({action, params}, sender, callback) => {
+ if (
+ action === 'popupPrepareCompleted' &&
+ typeof params === 'object' &&
+ params !== null &&
+ params.uniqueId === uniqueId
+ ) {
+ chrome.runtime.onMessage.removeListener(runtimeMessageCallback);
+ callback();
+ resolve();
+ return false;
+ }
+ };
+ chrome.runtime.onMessage.addListener(runtimeMessageCallback);
}
static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {
@@ -492,15 +519,6 @@ class Popup {
];
}
- static _isOnExtensionPage() {
- try {
- const url = chrome.runtime.getURL('/');
- return window.location.href.substring(0, url.length) === url;
- } catch (e) {
- // NOP
- }
- }
-
static _getViewport(useVisualViewport) {
const visualViewport = window.visualViewport;
if (visualViewport !== null && typeof visualViewport === 'object') {
@@ -533,6 +551,80 @@ class Popup {
bottom: window.innerHeight
};
}
+
+ static _isOnExtensionPage() {
+ try {
+ const url = chrome.runtime.getURL('/');
+ return window.location.href.substring(0, url.length) === url;
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ static async _injectStylesheet(id, type, value, useWebExtensionApi) {
+ const injectedStylesheets = Popup._injectedStylesheets;
+
+ if (Popup._isOnExtensionPage()) {
+ // Permissions error will occur if trying to use the WebExtension API to inject
+ // into an extension page.
+ useWebExtensionApi = false;
+ }
+
+ let styleNode = injectedStylesheets.get(id);
+ if (typeof styleNode !== 'undefined') {
+ if (styleNode === null) {
+ // Previously injected via WebExtension API
+ throw new Error(`Stylesheet with id ${id} has already been injected using the WebExtension API`);
+ }
+ } else {
+ styleNode = null;
+ }
+
+ if (useWebExtensionApi) {
+ // Inject via WebExtension API
+ if (styleNode !== null && styleNode.parentNode !== null) {
+ styleNode.parentNode.removeChild(styleNode);
+ }
+
+ await apiInjectStylesheet(type, value);
+
+ injectedStylesheets.set(id, null);
+ return null;
+ }
+
+ // Create node in document
+ const parentNode = document.head;
+ if (parentNode === null) {
+ throw new Error('No parent node');
+ }
+
+ // Create or reuse node
+ const isFile = (type === 'file');
+ const tagName = isFile ? 'link' : 'style';
+ if (styleNode === null || styleNode.nodeName.toLowerCase() !== tagName) {
+ if (styleNode !== null && styleNode.parentNode !== null) {
+ styleNode.parentNode.removeChild(styleNode);
+ }
+ styleNode = document.createElement(tagName);
+ styleNode.id = id;
+ }
+
+ // Update node style
+ if (isFile) {
+ styleNode.rel = value;
+ } else {
+ styleNode.textContent = value;
+ }
+
+ // Update parent
+ if (styleNode.parentNode !== parentNode) {
+ parentNode.appendChild(styleNode);
+ }
+
+ // Add to map
+ injectedStylesheets.set(id, styleNode);
+ return styleNode;
+ }
}
-Popup.outerStylesheet = null;
+Popup._injectedStylesheets = new Map();
diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js
index 11d3ff0e..6dc482bd 100644
--- a/ext/fg/js/source.js
+++ b/ext/fg/js/source.js
@@ -82,7 +82,11 @@ class TextSourceRange {
}
equals(other) {
- if (other === null) {
+ if (!(
+ typeof other === 'object' &&
+ other !== null &&
+ other instanceof TextSourceRange
+ )) {
return false;
}
if (this.imposterSourceElement !== null) {
@@ -362,7 +366,7 @@ class TextSourceElement {
setEndOffset(length) {
switch (this.element.nodeName.toUpperCase()) {
case 'BUTTON':
- this.content = this.element.innerHTML;
+ this.content = this.element.textContent;
break;
case 'IMG':
this.content = this.element.getAttribute('alt');
@@ -409,6 +413,12 @@ class TextSourceElement {
}
equals(other) {
- return other && other.element === this.element && other.content === this.content;
+ return (
+ typeof other === 'object' &&
+ other !== null &&
+ other instanceof TextSourceElement &&
+ other.element === this.element &&
+ other.content === this.content
+ );
}
}