treehouse/static/js/spells.js

89 lines
3.2 KiB
JavaScript

// 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.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";
});