UNPKG

rvx

Version:

A signal based rendering library

327 lines 10.6 kB
import { NODE } from "./element-common.js"; import { ENV } from "./env.js"; import { createText } from "./internals/create-text.js"; import { createMapArrayState, mapArrayUpdate } from "./internals/map-array.js"; import { $, capture, get, isolate, memo, teardown, watch } from "./signals.js"; function createPlaceholder(env) { return env.document.createComment("g"); } function createParent(env) { return env.document.createDocumentFragment(); } function empty(setBoundary, env) { const node = createPlaceholder(env); setBoundary(node, node); } function use(setBoundary, node, env) { if (node.firstChild === null) { empty(setBoundary, env); } else { setBoundary(node.firstChild, node.lastChild); } } export function render(content) { if (content instanceof View) { return content; } return new View(setBoundary => { const env = ENV.current; if (Array.isArray(content)) { const flat = content.flat(Infinity); if (flat.length > 1) { const parent = createParent(env); for (let i = 0; i < flat.length; i++) { let part = flat[i]; if (part === null || part === undefined) { parent.appendChild(createPlaceholder(env)); } else if (typeof part === "object") { if (NODE in part) { part = part[NODE]; } if (part instanceof env.Node) { if (part.nodeType === 11 && part.childNodes.length === 0) { parent.appendChild(createPlaceholder(env)); } else { parent.appendChild(part); } } else if (part instanceof View) { part.appendTo(parent); if (i === 0) { part.setBoundaryOwner((first, _last) => setBoundary(first, undefined)); } else if (i === flat.length - 1) { part.setBoundaryOwner((_first, last) => setBoundary(undefined, last)); } } else { parent.appendChild(createText(part, env)); } } else { parent.appendChild(createText(part, env)); } } use(setBoundary, parent, env); return; } content = flat[0]; } if (content === null || content === undefined) { empty(setBoundary, env); } else if (typeof content === "object") { if (NODE in content) { content = content[NODE]; } if (content instanceof env.Node) { if (content.nodeType === 11) { use(setBoundary, content, env); } else { setBoundary(content, content); } } else if (content instanceof View) { setBoundary(content.first, content.last); content.setBoundaryOwner(setBoundary); } else { const text = createText(content, env); setBoundary(text, text); } } else { const text = createText(content, env); setBoundary(text, text); } }); } export function mount(parent, content) { const view = render(content); view.appendTo(parent); teardown(() => view.detach()); return view; } export class View { #first; #last; #owner; constructor(init) { init((first, last) => { if (first) { this.#first = first; } if (last) { this.#last = last; } this.#owner?.(this.#first, this.#last); }, this); if (!this.#first || !this.#last) { throw new Error("G1"); } } get first() { return this.#first; } get last() { return this.#last; } get parent() { return this.#first?.parentNode ?? undefined; } setBoundaryOwner(owner) { if (this.#owner !== undefined) { throw new Error("G2"); } this.#owner = owner; teardown(() => this.#owner = undefined); } appendTo(parent) { let node = this.#first; const last = this.#last; for (;;) { const next = node.nextSibling; parent.appendChild(node); if (node === last) { break; } node = next; } } insertBefore(parent, ref) { if (ref === null) { return this.appendTo(parent); } let node = this.#first; const last = this.#last; for (;;) { const next = node.nextSibling; parent.insertBefore(node, ref); if (node === last) { break; } node = next; } } detach() { const first = this.#first; const last = this.#last; if (first === last) { first.parentNode?.removeChild(first); return first; } else { const fragment = ENV.current.document.createDocumentFragment(); this.appendTo(fragment); return fragment; } } } export function* viewNodes(view) { let node = view.first; for (;;) { const next = node.nextSibling; yield node; if (node === view.last) { break; } node = next; } } const _nestDefault = ((component) => component?.()); export function nest(expr, component = _nestDefault) { return new View((setBoundary, self) => { watch(expr, value => { const last = self.last; const parent = last?.parentNode; let view; if (parent) { const anchor = last.nextSibling; self.detach(); view = render(component(value)); view.insertBefore(parent, anchor); } else { view = render(component(value)); } setBoundary(view.first, view.last); view.setBoundaryOwner(setBoundary); }); }); } export function when(condition, truthy, falsy) { return nest(memo(condition), value => value ? truthy(value) : falsy?.()); } export function forEach(each, component) { return new View(setBoundary => { const env = ENV.current; const first = createPlaceholder(env); setBoundary(first, first); const mapFn = (input, index) => render(component(input, index)); const state = createMapArrayState(); watch(() => { const update = mapArrayUpdate(state, get(each), mapFn); if (update !== null) { let parent = first.parentNode; if (parent === null) { parent = createParent(env); parent.appendChild(first); } for (let i = 0; i < update.p.length; i++) { const entry = update.p[i]; if (entry.r) { entry.o.detach(); } } let prev = update.s > 0 ? state[update.s - 1].o.last : first; for (let i = 0; i < update.n.length; i++) { const view = update.n[i].o; if (prev.nextSibling !== view.first) { view.insertBefore(parent, prev.nextSibling); } prev = view.last; } if (update.e === state.length) { setBoundary(undefined, prev); } } }); }); } export function indexEach(each, component) { return new View((setBoundary, self) => { const env = ENV.current; const state = []; const first = createPlaceholder(env); setBoundary(first, first); teardown(() => { for (let i = state.length - 1; i >= 0; i--) { state[i].d(); } }); watch(() => { let parent = first.parentNode; if (!parent) { parent = createParent(env); parent.appendChild(first); } let index = 0; for (const value of get(each)) { if (index < state.length) { const instance = state[index]; instance.i.value = value; } else { const signal = $(value); let view; const dispose = isolate(capture, () => { view = render(component(() => signal.value, index)); }); state.push({ i: signal, v: view, d: dispose, }); const prev = index > 0 ? state[index - 1].v.last : first; view.insertBefore(parent, prev.nextSibling); } index++; } while (state.length > index) { const instance = state.pop(); instance.d(); instance.v.detach(); } const last = state.length > 0 ? state[state.length - 1].v.last : first; if (self.last !== last) { setBoundary(undefined, last); } }); }); } export class MovableView { #view; #target = $(); constructor(view) { this.#view = view; } move = () => { this.#target.value = undefined; const target = this.#target = $(this.#view); return nest(target, v => v); }; detach() { this.#target.value = undefined; } } export function movable(content) { return new MovableView(render(content)); } export function attachWhen(condition, content) { return nest(condition, value => value ? content : undefined); } //# sourceMappingURL=view.js.map