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
This commit is contained in:
parent
96932119f8
commit
3e68af8666
@ -110,7 +110,8 @@
|
|||||||
"showPitchAccentPositionNotation",
|
"showPitchAccentPositionNotation",
|
||||||
"showPitchAccentGraph",
|
"showPitchAccentGraph",
|
||||||
"showIframePopupsInRootFrame",
|
"showIframePopupsInRootFrame",
|
||||||
"useSecurePopupFrameUrl"
|
"useSecurePopupFrameUrl",
|
||||||
|
"usePopupShadowDom"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enable": {
|
"enable": {
|
||||||
@ -252,6 +253,10 @@
|
|||||||
"useSecurePopupFrameUrl": {
|
"useSecurePopupFrameUrl": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
"usePopupShadowDom": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -108,6 +108,7 @@ class Backend {
|
|||||||
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
|
['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
|
||||||
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
|
['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
|
||||||
['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.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)}],
|
['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
|
||||||
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
|
['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
|
||||||
['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.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() {
|
_onApiGetEnvironmentInfo() {
|
||||||
return this.environment.getInfo();
|
return this.environment.getInfo();
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,8 @@ function profileOptionsCreateDefaults() {
|
|||||||
showPitchAccentPositionNotation: true,
|
showPitchAccentPositionNotation: true,
|
||||||
showPitchAccentGraph: false,
|
showPitchAccentGraph: false,
|
||||||
showIframePopupsInRootFrame: false,
|
showIframePopupsInRootFrame: false,
|
||||||
useSecurePopupFrameUrl: true
|
useSecurePopupFrameUrl: true,
|
||||||
|
usePopupShadowDom: true
|
||||||
},
|
},
|
||||||
|
|
||||||
audio: {
|
audio: {
|
||||||
|
@ -186,6 +186,10 @@
|
|||||||
<label><input type="checkbox" data-setting="general.useSecurePopupFrameUrl"> Use secure popup frame URL</label>
|
<label><input type="checkbox" data-setting="general.useSecurePopupFrameUrl"> Use secure popup frame URL</label>
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
</div>
|
||||||
|
@ -345,7 +345,7 @@ class Frontend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ignoreElements() {
|
_ignoreElements() {
|
||||||
return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getFrame()];
|
return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getContainer()];
|
||||||
}
|
}
|
||||||
|
|
||||||
_ignorePoint(x, y) {
|
_ignorePoint(x, y) {
|
||||||
|
@ -47,6 +47,9 @@ class Popup {
|
|||||||
this._frame.style.width = '0';
|
this._frame.style.width = '0';
|
||||||
this._frame.style.height = '0';
|
this._frame.style.height = '0';
|
||||||
|
|
||||||
|
this._container = this._frame;
|
||||||
|
this._shadow = null;
|
||||||
|
|
||||||
this._fullscreenEventListeners = new EventListenerCollection();
|
this._fullscreenEventListeners = new EventListenerCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +183,12 @@ class Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setCustomOuterCss(css, useWebExtensionApi) {
|
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) {
|
setChildrenSupported(value) {
|
||||||
@ -195,6 +203,10 @@ class Popup {
|
|||||||
return this._frame.getBoundingClientRect();
|
return this._frame.getBoundingClientRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContainer() {
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
// Private functions
|
// Private functions
|
||||||
|
|
||||||
_inject() {
|
_inject() {
|
||||||
@ -330,9 +342,9 @@ class Popup {
|
|||||||
throw new Error('Options not initialized');
|
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) => {
|
const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => {
|
||||||
frame.removeAttribute('src');
|
frame.removeAttribute('src');
|
||||||
@ -382,9 +394,9 @@ class Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_resetFrame() {
|
_resetFrame() {
|
||||||
const parent = this._frame.parentNode;
|
const parent = this._container.parentNode;
|
||||||
if (parent !== null) {
|
if (parent !== null) {
|
||||||
parent.removeChild(this._frame);
|
parent.removeChild(this._container);
|
||||||
}
|
}
|
||||||
this._frame.removeAttribute('src');
|
this._frame.removeAttribute('src');
|
||||||
this._frame.removeAttribute('srcdoc');
|
this._frame.removeAttribute('srcdoc');
|
||||||
@ -395,9 +407,31 @@ class Popup {
|
|||||||
this._injectPromiseComplete = false;
|
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() {
|
async _injectStyles() {
|
||||||
try {
|
try {
|
||||||
await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true);
|
await this._injectPopupOuterStylesheet();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// NOP
|
// 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) {
|
_observeFullscreen(observe) {
|
||||||
if (!observe) {
|
if (!observe) {
|
||||||
this._fullscreenEventListeners.removeAllEventListeners();
|
this._fullscreenEventListeners.removeAllEventListeners();
|
||||||
@ -425,8 +471,8 @@ class Popup {
|
|||||||
|
|
||||||
_onFullscreenChanged() {
|
_onFullscreenChanged() {
|
||||||
const parent = this._getFrameParentElement();
|
const parent = this._getFrameParentElement();
|
||||||
if (parent !== null && this._frame.parentNode !== parent) {
|
if (parent !== null && this._container.parentNode !== parent) {
|
||||||
parent.appendChild(this._frame);
|
parent.appendChild(this._container);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +121,10 @@ const api = (() => {
|
|||||||
return this._invoke('injectStylesheet', {type, value});
|
return this._invoke('injectStylesheet', {type, value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStylesheetContent(url) {
|
||||||
|
return this._invoke('getStylesheetContent', {url});
|
||||||
|
}
|
||||||
|
|
||||||
getEnvironmentInfo() {
|
getEnvironmentInfo() {
|
||||||
return this._invoke('getEnvironmentInfo');
|
return this._invoke('getEnvironmentInfo');
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,36 @@
|
|||||||
|
|
||||||
const dynamicLoader = (() => {
|
const dynamicLoader = (() => {
|
||||||
const injectedStylesheets = new Map();
|
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)) {
|
if (useWebExtensionApi && yomichan.isExtensionUrl(window.location.href)) {
|
||||||
// Permissions error will occur if trying to use the WebExtension API to inject into an extension page
|
// Permissions error will occur if trying to use the WebExtension API to inject into an extension page
|
||||||
useWebExtensionApi = false;
|
useWebExtensionApi = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let styleNode = injectedStylesheets.get(id);
|
let styleNode = getInjectedStylesheet(id, parentNode);
|
||||||
if (typeof styleNode !== 'undefined') {
|
if (typeof styleNode !== 'undefined') {
|
||||||
if (styleNode === null) {
|
if (styleNode === null) {
|
||||||
// Previously injected via WebExtension API
|
// Previously injected via WebExtension API
|
||||||
@ -38,22 +60,31 @@ const dynamicLoader = (() => {
|
|||||||
styleNode = null;
|
styleNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'file-content') {
|
||||||
|
value = await api.getStylesheetContent(value);
|
||||||
|
type = 'code';
|
||||||
|
useWebExtensionApi = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (useWebExtensionApi) {
|
if (useWebExtensionApi) {
|
||||||
// Inject via WebExtension API
|
// Inject via WebExtension API
|
||||||
if (styleNode !== null && styleNode.parentNode !== null) {
|
if (styleNode !== null && styleNode.parentNode !== null) {
|
||||||
styleNode.parentNode.removeChild(styleNode);
|
styleNode.parentNode.removeChild(styleNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
injectedStylesheets.set(id, null);
|
setInjectedStylesheet(id, parentNode, null);
|
||||||
await api.injectStylesheet(type, value);
|
await api.injectStylesheet(type, value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create node in document
|
// Create node in document
|
||||||
const parentNode = document.head;
|
let parentNode2 = parentNode;
|
||||||
if (parentNode === null) {
|
if (parentNode2 === null) {
|
||||||
|
parentNode2 = document.head;
|
||||||
|
if (parentNode2 === null) {
|
||||||
throw new Error('No parent node');
|
throw new Error('No parent node');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create or reuse node
|
// Create or reuse node
|
||||||
const isFile = (type === 'file');
|
const isFile = (type === 'file');
|
||||||
@ -74,12 +105,12 @@ const dynamicLoader = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update parent
|
// Update parent
|
||||||
if (styleNode.parentNode !== parentNode) {
|
if (styleNode.parentNode !== parentNode2) {
|
||||||
parentNode.appendChild(styleNode);
|
parentNode2.appendChild(styleNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to map
|
// Add to map
|
||||||
injectedStylesheets.set(id, styleNode);
|
setInjectedStylesheet(id, parentNode, styleNode);
|
||||||
return styleNode;
|
return styleNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user