ripple
Version:
Ripple is an elegant TypeScript UI framework
597 lines (532 loc) • 14.6 kB
JavaScript
/** @import { AppendIntoAnchor, Block, Derived, Component } from '#client' */
import {
BLOCK_HAS_RUN,
BRANCH_BLOCK,
DERIVED,
DESTROYED,
EFFECT_BLOCK,
PAUSED,
PRE_EFFECT_BLOCK,
RENDER_BLOCK,
ROOT_BLOCK,
TRY_BLOCK,
DETACHED_BLOCK,
HEAD_BLOCK,
IF_BLOCK,
CREATES_DERIVEDS,
COMPOSITE_BLOCK,
DEFAULT_NAMESPACE,
SVG_BLOCK,
MATHML_BLOCK,
} from './constants.js';
import { hydrating } from './hydration.js';
import { next_sibling } from './operations.js';
import { is_array } from '@tsrx/core/runtime/language-helpers';
import {
active_block,
active_component,
active_reaction,
is_block_dirty,
release_deriveds,
take_created_deriveds,
remove_dependencies,
run_block,
run_teardown,
schedule_update,
untrack,
active_namespace,
} from './runtime.js';
import { is_ripple_object } from './utils.js';
import { effect_orphan } from './errors.js';
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
/**
* @param {Function} fn
*/
export function user_effect(fn) {
if (active_block === null) {
effect_orphan();
}
var component = active_component;
if (component !== null && !component.m) {
// Flat triples, created once the component has rendered (`pop_component`).
var e = (component.e ??= []);
e.push(fn, active_block, active_reaction);
return;
}
return block(EFFECT_BLOCK, fn);
}
/**
* @param {Function} fn
*/
export function effect(fn) {
return block(EFFECT_BLOCK, fn);
}
/**
* Creates a pre-effect block that runs eagerly before render blocks in the flush cycle.
* @param {Function} fn
*/
export function pre_effect(fn) {
return block(PRE_EFFECT_BLOCK, fn);
}
/**
* @param {Function} fn
* @param {any} [state]
* @param {number} [flags]
*/
export function render(fn, state, flags = 0) {
// block(), inline: a render block runs at once.
var block = create_block(RENDER_BLOCK | flags, fn, state);
run_block(block, true);
block.f ^= BLOCK_HAS_RUN;
return block;
}
/**
* @param {Function} fn
* @param {number} [flags]
* @param {any} [state]
*/
export function branch(fn, flags = 0, state = null) {
return block(BRANCH_BLOCK | flags, fn, state);
}
function noop() {}
/**
* Gives a text anchor materialized by `resolve_anchor` a branch block of its
* own, so destroying the enclosing block removes the anchor along with the
* content: a portal target outlives its content, and nothing else owns a node
* appended there. Call it after creating the anchored block so block order
* matches DOM order (the content is inserted before the anchor). A no-op when
* `anchor` is the node as received (an ordinary anchor belongs to a template)
* or the SSR marker the sentinel resolves to during hydration.
* @param {Node | AppendIntoAnchor} node
* @param {Node} anchor
*/
export function own_anchor(node, anchor) {
if (anchor === node || (HYDRATION && hydrating)) return;
branch(noop, 0, { start: anchor, end: anchor });
}
/**
* Wire up a `ref={expr}` attribute. `expr` may be:
* - a callback function — invoked with the element on mount; if it returns
* a function, that function runs as the cleanup on unmount.
* - a `Tracked` (e.g. from `track()`) — `tracked.value` is set to the
* element on mount and reset to `null` on unmount.
* - a plain mutable var (`let foo;`) — the element is assigned to the
* variable on mount and reset to `null` on unmount.
* - an array of any of the above.
*
* `get_fn` is invoked through `untrack` so the surrounding render block
* doesn't subscribe to whatever the thunk happens to read. The supported
* shape is to pass the ref slot itself (`ref={tracker}`); a foot-gun like
* `ref={tracker.value}` would otherwise read the cell reactively and cause
* spurious re-runs. Read untracked, the value is fixed for the life of the
* enclosing block, so each ref is a single effect block keyed on its state
* rather than a render block re-evaluating the thunk.
*
* @param {Element} element
* @param {() => any} get_fn
* @param {(value: any) => void} [set_fn]
* @returns {void}
*/
export function ref(element, get_fn, set_fn) {
apply_ref(element, untrack(get_fn), set_fn);
}
/**
* @param {Element} element
* @param {any} ref_value
* @param {((value: any) => void) | undefined} set_fn
* @returns {void}
*/
function apply_ref(element, ref_value, set_fn) {
if (is_array(ref_value)) {
for (var i = 0; i < ref_value.length; i++) {
apply_ref(element, ref_value[i], undefined);
}
} else if (typeof ref_value === 'function') {
block(EFFECT_BLOCK, run_function_ref, { e: element, f: ref_value });
} else if (is_ripple_object(ref_value)) {
block(EFFECT_BLOCK, run_tracked_ref, { e: element, t: ref_value });
} else if (set_fn !== undefined) {
block(EFFECT_BLOCK, run_set_ref, { e: element, f: set_fn });
}
}
/**
* The callback's return value is the effect's teardown.
* @param {{ e: Element, f: (element: Element) => any }} s
*/
function run_function_ref(s) {
return s.f(s.e);
}
/**
* @param {{ e: Element, t: { value: any } }} s
*/
function run_tracked_ref(s) {
s.t.value = s.e;
return clear_tracked_ref;
}
/**
* Teardowns keyed on block state receive it (see `run_teardown`).
* @param {{ e: Element, t: { value: any } }} s
*/
function clear_tracked_ref(s) {
s.t.value = null;
}
/**
* @param {{ e: Element, f: (value: any) => void }} s
*/
function run_set_ref(s) {
s.f(s.e);
return clear_set_ref;
}
/**
* @param {{ e: Element, f: (value: any) => void }} s
*/
function clear_set_ref(s) {
s.f(null);
}
/**
* @param {() => (void | (() => void))} fn
* @returns {Block}
*/
export function root(fn) {
// create_component_ctx, inline: the root is the first component context.
return block(
ROOT_BLOCK,
fn,
{ start: null, end: null },
{ b: active_block, c: null, e: null, m: false, p: active_component },
);
}
/** Creation counter: a parent always has a lower id than its descendants. */
var block_id = 0;
/**
* @param {Block} block
* @param {Block} parent_block
*/
function push_block(block, parent_block) {
var parent_last = parent_block.last;
if (parent_last === null) {
parent_block.last = parent_block.first = block;
} else {
parent_last.next = block;
block.prev = parent_last;
parent_block.last = block;
}
}
/**
* @param {number} flags
* @param {Function} fn
* @param {any} [state]
* @param {Component} [co]
* @returns {Block}
*/
export function block(flags, fn, state = null, co) {
var block = create_block(flags, fn, state, co);
if ((flags & EFFECT_BLOCK) !== 0) {
schedule_update(block);
} else {
run_block(block, true);
block.f ^= BLOCK_HAS_RUN;
}
return block;
}
/**
* Allocates a block and links it under the active block without running it.
* @param {number} flags
* @param {Function} fn
* @param {any} [state]
* @param {Component} [co]
* @returns {Block}
*/
export function create_block(flags, fn, state = null, co) {
/** @type {Block} */
var block = {
co: co || active_component,
d: null,
first: null,
f:
active_namespace === DEFAULT_NAMESPACE
? flags
: flags | (active_namespace === 'svg' ? SVG_BLOCK : MATHML_BLOCK),
fn,
i: ++block_id,
last: null,
next: null,
p: active_block,
prev: null,
s: state,
t: null,
};
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
/* prettier-ignore */
(/** @type {Derived} */ (active_reaction).blocks ??= []).push(block);
}
var parent_block = active_block;
if (parent_block !== null) {
// push_block, inline: block creation is the hot path.
var parent_last = parent_block.last;
if (parent_last === null) {
parent_block.last = parent_block.first = block;
} else {
parent_last.next = block;
block.prev = parent_last;
parent_block.last = block;
}
}
return block;
}
/**
* @param {Block} parent
* @param {boolean} [remove_dom]
*/
export function destroy_block_children(parent, remove_dom = false) {
var block = parent.first;
parent.first = parent.last = null;
while (block !== null) {
var next = block.next;
destroy_block(block, remove_dom);
block = next;
}
}
/**
* @param {Block} parent
* @param {boolean} [remove_dom]
*/
export function destroy_non_branch_children(parent, remove_dom = false) {
var block = parent.first;
while (block !== null) {
var next = block.next;
if ((block.f & BRANCH_BLOCK) === 0) {
destroy_block(block, remove_dom);
}
block = next;
}
}
/**
* @param {Block} block
*/
export function unlink_block(block) {
var parent = block.p;
var prev = block.prev;
var next = block.next;
if (prev !== null) prev.next = next;
if (next !== null) next.prev = prev;
if (parent !== null) {
if (parent.first === block) parent.first = next;
if (parent.last === block) parent.last = prev;
}
}
/**
* Moves `block` to the end of its parent's child list. A block whose content
* is inserted before an anchor the parent already owns is created after the
* anchor's block; relinking keeps block order equal to DOM order, which the
* first/last node descent in `get_first_node`/`get_last_node` relies on.
* @param {Block} block
*/
export function move_block_last(block) {
var parent = /** @type {Block} */ (block.p);
if (parent.last === block) {
return;
}
unlink_block(block);
block.prev = block.next = null;
push_block(block, parent);
}
/**
* @param {Block} block
*/
export function pause_block(block) {
if ((block.f & PAUSED) !== 0) {
return;
}
block.f ^= PAUSED;
var child = block.first;
while (child !== null) {
var next = child.next;
pause_block(child);
child = next;
}
run_teardown(block);
}
/**
* @param {Block} block
*/
export function resume_block(block) {
if ((block.f & PAUSED) === 0) {
return;
}
block.f ^= PAUSED;
if (is_block_dirty(block)) {
schedule_update(block);
}
var child = block.first;
while (child !== null) {
var next = child.next;
resume_block(child);
child = next;
}
}
/**
* @param {Block} target_block
* @returns {boolean}
*/
export function is_destroyed(target_block) {
/** @type {Block | null} */
var block = target_block;
while (block !== null) {
var flags = block.f;
if ((flags & DESTROYED) !== 0) {
return true;
}
if ((flags & ROOT_BLOCK) !== 0) {
return false;
}
block = block.p;
}
return true;
}
/**
* @param {Node | null} node
* @param {Node} end
*/
export function remove_block_dom(node, end) {
while (node !== null) {
/** @type {Node | null} */
var next = node === end ? null : next_sibling(node);
/** @type {Element | Text | Comment} */ (node).remove();
node = next;
}
}
/**
* Moves DOM nodes from a block to a target element (typically a DocumentFragment).
* If the block has state (start/end), moves that range.
* If not, recursively moves content from child branch blocks.
* @param {Block} block - The block to move content from
* @param {Element | DocumentFragment} target - Where to move the nodes
* @returns {boolean} - True if content was moved
*/
export function move_block(block, target) {
var f = block.f;
// Only branch, if and composite blocks (excluding TRY_BLOCK) can have DOM state to move
if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
var s = block.s;
if (s !== null && s.start !== null) {
var node = s.start;
var end = s.end;
while (node !== null) {
var next = node === end ? null : next_sibling(node);
target.append(node);
node = next;
}
// An if's materialized anchor follows its range.
if ((f & IF_BLOCK) !== 0 && s.o !== null) {
move_block(s.o, target);
}
return true;
}
}
// If this block has no DOM, try moving from child branch blocks
var moved = false;
var child = block.first;
while (child !== null) {
if (move_block(child, target)) {
moved = true;
}
child = child.next;
}
return moved;
}
/**
* Resolve the first DOM node owned by a block. A branch or if block normally
* records its range in `s.start`/`s.end`, but an optimized single control-flow / component
* root scope renders its content through a descendant block instead of a
* synthesized `<!>` wrapper, so its own `s.start` is null. In that case we
* descend into child blocks to find the real first node. Returns null when the
* block currently renders no DOM.
* @param {Block} block
* @returns {Node | null}
*/
export function get_first_node(block) {
var f = block.f;
if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
var s = block.s;
if (s !== null && s.start !== null) {
return s.start;
}
}
var child = block.first;
while (child !== null) {
var node = get_first_node(child);
if (node !== null) {
return node;
}
child = child.next;
}
return null;
}
/**
* Mirror of {@link get_first_node} for the last DOM node owned by a block.
* @param {Block} block
* @returns {Node | null}
*/
export function get_last_node(block) {
var f = block.f;
if ((f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 && (f & TRY_BLOCK) === 0) {
var s = block.s;
if (s !== null && s.start !== null) {
// An if's materialized anchor is its last node.
return (f & IF_BLOCK) !== 0 && s.o !== null ? s.o.s.end : s.end;
}
}
var child = block.last;
while (child !== null) {
var node = get_last_node(child);
if (node !== null) {
return node;
}
child = child.prev;
}
return null;
}
/**
* @param {Block} block
* @param {boolean} [remove_dom]
*/
export function destroy_block(block, remove_dom = true) {
var f = (block.f |= DESTROYED);
var removed = false;
// An ancestor that removed its own range tells its subtree not to bother,
// but detached content (a portal target, `<head>`) is not inside that
// range and must still be removed, by this block or by its children.
if ((f & DETACHED_BLOCK) !== 0) {
remove_dom = true;
}
if (
(remove_dom &&
(f & (BRANCH_BLOCK | ROOT_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0 &&
(f & TRY_BLOCK) === 0) ||
(f & HEAD_BLOCK) !== 0
) {
var s = block.s;
if (s !== null && s.start !== null) {
remove_block_dom(s.start, s.end);
removed = true;
}
}
destroy_block_children(block, remove_dom && !removed);
run_teardown(block);
if (block.d !== null) {
remove_dependencies(block);
}
if ((f & CREATES_DERIVEDS) !== 0) {
var deriveds = take_created_deriveds(block);
if (deriveds !== null) {
release_deriveds(deriveds);
}
}
var parent = block.p;
// If the parent doesn't have any children, then skip this work altogether
if (parent !== null && parent.first !== null) {
unlink_block(block);
}
block.fn = block.s = block.d = block.p = block.co = block.t = null;
}