// Spells are a simplistic, composition-based replacement for the is="" attribute, which is not // supported on Webkit due to Apple's engineering being a bunch of obstinate idiots who explicitly // choose not to follow web standards. // Limitations: // - The data-cast attribute cannot be added dynamically. // - There is no disconnectedCallback. Only a constructor which initializes a spell. let spells = new Map(); let elementsWithUnknownSpells = new Map(); const sSpells = Symbol("spells"); function castSpellOnElement(spellName, element) { element[sSpells] ??= new Map(); if (!elementsWithUnknownSpells.has(spellName)) { elementsWithUnknownSpells.set(spellName, new Set()); } let Spell = spells.get(spellName); if (Spell != null && !element[sSpells].has(Spell)) { element[sSpells].set(Spell, new Spell(element)); elementsWithUnknownSpells.get(spellName).delete(element); } else { elementsWithUnknownSpells.get(spellName).add(element); } } function applySpells(elements) { for (let element of elements) { if (element instanceof Element) { let spellListString = element.getAttribute("data-cast"); if (spellListString != null) { let spellList = spellListString.split(' '); for (let spellName of spellList) { castSpellOnElement(spellName, element); } } } } } export function addSpell(name, spell) { spells.set(name, spell); let elementsWithThisSpell = elementsWithUnknownSpells.get(name); if (elementsWithThisSpell != null) { for (let element of elementsWithThisSpell) { castSpellOnElement(name, element); } } } // Returns a spell's data. Gotchas: the spell needs to already be on the element. // Therefore, if this is used from within a spell, the requested spell must have already been // applied by this point. // Someday I may change this to an async function that resumes whenever the spell is available to // iron over this limitation. But today is not that day. export function spell(element, spell) { return element[sSpells].get(spell); } // Apply spells to elements which have them and have been loaded so far. let loadedSoFar = document.querySelectorAll("[data-cast]"); applySpells(loadedSoFar); // For all other elements, add a mutation observer that will handle them. let mutationObserver = new MutationObserver(records => { for (let record of records) { let mutatedNodes = new Set(); // NOTE: Added nodes may contain children which also need to be processed. // Collect those that have [data-cast] on them and apply spells to them. for (let addedNode of record.addedNodes) { if (!(addedNode instanceof Text)) { if (addedNode.getAttribute("data-cast") != null) { mutatedNodes.add(addedNode); } addedNode.querySelectorAll("[data-cast]").forEach(element => mutatedNodes.add(element)); } } applySpells(mutatedNodes); } }); mutationObserver.observe(document, { subtree: true, childList: true }); // ------------ Common spells ------------ // js makes things visible only when JavaScript is enabled. addSpell("js", function (element) { element.style.display = "block"; });