|
1 | 1 | import { has_prop } from './utils';
|
2 | 2 |
|
3 |
| -export function append(target: Node, node: Node) { |
4 |
| - target.appendChild(node); |
| 3 | +// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM |
| 4 | +// at the end of hydration without touching the remaining nodes. |
| 5 | +let is_hydrating = false; |
| 6 | + |
| 7 | +export function start_hydrating() { |
| 8 | + is_hydrating = true; |
| 9 | +} |
| 10 | +export function end_hydrating() { |
| 11 | + is_hydrating = false; |
| 12 | +} |
| 13 | + |
| 14 | +export function append(target: Node & {actual_end_child?: Node | null}, node: Node) { |
| 15 | + if (is_hydrating) { |
| 16 | + // If we are just starting with this target, we will insert before the firstChild (which may be null) |
| 17 | + if (target.actual_end_child === undefined) { |
| 18 | + target.actual_end_child = target.firstChild; |
| 19 | + } |
| 20 | + if (node.parentNode !== target) { |
| 21 | + target.insertBefore(node, target.actual_end_child); |
| 22 | + } else { |
| 23 | + target.actual_end_child = node.nextSibling; |
| 24 | + } |
| 25 | + } else if (node.parentNode !== target) { |
| 26 | + target.appendChild(node); |
| 27 | + } |
5 | 28 | }
|
6 | 29 |
|
7 | 30 | export function insert(target: Node, node: Node, anchor?: Node) {
|
8 |
| - target.insertBefore(node, anchor || null); |
| 31 | + if (is_hydrating && !anchor) { |
| 32 | + append(target, node); |
| 33 | + } else if (node.parentNode !== target || (anchor && node.nextSibling !== anchor)) { |
| 34 | + target.insertBefore(node, anchor || null); |
| 35 | + } |
9 | 36 | }
|
10 | 37 |
|
11 | 38 | export function detach(node: Node) {
|
@@ -149,42 +176,80 @@ export function time_ranges_to_array(ranges) {
|
149 | 176 | return array;
|
150 | 177 | }
|
151 | 178 |
|
152 |
| -export function children(element) { |
| 179 | +export function children(element: HTMLElement) { |
153 | 180 | return Array.from(element.childNodes);
|
154 | 181 | }
|
155 | 182 |
|
156 |
| -export function claim_element(nodes, name, attributes, svg) { |
157 |
| - for (let i = 0; i < nodes.length; i += 1) { |
| 183 | +type ChildNodeArray = ChildNode[] & { |
| 184 | + /** |
| 185 | + * All nodes at or after this index are available for preservation (not getting detached) |
| 186 | + */ |
| 187 | + lastKeepIndex?: number; |
| 188 | +}; |
| 189 | + |
| 190 | +function claim_node<R extends ChildNode>(nodes: ChildNodeArray, predicate: (node: ChildNode) => node is R, processNode: (node: ChildNode) => void, createNode: () => R) { |
| 191 | + if (nodes.lastKeepIndex === undefined) { |
| 192 | + nodes.lastKeepIndex = 0; |
| 193 | + } |
| 194 | + |
| 195 | + // We first try to find a node we can actually keep without detaching |
| 196 | + // This node should be after the previous node that we chose to keep without detaching |
| 197 | + for (let i = nodes.lastKeepIndex; i < nodes.length; i++) { |
| 198 | + const node = nodes[i]; |
| 199 | + |
| 200 | + if (predicate(node)) { |
| 201 | + processNode(node); |
| 202 | + |
| 203 | + nodes.splice(i, 1); |
| 204 | + nodes.lastKeepIndex = i; |
| 205 | + return node; |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + |
| 210 | + // Otherwise, we try to find a node that we should detach |
| 211 | + for (let i = 0; i < nodes.lastKeepIndex; i++) { |
158 | 212 | const node = nodes[i];
|
159 |
| - if (node.nodeName === name) { |
160 |
| - let j = 0; |
| 213 | + |
| 214 | + if (predicate(node)) { |
| 215 | + processNode(node); |
| 216 | + |
| 217 | + nodes.splice(i, 1); |
| 218 | + nodes.lastKeepIndex -= 1; |
| 219 | + detach(node); |
| 220 | + return node; |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + // If we can't find any matching node, we create a new one |
| 225 | + return createNode(); |
| 226 | +} |
| 227 | + |
| 228 | +export function claim_element(nodes: ChildNodeArray, name: string, attributes: {[key: string]: boolean}, svg) { |
| 229 | + return claim_node<Element | SVGElement>( |
| 230 | + nodes, |
| 231 | + (node: ChildNode): node is Element | SVGElement => node.nodeName === name, |
| 232 | + (node: Element) => { |
161 | 233 | const remove = [];
|
162 |
| - while (j < node.attributes.length) { |
163 |
| - const attribute = node.attributes[j++]; |
| 234 | + for (let j = 0; j < node.attributes.length; j++) { |
| 235 | + const attribute = node.attributes[j]; |
164 | 236 | if (!attributes[attribute.name]) {
|
165 | 237 | remove.push(attribute.name);
|
166 | 238 | }
|
167 | 239 | }
|
168 |
| - for (let k = 0; k < remove.length; k++) { |
169 |
| - node.removeAttribute(remove[k]); |
170 |
| - } |
171 |
| - return nodes.splice(i, 1)[0]; |
172 |
| - } |
173 |
| - } |
174 |
| - |
175 |
| - return svg ? svg_element(name) : element(name); |
| 240 | + remove.forEach(v => node.removeAttribute(v)); |
| 241 | + }, |
| 242 | + () => svg ? svg_element(name as keyof SVGElementTagNameMap) : element(name as keyof HTMLElementTagNameMap) |
| 243 | + ); |
176 | 244 | }
|
177 | 245 |
|
178 |
| -export function claim_text(nodes, data) { |
179 |
| - for (let i = 0; i < nodes.length; i += 1) { |
180 |
| - const node = nodes[i]; |
181 |
| - if (node.nodeType === 3) { |
182 |
| - node.data = '' + data; |
183 |
| - return nodes.splice(i, 1)[0]; |
184 |
| - } |
185 |
| - } |
186 |
| - |
187 |
| - return text(data); |
| 246 | +export function claim_text(nodes: ChildNodeArray, data) { |
| 247 | + return claim_node<Text>( |
| 248 | + nodes, |
| 249 | + (node: ChildNode): node is Text => node.nodeType === 3, |
| 250 | + (node: Text) => node.data = '' + data, |
| 251 | + () => text(data) |
| 252 | + ); |
188 | 253 | }
|
189 | 254 |
|
190 | 255 | export function claim_space(nodes) {
|
|
0 commit comments