Skip to content

Collapse range before mutations in deleteContents and extract#1452

Merged
annevk merged 3 commits intowhatwg:mainfrom
stevenobiajulu:fix-range-collapse-before-mutations
Mar 9, 2026
Merged

Collapse range before mutations in deleteContents and extract#1452
annevk merged 3 commits intowhatwg:mainfrom
stevenobiajulu:fix-range-collapse-before-mutations

Conversation

@stevenobiajulu
Copy link
Copy Markdown
Contributor

@stevenobiajulu stevenobiajulu commented Feb 24, 2026

Collapse range before mutations in deleteContents and extract

Move the step that sets the range's start and end to (newNode, newOffset) from after DOM mutations to before them, in both deleteContents() and the extract algorithm. The DOM's built-in live range maintenance mechanisms (live range pre-remove steps and replace data range adjustments) keep the range valid through subsequent operations. Collapsing after mutations is unreliable because script can run during removal (via removing steps) and modify the DOM, invalidating the pre-computed values.

Fixes #1446.


(See WHATWG Working Mode: Changes for more details.)

Move the step that sets the range's start and end to (newNode, newOffset)
from after DOM mutations to before them, in both the deleteContents()
and extract() algorithms.

The pre-computed collapse target can become invalid if script runs during
the remove operation (via "removing steps" hooks) and modifies the DOM.
By collapsing the range before mutations, the DOM's built-in live range
maintenance mechanisms (live range pre-remove steps and replace data
range adjustments) automatically keep the range valid through all
subsequent operations.

Fixes whatwg#1446.
Comment thread dom.bs Outdated
Copy link
Copy Markdown
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this. Let's remove the elaborate notes and put a variant of them in the commit message.

Comment thread dom.bs Outdated
@stevenobiajulu
Copy link
Copy Markdown
Contributor Author

@annevk Updated per your review — removed the explanatory notes from both algorithms. The rationale is in the commit message. Also updated the implementer interest checklist.

@stevenobiajulu
Copy link
Copy Markdown
Contributor Author

@annevk Thanks for the review! It looks like the Participation check is pending because our organization (UseJunior) hasn't been verified yet. Happy to provide anything that's needed on our end to move that along.

@annevk
Copy link
Copy Markdown
Member

annevk commented Mar 6, 2026

I would like @smaug---- to double check as well since he filed the original issue.

I saw that you filed browser bugs but I think browsers already handle this correctly. You can execute script during removal by using a pagehide listener in an <iframe> element's child document and making sure the <iframe> element gets removed if you want to double check. Only if that demonstrates something I think new tests would be warranted.

@stevenobiajulu
Copy link
Copy Markdown
Contributor Author

I ran the empirical test. @annevk you're right — all three browsers already implement the corrected behavior.

Test structure

container > [p#before(0), trigger(1), p#inside(2), p#sentinel(3)]
Range: (container, 1) → (container, 3)

The mutation callback removes p#before (at index 0, before the range's start boundary) while the operation is executing. Under the old spec, collapse(container, 1) would be applied after this removal, overwriting the live-range adjustment with a stale offset. Under the new spec the range collapses first, so the live-range machinery tracks the removal and produces startOffset: 0. The observable difference is startOffset: 0 (new spec) vs startOffset: 1 (old spec).

Results — Chrome 133, Firefox 135, Safari 18

Track Trigger mechanism Operation startOffset Verdict
A disconnectedCallback (custom element) deleteContents 0 (all three) ✅ new-spec behavior
A disconnectedCallback (custom element) extractContents 0 (all three) ✅ new-spec behavior
B pagehide/unload in iframe srcdoc deleteContents 0 (all three) ✅ new-spec behavior
B pagehide/unload in iframe srcdoc extractContents 0 (all three) ✅ new-spec behavior

Track A: disconnectedCallback fired synchronously mid-operation in all three browsers (not deferred past the [CEReactions] boundary on deleteContents() as I'd initially worried — reactions flush when each internal remove() completes).

Track B: pagehide and unload both fired synchronously in all three browsers including Firefox, which surprised me given the HTML spec's "without any unload events firing" language and Bugzilla #1850228. That bug may describe a different scenario.

Screenshots attached.
Chrome:
Screenshot 2026-03-06 at 11 25 28 AM

Safari:
Screenshot 2026-03-06 at 11 27 11 AM

Firefox:
Screenshot 2026-03-06 at 12 00 38 PM

What this means for the open items

  • The spec change is a correctness clarification, not a fix for a bug in shipping browsers. I'll update the filed browser bugs to note that.
  • On the WPT tests in #58008: given that there's nothing for browsers to fix, I'm happy to close that PR. Alternatively the static collapse-position tests could stay as regression guards for the spec ordering itself — up to you.

Copy link
Copy Markdown
Contributor

@smaug---- smaug---- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@annevk annevk merged commit 9363c6d into whatwg:main Mar 9, 2026
2 checks passed
@annevk
Copy link
Copy Markdown
Member

annevk commented Mar 9, 2026

Thanks again @stevenobiajulu!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

https://dom.spec.whatwg.org/#dom-range-deletecontents may collapse the Range to an invalid position

4 participants