use adoptedStyleSheets

This commit is contained in:
tophf 2020-10-12 21:52:31 +03:00
parent 60fc6f2456
commit 124dce44e8
7 changed files with 176 additions and 48 deletions

View File

@ -943,6 +943,9 @@
"optionsAdvanced": { "optionsAdvanced": {
"message": "Advanced" "message": "Advanced"
}, },
"optionsAdvancedAdoptedStyleSheets": {
"message": "Apply styles directly via <a href='https://developers.google.com/web/updates/2019/02/constructable-stylesheets'>AdoptedStyleSheets</a>"
},
"optionsAdvancedContextDelete": { "optionsAdvancedContextDelete": {
"message": "Add 'Delete' in editor context menu" "message": "Add 'Delete' in editor context menu"
}, },

View File

@ -1,6 +1,6 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg sync uuidv4 URLS */ getStyleWithNoCode msg sync uuidv4 URLS prefs */
/* exported styleManager */ /* exported styleManager */
'use strict'; 'use strict';
@ -332,8 +332,10 @@ const styleManager = (() => {
function ensurePrepared(methods) { function ensurePrepared(methods) {
const prepared = {}; const prepared = {};
for (const [name, fn] of Object.entries(methods)) { for (const [name, fn] of Object.entries(methods)) {
prepared[name] = (...args) => prepared[name] = async function () {
preparing.then(() => fn(...args)); await preparing;
return fn.apply(this, arguments);
};
} }
return prepared; return prepared;
} }
@ -475,7 +477,7 @@ const styleManager = (() => {
return result; return result;
} }
function getSectionsByUrl(url, id) { function getSectionsByUrl(url, id, checkASS) {
let cache = cachedStyleForUrl.get(url); let cache = cachedStyleForUrl.get(url);
if (!cache) { if (!cache) {
cache = { cache = {
@ -491,13 +493,12 @@ const styleManager = (() => {
.map(i => styles.get(i)) .map(i => styles.get(i))
); );
} }
if (id) { const sections = id
if (cache.sections[id]) { ? cache.sections[id] && {[id]: cache.sections[id]} || {}
return {[id]: cache.sections[id]}; : cache.sections;
} return checkASS
return {}; ? Object.assign({ASS: prefs.get('adoptedStyleSheets')}, sections)
} : sections;
return cache.sections;
function buildCache(styleList) { function buildCache(styleList) {
const query = createMatchQuery(url); const query = createMatchQuery(url);

View File

@ -59,7 +59,7 @@ self.INJECTED !== 1 && (() => {
function init() { function init() {
return STYLE_VIA_API ? return STYLE_VIA_API ?
API.styleViaAPI({method: 'styleApply'}) : API.styleViaAPI({method: 'styleApply'}) :
API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); API.getSectionsByUrl(getMatchUrl(), null, true).then(styleInjector.apply);
} }
function getMatchUrl() { function getMatchUrl() {

View File

@ -1,3 +1,4 @@
/* global prefs */
'use strict'; 'use strict';
self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
@ -16,6 +17,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
const table = new Map(); const table = new Map();
let isEnabled = true; let isEnabled = true;
let isTransitionPatched; let isTransitionPatched;
let ASS;
// will store the original method refs because the page can override them // will store the original method refs because the page can override them
let creationDoc, createElement, createElementNS; let creationDoc, createElement, createElementNS;
@ -24,6 +26,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
list, list,
apply(styleMap) { apply(styleMap) {
if ('ASS' in styleMap) _init(styleMap);
const styles = _styleMapToArray(styleMap); const styles = _styleMapToArray(styleMap);
return ( return (
!styles.length ? !styles.length ?
@ -45,6 +48,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
}, },
clearOrphans() { clearOrphans() {
if (ASS) ASS.list = ASS.wipedList;
for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) { for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) {
const id = el.id.slice(PREFIX.length); const id = el.id.slice(PREFIX.length);
if (/^\d+$/.test(id) || id === PATCH_ID) { if (/^\d+$/.test(id) || id === PATCH_ID) {
@ -82,24 +86,28 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
}; };
function _add(style) { function _add(style) {
const el = style.el = _createStyle(style.id, style.code); _domCreate(style);
const i = list.findIndex(item => compare(item, style) > 0); const i = list.findIndex(item => compare(item, style) > 0);
table.set(style.id, style); table.set(style.id, style);
if (isEnabled) { if (isEnabled) _domInsert(style, (list[i] || {}).el);
document.documentElement.insertBefore(el, i < 0 ? null : list[i].el);
}
list.splice(i < 0 ? list.length : i, 0, style); list.splice(i < 0 ? list.length : i, 0, style);
return el; return style.el;
} }
function _addRemoveElements(add) { function _addRemoveElements(add) {
for (const {el} of list) { const asses = [];
if (add) { for (const style of list) {
document.documentElement.appendChild(el); if (style.ass) {
asses.push(style.el);
} else if (add) {
_domInsert(style);
} else { } else {
el.remove(); _domDelete(style);
} }
} }
if (ASS) {
ASS.list = ASS.wipedList.concat(add ? asses : []);
}
} }
function _addUpdate(style) { function _addUpdate(style) {
@ -115,20 +123,35 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
!styles.some(s => s.code.includes('transition'))) { !styles.some(s => s.code.includes('transition'))) {
return; return;
} }
const el = _createStyle(PATCH_ID, ` const style = {
id: PATCH_ID,
code: `
:root:not(#\\0):not(#\\0) * { :root:not(#\\0):not(#\\0) * {
transition: none !important; transition: none !important;
} }
`); `,
document.documentElement.appendChild(el); };
_domCreate(style);
_domInsert(style);
// wait for the next paint to complete // wait for the next paint to complete
// note: requestAnimationFrame won't fire in inactive tabs // note: requestAnimationFrame won't fire in inactive tabs
requestAnimationFrame(() => setTimeout(() => el.remove())); requestAnimationFrame(() => setTimeout(_domDelete, 0, style));
} }
function _createStyle(id, code = '') { /**
if (!creationDoc) _initCreationDoc(); * @returns {Element|CSSStyleSheet}
* @mutates style
*/
function _domCreate(style = {}) {
const {id, code = ''} = style;
let el; let el;
if (ASS) {
el = style.el = new CSSStyleSheet(id ? {media: PREFIX + id} : {});
if (_setASSCode(style, code)) {
return el;
}
}
if (!creationDoc) _initCreationDoc();
if (document.documentElement instanceof SVGSVGElement) { if (document.documentElement instanceof SVGSVGElement) {
// SVG document style // SVG document style
el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style'); el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style');
@ -148,9 +171,45 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
// SVG className is not a string, but an instance of SVGAnimatedString // SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus'); el.classList.add('stylus');
el.textContent = code; el.textContent = code;
style.el = el;
style.ass = false;
return el; return el;
} }
function _domInsert({ass, el}, beforeEl) {
if (ass) {
const list = ASS.omit({el});
const i = list.indexOf(beforeEl);
list.splice(i >= 0 ? i : list.length, 0, el);
ASS.list = list;
} else {
document.documentElement.insertBefore(el, beforeEl);
}
}
function _domDelete({ass, el}) {
if (ass) {
ASS.omit({el, commit: true});
} else {
el.remove();
}
}
function _setASSCode(style, code) {
try {
style.el.replaceSync(code); // throws on @import per spec
style.ass = true;
} catch (err) {
style.ass = false;
}
const isTight = style.ass && list.every(s => s.ass);
if (ASS.isTight !== isTight) {
ASS.isTight = isTight;
docRootObserver[isTight ? 'stop' : 'start']();
}
return style.ass;
}
function _toggleObservers(shouldStart) { function _toggleObservers(shouldStart) {
const onOff = shouldStart && isEnabled ? 'start' : 'stop'; const onOff = shouldStart && isEnabled ? 'start' : 'stop';
docRewriteObserver[onOff](); docRewriteObserver[onOff]();
@ -163,6 +222,51 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
return value; return value;
} }
function _init(styleMap) {
if (ASS == null && Array.isArray(document.adoptedStyleSheets)) {
_initASS(styleMap.ASS);
prefs.subscribe(['adoptedStyleSheets'], (key, value) => {
_toggleObservers(false);
_addRemoveElements(false);
_initASS(value);
list.forEach(_domCreate);
if (isEnabled) _addRemoveElements(true);
_toggleObservers(true);
});
}
delete styleMap.ASS;
}
function _initASS(enable) {
if (ASS && !enable) {
ASS = false;
} else if (!ASS && enable) {
ASS = {
isTight: true,
/** @param {CSSStyleSheet[]} sheets */
set list(sheets) {
document.adoptedStyleSheets = sheets;
},
/** @returns {CSSStyleSheet[]} */
get list() {
return [...document.adoptedStyleSheets];
},
/** @returns {CSSStyleSheet[]} without our elements */
get wipedList() {
return ASS.list.filter(({media: {0: id = ''}}) =>
!(id.startsWith(PREFIX) && Number(id.slice(PREFIX.length)) || id.endsWith(PATCH_ID)));
},
omit({el, list = ASS.list, commit}) {
const i = list.indexOf(el);
if (i >= 0) list.splice(i, 1);
if (commit) ASS.list = list;
return list;
},
};
}
}
/* /*
FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461 FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461
First we're trying the page context document where inline styles may be forbidden by CSP First we're trying the page context document where inline styles may be forbidden by CSP
@ -174,7 +278,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject; creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
if (creationDoc) { if (creationDoc) {
({createElement, createElementNS} = creationDoc); ({createElement, createElementNS} = creationDoc);
const el = document.documentElement.appendChild(_createStyle()); const el = document.documentElement.appendChild(_domCreate());
const isApplied = el.sheet; const isApplied = el.sheet;
el.remove(); el.remove();
if (isApplied) return; if (isApplied) return;
@ -188,7 +292,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
if (!style) return; if (!style) return;
table.delete(id); table.delete(id);
list.splice(list.indexOf(style), 1); list.splice(list.indexOf(style), 1);
style.el.remove(); _domDelete(style);
} }
function _sort() { function _sort() {
@ -237,13 +341,16 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
// workaround for Chrome devtools bug fixed in v65 // workaround for Chrome devtools bug fixed in v65
if (isChromePre65) { if (isChromePre65) {
const oldEl = style.el; const oldEl = style.el;
style.el = _createStyle(id, code); style.el = _domCreate({id, code});
if (isEnabled) { if (isEnabled) {
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling); oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling);
oldEl.remove(); oldEl.remove();
} }
} else { } else if (!style.ass) {
style.el.textContent = code; style.el.textContent = code;
} else if (!_setASSCode(style, code)) {
_remove(id);
_add(style);
} }
} }
@ -283,7 +390,11 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
let digest = 0; let digest = 0;
let lastCalledTime = NaN; let lastCalledTime = NaN;
let observing = false; let observing = false;
const observer = new MutationObserver(() => { let observer;
return {evade, start, stop};
function create() {
observer = new MutationObserver(() => {
if (digest) { if (digest) {
if (performance.now() - lastCalledTime > 1000) { if (performance.now() - lastCalledTime > 1000) {
digest = 0; digest = 0;
@ -296,7 +407,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
lastCalledTime = performance.now(); lastCalledTime = performance.now();
} }
}); });
return {evade, start, stop}; }
function evade(fn) { function evade(fn) {
const restore = observing && start; const restore = observing && start;
@ -306,7 +417,8 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
} }
function start() { function start() {
if (observing) return; if (observing || ASS && ASS.isTight) return;
if (!observer) create();
observer.observe(document.documentElement, {childList: true}); observer.observe(document.documentElement, {childList: true});
observing = true; observing = true;
} }

View File

@ -9,6 +9,7 @@ self.prefs = self.INJECTED === 1 ? self.prefs : (() => {
'disableAll': false, // boss key 'disableAll': false, // boss key
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes 'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
'newStyleAsUsercss': false, // create new style in usercss format 'newStyleAsUsercss': false, // create new style in usercss format
'adoptedStyleSheets': false, // try to apply styles via document.adoptedStyleSheets
// checkbox in style config dialog // checkbox in style config dialog
'config.autosave': true, 'config.autosave': true,

View File

@ -232,6 +232,13 @@
</h1> </h1>
</div> </div>
<div class="items"> <div class="items">
<label>
<span i18n-html="optionsAdvancedAdoptedStyleSheets"></span>
<span class="onoffswitch">
<input type="checkbox" id="adoptedStyleSheets" class="slider">
<span></span>
</span>
</label>
<label> <label>
<span i18n-text="optionsAdvancedExposeIframes"> <span i18n-text="optionsAdvancedExposeIframes">
<a data-cmd="note" <a data-cmd="note"

View File

@ -38,6 +38,10 @@ if (FIREFOX && 'update' in (chrome.commands || {})) {
}); });
} }
if (!Array.isArray(document.adoptedStyleSheets)) {
$('#adoptedStyleSheets').closest('label').classList.add('hidden');
}
// actions // actions
$('#options-close-icon').onclick = () => { $('#options-close-icon').onclick = () => {
top.dispatchEvent(new CustomEvent('closeOptions')); top.dispatchEvent(new CustomEvent('closeOptions'));