domsubi
Version:
A Virtual-DOM Library for JavaScript
187 lines (150 loc) • 6.3 kB
text/typescript
import { jsxmlComponent } from "./jsxmlComponent";
import { jsxmlAttr } from "./jsxmlAttr";
import { Cell, Operational, Stream, lambda1 } from "sodiumjs";
import { jsxmlAttributes } from "./jsxmlAttributes";
import { jsxmlNode } from "./jsxmlNode";
import { DOMSource, ElementSource, AttributesSource, AttrValue } from "./types";
import { jsxmlComponentVisitor } from "./jsxmlComponentVisitor";
export class jsxmlVisitor implements jsxmlComponentVisitor {
/**
* componentの入れ子状況
*/
private _lineage: jsxmlComponent[] = [];
/**
* Cellをソースに持つcomponentの配列
*/
private _listening = [] as jsxmlComponent[];
/**
* Stream::listenの戻り値。
*/
private _unlisten : Function = () => void 0;
constructor() {
this._listening = [];
}
get context() {
return this._lineage[this._lineage.length - 1];
}
visitCell(c: jsxmlComponent) {
this._unlisten();
this._listening.push(c);
this._lineage.push(c);
c.nest((c.source as Cell<DOMSource>).sample()).accept(this);
this._lineage.pop();
if(!this._lineage.length) this._unlisten = merge_listening(this._listening).listen((updated) => {
this._unlisten();
this._listening = this._listening.filter((_c) => !updated.some((u) => u.isAncestorOf(_c) || u === _c));
updated.forEach((c) => c.accept(this));
});
}
buildNode(s: DOMSource, p: ParentNode) {
if (s instanceof Cell)
this.visitCell(new jsxmlNode(p.appendChild(new Comment('jsxml-placeholder')), s, this.context));
else if (s instanceof Node)
p.append(s);
else if (Object(s) !== s)
p.append(new Text(s === undefined || s === null ? '' : s + ''));
else if (Array.isArray(s))
s.forEach((s) => this.buildNode(s, p));
else
this.buildElement(s as ElementSource, p);
}
buildElement(source: ElementSource, p: ParentNode) {
const tag = Object.keys(source).find((k) => k !== '$') as keyof HTMLElementTagNameMap;
const attrs = source.$ as AttributesSource;
const children = source[tag];
const elm = (p.ownerDocument as Document).createElement(tag);
p.append(elm);
if (children != null)
this.buildNode(children, elm);
if (Object(attrs) === attrs)
this.visitAttributes(new jsxmlAttributes(elm, attrs, undefined, this.context));
}
visitNode(o: jsxmlNode): void {
const [a,b] = o.reference.slice(0);
const doc = a.ownerDocument as Document;
const df = doc.createDocumentFragment();
this.buildNode(o.source, df);
// リファレンス更新
o.reference.splice(0,2, ...(df.childNodes.length === 2 ? [df.firstChild,df.lastChild] : [df.firstChild]) as Node[]);
if(!b) a.parentNode!.replaceChild(df, a);
// DOM更新(Document接続済み)
else if (a.isConnected) {
const r = new Range();
r.setStartBefore(a);
r.setEndAfter(b);
const o = r.endOffset;
r.insertNode(df);
r.setStart(r.startContainer, r.startOffset + r.endOffset - o);
r.deleteContents();
}
// DOM更新(Document接続なし)
else {
const parent = a.parentNode; if (!parent) return;
parent.insertBefore(df, a);
while (a.nextSibling && a.nextSibling !== b) parent.removeChild(a.nextSibling);
parent.removeChild(a);
parent.removeChild(b);
}
}
visitAttributes(attrs: jsxmlAttributes) {
const source = attrs.source;
if (source instanceof Cell) {
this.visitCell(attrs);
} else {
type V = { [key: string]: AttrValue; };
const elm = attrs.element;
const prev = attrs.prevValues || {};
Object.keys(Object.assign({}, attrs.source, attrs.prevValues))
.map((k) => new jsxmlAttr(elm, k, (source as V)[k], (prev as V)[k]))
.forEach((attr) => this.visitAttr(attr));
}
}
visitAttr(attr: jsxmlAttr) {
const { element, name, source, prevValue } = attr;
if (source instanceof Cell) {
this.visitCell(attr);
}
else if (/^on./.test(name)) {
if (validateEventListenable(prevValue))
element.removeEventListener(name.slice(2), <EventListenerOrEventListenerObject>prevValue, false);
if (validateEventListenable(source))
element.addEventListener(name.slice(2), <EventListenerOrEventListenerObject>source, false);
}
else if (source == null || source == '')
element.removeAttribute(name);
else if (Object(source) !== source)
element.setAttribute(name, "" + source);
else if (/^class(List)?$/.test(name))
element.className = Array.isArray(source)
? source.filter(Boolean).join(" ")
: '' + source;
}
detach() {
this._unlisten();
this._listening = [];
}
}
/**
* 引数がイベントリスナか判定する
* @param value
*/
function validateEventListenable(value: unknown): boolean {
return !!(value) && (typeof value === "function" || typeof (<EventListenerObject>value).handleEvent === 'function');
}
/**
* アップデートをmergeしたStreamを返す
* @param components
*/
function merge_listening(components: jsxmlComponent[]) {
return Operational
.defer(components
.map((c) => Operational.updates((c.source as Cell<DOMSource>).map(lambda1(() => [c],[c.source as Cell<DOMSource>]))))
.reduce((a,b) => a.merge(b, concat)))
.map(calm);
function concat<S>(a: S[], b: S[]) {
return a.concat(b)
}
function calm(components: jsxmlComponent[]) {
return components.filter((c,i,a) => a.every((a,j) => i === j || !a.isAncestorOf(c)))
}
}