From ccf89284060637b889e8a502a304dd7b41b7118e Mon Sep 17 00:00:00 2001 From: Stanislav A Date: Tue, 17 Jan 2023 20:43:10 +0300 Subject: [PATCH] add hostSelector, limit selectors to 1 rule and 1 host selectors, add testcase --- src/helpers/shadow-dom-utils.js | 11 ++- src/scriptlets/inject-css-in-shadow-dom.js | 38 ++++----- .../inject-css-in-shadow-dom.test.js | 81 ++++++++++++++----- 3 files changed, 87 insertions(+), 43 deletions(-) diff --git a/src/helpers/shadow-dom-utils.js b/src/helpers/shadow-dom-utils.js index df02bc79..005ce09c 100644 --- a/src/helpers/shadow-dom-utils.js +++ b/src/helpers/shadow-dom-utils.js @@ -1,5 +1,5 @@ /** - * Function to make arbitrary operations on shadow root element, + * Makes arbitrary operations on shadow root element, * to be passed as callback to hijackAttachShadow * * @callback attachShadowCallback @@ -12,12 +12,17 @@ * to pass retrieved shadowRoots to callback * * @param {Object} context e.g global window object or contentWindow of an iframe + * @param {string} hostSelector selector to determine if callback should be called on current shadow subtree * @param {attachShadowCallback} callback callback to call on shadow root */ -export const hijackAttachShadow = (context, callback) => { +export const hijackAttachShadow = (context, hostSelector, callback) => { const handlerWrapper = (target, thisArg, args) => { const shadowRoot = Reflect.apply(target, thisArg, args); - callback(shadowRoot); + + if (thisArg && thisArg.matches(hostSelector || '*')) { + callback(shadowRoot); + } + return shadowRoot; }; diff --git a/src/scriptlets/inject-css-in-shadow-dom.js b/src/scriptlets/inject-css-in-shadow-dom.js index c9f71271..72a9f558 100644 --- a/src/scriptlets/inject-css-in-shadow-dom.js +++ b/src/scriptlets/inject-css-in-shadow-dom.js @@ -8,45 +8,45 @@ import { /** * @scriptlet inject-css-in-shadow-dom * @description - * Injects css rules into all Shadow DOM subtrees on a page + * Injects css rule into selected Shadow DOM subtrees on a page * * **Syntax** * ``` - * example.org#%#//scriptlet('inject-css-in-shadow-dom', cssText) + * example.org#%#//scriptlet('inject-css-in-shadow-dom', cssRule[, hostSelector]) * ``` * - * - `cssText` - required, string of comma-separated css rules + * - `cssRule` - required, string representing a single css rule + * - `hostSelector` - optional, string, selector to match `ShadowRoot` host element of which shadow doms should be injected with css. + * Defaults to injecting css rule into all available roots. * * **Examples** + * 1. Apply style to all shadow dom subtrees * ``` - * ! apply single style * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#advertisement { display: none !important; }') + * ``` * - * ! apply multiple css rules - * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#advertisement { display: none !important; }|#content { margin-top: 0 !important; }') + * 2. Apply style to a specific shadow dom subtree + * ``` + * example.org#%#//scriptlet('inject-css-in-shadow-dom', '#content { margin-top: 0 !important; }', '.row > #hidden') * ``` */ /* eslint-enable max-len */ -export function injectCssInShadowDom(source, cssText) { +export function injectCssInShadowDom(source, cssRule, hostSelector = '') { // do nothing if browser does not support ShadowRoot // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot if (!Element.prototype.attachShadow) { return; } - const parsedStyleRules = cssText.split('|'); const callback = (shadowRoot) => { const stylesheet = new CSSStyleSheet(); - - // fill stylesheet with rules - parsedStyleRules.forEach((rule) => { - try { - stylesheet.insertRule(rule); - } catch { - logMessage(source, `Failed to parse the rule: ${rule}`); - } - }); + try { + stylesheet.insertRule(cssRule); + } catch { + logMessage(source, `Failed to parse the rule: ${cssRule}`); + return; + } // attach stylesheet to shadow root so the whole subtree would be affected if (shadowRoot.adoptedStyleSheets) { @@ -55,14 +55,14 @@ export function injectCssInShadowDom(source, cssText) { shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, stylesheet]; } else { const styleTag = document.createElement('style'); - styleTag.innerText = cssText; + styleTag.innerText = cssRule; shadowRoot.appendChild(styleTag); } hit(source); }; - hijackAttachShadow(window, callback); + hijackAttachShadow(window, hostSelector, callback); } injectCssInShadowDom.names = [ diff --git a/tests/scriptlets/inject-css-in-shadow-dom.test.js b/tests/scriptlets/inject-css-in-shadow-dom.test.js index 31119aa3..7b93f78f 100644 --- a/tests/scriptlets/inject-css-in-shadow-dom.test.js +++ b/tests/scriptlets/inject-css-in-shadow-dom.test.js @@ -4,27 +4,35 @@ import { runScriptlet, clearGlobalProps } from '../helpers'; const { test, module } = QUnit; const name = 'inject-css-in-shadow-dom'; -const HOST_ID = 'host'; -const TARGET_ID = 'target'; -const TARGET_CSS_PROP = 'color'; -const TARGET_CSS_VALUE = 'rgb(255, 0, 0)'; -const CSS_TEXT = `#target { ${TARGET_CSS_PROP}: ${TARGET_CSS_VALUE} !important}`; -// const CSS_TEXT = '#target { color: red !important}'; - -const appendTarget = (parent) => { +const nativeAttachShadow = window.Element.prototype.attachShadow; + +const TARGET_ID1 = 'target1'; +const CSS_TEXT1 = `#${TARGET_ID1} { color: rgb(255, 0, 0) !important }`; +const HOST_ID1 = 'host1'; +const HOST_ID2 = 'host2'; + +const appendTarget = (parent, id) => { const target = document.createElement('h1'); - target.id = TARGET_ID; - target.innerText = 'Target element'; + target.id = id; + target.innerText = id; return parent.appendChild(target); }; -const appendHost = () => { +const appendHost = (id) => { const host = document.createElement('div'); - host.id = HOST_ID; + host.id = id; return document.body.appendChild(host); }; -const removeHost = () => document.getElementById(HOST_ID)?.remove(); +const removeHosts = () => { + const hostIds = [HOST_ID1, HOST_ID2]; + hostIds.forEach((id) => { + const host = document.getElementById(id); + if (host) { + host.remove(); + } + }); +}; const beforeEach = () => { window.__debug = () => { @@ -34,7 +42,8 @@ const beforeEach = () => { const afterEach = () => { clearGlobalProps('hit', '__debug'); - removeHost(); + removeHosts(); + window.Element.prototype.attachShadow = nativeAttachShadow; }; module(name, { beforeEach, afterEach }); @@ -49,15 +58,45 @@ if (!isSupported) { assert.ok(true, 'Browser does not support it'); }); } else { - test('styles applied to shadow root subtree with adoptedStyleSheets', (assert) => { - runScriptlet(name, [CSS_TEXT]); + test('apply style to all shadow dom subtrees', (assert) => { + runScriptlet(name, [CSS_TEXT1]); + + const host1 = appendHost(HOST_ID1); + const shadowRoot1 = host1.attachShadow({ mode: 'closed' }); + appendTarget(shadowRoot1, TARGET_ID1); + + const host2 = appendHost(HOST_ID2); + const shadowRoot2 = host2.attachShadow({ mode: 'closed' }); + appendTarget(shadowRoot2, TARGET_ID1); + + // First shadow root, style applied + const target1 = shadowRoot1.getElementById(TARGET_ID1); + assert.strictEqual(getComputedStyle(target1).color, 'rgb(255, 0, 0)', 'style was applied to shadowRoot #1'); + // Second shadow root, style applied + const target2 = shadowRoot2.getElementById(TARGET_ID1); + assert.strictEqual(getComputedStyle(target2).color, 'rgb(255, 0, 0)', 'style was applied to shadowRoot #2'); + + assert.strictEqual(window.hit, 'FIRED', 'hit function was executed'); + }); + + test('apply style to specific shadow dom subtree', (assert) => { + runScriptlet(name, [CSS_TEXT1, `#${HOST_ID1}`]); + + const host1 = appendHost(HOST_ID1); + const shadowRoot1 = host1.attachShadow({ mode: 'closed' }); + appendTarget(shadowRoot1, TARGET_ID1); + + const host2 = appendHost(HOST_ID2); + const shadowRoot2 = host2.attachShadow({ mode: 'closed' }); + appendTarget(shadowRoot2, TARGET_ID1); - const host = appendHost(); - const shadowRoot = host.attachShadow({ mode: 'closed' }); - appendTarget(shadowRoot); + // First shadow root, style applied + const target1 = shadowRoot1.getElementById(TARGET_ID1); + assert.strictEqual(getComputedStyle(target1).color, 'rgb(255, 0, 0)', 'style was applied to shadowRoot #1'); + // Second shadow root, style should no be applied + const target2 = shadowRoot2.getElementById(TARGET_ID1); + assert.strictEqual(getComputedStyle(target2).color, 'rgb(0, 0, 0)', 'style was not applied to shadowRoot #2'); - const target = shadowRoot.getElementById(TARGET_ID); - assert.strictEqual(getComputedStyle(target)[TARGET_CSS_PROP], TARGET_CSS_VALUE, 'style was applied on target'); assert.strictEqual(window.hit, 'FIRED', 'hit function was executed'); }); }