ripple
Version:
Ripple is an elegant TypeScript UI framework
836 lines (752 loc) • 18.3 kB
JavaScript
/** @import { Block, Tracked } from '#client' */
import { IS_CONTROLLED, IS_INDEXED } from '../../../constants.js';
import { branch, destroy_block, destroy_block_children, 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 { 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 b = branch(() => {
var tracked_index;
/** @type {V | Tracked} */
var tracked_value = value;
if (is_indexed || is_keyed) {
var block = /** @type {Block} */ (active_block);
if (block.s === null) {
if (is_indexed) {
tracked_index = tracked(index, block);
}
if (is_keyed) {
tracked_value = tracked(value, block);
}
block.s = {
start: null,
end: null,
i: tracked_index,
v: tracked_value,
};
} else {
if (is_indexed) {
tracked_index = block.s.i;
}
if (is_keyed) {
tracked_index = block.s.v;
}
}
render_fn(anchor, tracked_value, tracked_index);
} else {
render_fn(anchor, tracked_value);
}
});
return b;
}
/**
* @param {Block} block
* @param {Element} anchor
* @returns {void}
*/
function move(block, anchor) {
var node = block.s.start;
var end = block.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(end);
break;
}
}
}
/**
* @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
* @returns {void}
*/
export function for_block(node, get_collection, render_fn, flags) {
var is_controlled = (flags & IS_CONTROLLED) !== 0;
var is_indexed = (flags & IS_INDEXED) !== 0;
var anchor = /** @type {Element | Text} */ (node);
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) {
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);
});
if (hydrating) {
anchor = /** @type {Element | Text} */ (hydrate_node);
}
},
null,
FOR_BLOCK,
);
}
/**
* @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]
* @returns {void}
*/
export function for_block_keyed(node, get_collection, render_fn, flags, get_key) {
var is_controlled = (flags & IS_CONTROLLED) !== 0;
var is_indexed = (flags & IS_INDEXED) !== 0;
var anchor = /** @type {Element | Text} */ (node);
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) {
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),
);
});
},
null,
FOR_BLOCK,
);
}
/**
* @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 = [];
}
/**
* @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) {
set(block.s.v, 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
* @returns {void}
*/
function reconcile_by_key(anchor, block, b, render_fn, is_controlled, is_indexed, get_key) {
var state = block.s;
// Variables used in conditional branches - declare with initial values
/** @type {number} */
var a_start = 0;
/** @type {number} */
var b_start = 0;
/** @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,
};
}
var a = state.array;
var a_length = a.length;
var b_length = b.length;
var j = 0;
// 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_val = a[j];
var b_val = b[j];
var a_key = a_keys[j];
var b_key = b_keys[j];
var a_end = a_length - 1;
var b_end = b_length - 1;
var b_block;
outer: {
while (a_key === b_key) {
a[j] = b_val;
b_block = b_blocks[j] = a_blocks[j];
if (is_indexed) {
update_index(b_block, j);
}
update_value(b_block, b_val);
++j;
if (j > a_end || j > b_end) {
break outer;
}
a_val = a[j];
b_val = b[j];
a_key = a_keys[j];
b_key = b_keys[j];
}
a_val = a[a_end];
b_val = b[b_end];
a_key = a_keys[a_end];
b_key = b_keys[b_end];
while (a_key === b_key) {
a[a_end] = b_val;
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--;
if (j > a_end || j > b_end) {
break outer;
}
a_val = a[a_end];
b_val = b[b_end];
a_key = a_keys[a_end];
b_key = b_keys[b_end];
}
}
var fast_path_removal = false;
if (j > a_end) {
if (j <= b_end) {
while (j <= b_end) {
b_val = b[j];
var target = j >= a_length ? anchor : a_blocks[j].s.start;
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, true);
j++;
}
}
} else if (j > b_end) {
while (j <= a_end) {
destroy_block(a_blocks[j++]);
}
} else {
a_start = j;
b_start = j;
a_left = a_end - j + 1;
b_left = b_end - j + 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) {
a_val = a[i];
a_key = a_keys[i];
if (patched < b_left) {
for (j = b_start; j <= b_end; j++) {
b_val = b[j];
b_key = b_keys[j];
if (a_key === b_key) {
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_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) {
a_val = a[i];
a_key = a_keys[i];
if (patched < b_left) {
j = map.get(a_key);
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;
}
block = b_blocks[j] = a_blocks[i];
b_val = b[j];
if (is_indexed) {
update_index(block, j);
}
update_value(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;
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 = next_pos < b_length ? b_blocks[next_pos].s.start : 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;
b_val = b[pos];
next_pos = pos + 1;
var target = next_pos < b_length ? b_blocks[next_pos].s.start : 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 = next_pos < b_length ? b_blocks[next_pos].s.start : 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
* @returns {void}
*/
function reconcile_by_ref(anchor, block, b, render_fn, is_controlled, is_indexed) {
var state = block.s;
// Variables used in conditional branches - declare with initial values
/** @type {number} */
var a_start = 0;
/** @type {number} */
var b_start = 0;
/** @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,
};
}
var a = state.array;
var a_length = a.length;
var b_length = b.length;
var j = 0;
// 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_val = a[j];
var b_val = b[j];
var a_end = a_length - 1;
var b_end = b_length - 1;
var b_block;
outer: {
while (a_val === b_val) {
a[j] = b_val;
b_block = b_blocks[j] = a_blocks[j];
if (is_indexed) {
update_index(b_block, j);
}
++j;
if (j > a_end || j > b_end) {
break outer;
}
a_val = a[j];
b_val = b[j];
}
a_val = a[a_end];
b_val = b[b_end];
while (a_val === b_val) {
a[a_end] = b_val;
b_block = b_blocks[b_end] = a_blocks[a_end];
if (is_indexed) {
update_index(b_block, b_end);
}
a_end--;
b_end--;
if (j > a_end || j > b_end) {
break outer;
}
a_val = a[a_end];
b_val = b[b_end];
}
}
var fast_path_removal = false;
if (j > a_end) {
if (j <= b_end) {
while (j <= b_end) {
b_val = b[j];
var target = j >= a_length ? anchor : a_blocks[j].s.start;
b_blocks[j] = create_item(target, b_val, j, render_fn, is_indexed, false);
j++;
}
}
} else if (j > b_end) {
while (j <= a_end) {
destroy_block(a_blocks[j++]);
}
} else {
a_start = j;
b_start = j;
a_left = a_end - j + 1;
b_left = b_end - j + 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) {
a_val = a[i];
if (patched < b_left) {
for (j = b_start; j <= b_end; j++) {
b_val = b[j];
if (a_val === b_val) {
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_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) {
a_val = a[i];
if (patched < b_left) {
j = map.get(a_val);
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;
}
block = b_blocks[j] = a_blocks[i];
if (is_indexed) {
update_index(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;
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 = next_pos < b_length ? b_blocks[next_pos].s.start : 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;
b_val = b[pos];
next_pos = pos + 1;
var target = next_pos < b_length ? b_blocks[next_pos].s.start : 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 = next_pos < b_length ? b_blocks[next_pos].s.start : 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;
}