domsubi
Version:
A Virtual-DOM Library for JavaScript
454 lines (438 loc) • 15.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var sodiumjs = require('sodiumjs');
class jsxmlComponent {
constructor(source, parent) {
this.parent = parent;
this.source = source;
}
nest(newValue) {
return Object.assign(this.migrate(newValue), { parent: this });
}
isAncestorOf(that) {
for (let ctx = that.parent; ctx; ctx = ctx.parent)
if (ctx === this)
return true;
return false;
}
}
class jsxmlAttr extends jsxmlComponent {
constructor(element, name, source, old_source, parent) {
super(source instanceof sodiumjs.StreamSink ? source.send.bind(source) : source, parent);
this.element = element;
this.name = name;
this.prevValue = old_source;
}
migrate(v) {
return new jsxmlAttr(this.element, this.name, v, this.source, this.parent);
}
accept(visitor) {
visitor.visitAttr(this);
}
}
class jsxmlAttributes extends jsxmlComponent {
constructor(elm, source, prev, parent) {
super(source, parent);
this.element = elm;
this.prevValues = prev;
}
migrate(newValue) {
return new jsxmlAttributes(this.element, newValue, this.source, this.parent);
}
accept(visitor) {
visitor.visitAttributes(this);
}
}
class jsxmlNode extends jsxmlComponent {
constructor(ref, source, parent) {
super(source, parent);
this.reference = Array.isArray(ref)
? ref
: ref.nodeType === ref.DOCUMENT_FRAGMENT_NODE
? [ref.firstChild, ref.lastChild]
: [ref];
}
migrate(source) {
return new jsxmlNode(this.reference, source, this.parent);
}
accept(visitor) {
visitor.visitNode(this);
}
}
class jsxmlVisitor {
constructor() {
/**
* componentの入れ子状況
*/
this._lineage = [];
/**
* Cellをソースに持つcomponentの配列
*/
this._listening = [];
/**
* Stream::listenの戻り値。
*/
this._unlisten = () => void 0;
this._listening = [];
}
get context() {
return this._lineage[this._lineage.length - 1];
}
visitCell(c) {
this._unlisten();
this._listening.push(c);
this._lineage.push(c);
c.nest(c.source.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, p) {
if (s instanceof sodiumjs.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, p);
}
buildElement(source, p) {
const tag = Object.keys(source).find((k) => k !== '$');
const attrs = source.$;
const children = source[tag];
const elm = p.ownerDocument.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) {
const [a, b] = o.reference.slice(0);
const doc = a.ownerDocument;
const df = doc.createDocumentFragment();
this.buildNode(o.source, df);
// リファレンス更新
o.reference.splice(0, 2, ...(df.childNodes.length === 2 ? [df.firstChild, df.lastChild] : [df.firstChild]));
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) {
const source = attrs.source;
if (source instanceof sodiumjs.Cell) {
this.visitCell(attrs);
}
else {
const elm = attrs.element;
const prev = attrs.prevValues || {};
Object.keys(Object.assign({}, attrs.source, attrs.prevValues))
.map((k) => new jsxmlAttr(elm, k, source[k], prev[k]))
.forEach((attr) => this.visitAttr(attr));
}
}
visitAttr(attr) {
const { element, name, source, prevValue } = attr;
if (source instanceof sodiumjs.Cell) {
this.visitCell(attr);
}
else if (/^on./.test(name)) {
if (validateEventListenable(prevValue))
element.removeEventListener(name.slice(2), prevValue, false);
if (validateEventListenable(source))
element.addEventListener(name.slice(2), 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) {
return !!(value) && (typeof value === "function" || typeof value.handleEvent === 'function');
}
/**
* アップデートをmergeしたStreamを返す
* @param components
*/
function merge_listening(components) {
return sodiumjs.Operational
.defer(components
.map((c) => sodiumjs.Operational.updates(c.source.map(sodiumjs.lambda1(() => [c], [c.source]))))
.reduce((a, b) => a.merge(b, concat)))
.map(calm);
function concat(a, b) {
return a.concat(b);
}
function calm(components) {
return components.filter((c, i, a) => a.every((a, j) => i === j || !a.isAncestorOf(c)));
}
}
class jshtmlInlineStyleRule extends jsxmlComponent {
constructor(element, cssprop, source, parent) {
super(source, parent);
this.element = element;
this.cssprop = cssprop;
}
accept(visitor) {
visitor.visitStyle(this);
}
migrate(v) {
return new jshtmlInlineStyleRule(this.element, this.cssprop, v, this.parent);
}
}
class jshtmlDatasetValue extends jsxmlComponent {
constructor(element, dataname, source, parent) {
super(source, parent);
this.element = element;
this.dataname = dataname;
}
accept(visitor) {
visitor.visitDataset(this);
}
migrate(v) {
return new jshtmlDatasetValue(this.element, this.dataname, v, this.parent);
}
}
class jshtmlVisitor extends jsxmlVisitor {
buildNode(source, parent) {
// HTMLTemplateElementだけはcontentをcloneしてビルド
return super.buildNode(source instanceof HTMLTemplateElement ? source.content.cloneNode(true) : source, parent);
}
/**
* インラインstyleとdatasetに対応する
*/
visitAttr(attr) {
const { element, name } = attr;
const v = attr.source;
const isHtmlSpecialized = element instanceof HTMLElement &&
(name === 'style' || name === 'dataset') &&
Object(v) === v;
if (!isHtmlSpecialized)
return super.visitAttr(attr);
const keys = Object.keys(Object.assign({}, v, attr.prevValue));
if (name === 'style')
keys.forEach((k) => this.visitStyle(new jshtmlInlineStyleRule(element, k, v[k], attr)));
else
keys.forEach((k) => this.visitDataset(new jshtmlDatasetValue(element, k, v[k], attr)));
}
visitStyle(style) {
const { source, element, cssprop } = style;
if (source instanceof sodiumjs.Cell)
this.visitCell(style);
else
// 簡略構文がtypescriptではサポートされていないので、強引な型処理でチェックを通す
element.style[cssprop] = source === null || source === undefined ? '' : '' + source;
}
visitDataset(data) {
const { source, dataname, element } = data;
if (source instanceof sodiumjs.Cell)
this.visitCell(data);
else if (source === null || source === undefined)
delete element.dataset[hyphenSeparatedToCamelize(dataname)];
else
element.dataset[hyphenSeparatedToCamelize(dataname)] = source;
}
}
/**
* キャメルケース -> ハイフン区切り変換
* CSSStyleDeclaraion::setProperty and removeProperty を使う場合は必要
function camelToHyphenSeparated(str: string) {
return str.replace(/[A-Z]/g, (s: string) => '-' + s.toLowerCase());
}
*/
/**
* ハイフン区切り -> キャメルケース変換
* datasetの名称処理に使用
*/
function hyphenSeparatedToCamelize(str) {
return str.replace(/-[a-z]/g, (s) => s.charAt(1).toUpperCase());
}
class jsxml$1 {
constructor(s) {
this.source = s;
}
map(f) {
return new this.constructor(f(this.source));
}
wrap(tag, attrs) {
return new this.constructor({ [tag]: this.source, $: attrs });
}
_mount(n) {
const v = new this.constructor.visitorConstructor();
n.accept(v);
return v.detach.bind(v);
}
mount(n) {
return this._mount(new jsxmlNode(n, this.source));
}
mountAsContents(n) {
return this._mount(n.hasChildNodes()
? new jsxmlNode([n.firstChild, n.lastChild], this.source)
: new jsxmlNode(n.appendChild(new Comment('jsxml-placeholder')), this.source));
}
appendTo(n) {
return this.mount(n.appendChild(new Comment('jsxml-placeholder')));
}
prependTo(n) {
return this.mount(n.insertBefore(new Comment('jsxml-placeholder'), n.firstChild));
}
}
jsxml$1.visitorConstructor = jsxmlVisitor;
class jshtml$1 extends jsxml$1 {
mountAsShadow(e, init) {
this.mountAsContents(e.shadowRoot || e.attachShadow(init || { mode: "closed" }));
}
}
jshtml$1.visitorConstructor = jshtmlVisitor;
/**
* ターゲットのイベントをStream化する。
*/
const events = (t) => {
const map = {};
const send = (e) => map[e.type].send(e);
return new Proxy(map, {
get: (o, p) => {
if (typeof p === 'symbol' || o.hasOwnProperty(p))
return Reflect.get(o, p);
t.addEventListener(p, send);
return (o[p] = new sodiumjs.StreamSink());
}
});
};
/**
* ターゲットの変異をStream化する。
*/
const mutations = (n, init) => {
const sink = new sodiumjs.StreamSink();
const observer = new MutationObserver(sink.send.bind(sink));
observer.observe(n, init);
return sodiumjs.Operational.split(sink);
};
/**
* ターゲットの属性をCell化する。
*/
const attributes = (e, filter) => {
const sAttrChanges = mutations(e, { attributes: true, attributeFilter: filter });
return new Proxy({}, {
get: (o, p) => {
if (typeof p === 'string' && !o.hasOwnProperty(p))
Reflect.set(o, p, sAttrChanges
.filter((r) => r.attributeName === p)
.map((r) => r.target.getAttribute(p) || '')
.hold(e.hasAttribute(p) ? e.getAttribute(p) || '' : ''));
return Reflect.get(o, p);
}
});
};
/**
* jshtml記法のノードをElementにラップする
*/
const wrap = (tag, attrs) => attrs
? (content) => ({ [tag]: content, $: attrs })
: (content) => ({ [tag]: content });
// jsxml, jshtmlは純粋関数としても活用できるようにProxyをかます
const proxy_handler = {
apply(target, b, c) { return new target(c[0]); }
};
const jsxml = new Proxy(jsxml$1, proxy_handler);
const jshtml = new Proxy(jshtml$1, proxy_handler);
class jshtmlElement extends HTMLElement {
constructor() {
super();
this._unmount = () => void 0;
const { observedShadowEvents, observedAttributes } = this.constructor;
this.shadowEvents = {};
if (observedShadowEvents)
observedShadowEvents.forEach((e) => this.shadowEvents[e] = new sodiumjs.StreamSink());
this.attrCells = {};
if (observedAttributes)
observedAttributes.forEach((a) => this.attrCells[a] = new sodiumjs.CellSink(''));
}
connectedCallback() {
if (this._mount)
return this._mount();
const shadow = this.attachShadow({ mode: "closed" });
const { observedShadowEvents } = this.constructor;
if (observedShadowEvents) {
const l = (e) => this.shadowEvents[e.type].send(e);
observedShadowEvents.forEach((e) => shadow.addEventListener(e, l));
}
const mounter = new jshtml$1(this.cssVars
? [{ style: expandCustomCSSVariables(this.cssVars) }, this.shadowSource]
: this.shadowSource);
this._mount = () => {
this._unmount();
this._unmount = mounter.mountAsContents(shadow);
this.dispatchEvent(new Event('jshtmlmount', { bubbles: true }));
};
this._mount();
}
disconnectedCallback() {
this._unmount();
this.dispatchEvent(new Event('jshtmlunmount', { bubbles: true }));
}
attributeChangedCallback(n, old, v) {
this.attrCells[n].send(v || '');
}
}
function expandCustomCSSVariables(vars) {
const rules = Object.keys(vars)
.map((n) => vars[n] instanceof sodiumjs.Cell ? [`--${n}:`, vars[n], ';'] : `--${n}: ${vars[n]};`);
return [':host {', rules, '}']
.flat();
}
exports.attributes = attributes;
exports.events = events;
exports.jshtml = jshtml;
exports.jshtmlDatasetValue = jshtmlDatasetValue;
exports.jshtmlElement = jshtmlElement;
exports.jshtmlInlineStyleRule = jshtmlInlineStyleRule;
exports.jshtmlVisitor = jshtmlVisitor;
exports.jsxml = jsxml;
exports.jsxmlAttr = jsxmlAttr;
exports.jsxmlAttributes = jsxmlAttributes;
exports.jsxmlNode = jsxmlNode;
exports.jsxmlVisitor = jsxmlVisitor;
exports.mutations = mutations;
exports.wrap = wrap;
//# sourceMappingURL=domsubi.cjs.js.map