wabi
Version:
Small yet an efficient JavaScript library for building UI with data binding.
526 lines (466 loc) • 12.5 kB
JavaScript
Node.prototype.__props = null
Node.prototype.__component = null
const bufferSize = 64
const namespaceSVG = "http://www.w3.org/2000/svg"
const stack = new Array(bufferSize)
const indices = new Uint32Array(bufferSize)
const indicesMinimal = new Uint32Array(bufferSize)
const indicesElement = new Array(bufferSize)
const components = {}
let stackIndex = 0
const elementOpen = (type, props = null, srcElement = null) => {
const parentElement = stack[stackIndex]
const prevElement = indicesElement[stackIndex]
let element = null
if(!prevElement) {
if(srcElement) {
element = srcElement
}
else {
const namespace = (type === "svg") ? namespaceSVG : parentElement.namespaceURI
element = document.createElementNS(namespace, type)
}
if(props) {
for(let key in props) {
setProp(element, key, props[key])
}
element.__props = props
}
parentElement.appendChild(element)
}
else {
if(prevElement.nodeType === 3 || prevElement.localName !== type) {
if(srcElement) {
element = srcElement
}
else {
const namespace = (type === "svg") ? namespaceSVG : parentElement.namespaceURI
element = document.createElementNS(namespace, type)
}
const component = prevElement.__component
if(component) {
if(component._depth === stackIndex) {
removeComponentExt(prevElement)
parentElement.replaceChild(element, component._base)
}
else {
parentElement.insertBefore(element, component._base)
}
}
else {
parentElement.replaceChild(element, prevElement)
indicesElement[stackIndex] = prevElement.nextSibling
}
if(props) {
for(let key in props) {
setProp(element, key, props[key])
}
prevElement.__props = props
}
}
else {
element = prevElement
indicesElement[stackIndex] = prevElement.nextSibling
const prevProps = element.__props
if(props !== prevProps) {
if(props) {
if(prevProps) {
for(let key in prevProps) {
if(props[key] === undefined) {
unsetProp(element, key)
}
}
for(let key in props) {
const value = props[key]
if(value !== prevProps[key]) {
setProp(element, key, value)
}
}
}
else {
for(let key in props) {
setProp(element, key, props[key])
}
}
element.__props = props
}
else {
if(prevProps) {
for(let key in prevProps) {
unsetProp(element, key)
}
element.__props = null
}
}
}
}
}
stackIndex++
stack[stackIndex] = element
indices[stackIndex] = 0
indicesMinimal[stackIndex] = 0
indicesElement[stackIndex] = (element.childNodes.length > 0) ? element.childNodes[0] : null
return element
}
const elementClose = (type) => {
const element = stack[stackIndex]
if(element.localName !== type) {
console.error(`(Element.close) Unexpected element closed: ${type} but was expecting: ${element.localName}`)
}
const index = indices[stackIndex]
if(index < element.childNodes.length) {
removeRange(element.childNodes, index - 1, element.childNodes.length - 1)
}
indices[stackIndex] = 0
stackIndex--
indices[stackIndex]++
indicesMinimal[stackIndex]++
indicesElement[stackIndex] = element.nextSibling
}
const elementVoid = (type, props) => {
const node = elementOpen(type, props)
elementClose(type)
return node
}
const element = (element, props) => {
const node = elementOpen(element.localName, props, element)
elementClose(element.localName)
return node
}
const componentVoid = (ctor, props = null) => {
const parentElement = stack[stackIndex]
let element = indicesElement[stackIndex]
let mounted = true
let component = null
if(element) {
component = element.__component
if(component) {
if(component.constructor === ctor) {
diffComponentProps(component, props)
mounted = false
}
else {
const newComponent = createComponent(ctor)
if(component._depth === stackIndex) {
parentElement.replaceChild(newComponent._base, component._base)
newComponent._numChildren = component._numChildren
removeComponent(element)
}
else {
parentElement.insertBefore(newComponent._base, component._base)
}
component = newComponent
diffComponentProps(component, props)
}
}
else {
component = createComponent(ctor)
parentElement.replaceChild(component._base, element)
removeNode(element)
diffComponentProps(component, props)
}
}
else {
component = createComponent(ctor)
parentElement.appendChild(component._base)
diffComponentProps(component, props)
}
if(mounted && component.mounted) {
component.mounted()
}
component._depth = stackIndex
const childrenCount = component._numChildren
stackIndex++
stack[stackIndex] = parentElement
indices[stackIndex] = 0
indicesMinimal[stackIndex] = 0
indicesElement[stackIndex] = component._base.nextSibling
component.render()
component._dirty = false
component._numChildren = indicesMinimal[stackIndex]
stackIndex--
indices[stackIndex] += (indices[stackIndex + 1] + 1)
indicesMinimal[stackIndex]++
if(component._numChildren < childrenCount) {
indicesElement[stackIndex] = removeSiblings(component._base, component._numChildren, childrenCount)
}
else {
indicesElement[stackIndex] = indicesElement[stackIndex + 1] ? indicesElement[stackIndex + 1] : null
}
return component
}
const diffComponentProps = (component, props) => {
const prevProps = component._base.__props
if(props !== prevProps) {
if(props) {
if(prevProps) {
for(let key in prevProps) {
if(props[key] === undefined) {
if(key[0] === "$") {
component[key] = component.state[key.slice(1)]
}
else {
component[key] = null
}
}
}
for(let key in props) {
const value = props[key]
if(component[key] !== value) {
component[key] = value
}
}
}
else {
for(let key in props) {
component[key] = props[key]
}
}
component._base.__props = props
}
else if(prevProps) {
for(let key in prevProps) {
if(key[0] === "$") {
component[key] = component.state[key.slice(1)]
}
else {
component[key] = null
}
}
component._base.__props = null
}
}
}
const createComponent = (ctor) => {
const buffer = components[ctor.prototype.__componentIndex]
let component = buffer ? buffer.pop() : null
if(!component) {
component = new ctor()
}
if(component.mount) {
component.mount()
}
component._dirty = true
return component
}
const text = (value) => {
const parentElement = stack[stackIndex]
const prevElement = indicesElement[stackIndex]
let element = null
if(prevElement) {
if(prevElement.nodeType === 3) {
element = prevElement
if(prevElement.nodeValue !== value) {
prevElement.nodeValue = value
}
}
else {
element = document.createTextNode(value)
const component = prevElement.__component
if(component) {
prevElement.replaceChild(element, component._base)
removeComponentExt(prevElement)
}
else {
parentElement.replaceChild(element, prevElement)
removeRange(prevElement.childNodes, 0, prevElement.childNodes.length - 1)
}
}
}
else {
element = document.createTextNode(value)
parentElement.appendChild(element)
}
indices[stackIndex]++
indicesMinimal[stackIndex]++
indicesElement[stackIndex] = element.nextSibling
return element
}
const setProp = (element, name, value) => {
if(name === "class") {
element.className = value
}
else if(name === "style")
{
if(typeof value === "object") {
const elementStyle = element.style
for(let key in value) {
elementStyle[key] = value[key]
}
}
else {
element.style.cssText = value
}
}
else if(name[0] === "o" && name[1] === "n") {
element[name] = value
}
else if(typeof element[name] === "boolean") {
element[name] = value
}
else {
element.setAttribute(name, value)
}
}
const unsetProp = (element, name) => {
if(name === "class") {
element.className = ""
}
else if(name === "style") {
element.style.cssText = ""
}
else if(name[0] === "o" && name[1] === "n") {
element[name] = null
}
else if(typeof element[name] === "boolean") {
element[name] = false
}
else {
element.removeAttribute(name)
}
}
const render = (componentCls, parentElement, props) => {
stackIndex = 0
stack[0] = parentElement
indices[0] = 0
indicesMinimal[0] = 0
indicesElement[0] = (parentElement.childNodes.length > 0) ? parentElement.childNodes[0] : null
componentVoid(componentCls, props)
if(indices[0] < parentElement.childNodes.length) {
removeRange(parentElement.childNodes, indices[0], parentElement.childNodes.length - 1)
}
}
const renderInstance = (component) => {
const childrenCount = component._numChildren
stackIndex = component._depth
indicesMinimal[stackIndex] = 0
stackIndex++
stack[stackIndex] = component._base.parentElement
indices[stackIndex] = 0
indicesMinimal[stackIndex] = 0
indicesElement[stackIndex] = component._base.nextSibling
component.render()
component._dirty = false
component._numChildren = indicesMinimal[stackIndex]
stackIndex--
if(component._numChildren < childrenCount) {
removeSiblings(component._base, component._numChildren, childrenCount)
}
}
const removeRange = (children, indexStart, indexEnd) => {
for(let n = indexEnd; n > indexStart; n--) {
const child = children[n]
removeRangeNode(child)
child.remove()
}
}
const removeRangeNode = (element) => {
if(element.nodeType === 3 && element.__component) {
removeComponent(element)
}
else {
const children = element.childNodes
for(let n = 0; n < children.length; n++) {
removeRangeNode(children[n])
}
}
}
const removeSiblings = (startChild, countIs, countWas) => {
for(let n = 0; n < countIs; n++) {
startChild = startChild.nextSibling
}
const num = countWas - countIs
for(let n = 0; n < num; n++) {
const child = startChild.nextSibling
if(child.nodeType === 3 && child.__component) {
const numChildren = child.__component._numChildrent
for(let m = 0; m < numChildren; m++) {
removeNode(child.nextSibling)
child.remove()
}
removeComponent(child)
}
else {
const children = child.childNodes
for(let n = 0; n < children.length; n++) {
removeNode(children[n])
}
}
child.remove()
}
return startChild.nextSibling
}
const removeNode = (element) => {
if(element.nodeType === 3 && element.__component) {
removeComponent(element)
}
else {
const children = element.childNodes
for(let n = 0; n < children.length; n++) {
removeNode(children[n])
}
}
}
const removeComponent = (element) => {
const component = element.__component
const buffer = components[component.__componentIndex]
if(buffer) {
buffer.push(component)
}
else {
components[component.__componentIndex] = [ component ]
}
const props = component.state
if(props) {
for(let key in props) {
component.$[key] = component.state[key]
}
element.__props = null
}
component.remove()
}
const removeComponentExt = (element) => {
const component = element.__component
const buffer = components[component.__componentIndex]
if(buffer) {
buffer.push(component)
}
else {
components[component.__componentIndex] = [ component ]
}
const props = element.__props
if(props) {
for(let key in props) {
component[key] = null
}
element.__props = null
}
for(let n = 0; n < component._numChildren; n++) {
const child = component._base.nextSibling
if(child.__component) {
removeComponent(child)
}
else {
const childChildren = child.childNodes
for(let n = 0; n < childChildren.length; n++) {
removeNode(childChildren[n])
}
}
child.remove()
}
component.remove()
}
const removeAll = () => {
removeRange(document.body.childNodes, 0, document.body.childNodes.length - 1)
}
export {
elementOpen,
elementClose,
elementVoid,
element,
componentVoid,
text,
render,
renderInstance,
removeAll
}