Skip to content

Commit bb766bb

Browse files
committed
AG-39295 Fix 'inject-css-in-shadow-dom' — scriptlet does not work if adoptedStyleSheets is overridden. #477
Squashed commit of the following: commit bcd2159 Author: Adam Wróblewski <[email protected]> Date: Thu Apr 10 09:17:55 2025 +0200 Check cssInjectionMethod eariler commit 0ee035b Merge: 39cc223 938bbe3 Author: Adam Wróblewski <[email protected]> Date: Wed Apr 9 10:08:06 2025 +0200 Merge branch 'master' into fix/AG-39295 commit 39cc223 Author: Adam Wróblewski <[email protected]> Date: Tue Apr 8 20:53:16 2025 +0200 Add try...catch to injectStyleTag Add JSDoc for injectAdoptedStyleSheets Check cssInjectionMethod earlier commit d66a9a6 Author: Slava Leleka <[email protected]> Date: Tue Apr 8 21:23:16 2025 +0300 Update documentation commit e7416d7 Author: Adam Wróblewski <[email protected]> Date: Tue Apr 8 13:30:26 2025 +0200 Add ability to choose CSS injection method in `inject-css-in-shadow-dom` scriptlet
1 parent 938bbe3 commit bb766bb

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1414

1515
### Added
1616

