;; NODE BUILDING — the whole reason this scaffold has no `esc` function.
;;
;; Every string a rooms app renders is untrusted: a room label is typed by
;; whoever named the room, a display name by whoever joined, a note by whoever
;; posted it. So the demo screens ship with no `innerHTML` string templates and
;; nothing to escape: `txt` sets `textContent`, and there is no path here
;; through which a value can become markup. test/source-hygiene.test.mjs holds
;; that as an ABSOLUTE — its raw-HTML allowlist is EMPTY — so a fresh scaffold
;; starts green instead of starting by weakening its own gate.
;;
;; Keep it that way. If you catch yourself reaching for `innerHTML`, add a
;; builder here instead; the moment you genuinely need a budgeted exception,
;; the hygiene test says how to record one (a number that may only fall).
(ns __NAME__.app.dom
(:require [ardegazu.rooms.js :as j]))
(defn el
"A fresh element with a class."
[tag cls]
(let [node (js/document.createElement tag)]
(when (j/truthy? cls) (set! (.-className node) cls))
node))
(defn txt
"A fresh element carrying `s` AS TEXT. `textContent`, never `innerHTML`: `s`
is a label, a name or a note from another member as often as not."
[tag cls s]
(let [node (el tag cls)]
(set! (.-textContent node) (js* "String(~{})" (j/nn s "")))
node))
(defn set-text!
"Replace one node's text. Same rule, same reason."
[node s]
(when (j/truthy? node)
(set! (.-textContent ^js node) (js* "String(~{})" (j/nn s ""))))
js/undefined)
(defn clear!
"Empty a node. `textContent = \"\"` removes every child, and unlike
`innerHTML = \"\"` it is not a markup sink at all."
[node]
(set! (.-textContent ^js node) "")
js/undefined)
(defn add!
"Append children to `parent`, skipping nils, and return `parent`."
[parent & kids]
(doseq [k kids] (when (some? k) (.appendChild ^js parent k)))
parent)
(defn btn
"A button. `kind` is the class; `on-tap` is wired here so no caller has to
remember to. `type=\"button\"` unless told otherwise — a bare <button> inside
a <form> submits it, which has silently sent more than one form in this
suite. (The demo's submit buttons are built with `txt` for exactly that
reason: inside a <form>, the default submit type is the point.)"
[kind label on-tap]
(let [b (txt "button" kind label)]
(set! (.-type b) "button")
(when (some? on-tap) (.addEventListener b "click" (fn [] (on-tap) js/undefined)))
b))
(defn input
"A text input with a placeholder and an optional maxlength."
[cls placeholder maxlen]
(let [i (el "input" cls)]
(set! (.-type i) "text")
(set! (.-autocomplete i) "off")
(set! (.-placeholder i) (j/nn placeholder ""))
(when (j/truthy? maxlen) (.setAttribute i "maxlength" (js* "String(~{})" maxlen)))
i))