vasille
Version:
The first Developer eXperience Orientated front-end framework (core library).
333 lines (332 loc) • 8.47 kB
JavaScript
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();
}
}