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
11enum Removal<'a, K> {
13 Sorted(reconcile::SortedKeys<'a, K>),
15 Direct(Vec<usize>),
17}
18
19struct Row<T> {
20 _state: Signal<T>,
23 scope: Scope,
24}
25
26impl Scope {
27 pub fn keyed<T, K>(
31 &mut self,
32 target: impl ElementTarget,
33 items: impl Fn() -> Vec<T> + 'static,
34 key: impl Fn(&T) -> K + 'static,
35 render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
36 ) -> Result<(), JsValue>
37 where
38 T: Clone + PartialEq + 'static,
39 K: Ord + Clone + 'static,
40 {
41 self.keyed_inner(target, items, key, render, None)
42 }
43
44 #[doc(hidden)]
46 pub fn keyed_hydrated<T, K>(
47 &mut self,
48 target: impl ElementTarget,
49 items: impl Fn() -> Vec<T> + 'static,
50 key: impl Fn(&T) -> K + 'static,
51 render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
52 encode: impl Fn(&K) -> Result<String, JsValue> + 'static,
53 ) -> Result<(), JsValue>
54 where
55 T: Clone + PartialEq + 'static,
56 K: Ord + Clone + 'static,
57 {
58 self.keyed_inner(target, items, key, render, Some(Box::new(encode)))
59 }
60
61 fn keyed_inner<T, K>(
62 &mut self,
63 target: impl ElementTarget,
64 items: impl Fn() -> Vec<T> + 'static,
65 key: impl Fn(&T) -> K + 'static,
66 render: impl Fn(Signal<T>) -> Result<Scope, JsValue> + 'static,
67 encode: Option<Box<EncodeKey<K>>>,
68 ) -> Result<(), JsValue>
69 where
70 T: Clone + PartialEq + 'static,
71 K: Ord + Clone + 'static,
72 {
73 let hydrating = self.is_hydrating();
74 let container = target.resolve(self)?;
75 let mut rows: BTreeMap<K, Row<T>> = BTreeMap::new();
79 let mut order: Vec<K> = Vec::new();
80 let mut states: Vec<Signal<T>> = Vec::new();
81 let mut initialized = false;
82 let mut settled = false;
84 let queue = self.mount_queue.clone();
85 self.bind(move || {
86 let items = items();
87 untrack(|| {
88 let keys: Vec<K> = items.iter().map(&key).collect();
89 let duplicate = || JsValue::from_str("fusor: duplicate key in list");
90 let (retained, mut removal) =
93 match reconcile::small_edit(&order, &keys, |key| rows.contains_key(key)) {
94 Some(plan) => {
95 let (positions, removed) = plan.map_err(|()| duplicate())?;
96 (positions, Removal::Direct(removed))
97 }
98 None => {
99 let unique = reconcile::SortedKeys::new(&keys).ok_or_else(duplicate)?;
100 let positions = reconcile::previous_positions(&order, &unique);
101 (positions, Removal::Sorted(unique))
102 }
103 };
104 let native_rows = if hydrating && !initialized {
105 let encode = encode.as_ref().ok_or_else(|| {
106 JsValue::from_str("hydrated lists require generated key metadata")
107 })?;
108 server_rows(&container, &keys, encode)?
109 } else {
110 Vec::new()
111 };
112 let document = document()?;
113 let focused = focused(&document, &container);
114 let mut staged = BTreeMap::new();
117 let mut positions = retained.clone();
118 let mut fresh = Vec::new();
119 for (index, (key, item)) in keys.iter().zip(&items).enumerate() {
120 if retained[index] != reconcile::NEW {
121 continue;
122 }
123 let state = signal(item.clone());
124 let native = native_rows.get(index);
125 let scope = with_native_root(native, || render(state.clone()))?;
126 if native.is_some() {
129 positions[index] = index;
130 }
131 fresh.push(state.clone());
132 staged.insert(
133 key.clone(),
134 Row {
135 _state: state,
136 scope,
137 },
138 );
139 }
140 for row in staged.values() {
141 row.scope.finish_prepare()?;
142 }
143 if !initialized {
144 if !hydrating {
145 #[cfg(feature = "islands")]
146 super::delivery::dispose_tree(&container);
147 container.set_text_content(None);
148 }
149 initialized = true;
150 }
151 let mut previous: Vec<_> =
154 std::mem::take(&mut states).into_iter().map(Some).collect();
155 let mut fresh = fresh.into_iter();
156 let next: Vec<Signal<T>> = retained
157 .iter()
158 .map(|&position| match position {
159 reconcile::NEW => fresh.next().expect("staged row"),
160 position => previous[position].take().expect("retained row"),
161 })
162 .collect();
163 drop(previous);
164 match &mut removal {
165 Removal::Sorted(unique) => rows.retain(|key, row| {
166 let keep = unique.contains_next(key);
167 if !keep {
168 remove_tree(&row.scope.root);
169 }
170 keep
171 }),
172 Removal::Direct(removed) => {
173 reconcile::sort_few(removed, &order);
175 for &index in removed.iter() {
176 if let Some(entry) = rows.remove_entry(&order[index]) {
177 remove_tree(&entry.1.scope.root);
180 drop(entry);
181 }
182 }
183 }
184 }
185 let fresh_keys: Option<Vec<K>> = settled.then(|| staged.keys().cloned().collect());
189 settled = false;
190 if rows.is_empty() {
191 rows = staged;
192 } else if staged.len() <= rows.len() / (rows.len().ilog2() as usize + 1) {
193 rows.extend(staged);
196 } else {
197 rows.append(&mut staged);
198 }
199 let stationary = reconcile::stationary(&positions);
200 for (state, item) in next.iter().zip(items) {
201 state.set(item);
202 }
203 (order, states) = (keys, next);
204 for (index, keep) in stationary.iter().enumerate().rev() {
205 if !keep {
206 let anchor = order
207 .get(index + 1)
208 .map(|key| rows[key].scope.root.as_ref());
209 strings::insert_before(
210 &container,
211 &rows[&order[index]].scope.root,
212 anchor,
213 )?;
214 }
215 }
216 let idle = queue.as_ref().is_none_or(|queue| queue.is_idle());
217 match fresh_keys.filter(|_| idle) {
218 Some(keys) => {
219 for key in &keys {
220 rows[key].scope.commit();
221 }
222 }
223 None => {
224 for row in rows.values() {
225 row.scope.commit();
226 }
227 }
228 }
229 settled = true;
230 restore_focus(&document, &container, focused)
231 })
232 })
233 }
234}
235
236fn server_rows<K>(
238 container: &Element,
239 keys: &[K],
240 encode: &EncodeKey<K>,
241) -> Result<Vec<Element>, JsValue> {
242 let mut encoded = String::new();
246 let mut failure = None;
247 let mut count = 0;
248 for key in keys {
249 match encode(key) {
250 Ok(value) if value.contains('\n') => {
252 return server_rows_one_by_one(container, keys, encode);
253 }
254 Ok(value) => {
255 if count > 0 {
256 encoded.push('\n');
257 }
258 encoded.push_str(&value);
259 count += 1;
260 }
261 Err(error) => {
262 failure = Some(error);
263 break;
264 }
265 }
266 }
267 let rows = strings::server_rows(container, &encoded, count as u32, failure.is_none())?;
268 if let Some(error) = failure {
269 return Err(error);
270 }
271 Ok((0..count as u32)
272 .map(|index| rows.get(index).unchecked_into())
273 .collect())
274}
275
276fn server_rows_one_by_one<K>(
277 container: &Element,
278 keys: &[K],
279 encode: &EncodeKey<K>,
280) -> Result<Vec<Element>, JsValue> {
281 let mut node = container.first_element_child();
282 let mut rows = Vec::with_capacity(keys.len());
283 for key in keys {
284 let row = node
285 .take()
286 .ok_or_else(|| JsValue::from_str("missing native row"))?;
287 if strings::attribute(&row, strings::Name::Key).as_deref() != Some(encode(key)?.as_str()) {
288 return Err(JsValue::from_str("native row key mismatch"));
289 }
290 node = row.next_element_sibling();
291 rows.push(row);
292 }
293 if node.is_some() {
294 return Err(JsValue::from_str("unexpected native row"));
295 }
296 Ok(rows)
297}
298
299type Selection = (u32, u32, String);
300
301fn focused(document: &Document, container: &Element) -> Option<(HtmlElement, Option<Selection>)> {
303 let focused = document
304 .active_element()
305 .filter(|node| container.contains(Some(node)))?
306 .dyn_into::<HtmlElement>()
307 .ok()?;
308 let selection = focused.dyn_ref::<HtmlInputElement>().and_then(|input| {
309 Some((
310 input.selection_start().ok()??,
311 input.selection_end().ok()??,
312 input.selection_direction().ok()??,
313 ))
314 });
315 Some((focused, selection))
316}
317
318fn restore_focus(
321 document: &Document,
322 container: &Element,
323 focused: Option<(HtmlElement, Option<Selection>)>,
324) -> Result<(), JsValue> {
325 let Some((focused, selection)) = focused.filter(|(node, _)| container.contains(Some(node)))
326 else {
327 return Ok(());
328 };
329 if document
330 .active_element()
331 .is_some_and(|node| node.is_same_node(Some(&focused)))
332 {
333 return Ok(());
334 }
335 focused.focus()?;
336 if let (Some(input), Some((start, end, direction))) =
337 (focused.dyn_ref::<HtmlInputElement>(), selection)
338 {
339 input.set_selection_range_with_direction(start, end, &direction)?;
340 }
341 Ok(())
342}