UNPKG

domsubi

Version:

A Virtual-DOM Library for JavaScript

454 lines (438 loc) 15.7 kB
'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