UNPKG

vasille

Version:

The first Developer eXperience Orientated front-end framework (core library).

333 lines (332 loc) 8.47 kB
import { Reactive } from "../core/core.js"; import { IValue } from "../core/ivalue.js"; import { SetModel } from "../models/set-model.js"; import { Reference } from "../value/reference.js"; import { userError } from "../core/errors.js"; /** * This class is symbolic * @extends Reactive */ export class Root extends Reactive { constructor(input, runner) { super(input); this.lastChild = undefined; this.runner = runner; this.children = runner.debugUi ? new SetModel() : new Set(); } /** * Pushes a node to children immediately * @param node {Fragment} A node to push * @protected */ pushNode(node) { node.parent = this; this.lastChild = node; this.children.add(node); } /** * Find the first node in the element if so exists * @return {?Element} * @protected */ findFirstChild() { let first; for (const child of this.children) { first = child.findFirstChild(); /* istanbul ignore else */ if (first) { break; } } return first; } /** * Defines a text fragment * @param text {String | IValue} A text fragment string * @param cb {function (TextNode)} Callback if previous is slot name */ text(text) { const node = this.runner.textNode(text); this.pushNode(node); node.compose(); } debug(text) { const node = this.runner.debugNode(text); this.pushNode(node); node.compose(); } /** * Defines a tag element * @param tagName {String} the tag name * @param input * @param cb {function(Tag, *)} callback */ tag(tagName, input, cb) { const tag = this.runner.tag(tagName, input, cb); this.pushNode(tag); tag.compose(); } /** * Defines a custom element * @param node {Fragment} vasille element to insert * @param callback {function($ : *)} */ create(node, callback) { this.pushNode(node); node.compose(); callback?.(node); } /** * Defines an if node * @param cond {IValue} condition * @param cb {function(Fragment)} callback to run on true * @return {this} */ if(cond, cb) { const node = new SwitchedNode(this.runner); this.pushNode(node); node.addCase(this.case(cond, cb)); } else(cb) { if (this.lastChild instanceof SwitchedNode) { this.lastChild.addCase(this.default(cb)); } else { throw userError("wrong `else` function use", "logic-error"); } } elif(cond, cb) { if (this.lastChild instanceof SwitchedNode) { this.lastChild.addCase(this.case(cond, cb)); } else { throw userError("wrong `elif` function use", "logic-error"); } } /** * Create a case for switch * @param cond {IValue<boolean>} * @param cb {function(Fragment) : void} * @return {{cond : IValue, cb : (function(Fragment) : void)}} */ case(cond, cb) { return { cond, cb }; } /** * @param cb {(function(Fragment) : void)} * @return {{cond : IValue, cb : (function(Fragment) : void)}} */ default(cb) { return { cond: trueIValue, cb }; } destroy() { this.children.forEach(child => child.destroy()); this.children.clear(); this.lastChild = undefined; super.destroy(); } } export class Fragment extends Root { constructor(input, runner, name) { super(input, runner); this.name = name; } /** * Pushes a node to children immediately * @param node {Fragment} A node to push * @protected */ pushNode(node) { if (this.lastChild) { this.lastChild.next = node; } node.prev = this.lastChild; super.pushNode(node); } /** * Append a node to the end of element * @param node {Node} node to insert */ appendNode(node) { if (this.next) { this.next.insertAdjacent(node); } else { this.parent.appendNode(node); } } /** * Insert a node as a sibling of this * @param node {Node} node to insert */ insertAdjacent(node) { const child = this.findFirstChild(); if (child) { this.runner.insertBefore(node, child); } else if (this.next) { this.next.insertAdjacent(node); } else { this.parent.appendNode(node); } } compose() { // do nothing // to override it } insertBefore(node) { node.prev = this.prev; node.next = this; if (this.prev) { this.prev.next = node; } this.prev = node; } insertAfter(node) { node.prev = this; node.next = this.next; this.next = node; } remove() { if (this.next) { this.next.prev = this.prev; } if (this.prev) { this.prev.next = this.next; } this.parent.children.delete(this); } destroy() { if (this.parent.lastChild === this) { this.parent.lastChild = this.prev; } super.destroy(); } } const trueIValue = new Reference(true); /** * Represents a text node * @class TextNode * @extends Fragment */ export class TextNode extends Fragment { constructor(input, runner) { super(input, runner, ":text"); } destroy() { const text = this.input.text; if (text instanceof IValue && this.handler) { text.off(this.handler); } super.destroy(); } } /** * Vasille node which can manipulate an element node * @class INode * @extends Fragment */ export class INode extends Fragment { get element() { return this.node; } insertAdjacent(node) { this.runner.insertBefore(node, this.node); } } /** * Represents an Vasille.js HTML element node * @class Tag * @extends INode */ export class Tag extends INode { constructor(input, runner, tagName) { super(input, runner, tagName); } findFirstChild() { return this.node; } appendNode(node) { this.runner.appendChild(this.node, node); } } /** * Defines a node which can switch its children conditionally */ export class SwitchedNode extends Fragment { /** * Constructs a switch node and define a sync function */ constructor(runner) { super({}, runner, ":switch"); /** * Array of possible cases * @type {Array<{cond : IValue<unknown>, cb : function(Fragment)}>} */ this.cases = []; this.sync = () => { let i = 0; for (; i < this.cases.length; i++) { if (this.cases[i].cond.$) { break; } } if (i === this.index) { return; } if (this.lastChild) { this.lastChild.destroy(); this.children.clear(); this.lastChild = undefined; } if (i !== this.cases.length) { this.index = i; this.createChild(this.cases[i].cb); } else { this.index = -1; } }; } addCase(case_) { this.cases.push(case_); case_.cond.on(this.sync); this.sync(); } /** * Creates a child node * @param cb {function(Fragment)} Call-back */ createChild(cb) { const node = new Fragment({}, this.runner, ":case"); node.parent = this; this.lastChild = node; this.children.add(node); cb(node); } destroy() { this.cases.forEach(c => { c.cond.off(this.sync); }); this.cases.splice(0); super.destroy(); } } /** * Represents a debug node * @class DebugNode * @extends Fragment */ export class DebugNode extends Fragment { constructor(input, runner) { super(input, runner, ":debug"); } destroy() { /* istanbul ignore else */ if (this.handler) { this.input.text.off(this.handler); } super.destroy(); } }