penguinmod-editor-2 / src /addons /conditional-style.js
soiz1's picture
Upload 2891 files
6bcb42f verified
// Stylesheets are added at the end of <body> so that they have higher precedence
// than those in <head> and above dark mode which is appended at the start of <body>
const stylesheetContainer = document.createElement('div');
stylesheetContainer.style.display = 'none';
document.body.appendChild(stylesheetContainer);
/**
* Maps opaque module IDs to its ConditionalStyle.
* @type {Map<unknown, ConditionalStyle>}
*/
const allSheets = new Map();
/**
* Determine if the contents of a list are equal (===) to each other.
* @param {unknown[]} a The first list
* @param {unknown[]} b The second list
* @returns {boolean} true if the lists are identical
*/
const areArraysEqual = (a, b) => {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; a++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
const updateAll = () => {
for (const sheet of allSheets.values()) {
sheet.update();
}
};
class ConditionalStyle {
/**
* @param {string} styleText CSS text
*/
constructor (styleText) {
/**
* Lazily created <style> element.
* @type {HTMLStyleElement}
*/
this.el = null;
/**
* Temporary storing place for the CSS text until the style sheet is created.
* @type {string|null}
*/
this.styleText = styleText;
/**
* Higher number indicates this element should override lower numbers.
* @type {number}
*/
this.precedence = 0;
/**
* List of [addonId, condition] tuples.
* @type {Array<[string, () => boolean>]}
*/
this.dependents = [];
/**
* List of addonIds that were enabled on the previous call to update()
* @type {string[]}
*/
this.previousEnabledDependents = [];
}
addDependent (addonId, precedence, condition) {
this.dependents.push([addonId, condition]);
if (precedence > this.precedence) {
this.precedence = precedence;
if (this.el) {
this.el.dataset.precedence = precedence;
}
}
this.update();
}
getEnabledDependents () {
const enabledDependents = [];
for (const [addonId, condition] of this.dependents) {
if (condition()) {
enabledDependents.push(addonId);
}
}
return enabledDependents;
}
dependsOn (addonId) {
return this.dependents.some(dependent => dependent[0] === addonId);
}
getElement () {
if (!this.el) {
const el = document.createElement('style');
el.className = 'scratch-addons-style';
el.dataset.precedence = this.precedence;
el.textContent = this.styleText;
this.styleText = null;
this.el = el;
}
return this.el;
}
update () {
const enabledDependents = this.getEnabledDependents();
if (areArraysEqual(enabledDependents, this.previousEnabledDependents)) {
// Nothing to do.
return;
}
this.previousEnabledDependents = enabledDependents;
if (enabledDependents.length > 0) {
const el = this.getElement();
el.dataset.addons = enabledDependents.join(',');
for (const child of stylesheetContainer.children) {
const otherPrecedence = +child.dataset.precedence || 0;
if (otherPrecedence >= this.precedence) {
// We need to be before this style.
stylesheetContainer.insertBefore(el, child);
return;
}
}
// We have higher precedence than all existing stylesheets.
stylesheetContainer.appendChild(el);
} else if (this.el) {
this.el.remove();
}
}
}
const create = (moduleId, styleText) => {
if (!allSheets.get(moduleId)) {
const newSheet = new ConditionalStyle(styleText);
allSheets.set(moduleId, newSheet);
}
return allSheets.get(moduleId);
};
export {
create,
updateAll
};