File size: 4,244 Bytes
6bcb42f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 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
};