640 lines
12 KiB
JavaScript
640 lines
12 KiB
JavaScript
|
/**
|
||
|
* @class Represents a dynamic attribute or element
|
||
|
*/
|
||
|
class Dynamic {
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {() => any} callback
|
||
|
*/
|
||
|
constructor(callback) {
|
||
|
this.callback = callback
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @class Represents a variable state
|
||
|
*/
|
||
|
class State {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {any} defaultValue
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor(defaultValue = undefined) {
|
||
|
/**
|
||
|
* @type {any}
|
||
|
* @public
|
||
|
* The value of this state
|
||
|
*/
|
||
|
this.value = defaultValue
|
||
|
|
||
|
/**
|
||
|
* @type {Renderable[]}
|
||
|
* @protected
|
||
|
* The renderable linked to this state
|
||
|
*/
|
||
|
this.renderables = []
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Renderable} renderable
|
||
|
* @protected
|
||
|
*/
|
||
|
addRenderable(renderable) {
|
||
|
this.renderables.push(renderable)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates the value of the state
|
||
|
* @param {any} newValue
|
||
|
* @public
|
||
|
*/
|
||
|
update(newValue) {
|
||
|
this.value = newValue
|
||
|
for (const renderable of this.renderables) {
|
||
|
renderable.render()
|
||
|
renderable.update()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {String}
|
||
|
* @public
|
||
|
*/
|
||
|
toString() {
|
||
|
return this.value.toString()
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @class Represents a rendererable component or html element
|
||
|
*/
|
||
|
class Renderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {Renderable[]} children
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor(children) {
|
||
|
|
||
|
if (!children)
|
||
|
children = []
|
||
|
|
||
|
/**
|
||
|
* The parent renderable of this renderable
|
||
|
* @type {HTMLElement | DocumentFragment}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.parentRenderable = null
|
||
|
|
||
|
/**
|
||
|
* The element that is currently in the dom
|
||
|
* @type {HTMLElement | DocumentFragment}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.domElement = null
|
||
|
|
||
|
/**
|
||
|
* The element from the most recent render
|
||
|
* @type {HTMLElement | DocumentFragment}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.renderedElement = null
|
||
|
|
||
|
/**
|
||
|
* The elements children
|
||
|
* @type {Renderable[]}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.children = children
|
||
|
|
||
|
for (const child of this.children) {
|
||
|
if (child instanceof App) {
|
||
|
throw new Error('App must be at root of component tree')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
/* Render all children */
|
||
|
for (const child of this.children) {
|
||
|
child.render()
|
||
|
child.parentRenderable = this
|
||
|
child.domElement = child.renderedElement
|
||
|
this.renderedElement.appendChild(child.renderedElement)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
update() {
|
||
|
this.domElement.replaceWith(this.renderedElement)
|
||
|
this.domElement = this.renderedElement
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @class Representsa HTML Element
|
||
|
*/
|
||
|
class Element extends Renderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @protected
|
||
|
* @param {String} type
|
||
|
* @param {any} attrs
|
||
|
* @param {Renderable[]} children
|
||
|
*/
|
||
|
constructor(type, attrs, children) {
|
||
|
|
||
|
// call parent constructor
|
||
|
super(children)
|
||
|
|
||
|
/**
|
||
|
* The element type
|
||
|
* @type {String}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.type = type
|
||
|
|
||
|
/**
|
||
|
* The element attributes
|
||
|
* @type {any}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.attrs = attrs
|
||
|
|
||
|
if (this.attrs['signals']) {
|
||
|
for (const signal of this.attrs['signals']) {
|
||
|
signal.addRenderable(this)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
|
||
|
let el;
|
||
|
|
||
|
if (this.type == 'path' || this.type == 'g' || this.type == 'svg') {
|
||
|
let svgns = "http://www.w3.org/2000/svg";
|
||
|
el = document.createElementNS(svgns, this.type);
|
||
|
} else {
|
||
|
el = document.createElement(this.type);
|
||
|
}
|
||
|
|
||
|
for (const key in this.attrs) {
|
||
|
|
||
|
let val = this.attrs[key]
|
||
|
|
||
|
if (key == 'signals') {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (val instanceof Dynamic)
|
||
|
val = val.callback()
|
||
|
|
||
|
if (key.startsWith("on")) {
|
||
|
el[key] = val
|
||
|
} else {
|
||
|
el.setAttribute(key, val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.renderedElement = el
|
||
|
|
||
|
super.render()
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @class Represents raw content to be added to HTML
|
||
|
*/
|
||
|
class Content extends Renderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {() => any} callback
|
||
|
* @param {State[]} states
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor(callback, states) {
|
||
|
|
||
|
super([])
|
||
|
|
||
|
/**
|
||
|
* @type {() => any}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.callback = callback
|
||
|
|
||
|
for (const state of states) {
|
||
|
state.addRenderable(this)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
this.renderedElement = document
|
||
|
.createRange()
|
||
|
.createContextualFragment(this.callback())
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
update() {
|
||
|
this.parentRenderable.render()
|
||
|
this.parentRenderable.update()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class SelectableRenderable extends Renderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor () {
|
||
|
super([])
|
||
|
|
||
|
/**
|
||
|
* @type {Renderable}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.selectedElement = null
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
this.selectedElement.render()
|
||
|
this.renderedElement = this.selectedElement.renderedElement
|
||
|
this.selectedElement.parentRenderable = this.parentRenderable
|
||
|
super.render()
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
var routerCallbacks = []
|
||
|
export var routeSignal = state(document.location.pathname.toString())
|
||
|
|
||
|
/**
|
||
|
* @class Represends a page router
|
||
|
*/
|
||
|
class Router extends SelectableRenderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {(string) => Renderable} callback
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor (callback) {
|
||
|
|
||
|
super()
|
||
|
|
||
|
/**
|
||
|
* @type {(string) => Renderable}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.callback = callback
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
*/
|
||
|
this.location = document.location.pathname.toString()
|
||
|
|
||
|
routerCallbacks.push(() => {
|
||
|
this.location = document.location.pathname.toString()
|
||
|
this.render()
|
||
|
this.update()
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
super.selectedElement = this.callback(this.location.toString())
|
||
|
super.render()
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @class Represends a component switch
|
||
|
*/
|
||
|
class Conditional extends SelectableRenderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {() => any} conditional
|
||
|
* @param {{[key: any]: Renderable}} cases
|
||
|
* @param {State[]} states
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor (conditional, cases, states) {
|
||
|
|
||
|
super()
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
*/
|
||
|
this.conditional = conditional
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
*/
|
||
|
this.cases = cases
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
*/
|
||
|
this.states = states
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @protected
|
||
|
* @override
|
||
|
*/
|
||
|
render() {
|
||
|
let value = this.conditional()
|
||
|
|
||
|
this.renderedElement = null
|
||
|
|
||
|
for (const [key, element] of Object.entries(this.routes)) {
|
||
|
if (key === value) {
|
||
|
super.selectedElement = element
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
super.render()
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
class App extends Renderable {
|
||
|
|
||
|
/**
|
||
|
* @constructor
|
||
|
* @param {Renderable} container
|
||
|
* @protected
|
||
|
*/
|
||
|
constructor(container) {
|
||
|
super([])
|
||
|
|
||
|
/**
|
||
|
* @type {Renderable}
|
||
|
* @protected
|
||
|
*/
|
||
|
this.container = container
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Renders the app into the DOM
|
||
|
* @param {HTMLElement} parent - the parent element to place the app
|
||
|
* @public
|
||
|
* @override
|
||
|
*/
|
||
|
render(parent) {
|
||
|
|
||
|
this.container.render()
|
||
|
this.container.parentRenderable = parent
|
||
|
|
||
|
if (this.container.domElement)
|
||
|
this.container.domElement.remove()
|
||
|
|
||
|
this.container.domElement = this.container.renderedElement
|
||
|
|
||
|
parent.appendChild(this.container.domElement)
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @private
|
||
|
* @override
|
||
|
*/
|
||
|
update() { /* empty */ }
|
||
|
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {App}
|
||
|
* @param {Renderable} container
|
||
|
*/
|
||
|
export function app(container) {
|
||
|
return new App(container)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {Dynamic}
|
||
|
* @param {() => any} callback
|
||
|
*/
|
||
|
export function dynamic(callback) {
|
||
|
return new Dynamic(callback)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {State}
|
||
|
* @param {any} defaultValue
|
||
|
*/
|
||
|
export function state(defaultValue) {
|
||
|
return new State(defaultValue)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {() => string} callback
|
||
|
* @param {...State} states
|
||
|
* @returns {Content}
|
||
|
*/
|
||
|
export function content(callback, ...states) {
|
||
|
return new Content(callback, states)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {(string) => Renderable} callback
|
||
|
* @returns {Router}
|
||
|
*/
|
||
|
export function router(callback) {
|
||
|
return new Router(callback)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {() => any} conditional
|
||
|
* @param {{[key: any]: Renderable}} cases
|
||
|
* @param {...State} states
|
||
|
* @returns {Conditional}
|
||
|
*/
|
||
|
export function conditional(conditional, cases, ...states) {
|
||
|
return new Conditional(conditional, cases, states)
|
||
|
}
|
||
|
|
||
|
export function backLocation() {
|
||
|
window.history.back()
|
||
|
routeSignal.update(document.location.pathname.toString())
|
||
|
for (const callback of routerCallbacks)
|
||
|
callback()
|
||
|
}
|
||
|
|
||
|
export function forwardLocation() {
|
||
|
window.history.forward()
|
||
|
routeSignal.update(document.location.pathname.toString())
|
||
|
for (const callback of routerCallbacks)
|
||
|
callback()
|
||
|
}
|
||
|
|
||
|
export function pushLocation(newLocation) {
|
||
|
window.history.pushState({}, '', newLocation)
|
||
|
routeSignal.update(document.location.pathname.toString())
|
||
|
for (const callback of routerCallbacks)
|
||
|
callback()
|
||
|
}
|
||
|
|
||
|
export function replaceLocation(newLocation) {
|
||
|
window.history.replaceState({}, '', newLocation)
|
||
|
routeSignal.update(document.location.pathname.toString())
|
||
|
for (const callback of routerCallbacks)
|
||
|
callback()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML paragraph
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function p(attrs, ...children) {
|
||
|
return new Element("p", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML span
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function span(attrs, ...children) {
|
||
|
return new Element("span", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML div
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function div(attrs, ...children) {
|
||
|
return new Element("div", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML anchor
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function a(attrs, ...children) {
|
||
|
return new Element("a", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML icon
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function i(attrs, ...children) {
|
||
|
return new Element("i", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML form
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function form(attrs, ...children) {
|
||
|
return new Element("form", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML img
|
||
|
* @param {String} alt - the alt tag
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function img(alt, attrs, children) {
|
||
|
attrs['onerror'] = (event) => event.target.remove()
|
||
|
attrs['alt'] = alt
|
||
|
return new Element("img", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML input
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function input(attrs, ...children) {
|
||
|
return new Element("input", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML button
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function button(attrs, ...children) {
|
||
|
return new Element("button", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML path
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function path(attrs, ...children) {
|
||
|
return new Element("path", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML g tah
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function g(attrs, ...children) {
|
||
|
return new Element("g", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML svg
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function svg(attrs, ...children) {
|
||
|
return new Element("svg", attrs, children)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a HTML text area
|
||
|
* @param {{[key: string]: any}} attrs - a map of the HTML attributes
|
||
|
* @param {Renderable[]} children - the elements children
|
||
|
*/
|
||
|
export function textarea(attrs, ...children) {
|
||
|
return new Element("textarea", attrs, children)
|
||
|
}
|