17+
- Ability to choose CSS injection method in `inject-css-in-shadow-dom` scriptlet [#477]
1718
- TypeScript types for CoreLibs provided [`ContentScriptApi`](./README.md#scriptlets-api--content-script-api).
1819
- Trusted Types API utility - [`PolicyApi`](./README.md#scriptlets-api--content-script-api--policy-api).
1920

2021
### Changed
2122

2223
- Improved docs for `json-prune`, `xml-prune` and `trusted-prune-inbound-object` scriptlets [#392]
2324

25+
[#477]: https://github.com/AdguardTeam/Scriptlets/issues/477
2426
[#392]: https://github.com/AdguardTeam/Scriptlets/issues/392
2527
[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v2.1.7...HEAD
2628

src/scriptlets/inject-css-in-shadow-dom.js

+56-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { hit, logMessage, hijackAttachShadow } from '../helpers';
1717
* - `hostSelector` — optional, string, selector to match shadow host elements.
1818
* CSS rule will be only applied to shadow roots inside these elements.
1919
* Defaults to injecting css rule into all available roots.
20+
* - `cssInjectionMethod` — optional, string, method to inject css rule into shadow dom.
21+
* Available methods are:
22+
* - `adoptedStyleSheets` — injects the CSS rule using adopted style sheets (default option).
23+
* - `styleTag` — injects the CSS rule using a `style` tag.
2024
*
2125
* ### Examples
2226
*
@@ -32,26 +36,64 @@ import { hit, logMessage, hijackAttachShadow } from '../helpers';
3236
* example.org#%#//scriptlet('inject-css-in-shadow-dom', '#content { margin-top: 0 !important; }', '#banner')
3337
* ```
3438
*
39+
* 1. Apply style to all shadow dom subtrees using style tag
40+
*
41+
* ```adblock
42+
* example.org#%#//scriptlet('inject-css-in-shadow-dom', '.ads { display: none !important; }', '', 'styleTag')
43+
* ```
44+
*
3545
* @added v1.8.2.
3646
*/
3747
/* eslint-enable max-len */
3848

39-
export function injectCssInShadowDom(source, cssRule, hostSelector = '') {
49+
export function injectCssInShadowDom(
50+
source,
51+
cssRule,
52+
hostSelector = '',
53+
cssInjectionMethod = 'adoptedStyleSheets',
54+
) {
4055
// do nothing if browser does not support ShadowRoot, Proxy or Reflect
4156
// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
4257
if (!Element.prototype.attachShadow || typeof Proxy === 'undefined' || typeof Reflect === 'undefined') {
4358
return;
4459
}
4560

61+
if (cssInjectionMethod !== 'adoptedStyleSheets' && cssInjectionMethod !== 'styleTag') {
62+
logMessage(source, `Unknown cssInjectionMethod: ${cssInjectionMethod}`);
63+
return;
64+
}
65+
4666
// Prevent url() and image-set() styles from being applied
4767
if (cssRule.match(/(url|image-set)\(.*\)/i)) {
4868
logMessage(source, '"url()" function is not allowed for css rules');
4969
return;
5070
}
5171

52-
const callback = (shadowRoot) => {
72+
const injectStyleTag = (shadowRoot) => {
5373
try {
54-
// adoptedStyleSheets and CSSStyleSheet constructor are not yet supported by Safari
74+
const styleTag = document.createElement('style');
75+
styleTag.innerText = cssRule;
76+
shadowRoot.appendChild(styleTag);
77+
hit(source);
78+
} catch (error) {
79+
logMessage(source, `Unable to inject style tag due to: \n'${error.message}'`);
80+
}
81+
};
82+
83+
/**
84+
* Injects CSS rules into a shadow root using the adoptedStyleSheets API
85+
*
86+
* @param {ShadowRoot} shadowRoot - The shadow root to inject styles into
87+
* @private
88+
*
89+
* @description
90+
* This function attempts to inject CSS using adoptedStyleSheets API.
91+
* If successful, it adds the stylesheet to the shadow root's adoptedStyleSheets array.
92+
* On failure, it falls back to using the injectStyleTag method.
93+
*/
94+
const injectAdoptedStyleSheets = (shadowRoot) => {
95+
try {
96+
// adoptedStyleSheets and CSSStyleSheet constructor are not supported by old browsers
5597
// https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
5698
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet
5799
const stylesheet = new CSSStyleSheet();
@@ -62,13 +104,19 @@ export function injectCssInShadowDom(source, cssRule, hostSelector = '') {
62104
return;
63105
}
64106
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, stylesheet];
65-
} catch {
66-
const styleTag = document.createElement('style');
67-
styleTag.innerText = cssRule;
68-
shadowRoot.appendChild(styleTag);
107+
hit(source);
108+
} catch (error) {
109+
logMessage(source, `Unable to inject adopted style sheet due to: \n'${error.message}'`);
110+
injectStyleTag(shadowRoot);
69111
}
112+
};
70113

71-
hit(source);
114+
const callback = (shadowRoot) => {
115+
if (cssInjectionMethod === 'adoptedStyleSheets') {
116+
injectAdoptedStyleSheets(shadowRoot);
117+
} else if (cssInjectionMethod === 'styleTag') {
118+
injectStyleTag(shadowRoot);
119+
}
72120
};
73121

74122
hijackAttachShadow(window, hostSelector, callback);

tests/scriptlets/inject-css-in-shadow-dom.test.js

+16
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,20 @@ if (!isSupported) {
208208
assert.strictEqual(getComputedStyle(target1).color, 'rgb(255, 0, 0)', 'style was applied to shadowRoot #1');
209209
assert.strictEqual(window.hit, 'FIRED', 'hit function was executed');
210210
});
211+
212+
test('test "styleTag" injection method', (assert) => {
213+
runScriptlet(name, [CSS_TEXT1, '', 'styleTag']);
214+
215+
const host1 = appendHost(HOST_ID1);
216+
const shadowRoot1 = host1.attachShadow({ mode: 'closed' });
217+
appendTarget(shadowRoot1, TARGET_ID1);
218+
219+
const target1 = shadowRoot1.getElementById(TARGET_ID1);
220+
const styleTag = shadowRoot1.querySelector('style');
221+
222+
assert.ok(styleTag, 'style tag was created');
223+
assert.strictEqual(styleTag.innerText, CSS_TEXT1, 'style tag contains the correct css rule');
224+
assert.strictEqual(getComputedStyle(target1).color, 'rgb(255, 0, 0)', 'style was applied to shadowRoot #1');
225+
assert.strictEqual(window.hit, 'FIRED', 'hit function was executed');
226+
});
211227
}

0 commit comments

Comments
 (0)