UNPKG

rvx

Version:

A signal based rendering library

415 lines 14.2 kB
import { NODE } from "./element-common.js"; import { ENV } from "./env.js"; import { createText } from "./internals/create-text.js"; import { NOOP } from "./internals/noop.js"; import { isolate } from "./isolate.js"; import { capture, teardown } from "./lifecycle.js"; import { $, get, memo, 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 Nest(props) { return nest(props.watch, props.children); } export function when(condition, truthy, falsy) { return nest(memo(condition), value => value ? truthy(value) : falsy?.()); } export function Show(props) { return when(props.when, props.children, props.else); } export function forEach(each, component) { return new View((setBoundary, self) => { function detach(instances) { for (let i = 0; i < instances.length; i++) { instances[i].v.detach(); } } const env = ENV.current; let cycle = 0; const instances = []; const instanceMap = new Map(); const first = createPlaceholder(env); setBoundary(first, first); teardown(() => { for (let i = 0; i < instances.length; i++) { instances[i].d(); } }); watch(() => { let parent = self.parent; if (!parent) { parent = createParent(env); parent.appendChild(first); } let index = 0; let last = first; try { for (const value of get(each)) { let instance = instances[index]; if (instance && Object.is(instance.u, value)) { instance.c = cycle; instance.i.value = index; last = instance.v.last; index++; } else { instance = instanceMap.get(value); if (instance === undefined) { const instance = { u: value, c: cycle, i: $(index), d: undefined, v: undefined, }; instance.d = isolate(capture, () => { instance.v = render(component(value, () => instance.i.value)); instance.v.setBoundaryOwner((_, last) => { if (instances[instances.length - 1] === instance && instance.c === cycle) { setBoundary(undefined, last); } }); }); instance.v.insertBefore(parent, last.nextSibling); instances.splice(index, 0, instance); instanceMap.set(value, instance); last = instance.v.last; index++; } else if (instance.c !== cycle) { instance.i.value = index; instance.c = cycle; const currentIndex = instances.indexOf(instance, index); if (currentIndex < 0) { detach(instances.splice(index, instances.length - index, instance)); instance.v.insertBefore(parent, last.nextSibling); } else { detach(instances.splice(index, currentIndex - index)); } last = instance.v.last; index++; } } } } finally { if (instances.length > index) { detach(instances.splice(index)); } for (const [value, instance] of instanceMap) { if (instance.c !== cycle) { instanceMap.delete(value); instance.v.detach(); instance.d(); } } cycle = (cycle + 1) | 0; setBoundary(undefined, last); } }); }); } export function For(props) { return forEach(props.each, props.children); } export function indexEach(each, component) { return new View((setBoundary, self) => { const env = ENV.current; const first = createPlaceholder(env); setBoundary(first, first); const instances = []; teardown(() => { for (let i = 0; i < instances.length; i++) { instances[i].d(); } }); watch(() => { let parent = self.parent; if (!parent) { parent = createParent(env); parent.appendChild(first); } let index = 0; let last = first; try { for (const value of get(each)) { if (index < instances.length) { const current = instances[index]; if (Object.is(current.u, value)) { last = current.v.last; index++; continue; } current.v.detach(); current.d(); current.d = NOOP; } const instance = { u: value, d: undefined, v: undefined, }; instance.d = isolate(capture, () => { instance.v = render(component(value, index)); instance.v.setBoundaryOwner((_, last) => { if (instances[instances.length - 1] === instance) { setBoundary(undefined, last); } }); }); instance.v.insertBefore(parent, last.nextSibling); instances[index] = instance; last = instance.v.last; index++; } } finally { if (instances.length > index) { for (let i = index; i < instances.length; i++) { const instance = instances[i]; instance.v.detach(); instance.d(); } instances.length = index; } setBoundary(undefined, last); } }); }); } export function Index(props) { return indexEach(props.each, props.children); } 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); } export function Attach(props) { return attachWhen(props.when, props.children); } //# sourceMappingURL=view.js.map