[whatwg] DOM Range: redefining behavior under DOM mutation

For execCommand(), it's very important that the position of the
selection be defined (and sensible) after taking an editing action.
So I've started looking at the problem of defining how Ranges behave
when the document mutates, and have determined that current browser
behavior (as specced by DOM Level 2 Range) is not optimal.

Right now, when a node's parent is changed, Range mutation treats it
like the inserted and removed nodes are entirely different.  For
instance, if you have a range like

  <p><b>Foo</b> bar <i>{baz}</i>

(with the curly braces meaning the range has start (<i>, 0) and end
(<i>, 1); square brackets mean the boundary point lies in a text node)
and you do p.insertBefore(baz, i), you get

  <p><b>Foo</b> bar baz<i>{}</i>

with the selection collapsed inside <i>.  This sort of thing comes up
a lot in execCommand().  For instance, if the user wanted to
de-italicize the selection given, the natural way to do it would be to
move the children of the <i> to its previous siblings, and then remove
the <i>.  But that gets you

  <p><b>Foo</b> bar baz{}

with the selection collapsed after the <i>'s former children, instead of

  <p><b>Foo</b> bar {baz}

which is what we want: the child selected.

I assume browsers hack around this specially for execCommand(), but
that's not only a pain to spec, it's also not nice for authors, since
their DOM mutations will not benefit from this magic.  I've compiled a
list of all the times in the spec so far that I mutate the DOM, and
the selection behavior I want in each case.  I've found that the
following behavior would get the desired effect in all cases so far:

If a node is moved to a position "immediately before" its original
position, then preserve any boundary points in it.  "Immediately
before" means that there are no nodes lying between the new and old
positions: if you made a range whose boundary points are the new and
old locations, and ran deleteContents(), it would be a no-op.  This
would include when the first child of an element is being moved to the
element's previous sibling, or to the last child of the previous
sibling, or if an element is being moved to its previous sibling's
last child, etc.  "Preserving boundary points" means that if a
boundary point is in the node itself or a descendant, it's transferred
as-is to the new location, and also that if it's "immediately before"
or "immediately after" the original location (in the same sense as
before), it gets moved to the new position's parent right before or
after the new location.  Likewise, if a node is moved to a position
immediately after its original position, preserve its boundary points
in the same fashion.


As an example of how this would work in a real case, suppose I have

  <p>{Foo<i>bar</i>}

and the user wants to bold the selected text.  First you could create
a new empty <b>:

  <p><b></b>{Foo<i>bar</i>}

This triggers no new behavior, since the old parent was null.  Then
move the first selected node there:

  <p><b>{Foo</b><i>bar</i>}

Since we're moving the node immediately before its current position,
and the boundary start was immediately before the moved node's
original position, we move the boundary start to the new parent of the
node, before the new location.

Then we move the second selected node:

  <p><b>{Foo<i>bar</i>}</b>

The end of the range was immediately after the moved node, so it gets
moved to the node's new parent.  (In principle we could have left it
in place here, but it makes no real difference, since the resulting
range is logically more or less the same, and this way winds up
looking a bit neater.)


I'd like to know if implementers would be interested in moving to new
behavior along these lines.  If not, what suggestions would you have
for how to spec all this?  How is it actually implemented?

Received on Monday, 28 March 2011 12:28:26 UTC