-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: rewrite style injector (#664)
* Refactor: style injector/docRootObserver/docRewriteObserver * Fix: minor * Fix: disabled state * Fix: use evade * Fix: apply.js is broken in our pages * Fix: transition patch is broken * Fix: also check elements after the last userstyle * Fix: remove outdated FIXME. styleInjector.toggle now toggle all styles * Fix: call Object.keys twice * Add a fixme * Fix: typo * Add a fixme * Fix: don't argue for mutations generated by other extensions
- Loading branch information
Showing
8 changed files
with
339 additions
and
343 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* exported createStyleInjector */ | ||
'use strict'; | ||
|
||
function createStyleInjector({compare, setStyleContent, onUpdate}) { | ||
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN; | ||
const PREFIX = 'stylus-'; | ||
// styles are out of order if any of these elements is injected between them | ||
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']); | ||
const list = []; | ||
const table = new Map(); | ||
let enabled = true; | ||
return { | ||
// manipulation | ||
add, | ||
addMany, | ||
remove, | ||
update, | ||
clear, | ||
replaceAll, | ||
|
||
// method | ||
toggle, | ||
sort, | ||
|
||
// state | ||
outOfOrder, | ||
list, | ||
|
||
// static util | ||
createStyle | ||
}; | ||
|
||
function outOfOrder() { | ||
if (!list.length) { | ||
return false; | ||
} | ||
let el = list[0].el; | ||
if (el.parentNode !== document.documentElement) { | ||
return true; | ||
} | ||
let i = 0; | ||
while (el) { | ||
if (i < list.length && el === list[i].el) { | ||
i++; | ||
} else if (ORDERED_TAGS.has(el.localName)) { | ||
return true; | ||
} | ||
el = el.nextSibling; | ||
} | ||
// some styles are not injected to the document | ||
return i < list.length; | ||
} | ||
|
||
function addMany(styles) { | ||
const pending = Promise.all(styles.map(_add)); | ||
emitUpdate(); | ||
return pending; | ||
} | ||
|
||
function add(style) { | ||
const pending = _add(style); | ||
emitUpdate(); | ||
return pending; | ||
} | ||
|
||
function _add(style) { | ||
if (table.has(style.id)) { | ||
return update(style); | ||
} | ||
style.el = createStyle(style.id); | ||
const pending = setStyleContent(style.el, style.code); | ||
table.set(style.id, style); | ||
const nextIndex = list.findIndex(i => compare(i, style) > 0); | ||
if (nextIndex < 0) { | ||
document.documentElement.appendChild(style.el); | ||
list.push(style); | ||
} else { | ||
document.documentElement.insertBefore(style.el, list[nextIndex].el); | ||
list.splice(nextIndex, 0, style); | ||
} | ||
// disabled flag is read-only when not attached to a document | ||
style.el.disabled = !enabled; | ||
return pending; | ||
} | ||
|
||
function remove(id) { | ||
_remove(id); | ||
emitUpdate(); | ||
} | ||
|
||
function _remove(id) { | ||
const style = table.get(id); | ||
if (!style) return; | ||
table.delete(id); | ||
list.splice(list.indexOf(style), 1); | ||
style.el.remove(); | ||
} | ||
|
||
function update({id, code}) { | ||
const style = table.get(id); | ||
if (style.code === code) return; | ||
style.code = code; | ||
// workaround for Chrome devtools bug fixed in v65 | ||
// https://github.com/openstyles/stylus/commit/0fa391732ba8e35fa68f326a560fc04c04b8608b | ||
let oldEl; | ||
if (CHROME < 3321) { | ||
oldEl = style.el; | ||
oldEl.id = ''; | ||
style.el = createStyle(id); | ||
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling); | ||
style.el.disabled = !enabled; | ||
} | ||
return setStyleContent(style.el, code) | ||
.then(() => oldEl && oldEl.remove()); | ||
} | ||
|
||
function createStyle(id) { | ||
let el; | ||
if (document.documentElement instanceof SVGSVGElement) { | ||
// SVG document style | ||
el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); | ||
} else if (document instanceof XMLDocument) { | ||
// XML document style | ||
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style'); | ||
} else { | ||
// HTML document style; also works on HTML-embedded SVG | ||
el = document.createElement('style'); | ||
} | ||
el.id = `${PREFIX}${id}`; | ||
el.type = 'text/css'; | ||
// SVG className is not a string, but an instance of SVGAnimatedString | ||
el.classList.add('stylus'); | ||
return el; | ||
} | ||
|
||
function clear() { | ||
for (const style of list) { | ||
style.el.remove(); | ||
} | ||
list.length = 0; | ||
table.clear(); | ||
emitUpdate(); | ||
} | ||
|
||
function toggle(_enabled) { | ||
if (enabled === _enabled) return; | ||
enabled = _enabled; | ||
for (const style of list) { | ||
style.el.disabled = !enabled; | ||
} | ||
} | ||
|
||
function sort() { | ||
list.sort(compare); | ||
for (const style of list) { | ||
// moving an element resets its 'disabled' state | ||
const disabled = style.el.disabled; | ||
// FIXME: do we need this? | ||
// const copy = document.importNode(el, true); | ||
// el.textContent += ' '; // invalidate CSSOM cache | ||
document.documentElement.appendChild(style.el); | ||
style.el.disabled = disabled; | ||
} | ||
} | ||
|
||
function emitUpdate() { | ||
if (onUpdate) { | ||
onUpdate(); | ||
} | ||
} | ||
|
||
function replaceAll(styles) { | ||
const added = new Set(styles.map(s => s.id)); | ||
const removed = []; | ||
for (const style of list) { | ||
if (!added.has(style.id)) { | ||
removed.push(style.id); | ||
} | ||
} | ||
// FIXME: is it possible that `docRootObserver` breaks the process? | ||
return Promise.all(styles.map(_add)) | ||
.then(() => { | ||
removed.forEach(_remove); | ||
emitUpdate(); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters