188 lines
4.7 KiB
JavaScript
188 lines
4.7 KiB
JavaScript
|
/* 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();
|
||
|
});
|
||
|
}
|
||
|
}
|