1use super::{
2 ElementTarget, JsValue, Scope, document, reconcile, remove_tree, strings, with_native_root,
3};
4use crate::{Signal, signal, untrack};
5use std::collections::BTreeMap;
6use wasm_bindgen::JsCast;
7use web_sys::{Document, Element, HtmlElement, HtmlInputElement};
8
9type EncodeKey<K> = dyn Fn(&K) -> Result<String, JsValue>;
10
11#[doc(hidden)]
13pub struct HydratedKeys<F, E> {
14 pub key: F,
15 pub encode: E,
16}
17
18enum Removal<'a, K> {
20 Sorted(reconcile::SortedKeys<'a, K>),
22 Direct(Vec<usize>),
24}
25
26struct Row<T> {
27 _state: Signal<T>,
30 scope: Scope,
31}
32
33impl Scope {
34 pub fn keyed<T, K>(
38 &mut self,
39 target: impl ElementTarget,
40 items: impl Fn() -> Vec<T> + 'static,
41 key: impl Fn(&T) -> K + 'static,
42 render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
43 ) -> Result<(), JsValue>
44 where
45 T: Clone + PartialEq + 'static,
46 K: Ord + Clone + 'static,
47 {
48 self.keyed_inner(
49 target,
50 items,
51 key,
52 RowFactory {
53 render,
54 encode: None,
55 },
56 )
57 }
58
59 #[doc(hidden)]
61 pub fn keyed_hydrated<T, K>(
62 &mut self,
63 target: impl ElementTarget,
64 items: impl Fn() -> Vec<T> + 'static,
65 keys: HydratedKeys<
66 impl Fn(&T) -> K + 'static,
67 impl Fn(&K) -> Result<String, JsValue> + 'static,
68 >,
69 render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
70 ) -> Result<(), JsValue>
71 where
72 T: Clone + PartialEq + 'static,
73 K: Ord + Clone + 'static,
74 {
75 self.keyed_inner(
76 target,
77 items,
78 keys.key,
79 RowFactory {
80 render,
81 encode: Some(Box::new(keys.encode)),
82 },
83 )
84 }
85
86 fn keyed_inner<T, K, R>(
87 &mut self,
88 target: impl ElementTarget,
89 items: impl Fn() -> Vec<T> + 'static,
90 key: impl Fn(&T) -> K + 'static,
91 factory: RowFactory<K, R>,
92 ) -> Result<(), JsValue>
93 where
94 T: Clone + PartialEq + 'static,
95 K: Ord + Clone + 'static,
96 R: Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
97 {
98 let hydrating = self.is_hydrating();
99 let mut list = KeyedRows {
100 container: target.resolve(self)?,
101 rows: BTreeMap::new(),
102 order: Vec::new(),
103 states: Vec::new(),
104 initialized: false,
105 settled: false,
106 hydrating,
107 factory,
108 queue: self.mount_queue.clone(),
109 };
110 self.bind(move || {
111 let items = items();
112 untrack(|| {
113 let keys = items.iter().map(&key).collect();
114 list.update(items, keys)
115 })
116 })
117 }
118}
119
120struct RowFactory<K, R> {
121 render: R,
122 encode: Option<Box<EncodeKey<K>>>,
123}
124
125struct KeyedRows<T, K, R> {
126 container: Element,
127 rows: BTreeMap<K, Row<T>>,
129 order: Vec<K>,
130 states: Vec<Signal<T>>,
131 initialized: bool,
132 settled: bool,
133 hydrating: bool,
134 factory: RowFactory<K, R>,
135 queue: Option<std::rc::Rc<super::commit::CommitQueue>>,
136}
137
138struct PreparedRows<T, K> {
139 rows: BTreeMap<K, Row<T>>,
140 positions: Vec<usize>,
141 states: Vec<Signal<T>>,
142}
143
144impl<T, K, R> KeyedRows<T, K, R>
145where
146 T: Clone + PartialEq + 'static,
147 K: Ord + Clone + 'static,
148 R: Fn(Signal<T>) -> Result<Scope, JsValue>,
149{
150 fn update(&mut self, items: Vec<T>, keys: Vec<K>) -> Result<(), JsValue> {
151 let (retained, removal) = self.plan(&keys)?;
152 let native_rows = self.native_rows(&keys)?;
153 let document = document()?;
154 let focused = focused(&document, &self.container);
155 let mut staged = self.prepare_rows(&items, &keys, &retained, &native_rows)?;
156 self.initialize();
157 let next = self.reorder_states(&retained, staged.states);
158 self.remove_rows(removal);
159 let fresh_keys = self.settled.then(|| staged.rows.keys().cloned().collect());
161 self.settled = false;
162 self.merge_rows(&mut staged.rows);
163 let stationary = reconcile::stationary(&staged.positions);
164 for (state, item) in next.iter().zip(items) {
165 state.set(item);
166 }
167 (self.order, self.states) = (keys, next);
168 self.position_rows(&stationary)?;
169 self.commit_rows(fresh_keys);
170 self.settled = true;
171 restore_focus(&document, &self.container, focused)
172 }
173
174 fn plan<'a>(&self, keys: &'a [K]) -> Result<(Vec<usize>, Removal<'a, K>), JsValue> {
175 let duplicate = || JsValue::from_str("fusor: duplicate key in list");
176 match reconcile::small_edit(&self.order, keys, |key| self.rows.contains_key(key)) {
178 Some(plan) => {
179 let reconcile::EditPlan { positions, removed } = plan.map_err(|()| duplicate())?;
180 Ok((positions, Removal::Direct(removed)))
181 }
182 None => {
183 let unique = reconcile::SortedKeys::new(keys).ok_or_else(duplicate)?;
184 let positions = reconcile::previous_positions(&self.order, &unique);
185 Ok((positions, Removal::Sorted(unique)))
186 }
187 }
188 }
189
190 fn native_rows(&self, keys: &[K]) -> Result<Vec<Element>, JsValue> {
191 if !self.hydrating || self.initialized {
192 return Ok(Vec::new());
193 }
194 let encode =
195 self.factory.encode.as_ref().ok_or_else(|| {
196 JsValue::from_str("hydrated lists require generated key metadata")
197 })?;
198 server_rows(&self.container, keys, encode)
199 }
200
201 fn prepare_rows(
202 &self,
203 items: &[T],
204 keys: &[K],
205 retained: &[usize],
206 native_rows: &[Element],
207 ) -> Result<PreparedRows<T, K>, JsValue> {
208 let mut staged = PreparedRows {
210 rows: BTreeMap::new(),
211 positions: retained.to_vec(),
212 states: Vec::new(),
213 };
214 for (index, (key, item)) in keys.iter().zip(items).enumerate() {
215 if retained[index] != reconcile::NEW {
216 continue;
217 }
218 let state = signal(item.clone());
219 let native = native_rows.get(index);
220 let scope = with_native_root(native, || (self.factory.render)(state.clone()))?;
221 if native.is_some() {
223 staged.positions[index] = index;
224 }
225 staged.states.push(state.clone());
226 staged.rows.insert(
227 key.clone(),
228 Row {
229 _state: state,
230 scope,
231 },
232 );
233 }
234 for row in staged.rows.values() {
235 row.scope.finish_prepare()?;
236 }
237 Ok(staged)
238 }
239
240 fn initialize(&mut self) {
241 if self.initialized {
242 return;
243 }
244 if !self.hydrating {
245 #[cfg(feature = "islands")]
246 super::delivery::dispose_tree(&self.container);
247 self.container.set_text_content(None);
248 }
249 self.initialized = true;
250 }
251
252 fn reorder_states(&mut self, retained: &[usize], fresh: Vec<Signal<T>>) -> Vec<Signal<T>> {
253 let mut previous: Vec<_> = std::mem::take(&mut self.states)
255 .into_iter()
256 .map(Some)
257 .collect();
258 let mut fresh = fresh.into_iter();
259 retained
260 .iter()
261 .map(|&position| match position {
262 reconcile::NEW => fresh.next().expect("staged row"),
263 position => previous[position].take().expect("retained row"),
264 })
265 .collect()
266 }
267
268 fn remove_rows(&mut self, mut removal: Removal<'_, K>) {
269 match &mut removal {
270 Removal::Sorted(unique) => self.rows.retain(|key, row| {
271 let keep = unique.contains_next(key);
272 if !keep {
273 remove_tree(&row.scope.root);
274 }
275 keep
276 }),
277 Removal::Direct(removed) => {
278 reconcile::sort_few(removed, &self.order);
280 for &index in removed.iter() {
281 if let Some(entry) = self.rows.remove_entry(&self.order[index]) {
282 remove_tree(&entry.1.scope.root);
283 drop(entry);
284 }
285 }
286 }
287 }
288 }
289
290 fn merge_rows(&mut self, staged: &mut BTreeMap<K, Row<T>>) {
291 if self.rows.is_empty() {
292 self.rows = std::mem::take(staged);
293 } else if staged.len() <= self.rows.len() / (self.rows.len().ilog2() as usize + 1) {
294 self.rows.extend(std::mem::take(staged));
296 } else {
297 self.rows.append(staged);
298 }
299 }
300
301 fn position_rows(&self, stationary: &[bool]) -> Result<(), JsValue> {
302 for (index, keep) in stationary.iter().enumerate().rev() {
303 if !keep {
304 let anchor = self
305 .order
306 .get(index + 1)
307 .map(|key| self.rows[key].scope.root.as_ref());
308 strings::insert_before(
309 &self.container,
310 &self.rows[&self.order[index]].scope.root,
311 anchor,
312 )?;
313 }
314 }
315 Ok(())
316 }
317
318 fn commit_rows(&self, fresh_keys: Option<Vec<K>>) {
319 let idle = self.queue.as_ref().is_none_or(|queue| queue.is_idle());
320 match fresh_keys.filter(|_| idle) {
321 Some(keys) => {
322 for key in &keys {
323 self.rows[key].scope.commit();
324 }
325 }
326 None => {
327 for row in self.rows.values() {
328 row.scope.commit();
329 }
330 }
331 }
332 }
333}
334
335fn server_rows<K>(
337 container: &Element,
338 keys: &[K],
339 encode: &EncodeKey<K>,
340) -> Result<Vec<Element>, JsValue> {
341 let mut encoded = String::new();
345 let mut failure = None;
346 let mut count = 0;
347 for key in keys {
348 match encode(key) {
349 Ok(value) if value.contains('\n') => {
351 return server_rows_one_by_one(container, keys, encode);
352 }
353 Ok(value) => {
354 if count > 0 {
355 encoded.push('\n');
356 }
357 encoded.push_str(&value);
358 count += 1;
359 }
360 Err(error) => {
361 failure = Some(error);
362 break;
363 }
364 }
365 }
366 let rows = strings::server_rows(container, &encoded, count as u32, failure.is_none())?;
367 if let Some(error) = failure {
368 return Err(error);
369 }
370 Ok((0..count as u32)
371 .map(|index| rows.get(index).unchecked_into())
372 .collect())
373}
374
375fn server_rows_one_by_one<K>(
376 container: &Element,
377 keys: &[K],
378 encode: &EncodeKey<K>,
379) -> Result<Vec<Element>, JsValue> {
380 let mut node = container.first_element_child();
381 let mut rows = Vec::with_capacity(keys.len());
382 for key in keys {
383 let row = node
384 .take()
385 .ok_or_else(|| JsValue::from_str("missing native row"))?;
386 if strings::attribute(&row, strings::Name::Key).as_deref() != Some(encode(key)?.as_str()) {
387 return Err(JsValue::from_str("native row key mismatch"));
388 }
389 node = row.next_element_sibling();
390 rows.push(row);
391 }
392 if node.is_some() {
393 return Err(JsValue::from_str("unexpected native row"));
394 }
395 Ok(rows)
396}
397
398type Selection = (u32, u32, String);
399
400fn focused(document: &Document, container: &Element) -> Option<(HtmlElement, Option<Selection>)> {
402 let focused = document
403 .active_element()
404 .filter(|node| container.contains(Some(node)))?
405 .dyn_into::<HtmlElement>()
406 .ok()?;
407 let selection = focused.dyn_ref::<HtmlInputElement>().and_then(|input| {
408 Some((
409 input.selection_start().ok()??,
410 input.selection_end().ok()??,
411 input.selection_direction().ok()??,
412 ))
413 });
414 Some((focused, selection))
415}
416
417fn restore_focus(
420 document: &Document,
421 container: &Element,
422 focused: Option<(HtmlElement, Option<Selection>)>,
423) -> Result<(), JsValue> {
424 let Some((focused, selection)) = focused.filter(|(node, _)| container.contains(Some(node)))
425 else {
426 return Ok(());
427 };
428 if document
429 .active_element()
430 .is_some_and(|node| node.is_same_node(Some(&focused)))
431 {
432 return Ok(());
433 }
434 focused.focus()?;
435 if let (Some(input), Some((start, end, direction))) =
436 (focused.dyn_ref::<HtmlInputElement>(), selection)
437 {
438 input.set_selection_range_with_direction(start, end, &direction)?;
439 }
440 Ok(())
441}