UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

478 lines (420 loc) 10.2 kB
/** @import { Block, Derived, CompatOptions, Component } from '#client' */ import { BLOCK_HAS_RUN, BRANCH_BLOCK, DERIVED, CONTAINS_TEARDOWN, DESTROYED, EFFECT_BLOCK, PAUSED, PRE_EFFECT_BLOCK, RENDER_BLOCK, ROOT_BLOCK, TRY_BLOCK, HEAD_BLOCK, DIRECT_CHILD_BLOCK, UNINITIALIZED, } from './constants.js'; import { next_sibling } from './operations.js'; import { apply_element_spread } from './render.js'; import { active_block, active_component, active_reaction, create_component_ctx, is_block_dirty, run_block, run_teardown, schedule_update, untrack, } from './runtime.js'; import { is_ripple_object } from './utils.js'; /** * @param {Function} fn */ export function user_effect(fn) { if (active_block === null) { throw new Error( 'effect() must be called within an active context, such as a component or effect', ); } var component = active_component; if (component !== null && !component.m) { var e = (component.e ??= []); e.push({ b: active_block, fn, r: 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) { return block(RENDER_BLOCK | flags, fn, state); } /** * @param {any} element * @param {any} fn * @param {number} [flags] */ export function render_spread(element, fn, flags = 0) { return block(RENDER_BLOCK | flags, apply_element_spread(element, fn)); } /** * @param {Function} fn * @param {number} [flags] * @param {any} [state] */ export function branch(fn, flags = 0, state = null) { return block(BRANCH_BLOCK | flags, fn, state); } /** * 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. * * `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. * * @param {Element} element * @param {() => any} get_fn * @param {(value: any) => void} [set_fn] * @returns {Block} */ export function ref(element, get_fn, set_fn) { // make sure the first run always enters the dispatch branch, /** @type {any} */ var ref_value = UNINITIALIZED; /** @type {Block | null} */ var e; return block(RENDER_BLOCK, () => { // avoid any reactive reads var next = untrack(get_fn); if (ref_value !== (ref_value = next)) { if (e) { destroy_block(e); e = null; } if (typeof ref_value === 'function') { e = branch(() => { effect(() => ref_value(element)); }); } else if (is_ripple_object(ref_value)) { e = branch(() => { effect(() => { ref_value.value = element; return () => { ref_value.value = null; }; }); }); } else if (set_fn !== undefined) { e = branch(() => { effect(() => { set_fn(element); return () => { set_fn(null); }; }); }); } } }); } /** * @param {() => (void | (() => void))} fn * @param {CompatOptions} [compat] * @returns {Block} */ export function root(fn, compat) { var target_fn = fn; if (compat != null) { /** @type {Array<void | (() => void)>} */ var unmounts = []; for (var key in compat) { var api = compat[key]; unmounts.push(api.createRoot()); } target_fn = () => { var component_unmount = fn(); return () => { component_unmount?.(); for (var unmount of unmounts) { unmount?.(); } }; }; } return block(ROOT_BLOCK, target_fn, { compat, start: null, end: null }, create_component_ctx()); } /** * @param {() => void} fn * @param {any} state * @returns {Block} */ export function create_try_block(fn, state) { return block(TRY_BLOCK, fn, state); } /** * @param {() => void} fn * @param {number} [flags] * @param {any} [state] */ export function boundary_fn_running_block(fn, flags = 0, state = null) { return branch(fn, DIRECT_CHILD_BLOCK | flags, state); } /** * @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) { /** @type {Block} */ var block = { co: co || active_component, d: null, first: null, f: flags, fn, 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); } if (active_block !== null) { push_block(block, active_block); } if ((flags & EFFECT_BLOCK) !== 0) { schedule_update(block); } else { run_block(block); block.f ^= BLOCK_HAS_RUN; } 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; if (remove_dom || (parent.f & CONTAINS_TEARDOWN) !== 0) { 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; if ( (parent.f & CONTAINS_TEARDOWN) === 0 && parent.first !== null && (parent.first.f & BRANCH_BLOCK) === 0 ) { parent.first = parent.last = null; } else { 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; } } /** * @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_BLOCKs (excluding TRY_BLOCK) can have DOM state to move if ((f & BRANCH_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; } 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; } /** * @param {Block} block * @param {boolean} [remove_dom] */ export function destroy_block(block, remove_dom = true) { block.f ^= DESTROYED; var removed = false; var f = block.f; if ( (remove_dom && (f & (BRANCH_BLOCK | ROOT_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); 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; }