summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-06-24 21:46:13 -0400
committerGitHub <noreply@github.com>2020-06-24 21:46:13 -0400
commit3e68af8666bdf9a6d8d605f7a3bb0432c8d6cb33 (patch)
tree5674e8a865b9a55983dd2e9d54a263fb0cb99105 /ext
parent96932119f8627725774ffdc66a82326d7302db30 (diff)
Shadow DOM container for popup iframes (#623)
* Add support for injecting stylesheets into a custom parent node * Add api.getStylesheetContent * Add support for injecting a CSS file's content * Add usePopupShadowDom option * Use a per-parentNode cache * Add support for using a shadow DOM wrapper around popup iframes * Ignore the popup container instead of the frame
Diffstat (limited to 'ext')
-rw-r--r--ext/bg/data/options-schema.json7
-rw-r--r--ext/bg/js/backend.js8
-rw-r--r--ext/bg/js/options.js3
-rw-r--r--ext/bg/settings.html4
-rw-r--r--ext/fg/js/frontend.js2
-rw-r--r--ext/fg/js/popup.js62
-rw-r--r--ext/mixed/js/api.js4
-rw-r--r--ext/mixed/js/dynamic-loader.js49
8 files changed, 119 insertions, 20 deletions
diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json
index f8791433..b56017bc 100644
--- a/ext/bg/data/options-schema.json
+++ b/ext/bg/data/options-schema.json
@@ -110,7 +110,8 @@
"showPitchAccentPositionNotation",
"showPitchAccentGraph",
"showIframePopupsInRootFrame",
- "useSecurePopupFrameUrl"
+ "useSecurePopupFrameUrl",
+ "usePopupShadowDom"
],
"properties": {
"enable": {
@@ -252,6 +253,10 @@
"useSecurePopupFrameUrl": {
"type": "boolean",
"default": true
+ },
+ "usePopupShadowDom": {
+ "type": "boolean",
+ "default": true
}
}
},
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index b89cb641..344706d1 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -108,6 +108,7 @@ class Backend {
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}],
+ ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}],
['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}],
@@ -719,6 +720,13 @@ class Backend {
});
}
+ async _onApiGetStylesheetContent({url}) {
+ if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) {
+ throw new Error('Invalid URL');
+ }
+ return await requestText(url, 'GET');
+ }
+
_onApiGetEnvironmentInfo() {
return this.environment.getInfo();
}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index 151c945b..ccc56848 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -177,7 +177,8 @@ function profileOptionsCreateDefaults() {
showPitchAccentPositionNotation: true,
showPitchAccentGraph: false,
showIframePopupsInRootFrame: false,
- useSecurePopupFrameUrl: true
+ useSecurePopupFrameUrl: true,
+ usePopupShadowDom: true
},
audio: {
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index 4de70b7e..51cb14e7 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -187,6 +187,10 @@
</div>
<div class="checkbox options-advanced">
+ <label><input type="checkbox" data-setting="general.usePopupShadowDom"> Use shadow DOM container for popup</label>
+ </div>
+
+ <div class="checkbox options-advanced">
<label><input type="checkbox" id="show-debug-info" data-setting="general.debugInfo" data-transform-pre="setDocumentAttribute" data-transform-post="setDocumentAttribute" data-document-attribute="data-options-general-debug-info"> Show debug information</label>
</div>
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index ae0953f9..f6b0d236 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -345,7 +345,7 @@ class Frontend {
}
_ignoreElements() {
- return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getFrame()];
+ return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getContainer()];
}
_ignorePoint(x, y) {
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 3b14d3d0..5ee62c9b 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -47,6 +47,9 @@ class Popup {
this._frame.style.width = '0';
this._frame.style.height = '0';
+ this._container = this._frame;
+ this._shadow = null;
+
this._fullscreenEventListeners = new EventListenerCollection();
}
@@ -180,7 +183,12 @@ class Popup {
}
async setCustomOuterCss(css, useWebExtensionApi) {
- return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi);
+ let parentNode = null;
+ if (this._shadow !== null) {
+ useWebExtensionApi = false;
+ parentNode = this._shadow;
+ }
+ return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode);
}
setChildrenSupported(value) {
@@ -195,6 +203,10 @@ class Popup {
return this._frame.getBoundingClientRect();
}
+ getContainer() {
+ return this._container;
+ }
+
// Private functions
_inject() {
@@ -330,9 +342,9 @@ class Popup {
throw new Error('Options not initialized');
}
- const {useSecurePopupFrameUrl} = this._options.general;
+ const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general;
- this._injectStyles();
+ await this._setUpContainer(usePopupShadowDom);
const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => {
frame.removeAttribute('src');
@@ -382,9 +394,9 @@ class Popup {
}
_resetFrame() {
- const parent = this._frame.parentNode;
+ const parent = this._container.parentNode;
if (parent !== null) {
- parent.removeChild(this._frame);
+ parent.removeChild(this._container);
}
this._frame.removeAttribute('src');
this._frame.removeAttribute('srcdoc');
@@ -395,9 +407,31 @@ class Popup {
this._injectPromiseComplete = false;
}
+ async _setUpContainer(usePopupShadowDom) {
+ if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') {
+ const container = document.createElement('div');
+ container.style.setProperty('all', 'initial', 'important');
+ const shadow = container.attachShadow({mode: 'closed', delegatesFocus: true});
+ shadow.appendChild(this._frame);
+
+ this._container = container;
+ this._shadow = shadow;
+ } else {
+ const frameParentNode = this._frame.parentNode;
+ if (frameParentNode !== null) {
+ frameParentNode.removeChild(this._frame);
+ }
+
+ this._container = this._frame;
+ this._shadow = null;
+ }
+
+ await this._injectStyles();
+ }
+
async _injectStyles() {
try {
- await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true);
+ await this._injectPopupOuterStylesheet();
} catch (e) {
// NOP
}
@@ -409,6 +443,18 @@ class Popup {
}
}
+ async _injectPopupOuterStylesheet() {
+ let fileType = 'file';
+ let useWebExtensionApi = true;
+ let parentNode = null;
+ if (this._shadow !== null) {
+ fileType = 'file-content';
+ useWebExtensionApi = false;
+ parentNode = this._shadow;
+ }
+ await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', fileType, '/fg/css/client.css', useWebExtensionApi, parentNode);
+ }
+
_observeFullscreen(observe) {
if (!observe) {
this._fullscreenEventListeners.removeAllEventListeners();
@@ -425,8 +471,8 @@ class Popup {
_onFullscreenChanged() {
const parent = this._getFrameParentElement();
- if (parent !== null && this._frame.parentNode !== parent) {
- parent.appendChild(this._frame);
+ if (parent !== null && this._container.parentNode !== parent) {
+ parent.appendChild(this._container);
}
}
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index c54196e2..5c17d50e 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -121,6 +121,10 @@ const api = (() => {
return this._invoke('injectStylesheet', {type, value});
}
+ getStylesheetContent(url) {
+ return this._invoke('getStylesheetContent', {url});
+ }
+
getEnvironmentInfo() {
return this._invoke('getEnvironmentInfo');
}
diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js
index 37f85112..981d1ee5 100644
--- a/ext/mixed/js/dynamic-loader.js
+++ b/ext/mixed/js/dynamic-loader.js
@@ -21,14 +21,36 @@
const dynamicLoader = (() => {
const injectedStylesheets = new Map();
+ const injectedStylesheetsWithParent = new WeakMap();
- async function loadStyle(id, type, value, useWebExtensionApi=false) {
+ function getInjectedStylesheet(id, parentNode) {
+ if (parentNode === null) {
+ return injectedStylesheets.get(id);
+ }
+ const map = injectedStylesheetsWithParent.get(parentNode);
+ return typeof map !== 'undefined' ? map.get(id) : void 0;
+ }
+
+ function setInjectedStylesheet(id, parentNode, value) {
+ if (parentNode === null) {
+ injectedStylesheets.set(id, value);
+ return;
+ }
+ let map = injectedStylesheetsWithParent.get(parentNode);
+ if (typeof map === 'undefined') {
+ map = new Map();
+ injectedStylesheetsWithParent.set(parentNode, map);
+ }
+ map.set(id, value);
+ }
+
+ async function loadStyle(id, type, value, useWebExtensionApi=false, parentNode=null) {
if (useWebExtensionApi && yomichan.isExtensionUrl(window.location.href)) {
// Permissions error will occur if trying to use the WebExtension API to inject into an extension page
useWebExtensionApi = false;
}
- let styleNode = injectedStylesheets.get(id);
+ let styleNode = getInjectedStylesheet(id, parentNode);
if (typeof styleNode !== 'undefined') {
if (styleNode === null) {
// Previously injected via WebExtension API
@@ -38,21 +60,30 @@ const dynamicLoader = (() => {
styleNode = null;
}
+ if (type === 'file-content') {
+ value = await api.getStylesheetContent(value);
+ type = 'code';
+ useWebExtensionApi = false;
+ }
+
if (useWebExtensionApi) {
// Inject via WebExtension API
if (styleNode !== null && styleNode.parentNode !== null) {
styleNode.parentNode.removeChild(styleNode);
}
- injectedStylesheets.set(id, null);
+ setInjectedStylesheet(id, parentNode, null);
await api.injectStylesheet(type, value);
return null;
}
// Create node in document
- const parentNode = document.head;
- if (parentNode === null) {
- throw new Error('No parent node');
+ let parentNode2 = parentNode;
+ if (parentNode2 === null) {
+ parentNode2 = document.head;
+ if (parentNode2 === null) {
+ throw new Error('No parent node');
+ }
}
// Create or reuse node
@@ -74,12 +105,12 @@ const dynamicLoader = (() => {
}
// Update parent
- if (styleNode.parentNode !== parentNode) {
- parentNode.appendChild(styleNode);
+ if (styleNode.parentNode !== parentNode2) {
+ parentNode2.appendChild(styleNode);
}
// Add to map
- injectedStylesheets.set(id, styleNode);
+ setInjectedStylesheet(id, parentNode, styleNode);
return styleNode;
}