UNPKG

vasille

Version:

The same framework which is designed to build bulletproof frontends (core library).

319 lines (318 loc) 7.54 kB
import { Reactive } from "../core/core.js"; import { IValue } from "../core/ivalue.js"; import { safe } from "../functional/safety.js"; import { SetModel } from "../models/set-model.js"; import { Reference } from "../value/reference.js"; /** * This class is symbolic * @extends Reactive */ export class Root extends Reactive { /** * The children list * @type Array */ children; runner; lastChild = undefined; constructor(runner) { super(); 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; this.children.forEach(child => { first = first ?? child.findFirstChild(); }); return first; } /** * Defines a text fragment * @param text {String | IValue} A text fragment string */ 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); } destroy() { this.children.forEach(child => child.destroy()); this.children.clear(); this.lastChild = undefined; super.destroy(); } } export class Fragment extends Root { parent; constructor(runner) { super(runner); } /** * Next node * @type {?Fragment} */ next; /** * Previous node * @type {?Fragment} */ prev; /** * 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(); } } /** * Represents a text node * @class TextNode * @extends Fragment */ export class TextNode extends Fragment { handler = null; data; constructor(input, runner) { super(runner); this.data = input.text; } destroy() { const text = this.data; 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 { /** * The element of vasille node * @type Element */ node; 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 { name; options; constructor(options, runner, tagName) { super(runner); this.options = options; this.name = tagName; } findFirstChild() { return this.node; } appendNode(node) { this.runner.appendChild(this.node, node); } } const alwaysTrue = new Reference(true); /** * Defines a node which can switch its children conditionally */ export class SwitchedNode extends Fragment { /** * Index of current true condition * @type number */ index = -1; /** * Array of possible cases * @type {Array<{cond : IValue<unknown>, cb : function(Fragment)}>} */ cases; /** * A function that syncs index and content will be bounded to each condition * @type {Function} */ sync; /** * Constructs a switch node and define a sync function */ constructor(runner, cases, _default) { super(runner); if (_default) { cases.push({ $case: alwaysTrue, slot: _default }); } this.cases = cases; this.sync = () => { let i = this.cases.findIndex(item => item.$case.V); if (i === this.index) { return; } if (this.lastChild) { this.lastChild.destroy(); this.children.clear(); this.lastChild = undefined; } if (i !== -1) { const node = new Fragment(this.runner); node.parent = this; this.lastChild = node; this.children.add(node); this.index = i; safe(this.cases[i].slot)(node); } else { this.index = -1; } }; cases.forEach(_case => { _case.$case.on(this.sync); }); } compose() { this.sync(); } destroy() { this.cases.forEach(c => { c.$case.off(this.sync); }); this.cases.splice(0); super.destroy(); } } /** * Represents a debug node * @class DebugNode * @extends Fragment */ export class DebugNode extends Fragment { handler = null; data; constructor(input, runner) { super(runner); this.data = input.text; } destroy() { /* istanbul ignore else */ if (this.handler) { this.data.off(this.handler); } super.destroy(); } }