rvx
Version:
A signal based rendering library
327 lines • 10.6 kB
JavaScript
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