aboutsummaryrefslogtreecommitdiff
path: root/src/runtime.ffi.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtime.ffi.mjs')
-rw-r--r--src/runtime.ffi.mjs344
1 files changed, 0 insertions, 344 deletions
diff --git a/src/runtime.ffi.mjs b/src/runtime.ffi.mjs
deleted file mode 100644
index f7b711b..0000000
--- a/src/runtime.ffi.mjs
+++ /dev/null
@@ -1,344 +0,0 @@
-import { Empty } from "./gleam.mjs";
-import { map as result_map } from "../gleam_stdlib/gleam/result.mjs";
-
-export function morph(prev, curr, dispatch, parent) {
- // The current node is an `Element` and the previous DOM node is also a DOM
- // element.
- if (curr?.tag && prev?.nodeType === 1) {
- const nodeName = curr.tag.toUpperCase();
- const ns = curr.namespace || "http://www.w3.org/1999/xhtml";
-
- // If the current node and the existing DOM node have the same tag and
- // namespace, we can morph them together: keeping the DOM node intact and just
- // updating its attributes and children.
- if (prev.nodeName === nodeName && prev.namespaceURI == ns) {
- return morphElement(prev, curr, dispatch, parent);
- }
- // Otherwise, we need to replace the DOM node with a new one. The `createElement`
- // function will handle replacing the existing DOM node for us.
- else {
- return createElement(prev, curr, dispatch, parent);
- }
- }
-
- // The current node is an `Element` but the previous DOM node either did not
- // exist or it is not a DOM element (eg it might be a text or comment node).
- if (curr?.tag) {
- return createElement(prev, curr, dispatch, parent);
- }
-
- // The current node is a `Text`.
- if (typeof curr?.content === "string") {
- return prev?.nodeType === 3
- ? morphText(prev, curr)
- : createText(prev, curr);
- }
-
- // If someone was naughty and tried to pass in something other than a Lustre
- // element (or if there is an actual bug with the runtime!) we'll render a
- // comment and ask them to report the issue.
- return document.createComment(
- [
- "[internal lustre error] I couldn't work out how to render this element. This",
- "function should only be called internally by lustre's runtime: if you think",
- "this is an error, please open an issue at",
- "https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new",
- ].join(" ")
- );
-}
-
-// ELEMENTS --------------------------------------------------------------------
-
-function createElement(prev, curr, dispatch, parent = null) {
- const el = curr.namespace
- ? document.createElementNS(curr.namespace, curr.tag)
- : document.createElement(curr.tag);
-
- el.$lustre = {
- __registered_events: new Set(),
- };
-
- let dangerousUnescapedHtml = "";
-
- for (const attr of curr.attrs) {
- if (attr[0] === "class") {
- morphAttr(el, attr[0], `${el.className} ${attr[1]}`);
- } else if (attr[0] === "style") {
- morphAttr(el, attr[0], `${el.style.cssText} ${attr[1]}`);
- } else if (attr[0] === "dangerous-unescaped-html") {
- dangerousUnescapedHtml += attr[1];
- } else if (attr[0] !== "") {
- morphAttr(el, attr[0], attr[1], dispatch);
- }
- }
-
- if (customElements.get(curr.tag)) {
- el._slot = curr.children;
- } else if (curr.tag === "slot") {
- let children = new Empty();
- let parentWithSlot = parent;
-
- while (parentWithSlot) {
- if (parentWithSlot._slot) {
- children = parentWithSlot._slot;
- break;
- } else {
- parentWithSlot = parentWithSlot.parentNode;
- }
- }
-
- for (const child of children) {
- el.appendChild(morph(null, child, dispatch, el));
- }
- } else if (dangerousUnescapedHtml) {
- el.innerHTML = dangerousUnescapedHtml;
- } else {
- for (const child of curr.children) {
- el.appendChild(morph(null, child, dispatch, el));
- }
- }
-
- if (prev) prev.replaceWith(el);
-
- return el;
-}
-
-function morphElement(prev, curr, dispatch, parent) {
- const prevAttrs = prev.attributes;
- const currAttrs = new Map();
-
- // This can happen if we're morphing an existing DOM element that *wasn't*
- // initially created by lustre.
- prev.$lustre ??= { __registered_events: new Set() };
-
- // We're going to convert the Gleam List of attributes into a JavaScript Map
- // so its easier to lookup specific attributes.
- for (const currAttr of curr.attrs) {
- if (currAttr[0] === "class" && currAttrs.has("class")) {
- currAttrs.set(currAttr[0], `${currAttrs.get("class")} ${currAttr[1]}`);
- } else if (currAttr[0] === "style" && currAttrs.has("style")) {
- currAttrs.set(currAttr[0], `${currAttrs.get("style")} ${currAttr[1]}`);
- } else if (
- currAttr[0] === "dangerous-unescaped-html" &&
- currAttrs.has("dangerous-unescaped-html")
- ) {
- currAttrs.set(
- currAttr[0],
- `${currAttrs.get("dangerous-unescaped-html")} ${currAttr[1]}`
- );
- } else if (currAttr[0] !== "") {
- currAttrs.set(currAttr[0], currAttr[1]);
- }
- }
-
- // TODO: Event listeners aren't currently removed when they are removed from
- // the attributes list. This is a bug!
- for (const { name, value: prevValue } of prevAttrs) {
- if (!currAttrs.has(name)) {
- prev.removeAttribute(name);
- } else {
- const value = currAttrs.get(name);
-
- if (value !== prevValue) {
- morphAttr(prev, name, value, dispatch);
- currAttrs.delete(name);
- }
- }
- }
-
- for (const name of prev.$lustre.__registered_events) {
- if (!currAttrs.has(name)) {
- const event = name.slice(2).toLowerCase();
-
- prev.removeEventListener(event, prev.$lustre[`${name}Handler`]);
- prev.$lustre.__registered_events.delete(name);
-
- delete prev.$lustre[name];
- delete prev.$lustre[`${name}Handler`];
- }
- }
-
- for (const [name, value] of currAttrs) {
- morphAttr(prev, name, value, dispatch);
- }
-
- if (customElements.get(curr.tag)) {
- prev._slot = curr.children;
- } else if (curr.tag === "slot") {
- let prevChild = prev.firstChild;
- let currChild = new Empty();
- let parentWithSlot = parent;
-
- while (parentWithSlot) {
- if (parentWithSlot._slot) {
- currChild = parentWithSlot._slot;
- break;
- } else {
- parentWithSlot = parentWithSlot.parentNode;
- }
- }
-
- while (prevChild) {
- if (Array.isArray(currChild) && currChild.length) {
- morph(prevChild, currChild.shift(), dispatch, prev);
- } else if (currChild.head) {
- morph(prevChild, currChild.head, dispatch, prev);
- currChild = currChild.tail;
- }
-
- prevChild = prevChild.nextSibling;
- }
-
- for (const child of currChild) {
- prev.appendChild(morph(null, child, dispatch, prev));
- }
- } else if (currAttrs.has("dangerous-unescaped-html")) {
- prev.innerHTML = currAttrs.get("dangerous-unescaped-html");
- } else {
- let prevChild = prev.firstChild;
- let currChild = curr.children;
-
- while (prevChild) {
- if (Array.isArray(currChild) && currChild.length) {
- const next = prevChild.nextSibling;
- morph(prevChild, currChild.shift(), dispatch, prev);
- prevChild = next;
- } else if (currChild.head) {
- const next = prevChild.nextSibling;
- morph(prevChild, currChild.head, dispatch, prev);
- currChild = currChild.tail;
- prevChild = next;
- } else {
- const next = prevChild.nextSibling;
- prevChild.remove();
- prevChild = next;
- }
- }
-
- for (const child of currChild) {
- prev.appendChild(morph(null, child, dispatch, prev));
- }
- }
-
- return prev;
-}
-
-// ATTRIBUTES ------------------------------------------------------------------
-
-function morphAttr(el, name, value, dispatch) {
- switch (typeof value) {
- case name.startsWith("data-lustre-on-") && "string": {
- if (!value) {
- el.removeAttribute(name);
- el.removeEventListener(event, el.$lustre[`${name}Handler`]);
-
- break;
- }
- if (el.hasAttribute(name)) break;
-
- const event = name.slice(15).toLowerCase();
- const handler = (e) => dispatch(serverEventHandler(e));
-
- if (el.$lustre[`${name}Handler`]) {
- el.removeEventListener(event, el.$lustre[`${name}Handler`]);
- }
-
- el.addEventListener(event, handler);
-
- el.$lustre[name] = value;
- el.$lustre[`${name}Handler`] = handler;
- el.$lustre.__registered_events.add(name);
- el.setAttribute(name, value);
-
- break;
- }
-
- case "string":
- if (el.getAttribute(name) !== value) el.setAttribute(name, value);
- if (value === "") el.removeAttribute(name);
- if (name === "value" && el.value !== value) el.value = value;
- break;
-
- // Event listeners need to be handled slightly differently because we need
- // to be able to support custom events. We
- case name.startsWith("on") && "function": {
- if (el.$lustre[name] === value) break;
-
- const event = name.slice(2).toLowerCase();
- const handler = (e) => result_map(value(e), dispatch);
-
- if (el.$lustre[`${name}Handler`]) {
- el.removeEventListener(event, el.$lustre[`${name}Handler`]);
- }
-
- el.addEventListener(event, handler);
-
- el.$lustre[name] = value;
- el.$lustre[`${name}Handler`] = handler;
- el.$lustre.__registered_events.add(name);
-
- break;
- }
-
- default:
- el[name] = value;
- }
-}
-
-// TEXT ------------------------------------------------------------------------
-
-function createText(prev, curr) {
- const el = document.createTextNode(curr.content);
-
- if (prev) prev.replaceWith(el);
- return el;
-}
-
-function morphText(prev, curr) {
- const prevValue = prev.nodeValue;
- const currValue = curr.content;
-
- if (!currValue) {
- prev?.remove();
- return null;
- }
-
- if (prevValue !== currValue) prev.nodeValue = currValue;
-
- return prev;
-}
-
-// UTILS -----------------------------------------------------------------------
-
-function serverEventHandler(event) {
- const el = event.target;
- const tag = el.getAttribute(`data-lustre-on-${event.type}`);
- const data = JSON.parse(el.getAttribute("data-lustre-data") || "{}");
- const include = JSON.parse(el.getAttribute("data-lustre-include") || "[]");
-
- switch (event.type) {
- case "input":
- case "change":
- include.push("target.value");
- break;
- }
-
- return {
- tag,
- data: include.reduce((data, property) => {
- const path = property.split(".");
-
- for (let i = 0, o = data, e = event; i < path.length; i++) {
- if (i === path.length - 1) {
- o[path[i]] = e[path[i]];
- } else {
- o[path[i]] ??= {};
- e = e[path[i]];
- o = o[path[i]];
- }
- }
-
- return data;
- }, data),
- };
-}