@!!!!!/elemental
Version:
Control the elements using JS
255 lines (240 loc) • 11.8 kB
JavaScript
// minimized xhtm from: https://github.com/dy/xhtm
var e="",t="";function o(r){var p,a,l,s,c=arguments,i=this,n=0,d=[],h=0,u=[],f=0;d.root=!0;var g=function(e,o,r){void 0===o&&(o=[]);var p=0;return(e=r||e!==t?e.replace(/\ue001/g,e=>u[f++]):u[f++].slice(1,-1))?(e.replace(/\ue000/g,(t,r)=>(r&&o.push(e.slice(p,r)),p=r+1,o.push(c[++h]))),p<e.length&&o.push(e.slice(p)),o.length>1?o:o[0]):e},m=()=>{[d,s,...p]=d,d.push(i(s,...p))};return r.join(e).replace(/<!--[^]*-->/g,"").replace(/<!\[CDATA\[[^]*\]\]>/g,"").replace(/('|")[^\1]*?\1/g,e=>(u.push(e),t)).replace(/\s+/g," ").replace(/(?:^|>)([^<]*)(?:$|<)/g,(e,t,r,p)=>{var c,i;if(r&&p.slice(n,r).replace(/(\S)\/$/,"$1 /").split(" ").map((e,t)=>{if("/"===e[0])c=i||e.slice(1)||1;else if(t){if(e){var r=d[2]||(d[2]={});"..."===e.slice(0,3)?Object.assign(r,arguments[++h]):([a,l]=e.split("="),r[g(a)]=!l||g(l))}}else{for(i=g(e);o.close[d[1]+i];)m();d=[d,i,null],o.empty[i]&&(c=i)}}),c)for(m();s!==c&&o.close[s];)m();n=r+e.length,t&&" "!==t&&g((s=0,t),d,!0)}),d.root||m(),d.length>1?d:d[0]}o.empty={},o.close={},"area base br col command embed hr img input keygen link meta param source track wbr ! !doctype ? ?xml".split(" ").map(e=>o.empty[e]=o.empty[e.toUpperCase()]=!0);var r={li:"",dt:"dd",dd:"dt",p:"address article aside blockquote details div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol pre section table",rt:"rp",rp:"rt",optgroup:"",option:"optgroup",caption:"tbody thead tfoot tr colgroup",colgroup:"thead tbody tfoot tr caption",thead:"tbody tfoot caption",tbody:"tfoot caption",tfoot:"caption",tr:"tbody tfoot",td:"th tr",th:"td tr tbody"},p=function(e){[...r[e].split(" "),e].map(t=>{o.close[e]=o.close[e.toUpperCase()]=o.close[e+t]=o.close[e.toUpperCase()+t]=o.close[e+t.toUpperCase()]=o.close[e.toUpperCase()+t.toUpperCase()]=!0})};for(var a in r)p(a);
const xhtm = o
const ElementalSymbol = Symbol.for("Elemental")
class ElementalClass {
constructor(components={}, options={}) {
const {middleware, errorComponentFactory} = options
this.components = components||{}
this.middleware = middleware||{}
this.errorComponentFactory = errorComponentFactory||defaultErrorComponentFactory
this.html = xhtm.bind((...args)=>this.createElement(...args))
this.html[ElementalSymbol] = this
this.css = Elemental.css
}
createElement(...args) {
Elemental.debug && console.debug(`args is:`,args)
// run middleware
for (const middleware of (this.middleware[Elemental.allTags]||[]).concat((this.middleware[args[0]]||[]))) {
try {
args = eachMiddleWare(args)
} catch (error) {
console.error("[ElementalClass] one of the middleware functions failed:", eachMiddleWare, args)
}
// TODO: handle middleware creating invalid arguments
}
let [ key, properties, ...children ] = args
Elemental.debug && console.debug(`key, properties, children is:`,key, properties, children)
// lookup custom components
if (this.components[key] instanceof Function) {
key = this.components[key]
}
// run custom components
if (key instanceof Function) {
let output
try {
output = isConstructor(key) ? new key({...properties, children}) : key({...properties, children})
} catch (error) {
return this.errorComponentFactory({...properties, children}, key, error)
}
// allow async components
if (output instanceof Promise) {
const elementPromise = output
const placeholder = elementPromise.placeholder || document.createElement("div")
setTimeout(async () => placeholder.replaceWith(await elementPromise), 0)
return placeholder
} else {
return output
}
}
// create either an html element or an svg element
const isSvg = Elemental.exclusivelySvgElements.has(key)
const element = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', key) : document.createElement(key)
if (properties instanceof Object) {
for (const [key, value] of Object.entries(properties)) {
try {
if (isSvg) {
const kebabCase = key.replace(/(?<=[a-z])([A-Z])(?=[a-z])/g, (each)=>`-${each.toLowerCase()}`)
element.setAttribute(kebabCase, value)
} else {
element.setAttribute(key.toLowerCase(), value)
}
} catch (error) {
}
try {
element[key] = value
} catch (error) {}
}
}
return appendChildren(element, ...children)
}
extend(additionalComponents={}, options={}) {
const {middleware, ...other} = options
return Elemental(
{...this.components, ...additionalComponents},
{
middleware:{...this.middleware, ...middleware},
...other
}
)
}
}
//
// This wrapper does two things
// const html = Elemental()
// 1. html() (the function call) behaves like (new ElementalClass()).html()
// 2. html.something (methods/attributes) behaves like (new ElementalClass()).something
//
const proxySymbol = Symbol.for('Proxy')
const Elemental = (...args) => {
const originalThing = (new ElementalClass(...args)).html
const proxyObject = new Proxy(originalThing, {
// Object.keys
ownKeys(original) { return Reflect.ownKeys(original[ElementalSymbol]) },
// function call (original value needs to be a function)
// apply(original, context, ...args) { console.log(args) },
get(original, key) {
if (key == proxySymbol) {return true}
const originalValue = original[ElementalSymbol][key]
if (originalValue) {
return originalValue
}
return Reflect.get(original, key, ...args)
},
})
return proxyObject
}
Elemental.allTags = Symbol.for("allTags")
Elemental.exclusivelySvgElements = new Set(["svg", "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "foreignObject", "g", "hatch", "hatchpath", "image", "line", "linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "switch", "symbol", "text", "textPath", "tspan", "unknown", "use", "view",])
Elemental.css = function(...args) {
const element = document.createElement("div")
if (args.length == 1) {
if (args instanceof Object) {
Object.assign(element.style, args)
}
// string case
} else if (args.length > 1) {
const [strings, ...values] = args
let finalString = ""
for (const each of strings) {
finalString += each
if (values.length > 0) {
finalString += `${values.shift()}`
}
}
element.style = finalString
}
return element.style
}
function appendChildren(element, ...children) {
for (const each of children) {
if (typeof each == 'string') {
element.appendChild(new window.Text(each))
} else if (each == null) {
// empty node
element.appendChild(new window.Text(""))
} else if (!(each instanceof Object)) {
element.appendChild(new window.Text(`${each}`))
} else if (each instanceof Node) {
element.appendChild(each)
} else if (each instanceof Array) {
appendChildren(element, ...each)
} else if (each instanceof Function) {
// recursively
appendChildren(element, each())
} else if (each instanceof Promise) {
const elementPromise = each
const placeholder = elementPromise.placeholder || document.createElement("div")
setTimeout(async () => placeholder.replaceWith(await elementPromise), 0)
element.appendChild(placeholder)
// some elements are not HTML nodes and are still valid
} else if (each != null && each instanceof Object) {
element.appendChild(each)
}
}
return element
}
function defaultErrorComponentFactory({children, ...properties}, key, error) {
const element = document.createElement("div")
const errorDetails = document.createElement("code")
const childContainer = document.createElement("div")
//
// error body
//
element.style.all = "unset"
element.style.display = "flex"
element.style.flexDirection = "column"
element.style.padding = "1.5rem"
element.style.backgroundColor = "#f5a5a8"
element.style.color = "white"
element.style.fontFamily = '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif'
element.style.fontSize = '18px'
element.style.fontWeight = '400'
element.style.overflow = 'auto'
element.innerHTML = `I'm sorry, there was an error when loading this part of the page 🙁 `
//
// error details
//
let errorElementPart
if (typeof key == 'string') {
errorElementPart = `<${key} />`
} else {
try {
errorElementPart = `<${key.prototype.constructor.name} />`
} catch (error) {
errorElementPart = `<${key} />`
}
}
let errorJsonObject = {}
for (const [key, value] of Object.entries(properties)) {
try {
errorJsonObject[key] = JSON.parse(JSON.stringify(value))
} catch (error) {
errorJsonObject[key] = `${value}`
}
}
errorDetails.innerHTML = `tag: ${errorElementPart}\nproperties: ${JSON.stringify(errorJsonObject)}\nerror: ${error}`
errorDetails.style.padding = "1rem"
errorDetails.style.backgroundColor = "#161b22"
errorDetails.style.color = "#789896"
element.appendChild(errorDetails)
//
// children
//
childContainer.style.all = "unset"
childContainer.style.display = "flex"
childContainer.style.flexDirection = "column"
childContainer.style.marginTop = "1.3rem"
if (children instanceof Array) {
for (const [key, value] of Object.entries(children)) {
if (typeof each == 'string') {
childContainer.appendChild(new window.Text(value))
} else if (value instanceof Node) {
childContainer.appendChild(value)
}
}
}
element.appendChild(childContainer)
return element
}
function isConstructor(obj) {
return !!obj.prototype && !!obj.prototype.constructor.name;
}
//
// protect document head by monkey patching it (this is the only monkeypatch)
//
try {
const originalHead = document.head
Object.defineProperty(document,"head", {
set: (element) => appendChildren(originalHead, ...element.childNodes),
get: ()=>originalHead,
writable: true,
})
} catch (error) {
}
module.exports = {
Elemental,
html: Elemental(),
css: Elemental.css,
allTags: Elemental.allTags,
}