class TemplateElement {
static XMLNS = {
svg: "http://www.w3.org/2000/svg",
xlink: "https://www.w3.org/1999/xlink"
};
static HTML = [
"a",
"abbr",
"acronym",
"address",
"applet",
"area",
"article",
"aside",
"audio",
"b",
"base",
"basefont",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"center",
"cite",
"code",
"col",
"colgroup",
"datalist",
"dd",
"del",
"dfn",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"font",
"footer",
"form",
"frame",
"frameset",
"head",
"header",
"h1 to h6",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"p",
"param",
"pre",
"progress",
"q",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strike",
"strong",
"style",
"sub",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"u",
"ul",
"var",
"video",
"wbr"
].reduce((r, v) => {
r[v] = true;
return r;
}, {});
static SVG = [
"a",
"altGlyph",
"altGlyphDef",
"altGlyphItem",
"animate",
"animateColor",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"color-profile",
"cursor",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"font",
"font-face",
"font-face-format",
"font-face-name",
"font-face-src",
"font-face-uri",
"foreignObject",
"g",
"glyph",
"glyphRef",
"hkern",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"missing-glyph",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"script",
"set",
"stop",
"style",
"svg",
"switch",
"symbol",
"text",
"textPath",
"title",
"tref",
"tspan",
"use",
"view",
"vkern"
].reduce((r, v) => {
if (TemplateElement.HTML[v] === undefined) {
r[v] = "http://www.w3.org/2000/svg";
}
return r;
}, {});
constructor(name, attributes = {}, children = []) {
this.name = name;
this.namespace = attributes?.xmlns || TemplateElement.SVG[name];
this.attributes = {};
this.children = [];
// We need to go through .set() as the contents need to be normalised.
this.set(attributes, ...children);
this._node = undefined;
}
get node() {
this._node = this._node ? this._node : this.toDOM();
return this._node;
}
set(attributes, ...rest) {
if (attributes instanceof Array) {
return this.set({}, attributes.concat(rest));
} else if (
attributes instanceof Node ||
attributes instanceof TemplateElement ||
attributes instanceof Cell ||
attributes instanceof Effector ||
attributes instanceof Selector
) {
return this.set({}, [attributes, ...rest]);
} else if (!(attributes instanceof Object)) {
return this.set({}, [attributes, ...rest]);
} else {
// We normalize child slots to be effects
this.attributes = Object.entries(attributes || {}).reduce((r, [k, v]) => {
r[k] =
v instanceof Cell
? k.match(RE_ATTRIBUTE_HANDLER)
? // The attribute is a handler, and the handler callback come from a cell
new InteractionEffector(v, null)
: new AttributeEffector(v, k)
: v instanceof Function
? new InteractionEffector(null, v)
: v;
return r;
}, this.attributes);
this.children = rest
.flat()
.map((v, k) =>
v instanceof Cell
? new ContentEffector(v)
: v instanceof Selector
? v.compile()
: v
);
return this;
}
}
walk(functor, path = []) {
functor(this, path);
for (let k in this.attributes) {
const v = this.attributes[k];
if (v instanceof Effector) {
functor(path.concat([k]));
}
}
this.children.forEach((v, k) => {
if (v instanceof TemplateElement) {
v.walk(functor, path.concat([k]));
} else if (v instanceof Effector) {
functor(v, path.concat([k]));
// TODO: We should probably recurse with the effectors's sub effectors.
/*v.templates.forEach((t, i) => {
t.walk(functor, path.concat([k, i]));
});*/
} else {
functor(v, path.concat([k]));
}
});
}
getSlots(element = this) {
// This extracts the slots defined in the template
const slots = [];
element.walk((node, path) => {
if (
node === null ||
node === undefined ||
typeof node === "string" ||
typeof node === "number"
) {
// Nothing, it's a straight up node.
} else if (node instanceof Effector) {
slots.push({ effect: node, path, index: slots.length });
} else {
Object.entries(node.attributes || {}).forEach(([k, v]) => {
if (v instanceof Effector) {
slots.push({
effect: v,
path: path.concat([k]),
index: slots.length
});
}
});
}
});
return slots;
}
getEffectors(element = this) {
return this.getSlots(element).reduce((r, _) => {
const m = _.effect.mapping.inputs;
if (!m) {
// Nothing
} else if (m instanceof Array) {
r = m.reduce((r, v) => {
r[v.name] = v;
return r;
}, r);
} else if (m instanceof Cell) {
r[m.name] = m;
} else {
r = Object.entries(m).reduce((r, [k, v]) => {
r[k] = v;
return r;
}, r);
}
return r;
}, {});
}
toDOM(element = this) {
return TemplateElement.ToDOM(element);
}
static ToDOM(element) {
let node = null;
if (typeof element === "string" || typeof element === "number") {
node = document.createTextNode(`${element}`);
} else if (element === null || element === undefined) {
node = document.createComment("");
} else if (element instanceof Effector) {
node = document.createComment("◌");
} else if (element instanceof TemplateElement) {
node = element.namespace
? document.createElementNS(
TemplateElement.XMLNS[element.namespace] || element.namespace,
element.name
)
: document.createElement(element.name);
Object.entries(element.attributes || {}).forEach(([k, v]) => {
switch (k) {
case "style":
if (!(v instanceof Effector)) {
Object.assign(node.style, v);
}
break;
// FIXME: We should probably have jsut one
case "class":
case "className":
case "_":
if (!(v instanceof Effector)) {
node.className = `${v}`;
}
break;
default:
const qualname = k.split(":");
qualname.length === 1
? node.setAttribute(k, v instanceof Effector ? "" : `${v}`)
: node.setAttributeNS(
TemplateElement.NS[qualname[0]] || qualname[0],
qualname[1],
v instanceof Effector ? "" : `${v}`
);
}
});
} else if (element instanceof RenderingContext) {
return element.node;
} else if (element instanceof Node) {
return element;
} else {
onError(
`TemplateElement: unsupported child of type '${typeof element}': ${element}`,
{ element }
);
}
return (element.children || []).reduce((r, v) => {
r.appendChild(TemplateElement.ToDOM(v));
return r;
}, node);
}
}