Skip to content

Commit

Permalink
add hostSelector, limit selectors to 1 rule and 1 host selectors, add…
Browse files Browse the repository at this point in the history
… testcase
  • Loading branch information
stanislav-atr committed Jan 17, 2023
1 parent ed308a9 commit ccf8928
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 43 deletions.
11 changes: 8 additions & 3 deletions src/helpers/shadow-dom-utils.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
};

Expand Down
38 changes: 19 additions & 19 deletions src/scriptlets/inject-css-in-shadow-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = [
Expand Down
81 changes: 60 additions & 21 deletions tests/scriptlets/inject-css-in-shadow-dom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -34,7 +42,8 @@ const beforeEach = () => {

const afterEach = () => {
clearGlobalProps('hit', '__debug');
removeHost();
removeHosts();
window.Element.prototype.attachShadow = nativeAttachShadow;
};

module(name, { beforeEach, afterEach });
Expand All @@ -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');
});
}

0 comments on commit ccf8928

Please sign in to comment.