UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

399 lines (360 loc) • 10.5 kB
/** @import { AppendIntoAnchor, Block } from '#client' */ import { is_array } from '@tsrx/core/runtime/language-helpers'; import { branch, destroy_block, render } from './blocks.js'; import { BRANCH_BLOCK, COMPOSITE_BLOCK, IF_BLOCK, UNINITIALIZED } from './constants.js'; import { create_text, get_next_sibling } from './operations.js'; import { assign_nodes } from './template.js'; import { active_block } from './runtime.js'; import { H, hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { COMMENT_NODE, HYDRATION_START, TEXT_NODE } from '../../../constants.js'; import { is_tsrx_element, TSRX_ELEMENT } from '../../element.js'; import { HYDRATION } from 'ripple/internal/client/hydration-enabled'; /** * Finds the nearest enclosing block that owns a DOM range (a branch, an if * or a composite block) in the block hierarchy. * @param {Block | null} block * @returns {Block | null} */ function find_enclosing_branch(block) { while (block !== null) { if ((block.f & (BRANCH_BLOCK | IF_BLOCK | COMPOSITE_BLOCK)) !== 0) { return block; } block = block.p; } return null; } /** * @param {any} value * @param {ChildNode} anchor * @param {Block | null} block * @returns {void} */ export function render_value(value, anchor, block) { // render_tsrx_element, inline: an element's render may return another // element (a wrapper), an array, or text. var rendered = false; // is_tsrx_element, inline: every component render passes through here. while (value != null && value[TSRX_ELEMENT] === true) { value = value.render(anchor, block, value.p); rendered = true; } if (is_array(value)) { render_tsrx_collection(value, anchor, block); } else if (value != null) { var text = value + ''; if (rendered || text !== '') { render_tsrx_collection_text(text, anchor, true); } } } /** * @param {any[]} value * @param {ChildNode} anchor * @param {Block | null} block * @returns {void} */ function render_tsrx_collection(value, anchor, block) { if (HYDRATION && hydrating) { assign_nodes(/** @type {Node} */ (hydrate_node ?? anchor), anchor); render_tsrx_collection_items(value, anchor, block); return; } var start = document.createComment(''); var end = document.createComment(''); insert_before(anchor, start); insert_before(anchor, end); assign_nodes(start, end); render_tsrx_collection_items(value, end, block); } /** * Inserts `node` before `anchor`, or appends it into the parent when `anchor` * is an append-into sentinel (see `append_into`). * @param {ChildNode | AppendIntoAnchor} anchor * @param {Node} node * @returns {void} */ function insert_before(anchor, node) { if (/** @type {AppendIntoAnchor} */ (anchor).into === true) { /** @type {AppendIntoAnchor} */ (anchor).parent.appendChild(node); } else { /** @type {ChildNode} */ (anchor).before(node); } } /** * @param {any[]} value * @param {ChildNode} anchor * @param {Block | null} block * @returns {void} */ function render_tsrx_collection_items(value, anchor, block) { for (var i = 0; i < value.length; i++) { var item = value[i]; if (is_tsrx_element(item)) { render_tsrx_element(item, anchor, block); } else if (is_array(item)) { render_tsrx_collection_items(item, anchor, block); } else if (item != null) { render_tsrx_collection_text(item + '', anchor); } } } /** * @param {import('../../element.js').TSRXElement} value * @param {ChildNode} anchor * @param {Block | null} block * @returns {void} */ function render_tsrx_element(value, anchor, block) { var result = value.render(anchor, block, value.p); if (is_tsrx_element(result)) { render_tsrx_element(result, anchor, block); } else if (is_array(result)) { render_tsrx_collection(result, anchor, block); } else if (result != null) { render_tsrx_collection_text(result + '', anchor, true); } } /** * @param {string} value * @param {ChildNode} anchor * @param {boolean} [assign=false] * @returns {void} */ function render_tsrx_collection_text(value, anchor, assign = false) { if (HYDRATION && hydrating) { /** @type {import('./hydrate.js').HydrationRuntime} */ (H).x(value, anchor, assign); return; } var text = create_text(value); insert_before(anchor, text); if (assign) { assign_nodes(text, text); } } /** * @typedef {{ * start: Node | null; * end: Node | null; * a: ChildNode; * n: number; * g: () => any; * b: Block | null; * m: Comment | null; * t: Text | null; * v: string | import('../../element.js').TSRXElement | typeof UNINITIALIZED; * e: boolean; * i: boolean; * p: Block | null; * o: Node | null; * }} ExpressionState */ /** * @param {Node} node * @param {() => any} get_value * @returns {void} */ export function expression(node, get_value) { // State lives on the render block instead of a per-expression closure, and // the anchor's node type is read once since the anchor never changes. render(run_expression, { start: null, end: null, a: /** @type {ChildNode} */ (node), n: node.nodeType, g: get_value, // child block rendering a TSRX element / collection b: null, // hydration end marker m: null, // text node inserted before a comment anchor t: null, // last rendered value v: UNINITIALIZED, // whether the last value was an element / collection e: false, // whether the block has rendered once i: false, // enclosing branch whose start was moved to include element content p: null, // that branch's original start node o: null, }); } /** * @param {ExpressionState} s * @returns {void} */ function run_expression(s) { var next_value = s.g(); var anchor = s.a; var type = typeof next_value; var is_hydration_marker = HYDRATION && hydrating && s.n === COMMENT_NODE && /** @type {Comment} */ (anchor).data === HYDRATION_START; if (is_hydration_marker) { s.m ??= /** @type {import('./hydrate.js').HydrationRuntime} */ (H).e(anchor); } var end = s.m; if (next_value !== null && (type === 'object' || type === 'function')) { var next_is_collection = is_array(next_value); if (next_is_collection || is_tsrx_element(next_value)) { if (s.i && s.e && s.v === next_value) { if (end !== null) { settle_hydration(end); } return; } if (s.n === TEXT_NODE) { /** @type {Text} */ (anchor).nodeValue = ''; } else if (s.t !== null) { s.t.remove(); s.t = null; } if (s.b !== null) { destroy_block(s.b); s.b = null; restore_parent_start(s); } if (end !== null && (s.i || !(HYDRATION && hydrating))) { clear_expression_range(anchor, end); } if (is_hydration_marker) { set_hydrate_node(get_next_sibling(anchor) ?? end); } // Find the enclosing branch block BEFORE creating the child block // so we can update its s.start to include content inserted before anchor var parent_branch = find_enclosing_branch(active_block); var child_block = (s.b = branch(() => { var block = /** @type {Block} */ (active_block); if (next_is_collection) { render_tsrx_collection(next_value, end ?? anchor, block); } else { render_tsrx_element(next_value, end ?? anchor, block); } })); // Update parent branch's s.start to include content inserted before anchor. // This ensures that when the parent branch is destroyed, the full DOM range // (including TSRXElement content) is removed. if ( parent_branch !== null && parent_branch.s !== null && child_block.s !== null && child_block.s.start !== null ) { // The child inserted content before the anchor. Update parent's start // to encompass this content. var child_start = child_block.s.start; var parent_start = parent_branch.s.start; // If parent's start is the anchor (or comes after child's start), // update it to include the child's content if (parent_start === anchor || parent_start === end) { // Save original so we can restore it when switching to non-TSRXElement if (s.p === null) { s.p = parent_branch; s.o = parent_start; } parent_branch.s.start = child_start; } } s.v = next_value; s.e = true; s.i = true; if (end !== null) { settle_hydration(end); } return; } } var next_text = next_value == null ? '' : next_value + ''; if (s.i && !s.e && s.v === next_text) { if (end !== null) { settle_hydration(end); } return; } if (s.b !== null) { destroy_block(s.b); s.b = null; // Restore parent branch's start to original value since the child's DOM nodes // have been removed and the old start reference would be stale restore_parent_start(s); } if (is_hydration_marker) { var text = (s.t = /** @type {import('./hydrate.js').HydrationRuntime} */ (H).h( anchor, /** @type {Comment} */ (end), )); if (next_text === '') { if (text !== null) { text.remove(); s.t = null; } } else if (text === null) { text = s.t = create_text(next_text); /** @type {Comment} */ (end).before(text); } else if (text.nodeValue !== next_text) { text.nodeValue = next_text; } } else if (s.n === COMMENT_NODE) { var text = s.t; if (next_text === '') { if (text !== null) { text.remove(); s.t = null; } } else if (text === null) { text = s.t = create_text(next_text); (end ?? anchor).before(text); } else if (text.nodeValue !== next_text) { text.nodeValue = next_text; } } else if (s.n === TEXT_NODE) { /** @type {Text} */ (anchor).nodeValue = next_text; } s.v = next_text; s.e = false; s.i = true; if (end !== null) { settle_hydration(end); } } /** * @param {ExpressionState} s * @returns {void} */ function restore_parent_start(s) { var parent = s.p; if (parent !== null && parent.s !== null) { parent.s.start = s.o; s.p = null; s.o = null; } } /** * @param {Node} anchor * @param {Node} end * @returns {void} */ export function clear_expression_range(anchor, end) { var current = get_next_sibling(anchor); while (current !== null && current !== end) { var next = get_next_sibling(current); /** @type {ChildNode} */ (current).remove(); current = next; } } /** * Leaves the hydration cursor on the expression's end marker, the last node * the expression owns: the parent steps past it with its own sibling * navigation, exactly as it does after an element or a control-flow block. * @param {Comment} end * @returns {void} */ function settle_hydration(end) { if (HYDRATION && hydrating) { set_hydrate_node(end); } }