vasille
Version:
The same framework which is designed to build bulletproof frontends (core library).
319 lines (318 loc) • 7.54 kB
JavaScript
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();
}
}