import { createElement } from "./skeleton/index.js"; import { ApplicationError } from "./error.js"; import { animate } from "./animate.js"; export function mutateForm(formSpec, formState) { Object.keys(formState).forEach((inputName) => { const value = formState[inputName]; const keys = inputName.split("."); let ptr = formSpec; while (keys.length > 1) { const k = keys.shift(); if (!k) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing key"); ptr = ptr[k]; } const key = keys.shift() || ""; if (ptr && ptr[key]) ptr[key].value = (value === "" ? null : value); }); return formSpec; } async function createFormNodes(node, { renderNode, renderLeaf, renderInput, path = [], level = 0 }) { // CASE 0: invalid form spec if (typeof node !== "object") { return [createElement(`
ERR: node[${typeof node}] path[${path.join(".")}] level[${level}]
`)]; } const $list = []; for (const key of Object.keys(node)) { if (typeof node[key] !== "object") { $list.push(createElement(`
ERR: node[${typeof node[key]}] path[${path.join(".")}] level[${level}]
`)); } // CASE 1: non leaf node else if (typeof node[key].type !== "string") { const $chunk = renderNode({ level, label: key }); const $children = $chunk.querySelector("[data-bind=\"children\"]") || $chunk; $children.removeAttribute("data-bind"); const $nested = await createForm(node[key], { path: path.concat(key), level: level + 1, label: key, renderNode, renderLeaf, renderInput }); $children.appendChild($nested); $list.push($chunk); } // CASE 2: leaf node else { const currentPath = path.concat(key); const $leaf = renderLeaf({ ...node[key], path: currentPath, label: key }); const $input = await renderInput({ ...node[key], path: currentPath.filter((chunk) => !!chunk) }); const $target = $leaf.querySelector("[data-bind=\"children\"]") || $leaf; // leaf node is either "classic" or can be the target of something that can be toggled // That's how we can hide input elements conditionally for use cases like the log level // settings that will not be visible unless log is first enabled or the advanced section // of the login screen const isAToggleElementItself = typeof node[key].id === "string"; const canToggleOtherElements = node[key].type === "enable" && node[key].target && node[key].target.length > 0; if (!isAToggleElementItself) { $target.removeAttribute("data-bind"); $target.appendChild($input); $list.push($leaf); } if (canToggleOtherElements) { // initialise the dom structure const $container = window.document.createElement("div"); $container.classList.add("advanced_form"); $container.style.setProperty("overflow", "hidden"); for (const k of Object.keys(node)) { if (typeof node[k] !== "object") continue; else if (!node[k].id) continue; else if (node[key].target.indexOf(node[k].id) === -1) continue; const $kleaf = renderLeaf({ ...node[k], path: path.concat(k), label: k }); const $kinput = await renderInput({ ...node[k], path: path.concat(k) }); const $ktarget = $kleaf.querySelector("[data-bind=\"children\"]") || $kleaf; $ktarget.removeAttribute("data-bind"); $ktarget.appendChild($kinput); $container.appendChild($kleaf); } $list.push($container); // initial state of the toggle const isToggled = typeof node[key].value === "boolean" ? node[key].value : node[key].default; if (!isToggled) $container.style.setProperty("display", "none"); let clientHeight = null; // this will only be known when the dom is mounted // setup events $input.onchange = async(e) => { $container.style.setProperty("display", "inherit"); if (clientHeight === null) clientHeight = $container.offsetHeight; if (e.target.checked) { animate($container, { time: Math.max(50, Math.min(clientHeight, 150)), keyframes: [{ height: "0" }, { height: `${clientHeight}px` }] }); } else { animate($container, { time: Math.max(25, Math.min(clientHeight, 75)), keyframes: [{ height: `${clientHeight}px` }, { height: "0" }] }); } }; } } } return $list; } export async function createForm(node, opts) { const $container = window.document.createElement("div"); if (!(opts.level >= 1)) $container.classList.add("formbuilder"); const $nodes = await createFormNodes(node, opts); $nodes.forEach(($node) => { $container.appendChild($node); }); return $container; }