nyooom/skooma.js
DarkWiiPlayer 64f28972c0
Remove promise handling from skooma.js
This was really just feature creep and doesn't have to be part of
skooma. It could easily be implemented as an independent function or
module.
2023-09-29 15:09:30 +02:00

117 lines
3.8 KiB
JavaScript

/*
A functional HTML generation library.
Example:
html.label(
html.span("Delete everything", {class: ["warning", "important"]}),
html.button("Click", {onClick: e => document.body.innerHTML=""}),
)
or
html.ul([1, 2, 3, 4, 5].map(x => html.li(x)), {class: "numbers"})
*/
const keyToPropName = key => key.replace(/^[A-Z]/, a => "-"+a).replace(/[A-Z]/g, a => '-'+a.toLowerCase())
export const empty = Symbol("Explicit empty argument for Skooma")
const insertStyles = (rule, styles) => {
for (let [key, value] of Object.entries(styles))
if (typeof value == "undefined")
rule.removeProperty(keyToPropName(key))
else
rule.setProperty(keyToPropName(key), value.toString())
}
const parseAttribute = (attribute) => {
if (typeof attribute == "string" || typeof attribute == "number")
return attribute
else if ("join" in attribute)
return attribute.join(" ")
else
return JSON.stringify(attribute)
}
const parseArgs = (element, before, ...args) => {
if (element.content) element = element.content
for (let arg of args) if (arg !== empty)
if (typeof arg == "string" || typeof arg == "number")
element.insertBefore(document.createTextNode(arg), before)
else if (arg === undefined || arg == null)
console.warn(`Argument is ${typeof arg}`, element)
else if (typeof arg == "function")
arg(element)
else if ("nodeName" in arg)
element.insertBefore(arg, before)
else if ("length" in arg)
parseArgs(element, before, ...arg)
else
for (let key in arg)
if (key == "style" && typeof(arg[key])=="object")
insertStyles(element.style, arg[key])
else if (key == "dataset" && typeof(arg[key])=="object")
for (let [key2, value] of Object.entries(arg[key]))
element.dataset[key2] = parseAttribute(value)
else if (key == "shadowRoot")
parseArgs((element.shadowRoot || element.attachShadow({mode: "open"})), null, arg[key])
else if (typeof arg[key] === "function")
element.addEventListener(key.replace(/^on[A-Z]/, x => x.charAt(x.length-1).toLowerCase()), arg[key])
else if (arg[key] === true)
{if (!element.hasAttribute(key)) element.setAttribute(key, '')}
else if (arg[key] === false)
element.removeAttribute(key)
else
element.setAttribute(key, parseAttribute(arg[key]))
}
const nop = object => object
const node = (_name, args, options) => {
let element
const [name, custom] = _name
.match(/[^$]+/g)
.map(options.nameFilter ?? nop)
if (options.xmlns)
element = document.createElementNS(options.xmlns, name, {is: custom})
else
element = document.createElement(name, {is: custom})
parseArgs(element, null, args)
return element
}
const nameSpacedProxy = (options={}) => new Proxy(Window, {
get: (target, prop, receiver) => { return (...args) => node(prop, args, options) },
has: (target, prop) => true,
})
export const bind = transform => {
let element
const inject = next => Object.defineProperty(next, 'current', {get: () => element})
const update = (...data) => {
const next = transform(...data)
if (next) {
if (element) element.replaceWith(next)
element = inject(next)
return element
}
}
return update
}
export const handle = fn => event => { event.preventDefault(); return fn(event) }
export const html = nameSpacedProxy({nameFilter: name => name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()})
export const svg = nameSpacedProxy({xmlns: "http://www.w3.org/2000/svg"})
const textFromTemplate = (literals, items) => {
const fragment = new DocumentFragment()
for (const key in items) {
fragment.append(document.createTextNode(literals[key]))
fragment.append(items[key])
}
fragment.append(document.createTextNode(literals.at(-1)))
return fragment
}
export const text = (data="", ...items) =>
typeof data == "object" && "at" in data
? textFromTemplate(data, items)
: document.createTextNode(data)