|
6 | 6 | isShadowRoot,
|
7 | 7 | needMaskingText,
|
8 | 8 | maskInputValue,
|
9 |
| - Mirror, |
10 | 9 | isNativeShadowDom,
|
11 | 10 | getInputType,
|
12 | 11 | toLowerCase,
|
@@ -173,49 +172,123 @@ export default class MutationBuffer {
|
173 | 172 | const adds: addedNodeMutation[] = [];
|
174 | 173 | const addedIds = new Set<number>();
|
175 | 174 |
|
176 |
| - const getNextId = (n: Node): number | null => { |
177 |
| - let ns: Node | null = n; |
178 |
| - let nextId: number | null = IGNORED_NODE; // slimDOM: ignored |
179 |
| - while (nextId === IGNORED_NODE) { |
180 |
| - ns = ns && ns.nextSibling; |
181 |
| - nextId = ns && this.mirror.getId(ns); |
| 175 | + while (this.mapRemoves.length) { |
| 176 | + this.mirror.removeNodeFromMap(this.mapRemoves.shift()!); |
| 177 | + } |
| 178 | + |
| 179 | + for (const n of this.movedSet) { |
| 180 | + const parentNode = dom.parentNode(n); |
| 181 | + if ( |
| 182 | + parentNode && // can't be removed if it doesn't exist |
| 183 | + this.removesSubTreeCache.has(parentNode) && |
| 184 | + !this.movedSet.has(parentNode) |
| 185 | + ) { |
| 186 | + continue; |
182 | 187 | }
|
183 |
| - return nextId; |
184 |
| - }; |
185 |
| - const pushAdd = (n: Node) => { |
186 |
| - const parent = dom.parentNode(n); |
187 |
| - if (!parent) { |
188 |
| - return; |
| 188 | + this.addedSet.add(n); |
| 189 | + } |
| 190 | + |
| 191 | + let n: Node | null = null; |
| 192 | + let parentNode: Node | null = null; |
| 193 | + let parentId: number | null = null; |
| 194 | + let nextSibling: Node | null = null; |
| 195 | + let ancestorBad = false; |
| 196 | + const missingParents = new Set<Node>(); |
| 197 | + while (this.addedSet.size) { |
| 198 | + if (n !== null && this.addedSet.has(n.previousSibling)) { |
| 199 | + // reuse parentNode, parentId, ancestorBad |
| 200 | + nextSibling = n; // n is a good next sibling |
| 201 | + n = n.previousSibling; |
| 202 | + } else { |
| 203 | + n = this.addedSet.values().next().value; // pop |
| 204 | + |
| 205 | + while (true) { |
| 206 | + parentNode = dom.parentNode(n); |
| 207 | + if (this.addedSet.has(parentNode)) { |
| 208 | + // start at top of added tree so as not to serialize children before their parents (parentId requirement) |
| 209 | + n = parentNode; |
| 210 | + continue; |
| 211 | + } |
| 212 | + break; |
| 213 | + } |
| 214 | + |
| 215 | + if (missingParents.has(parentNode)) { |
| 216 | + parentNode = null; |
| 217 | + } else if (parentNode) { |
| 218 | + // we have a new parentNode for a 'row' of DOM children |
| 219 | + // perf: we reuse these calculations across all child nodes |
| 220 | + |
| 221 | + ancestorBad = |
| 222 | + isSelfOrAncestorInSet(this.droppedSet, parentNode) || |
| 223 | + this.removesSubTreeCache.has(parentNode); |
| 224 | + |
| 225 | + if (ancestorBad && isSelfOrAncestorInSet(this.movedSet, n)) { |
| 226 | + // not bad, just moved |
| 227 | + ancestorBad = false; |
| 228 | + } |
| 229 | + |
| 230 | + if (!inDom(parentNode)) { |
| 231 | + ancestorBad = true; |
| 232 | + } |
| 233 | + |
| 234 | + while (true) { |
| 235 | + nextSibling = n.nextSibling; |
| 236 | + if (this.addedSet.has(nextSibling)) { |
| 237 | + // keep going as we can't serialize a node before it's next sibling (nextId requirement) |
| 238 | + n = nextSibling; |
| 239 | + continue; |
| 240 | + } |
| 241 | + break; |
| 242 | + } |
| 243 | + |
| 244 | + parentId = isShadowRoot(parentNode) |
| 245 | + ? this.mirror.getId(getShadowHost(n)) |
| 246 | + : this.mirror.getId(parentNode); |
| 247 | + |
| 248 | + // If the node is the direct child of a shadow root, we treat the shadow host as its parent node. |
| 249 | + if ( |
| 250 | + parentId === -1 && |
| 251 | + parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE |
| 252 | + ) { |
| 253 | + const shadowHost = dom.host(parentNode as ShadowRoot); |
| 254 | + parentId = this.mirror.getId(shadowHost); |
| 255 | + } |
| 256 | + } |
189 | 257 | }
|
190 | 258 |
|
191 |
| - let parentId = isShadowRoot(parent) |
192 |
| - ? this.mirror.getId(getShadowHost(n)) |
193 |
| - : this.mirror.getId(parent); |
| 259 | + this.addedSet.delete(n); // don't re-iterate |
194 | 260 |
|
195 |
| - // If the node is the direct child of a shadow root, we treat the shadow host as its parent node. |
196 |
| - if (parentId === -1 && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { |
197 |
| - const shadowHost = dom.host(parent as ShadowRoot); |
198 |
| - parentId = this.mirror.getId(shadowHost); |
199 |
| - } else if (!inDom(n)) { |
200 |
| - return; |
| 261 | + if (!parentNode || parentId === -1) { |
| 262 | + missingParents.add(n); // ensure any added child nodes can also early-out |
| 263 | + continue; |
| 264 | + } else if (ancestorBad) { |
| 265 | + // it's possible we could unify missingParents and this.droppedSet |
| 266 | + // but would need to check the subtleties |
| 267 | + this.droppedSet.add(n); |
| 268 | + continue; |
201 | 269 | }
|
202 | 270 |
|
203 | 271 | let cssCaptured = false;
|
204 | 272 | if (n.nodeType === Node.TEXT_NODE) {
|
205 |
| - const parentTag = (parent as Element).tagName; |
| 273 | + const parentTag = (parentNode as Element).tagName; |
206 | 274 | if (parentTag === 'TEXTAREA') {
|
207 | 275 | // genTextAreaValueMutation already called via parent
|
208 |
| - return; |
209 |
| - } else if (parentTag === 'STYLE' && (this.addedSet.has(parent) || addedIds.has(parentId))) { |
| 276 | + continue; |
| 277 | + } else if (parentTag === 'STYLE' && addedIds.has(parentId)) { |
210 | 278 | // css content will be recorded via parent's _cssText attribute when
|
211 | 279 | // mutation adds entire <style> element
|
212 | 280 | cssCaptured = true;
|
213 | 281 | }
|
214 | 282 | }
|
215 | 283 |
|
216 |
| - const nextId = getNextId(n); |
217 |
| - if (parentId === -1 || nextId === -1) { |
218 |
| - return; |
| 284 | + let ns: Node | null = n; |
| 285 | + let nextId: number | null = IGNORED_NODE; // slimDOM: ignored |
| 286 | + while (nextId === IGNORED_NODE) { |
| 287 | + ns = ns && ns.nextSibling; |
| 288 | + nextId = ns && this.mirror.getId(ns); |
| 289 | + } |
| 290 | + if (nextId === -1) { |
| 291 | + continue; |
219 | 292 | }
|
220 | 293 | const sn = serializeNodeWithId(n, {
|
221 | 294 | doc: this.doc,
|
@@ -265,49 +338,6 @@ export default class MutationBuffer {
|
265 | 338 | });
|
266 | 339 | addedIds.add(sn.id);
|
267 | 340 | }
|
268 |
| - }; |
269 |
| - |
270 |
| - while (this.mapRemoves.length) { |
271 |
| - this.mirror.removeNodeFromMap(this.mapRemoves.shift()!); |
272 |
| - } |
273 |
| - |
274 |
| - for (const n of this.movedSet) { |
275 |
| - if ( |
276 |
| - isParentRemoved(this.removesSubTreeCache, n, this.mirror) && |
277 |
| - !this.movedSet.has(dom.parentNode(n)!) |
278 |
| - ) { |
279 |
| - continue; |
280 |
| - } |
281 |
| - this.addedSet.add(n); |
282 |
| - } |
283 |
| - |
284 |
| - let n = null; |
285 |
| - while (this.addedSet.size) { |
286 |
| - if (n === null || !this.addedSet.has(n.previousSibling)) { |
287 |
| - n = this.addedSet.values().next().value; // pop |
288 |
| - while (this.addedSet.has(dom.parentNode(n))) { |
289 |
| - // start as high up as we can |
290 |
| - n = dom.parentNode(n); |
291 |
| - } |
292 |
| - while (this.addedSet.has(n.nextSibling)) { |
293 |
| - // keep going until we find one that can be pushed now |
294 |
| - n = n.nextSibling; |
295 |
| - } |
296 |
| - } else { |
297 |
| - // n will have a good nextSibling |
298 |
| - n = n.previousSibling; |
299 |
| - } |
300 |
| - this.addedSet.delete(n); |
301 |
| - if ( |
302 |
| - !isAncestorInSet(this.droppedSet, n) && |
303 |
| - !isParentRemoved(this.removesSubTreeCache, n, this.mirror) |
304 |
| - ) { |
305 |
| - pushAdd(n); |
306 |
| - } else if (isAncestorInSet(this.movedSet, n)) { |
307 |
| - pushAdd(n); |
308 |
| - } else { |
309 |
| - this.droppedSet.add(n); |
310 |
| - } |
311 | 341 | }
|
312 | 342 |
|
313 | 343 | const payload = {
|
@@ -690,33 +720,18 @@ function processRemoves(n: Node, cache: Set<Node>) {
|
690 | 720 | return;
|
691 | 721 | }
|
692 | 722 |
|
693 |
| -function isParentRemoved(removes: Set<Node>, n: Node, mirror: Mirror): boolean { |
694 |
| - if (removes.size === 0) return false; |
695 |
| - return _isParentRemoved(removes, n, mirror); |
696 |
| -} |
697 |
| - |
698 |
| -function _isParentRemoved( |
699 |
| - removes: Set<Node>, |
700 |
| - n: Node, |
701 |
| - _mirror: Mirror, |
702 |
| -): boolean { |
703 |
| - const node: ParentNode | null = dom.parentNode(n); |
704 |
| - if (!node) return false; |
705 |
| - return removes.has(node); |
706 |
| -} |
707 |
| - |
708 |
| -function isAncestorInSet(set: Set<Node>, n: Node): boolean { |
| 723 | +function isSelfOrAncestorInSet(set: Set<Node>, n: Node): boolean { |
709 | 724 | if (set.size === 0) return false;
|
710 |
| - return _isAncestorInSet(set, n); |
| 725 | + return _isSelfOrAncestorInSet(set, n); |
711 | 726 | }
|
712 | 727 |
|
713 |
| -function _isAncestorInSet(set: Set<Node>, n: Node): boolean { |
| 728 | +function _isSelfOrAncestorInSet(set: Set<Node>, n: Node): boolean { |
| 729 | + if (set.has(n)) { |
| 730 | + return true; |
| 731 | + } |
714 | 732 | const parent = dom.parentNode(n);
|
715 | 733 | if (!parent) {
|
716 | 734 | return false;
|
717 | 735 | }
|
718 |
| - if (set.has(parent)) { |
719 |
| - return true; |
720 |
| - } |
721 |
| - return _isAncestorInSet(set, parent); |
| 736 | + return _isSelfOrAncestorInSet(set, parent); |
722 | 737 | }
|
0 commit comments