UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

1,056 lines (966 loc) 25.5 kB
/** @import { Block, Tracked } from '#client' */ import { IS_CONTROLLED, IS_INDEXED, ROOT_CONTROLLED } from '../../../constants.js'; import { branch, destroy_block, destroy_block_children, get_first_node, get_last_node, render, } from './blocks.js'; import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { create_text, get_first_child, get_last_child, next_sibling } from './operations.js'; import { append } from './template.js'; import { active_block, set, tracked, untrack } from './runtime.js'; import { array_from, is_array } from '@tsrx/core/runtime/language-helpers'; /** * @template V * @param {Node} anchor * @param {V} value * @param {number} index * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn * @param {boolean} is_indexed * @param {boolean} is_keyed * @returns {Block} */ function create_item(anchor, value, index, render_fn, is_indexed, is_keyed) { var block = /** @type {Block} */ (active_block); var tracked_index = is_indexed ? tracked(index, block) : undefined; var tracked_value = is_keyed ? tracked(value, block) : value; var state = { start: null, end: null, i: tracked_index, v: tracked_value, }; // Passed through module state rather than a per-item closure; run_item // reads them before rendering, so nested loops cannot observe a stale pair. item_anchor = anchor; item_render_fn = render_fn; var b = branch(run_item, 0, state); // The item's tracked value and index are owned by the item block itself. if (is_keyed) { /** @type {Tracked} */ (tracked_value).b = b; } if (is_indexed) { /** @type {Tracked} */ (tracked_index).b = b; } return b; } /** @type {Node | null} */ var item_anchor = null; /** @type {((anchor: Node, value: any, index?: any) => Block) | null} */ var item_render_fn = null; /** * @param {{ i: Tracked | undefined, v: any }} state */ function run_item(state) { var render_fn = /** @type {(anchor: Node, value: any, index?: any) => Block} */ (item_render_fn); var anchor = /** @type {Node} */ (item_anchor); item_render_fn = null; item_anchor = null; render_fn(anchor, state.v, state.i); } /** * @param {Node} anchor * @param {(anchor: Node) => void} render_empty * @returns {Block} */ function create_empty(anchor, render_empty) { return branch(() => { render_empty(anchor); }); } /** * @param {Block} block * @param {ChildNode} anchor * @returns {void} */ function move(block, anchor) { // Fast path: a normal item records its own range. Only an optimized single // control-flow / component root item (DOM rendered through a descendant // block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`. var s = block.s; var node = s.start; var end; if (node === null) { node = get_first_node(block); if (node === null) { return; } end = get_last_node(block); } else { end = s.end; } if (node === end) { anchor.before(node); return; } while (node !== null) { var next_node = /** @type {Node} */ (next_sibling(node)); anchor.before(node); node = next_node; if (node === end) { anchor.before(/** @type {Node} */ (end)); break; } } } /** * Resolve the insertion anchor for a block at `index`: the first real DOM node * at or after `index`, or `fallback` when every remaining block renders nothing. * Scanning forward keeps insertions correct even when an optimized item renders * no DOM (e.g. a single `@if` that is currently false), which previously relied * on a synthesized `<!>` wrapper as a stable position marker. * @param {Block[]} blocks * @param {number} index * @param {number} length * @param {Element | Text} fallback * @returns {ChildNode} */ function block_start(blocks, index, length, fallback) { if (index >= length) { return fallback; } // Fast path: a normal item records its own boundary, so this is the same // single property read the pre-#1307 code did — no descent, no scan. var first = blocks[index].s.start; if (first !== null) { return first; } for (var k = index; k < length; k++) { var node = get_first_node(blocks[k]); if (node !== null) { return /** @type {ChildNode} */ (node); } } return fallback; } /** * @template V * @param {V[] | Iterable<V>} collection * @returns {V[]} */ function collection_to_array(collection) { var array = is_array(collection) ? collection : collection == null ? [] : array_from(collection); // If we are working with a tracked array, then we need to get a copy of // the elements, as the array itself is proxied, and not useful in diffing if (TRACKED_ARRAY in array) { array = array_from(array); } return array; } /** * @template V * @param {Element} node * @param {() => V[] | Iterable<V>} get_collection * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn * @param {number} flags * @param {(anchor: Node) => void} [render_empty] * @returns {void} */ export function for_block(node, get_collection, render_fn, flags, render_empty) { var is_controlled = (flags & IS_CONTROLLED) !== 0; var is_indexed = (flags & IS_INDEXED) !== 0; var root_controlled = (flags & ROOT_CONTROLLED) !== 0; var anchor = /** @type {Element | Text} */ (node); /** @type {Node | undefined} */ var boundary; if (is_controlled) { if (hydrating) { var parent_node = /** @type {Element} */ (node); /** @type {Element | Text} */ (set_hydrate_node(get_first_child(parent_node))); } else { anchor = node.appendChild(create_text()); } } if (hydrating) { if (root_controlled) { boundary = /** @type {Node} */ (hydrate_node); } hydrate_next(); } render( () => { var block = /** @type {Block} */ (active_block); var collection = get_collection(); var array = collection_to_array(collection); untrack(() => { reconcile_by_ref(anchor, block, array, render_fn, is_controlled, is_indexed, render_empty); }); if (hydrating) { anchor = /** @type {Element | Text} */ (hydrate_node); } }, null, FOR_BLOCK, ); if (hydrating && root_controlled) { append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary)); } } /** * @template V * @template K * @param {Element} node * @param {() => V[] | Iterable<V>} get_collection * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn * @param {number} flags * @param {(item: V) => K} [get_key] * @param {(anchor: Node) => void} [render_empty] * @returns {void} */ export function for_block_keyed(node, get_collection, render_fn, flags, get_key, render_empty) { var is_controlled = (flags & IS_CONTROLLED) !== 0; var is_indexed = (flags & IS_INDEXED) !== 0; var root_controlled = (flags & ROOT_CONTROLLED) !== 0; var anchor = /** @type {Element | Text} */ (node); /** @type {Node | undefined} */ var boundary; if (is_controlled) { var parent_node = /** @type {Element} */ (node); if (hydrating) { /** @type {Element | Text} */ (set_hydrate_node(get_first_child(parent_node))); anchor = /** @type {Element | Text} */ (get_last_child(parent_node)); } else { anchor = node.appendChild(create_text()); } } if (hydrating) { if (root_controlled) { boundary = /** @type {Node} */ (hydrate_node); } hydrate_next(); } render( () => { var block = /** @type {Block} */ (active_block); var collection = get_collection(); var array = collection_to_array(collection); untrack(() => { reconcile_by_key( anchor, block, array, render_fn, is_controlled, is_indexed, /** @type {(item: V) => K} */ (get_key), render_empty, ); }); }, null, FOR_BLOCK, ); if (hydrating && root_controlled) { append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary)); } } /** * @template V * @param {Element | Text} anchor * @param {Block} block * @param {V[]} array * @returns {void} */ function reconcile_fast_clear(anchor, block, array) { var state = block.s; var parent_node = /** @type {Element} */ (anchor.parentNode); parent_node.textContent = ''; destroy_block_children(block); parent_node.append(anchor); state.array = array; state.blocks = []; state.empty = null; } /** * @param {Block} block * @param {number} index * @returns {void} */ function update_index(block, index) { set(block.s.i, index); } /** * @param {Block} block * @param {any} value * @returns {void} */ function update_value(block, value) { var tracked_value = block.s.v; if (tracked_value.__v !== value) { set(tracked_value, value); } } /** * @template V * @template K * @param {Element | Text} anchor * @param {Block} block * @param {V[]} b * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn * @param {boolean} is_controlled * @param {boolean} is_indexed * @param {(item: V) => K} get_key * @param {(anchor: Node) => void} [render_empty] * @returns {void} */ function reconcile_by_key( anchor, block, b, render_fn, is_controlled, is_indexed, get_key, render_empty, ) { var state = block.s; // Variables used in conditional branches - declare with initial values /** @type {number} */ var a_left = 0; /** @type {number} */ var b_left = 0; /** @type {Int32Array} */ var sources = new Int32Array(0); /** @type {boolean} */ var moved = false; /** @type {number} */ var pos = 0; /** @type {number} */ var patched = 0; /** @type {number} */ var i = 0; if (state === null) { state = block.s = { array: [], blocks: [], keys: null, empty: null, }; } var a = state.array; var a_length = a.length; var b_length = b.length; var j = 0; if (b_length === 0) { if (a_length > 0) { if (is_controlled) { reconcile_fast_clear(anchor, block, b); } else { for (; j < a_length; j++) { destroy_block(state.blocks[j]); } state.array = b; state.blocks = []; state.keys = []; } } if (render_empty && state.empty === null) { state.empty = create_empty(anchor, render_empty); } return; } if (state.empty !== null) { destroy_block(state.empty); state.empty = null; } // Fast-path for clear if (is_controlled && b_length === 0) { if (a_length > 0) { reconcile_fast_clear(anchor, block, b); } return; } var b_blocks = Array(b_length); var b_keys = b.map(get_key); // Fast-path for create if (a_length === 0) { for (; j < b_length; j++) { b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, true); } state.array = b; state.blocks = b_blocks; state.keys = b_keys; return; } var a_blocks = state.blocks; var a_keys = state.keys; var a_start = 0; var b_start = 0; var a_end = a_length - 1; var b_end = b_length - 1; var b_val; var b_block; // Match from both ends first, including the two end-crossing cases: an old // item that moved to the far end of the new list, and a run whose ends were // exchanged (a reversal, or a swap of two items). Those complete with plain // moves; only what is left afterwards needs the map and LIS below. while (a_start <= a_end && b_start <= b_end) { if (a_keys[a_start] === b_keys[b_start]) { b_val = b[b_start]; b_block = b_blocks[b_start] = a_blocks[a_start]; if (is_indexed) { update_index(b_block, b_start); } update_value(b_block, b_val); a_start++; b_start++; continue; } if (a_keys[a_end] === b_keys[b_end]) { b_val = b[b_end]; b_block = b_blocks[b_end] = a_blocks[a_end]; if (is_indexed) { update_index(b_block, b_end); } update_value(b_block, b_val); a_end--; b_end--; continue; } if (a_start === a_end || a_blocks[a_start].s.start === null) { break; } if (a_keys[a_end] === b_keys[b_start]) { // Last old item is the next new one: move it in front of the old run. b_val = b[b_start]; b_block = b_blocks[b_start] = a_blocks[a_end]; if (is_indexed) { update_index(b_block, b_start); } update_value(b_block, b_val); move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start)); a_end--; b_start++; continue; } if (a_keys[a_start] === b_keys[b_end]) { // First old item is the last new one: move it behind the old run. b_val = b[b_end]; b_block = b_blocks[b_end] = a_blocks[a_start]; if (is_indexed) { update_index(b_block, b_end); } update_value(b_block, b_val); move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor)); a_start++; b_end--; continue; } break; } var fast_path_removal = false; if (a_start > a_end) { if (b_start <= b_end) { var target_node = block_start(b_blocks, b_end + 1, b_length, anchor); while (b_start <= b_end) { b_blocks[b_start] = create_item( target_node, b[b_start], b_start, render_fn, is_indexed, true, ); b_start++; } } } else if (b_start > b_end) { while (a_start <= a_end) { destroy_block(a_blocks[a_start++]); } } else { a_left = a_end - a_start + 1; b_left = b_end - b_start + 1; sources = new Int32Array(b_left + 1); moved = false; pos = 0; patched = 0; i = 0; fast_path_removal = is_controlled && a_left === a_length; // When sizes are small, just loop them through if (b_length < 4 || (a_left | b_left) < 32) { for (i = a_start; i <= a_end; ++i) { if (patched < b_left) { for (j = b_start; j <= b_end; j++) { if (a_keys[i] === b_keys[j]) { sources[j - b_start] = i + 1; if (fast_path_removal) { fast_path_removal = false; while (a_start < i) { destroy_block(a_blocks[a_start++]); } } if (pos > j) { moved = true; } else { pos = j; } b_val = b[j]; b_block = b_blocks[j] = a_blocks[i]; if (is_indexed) { update_index(b_block, j); } update_value(b_block, b_val); ++patched; break; } } if (!fast_path_removal && j > b_end) { destroy_block(a_blocks[i]); } } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } } else { var map = new Map(); for (i = b_start; i <= b_end; ++i) { map.set(b_keys[i], i); } for (i = a_start; i <= a_end; ++i) { if (patched < b_left) { j = map.get(a_keys[i]); if (j !== undefined) { if (fast_path_removal) { fast_path_removal = false; while (i > a_start) { destroy_block(a_blocks[a_start++]); } } sources[j - b_start] = i + 1; if (pos > j) { moved = true; } else { pos = j; } b_val = b[j]; b_block = b_blocks[j] = a_blocks[i]; if (is_indexed) { update_index(b_block, j); } update_value(b_block, b_val); ++patched; } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } } } if (fast_path_removal) { reconcile_fast_clear(anchor, block, []); reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed, get_key); return; } else if (moved) { var next_pos = 0; var seq = lis_algorithm(sources); j = seq.length - 1; // When most surviving items have to move anyway, re-lay the whole range // in order before a fixed target: inserting before the same node is // noticeably cheaper per item than moving into arbitrary positions. if ((patched - seq.length) * 3 > b_left * 2) { var relay_target = block_start(b_blocks, b_end + 1, b_length, anchor); for (i = 0; i < b_left; i++) { pos = i + b_start; if (sources[i] === 0) { b_blocks[pos] = create_item(relay_target, b[pos], pos, render_fn, is_indexed, true); } else { move(b_blocks[pos], relay_target); } } state.array = b; state.blocks = b_blocks; state.keys = b_keys; return; } for (i = b_left - 1; i >= 0; i--) { if (sources[i] === 0) { pos = i + b_start; b_val = b[pos]; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true); } else if (j < 0 || i !== seq[j]) { pos = i + b_start; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); move(b_blocks[pos], target); } else { j--; } } } else if (patched !== b_left) { for (i = b_left - 1; i >= 0; i--) { if (sources[i] === 0) { pos = i + b_start; b_val = b[pos]; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, true); } } } state.array = b; state.blocks = b_blocks; state.keys = b_keys; } /** * @template V * @param {Element | Text} anchor * @param {Block} block * @param {V[]} b * @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn * @param {boolean} is_controlled * @param {boolean} is_indexed * @param {(anchor: Node) => void} [render_empty] * @returns {void} */ function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed, render_empty) { var state = block.s; // Variables used in conditional branches - declare with initial values /** @type {number} */ var a_left = 0; /** @type {number} */ var b_left = 0; /** @type {Int32Array} */ var sources = new Int32Array(0); /** @type {boolean} */ var moved = false; /** @type {number} */ var pos = 0; /** @type {number} */ var patched = 0; /** @type {number} */ var i = 0; if (state === null) { state = block.s = { array: [], blocks: [], keys: null, empty: null, }; } var a = state.array; var a_length = a.length; var b_length = b.length; var j = 0; if (b_length === 0) { if (a_length > 0) { if (is_controlled) { reconcile_fast_clear(anchor, block, b); } else { for (; j < a_length; j++) { destroy_block(state.blocks[j]); } state.array = b; state.blocks = []; } } if (render_empty && state.empty === null) { state.empty = create_empty(anchor, render_empty); } return; } if (state.empty !== null) { destroy_block(state.empty); state.empty = null; } // Fast-path for clear if (is_controlled && b_length === 0) { if (a_length > 0) { reconcile_fast_clear(anchor, block, b); } return; } var b_blocks = Array(b_length); // Fast-path for create if (a_length === 0) { for (; j < b_length; j++) { b_blocks[j] = create_item(anchor, b[j], j, render_fn, is_indexed, false); } state.array = b; state.blocks = b_blocks; return; } var a_blocks = state.blocks; var a_start = 0; var b_start = 0; var a_end = a_length - 1; var b_end = b_length - 1; var b_val; var b_block; // Match from both ends first, including the two end-crossing cases: an old // item that moved to the far end of the new list, and a run whose ends were // exchanged (a reversal, or a swap of two items). Those complete with plain // moves; only what is left afterwards needs the map and LIS below. while (a_start <= a_end && b_start <= b_end) { if (a[a_start] === b[b_start]) { b_val = b[b_start]; b_block = b_blocks[b_start] = a_blocks[a_start]; if (is_indexed) { update_index(b_block, b_start); } a_start++; b_start++; continue; } if (a[a_end] === b[b_end]) { b_val = b[b_end]; b_block = b_blocks[b_end] = a_blocks[a_end]; if (is_indexed) { update_index(b_block, b_end); } a_end--; b_end--; continue; } if (a_start === a_end || a_blocks[a_start].s.start === null) { break; } if (a[a_end] === b[b_start]) { // Last old item is the next new one: move it in front of the old run. b_val = b[b_start]; b_block = b_blocks[b_start] = a_blocks[a_end]; if (is_indexed) { update_index(b_block, b_start); } move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start)); a_end--; b_start++; continue; } if (a[a_start] === b[b_end]) { // First old item is the last new one: move it behind the old run. b_val = b[b_end]; b_block = b_blocks[b_end] = a_blocks[a_start]; if (is_indexed) { update_index(b_block, b_end); } move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor)); a_start++; b_end--; continue; } break; } var fast_path_removal = false; if (a_start > a_end) { if (b_start <= b_end) { var target_node = block_start(b_blocks, b_end + 1, b_length, anchor); while (b_start <= b_end) { b_blocks[b_start] = create_item( target_node, b[b_start], b_start, render_fn, is_indexed, false, ); b_start++; } } } else if (b_start > b_end) { while (a_start <= a_end) { destroy_block(a_blocks[a_start++]); } } else { a_left = a_end - a_start + 1; b_left = b_end - b_start + 1; sources = new Int32Array(b_left + 1); moved = false; pos = 0; patched = 0; i = 0; fast_path_removal = is_controlled && a_left === a_length; // When sizes are small, just loop them through if (b_length < 4 || (a_left | b_left) < 32) { for (i = a_start; i <= a_end; ++i) { if (patched < b_left) { for (j = b_start; j <= b_end; j++) { if (a[i] === b[j]) { sources[j - b_start] = i + 1; if (fast_path_removal) { fast_path_removal = false; while (a_start < i) { destroy_block(a_blocks[a_start++]); } } if (pos > j) { moved = true; } else { pos = j; } b_val = b[j]; b_block = b_blocks[j] = a_blocks[i]; if (is_indexed) { update_index(b_block, j); } ++patched; break; } } if (!fast_path_removal && j > b_end) { destroy_block(a_blocks[i]); } } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } } else { var map = new Map(); for (i = b_start; i <= b_end; ++i) { map.set(b[i], i); } for (i = a_start; i <= a_end; ++i) { if (patched < b_left) { j = map.get(a[i]); if (j !== undefined) { if (fast_path_removal) { fast_path_removal = false; while (i > a_start) { destroy_block(a_blocks[a_start++]); } } sources[j - b_start] = i + 1; if (pos > j) { moved = true; } else { pos = j; } b_val = b[j]; b_block = b_blocks[j] = a_blocks[i]; if (is_indexed) { update_index(b_block, j); } ++patched; } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } else if (!fast_path_removal) { destroy_block(a_blocks[i]); } } } } if (fast_path_removal) { reconcile_fast_clear(anchor, block, []); reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed); return; } else if (moved) { var next_pos = 0; var seq = lis_algorithm(sources); j = seq.length - 1; // When most surviving items have to move anyway, re-lay the whole range // in order before a fixed target: inserting before the same node is // noticeably cheaper per item than moving into arbitrary positions. if ((patched - seq.length) * 3 > b_left * 2) { var relay_target = block_start(b_blocks, b_end + 1, b_length, anchor); for (i = 0; i < b_left; i++) { pos = i + b_start; if (sources[i] === 0) { b_blocks[pos] = create_item(relay_target, b[pos], pos, render_fn, is_indexed, false); } else { move(b_blocks[pos], relay_target); } } state.array = b; state.blocks = b_blocks; return; } for (i = b_left - 1; i >= 0; i--) { if (sources[i] === 0) { pos = i + b_start; b_val = b[pos]; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false); } else if (j < 0 || i !== seq[j]) { pos = i + b_start; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); move(b_blocks[pos], target); } else { j--; } } } else if (patched !== b_left) { for (i = b_left - 1; i >= 0; i--) { if (sources[i] === 0) { pos = i + b_start; b_val = b[pos]; next_pos = pos + 1; var target = block_start(b_blocks, next_pos, b_length, anchor); b_blocks[pos] = create_item(target, b_val, pos, render_fn, is_indexed, false); } } } state.array = b; state.blocks = b_blocks; } /** @type {Int32Array} */ let result; /** @type {Int32Array} */ let p; let max_len = 0; // https://en.wikipedia.org/wiki/Longest_increasing_subsequence /** * @param {Int32Array} arr * @returns {Int32Array} */ function lis_algorithm(arr) { let arrI = 0; let i = 0; let j = 0; let k = 0; let u = 0; let v = 0; let c = 0; var len = arr.length; if (len > max_len) { max_len = len; result = new Int32Array(len); p = new Int32Array(len); } for (; i < len; ++i) { arrI = arr[i]; if (arrI !== 0) { j = result[k]; if (arr[j] < arrI) { p[i] = j; result[++k] = i; continue; } u = 0; v = k; while (u < v) { c = (u + v) >> 1; if (arr[result[c]] < arrI) { u = c + 1; } else { v = c; } } if (arrI < arr[result[u]]) { if (u > 0) { p[i] = result[u - 1]; } result[u] = i; } } } u = k + 1; var seq = new Int32Array(u); v = result[u - 1]; while (u-- > 0) { seq[u] = v; v = p[v]; result[u] = 0; } return seq; }