CARVIEW |
Selection API Specification
Unofficial Draft
- Editor:
- Ryosuke Niwa, Apple Inc., rniwa@apple.com
This document is licensed under a Creative Commons Attribution 3.0 License.
Abstract
This document is a preliminary draft of a specification for the Selection API and selection related functionality. It replaces a couple of old sections of the HTML specification, the selection part of the old DOM Range specification.
Status of This Document
This document is merely a public working draft of a potential specification. It has no official standing of any kind and does not represent the support or consensus of any standards organisation.
Since this specification is intended to be charted by the Web Applications Working Group, any feedback or discussion of this specification should be posted to public-webapps mailing list with "[Selection API]" in the subject.
This specification is based on the HTML Editing API specification edited by Aryeh Gregor and published by the W3C Editing APIs Community Group. Copyright © 2011-2014 the Contributors to the HTML Editing APIs Specification, published by the W3C Editing APIs Community Group under the W3C Community Contributor License Agreement (CLA).
Table of Contents
1. Background
This section is non-normative.
IE9 and Firefox 6.0a2 allow arbitrary ranges in the selection, which follows what this spec originally said. However, this leads to unpleasant corner cases that authors, implementers, and spec writers all have to deal with, and they don't make any real sense. Chrome 14 dev and Opera 11.11 aggressively normalize selections, like not letting them lie inside empty elements and things like that, but this is also viewed as a bad idea, because it takes flexibility away from authors.
So I changed the spec to a made-up compromise that allows some simplification but doesn't constrain authors much. See discussion. Basically it would throw exceptions in some places to try to stop the selection from containing a range that had a boundary point other than an Element or Text node, or a boundary point that didn't descend from a Document.
But this meant getRangeAt() had to start returning a copy, not a reference. Also, it would be prone to things failing weirdly in corner cases. Perhaps most significantly, all sorts of problems might arise when DOM mutations transpire, like if a boundary point's node is removed from its parent and the mutation rules would place the new boundary point inside a non-Text/Element node. And finally, the previously-specified behavior had the advantage of matching two major implementations, while the new behavior matched no one. So I changed it back.
See bug 15470. IE9, Firefox 12.0a1, Chrome 17 dev, and Opera Next 12.00 alpha all make the range initially null.
2. Definition
Every document ([DOM4]) with
a browsing context ([HTML5])
has a unique Selection
object associated with it.
Selection
objects are known as selections.
This is a requirement of the HTML spec. IE9 and Opera Next 12.00 alpha seem to follow it, while Firefox 12.0a1 and Chrome 17 dev seem not to. See Mozilla bug, WebKit bug.
This one selection must be shared by all the content of the document (though not by nested documents), including any editing hosts in the document. Editing hosts (defined in [HTML5]) that are not inside a document cannot have a selection
We need to define what it means to say editing hosts can't have selection.
Each selection can be associated with a single range (defined in [DOM4]). When there is no range associated with the selection, the selection is empty. The selection must be initially empty.
A document's selection is a singleton object associated with that document,
so it gets replaced with a new object when Document.open()
is called.
See bug 15470.
IE9 and Opera Next 12.00 alpha allow the user to reset the range to null after the fact by clicking somewhere;
Firefox 12.0a1 and Chrome 17 dev do not.
We follow Gecko/WebKit, because it lessens the chance of getRangeAt(0) throwing.
Once a selection is associated with a given range, it must continue to be associated with that same range until this specification requires otherwise.
For instance, if the DOM changes in a way that changes the range's boundary points,
or a script modifies the boundary points of the range, the same range object must continue to be associated with the selection.
However, if the user changes the selection or a script calls addRange()
,
the selection must be associated with a new range object, as required elsewhere in this specification.
This paragraph is vague. It needs to be replaced by detailed conformance requirements saying exactly what to do for particular keystrokes, like we have for backspace/delete/etc.
If the selection's range is not null and is collapsed ([DOM4]), then the caret position must be at that range's boundary point. When the selection is not empty, this specification does not define the caret position; user agents should follow platform conventions in deciding whether the caret is at the start of the selection, the end of the selection, or somewhere else.
This short-changes Mac users. See bug 13909.
Each selection has a direction, forwards, backwards, or directionless. If the user creates a selection by indicating first one boundary point of the range and then the other (such as by clicking on one point and dragging to another), and the first indicated boundary point is after ([DOM4]) the second, then the corresponding selection must initially be backwards. If the first indicated boundary point is before ([DOM4]) the second, then the corresponding selection must initially be forwards. Otherwise, it must be directionless.
Should addRange()
and removeAllRanges()
reset direction?
Selection
s also have an anchor and a focus.
If the selection's range is null, its anchor and focus are both null.
If the selection's range is not null and its direction is forwards, its anchor is the range's
start, and its focus is the end.
Otherwise, its focus is the start and its anchor is the end.
interface Selection {
readonly attribute Node? anchorNode;
readonly attribute unsigned long anchorOffset;
readonly attribute Node? focusNode;
readonly attribute unsigned long focusOffset;
readonly attribute boolean isCollapsed;
readonly attribute unsigned long rangeCount;
Range getRangeAt (unsigned long index);
void addRange (Range range);
void removeRange (Range range);
void removeAllRanges ();
void collapse (Node node, unsigned long offset);
void collapseToStart ();
void collapseToEnd ();
void extend (Node node, unsigned long offset);
void selectAllChildren (Node node);
void deleteFromDocument ();
stringifier DOMString ();
};
2.1 Attributes
anchorNode
of type Node, readonly , nullable-
The attribute must return the anchor node ([DOM4]) of the context object ([DOM4]), or
null
if the anchor is null. anchorOffset
of type unsigned long, readonly-
The attribute must return the anchor offset ([DOM4]) of the context object, or
0
if the anchor is null. focusNode
of type Node, readonly , nullable-
The attribute must return the focus node of the context object, or
null
if the anchor is null. focusOffset
of type unsigned long, readonly-
The attribute must return the focus offset of the context object, or
0
if the focus is null. isCollapsed
of type boolean, readonly-
The attribute must return true if and only if the anchor and focus are the same (including if both are null). Otherwise it must return false.
rangeCount
of type unsigned long, readonly-
The attribute must return
0
if the context object's range is empty, and must return1
otherwise.
2.2 Methods
DOMString
-
See W3C bug 10583.
Issue 8
Completely under-spec'ed.
No parameters.Return type:stringifier
addRange
-
The method must set the context object's range to range by a strong reference (not by making a copy).
NoteSince range is added by reference, subsequent calls to
getRangeAt(0)
returns the same object, and any changes that a script makes to range after it is added must be reflected in the selection, until something else removes or replaces the context object's range. In particular, the selection will contain b as opposed to a after running the following code:var r = document.createRange(); r.selectNode(a); getSelection().addRange(r); r.selectNode(b);
NoteIE9 and Firefox 4.0 store a reference, as described here. Chrome 12 dev and Opera 11.10 appear to store a copy, so changes don't affect the selection.
Chrome 15 dev seems to ignore addRange() if there's already a range. IE9 replaces the existing range. Firefox 9.0a2, of course, just gives you a multi-range selection. IE is likely to behave closest to Firefox, and is also more useful than silent failure, so the spec goes with that.
Parameter Type Nullable Optional Description range Range
✘ ✘ Return type:void
collapse
-
The method must throw an
IndexSizeError
exception if if offset is negative or longer than node's length ([DOM4]). Otherwise, it must create a new range, set ([DOM4]) both its start and end to (node, offset), and set the context object's range to the newly-created range.Parameter Type Nullable Optional Description node Node
✘ ✘ offset unsigned long
✘ ✘ Return type:void
collapseToEnd
-
The method must throw
InvalidStateError
exception if the context object is empty. Otherwise, it must create a new range, set both its start and end to the end of the context object's range, and then set the context object's range to the newly-created range.No parameters.Return type:void
collapseToStart
-
The method must throw
InvalidStateError
([DOM4]) exception if the context object is empty. Otherwise, it must create a new range, set both its start and end to the start of the context object's range, and then set the context object's range to the newly-created range.NoteFor collapseToStart/End, IE9 mutates the existing range, while Firefox 9.0a2 and Chrome 15 dev replace it with a new one. The spec follows the majority and replaces it with a new one, leaving the old Range object unchanged.
No parameters.Return type:void
deleteFromDocument
-
The method must invoke
deleteContents()
([DOM4]) on the context object's range if the context object is not empty. Otherwise the method must do nothing.NoteThis is the one method that actually mutates the range instead of replacing it. This matches IE9 and Firefox 12.0a1. (Chrome 17 dev and Opera Next 12.00 alpha can't be tested, because getRangeAt() returns a copy anyway.)
Issue 7Should we replace the range rather than mutating it here? This is currently the only Selection method that actually mutates an existing range in place.
No parameters.Return type:void
extend
-
The method must follow these steps:
- If the context object is empty, throw an
InvalidStateError
exception and abort these steps. - Let oldAnchor and oldFocus be the context object's anchor and focus, and let newFocus be the boundary point (node, offset).
- Let newRange be a new range.
- If node's root ([DOM4]) is not the same as the context object's range's root, set newRange's start and end to newFocus.
- Otherwise, if oldAnchor is before or equal to newFocus, set newRange's start to oldAnchor, then set its end to newFocus.
- Otherwise, set newRange's start to newFocus, then set its end to oldAnchor.
- Set the context object's range to newRange.
- If newFocus is before oldAnchor, set the context object's direction to backwards. Otherwise, set it to forwards.
NoteReverse-engineered circa January 2011. IE doesn't support it, so I'm relying on Firefox (implemented extend() sometime before 2000) and WebKit (implemented extend() in 2007). I'm mostly ignoring Opera, because gsnedders tells me its implementation isn't compatible. Firefox 12.0a1 seems to mutate the existing range. IE9 doesn't support extend(), and it's impossible to tell whether Chrome 17 dev or Opera Next 12.00 alpha mutate or replace, because getRangeAt() returns a copy anyway. Nevertheless, I go against Gecko here, to be consistent with collapse().
Parameter Type Nullable Optional Description node Node
✘ ✘ offset unsigned long
✘ ✘ Return type:void
- If the context object is empty, throw an
getRangeAt
-
The method must throw an
IndexSizeError
([DOM4]) exception if index is not0
, or if the context object is empty. Otherwise, it must return a reference to (not a copy of) the context object's range.NoteThus subsequent calls of this method returns the same range object if nothing has removed the context object's range in the meantime. In particular,
getSelection().getRangeAt(0) === getSelection().getRangeAt(0)
evaluates totrue
if the selection is not empty.NoteIE9 and Firefox 4.0 return the same object every time, as the spec says. Chrome 12 dev and Opera 11.10 return a different object every time.
Parameter Type Nullable Optional Description index unsigned long
✘ ✘ Return type:Range
removeAllRanges
-
The method must make the context object empty by disassociating its range if the context object's range is range. Otherwise, it must do nothing.
Issue 6Should we check for object equality here or just equality of boundary points?
No parameters.Return type:void
removeRange
-
The method must make the context object empty by disassociating its range if the context object's range is range. Otherwise, it must do nothing.
Issue 5Should we check for object equality here or just equality of boundary points?
Parameter Type Nullable Optional Description range Range
✘ ✘ Return type:void
selectAllChildren
-
The method must follow these steps:
- Let newRange be a new range and nodeLength be the length of node.
- Set newRange's start to (node,
0
). - Set newRange's end to (node, nodeLength).
- Set the context object's range to newRange.
- Set the context object's direction to forwards.
NoteBased mostly on Firefox 9.0a2. It has a bug that I didn't reproduce, namely that if you pass a Document as the argument, the end offset becomes 1 instead of the number of children it has. It also throws a RangeException instead of DOMException, because its implementation predated their merging.
IE9 behaves similarly but with glitches. It throws "Unspecified error." if the node is detached or display:none, and apparently in some random other cases too. It throws "Invalid argument." for detached comments (only!). Finally, if you pass it a comment, it seems to select the whole comment, unlike with text nodes.
Chrome 16 dev behaves as you'd expect given its Selection implementation. It refuses to select anything that's not visible, so it's almost always wrong. Opera 11.50 just does nothing in all my tests, as usual.
The new range replaces any existing one, doesn't mutate it. This matches IE9 and Firefox 12.0a1. (Chrome 17 dev and Opera Next 12.00 alpha can't be tested, because getRangeAt() returns a copy anyway.)
Parameter Type Nullable Optional Description node Node
✘ ✘ Return type:void
See also nsISelection.idl from Gecko. This spec doesn't have everything from there yet, in particular selectionLanguageChange() and containsNode() are missing. They are missing because I couldn't work out how to define them in terms of Ranges.
Originally, the Selection interface was a Netscape feature. The original implementation was carried on into Gecko (Firefox), and the feature was later implemented independently by other browser engines. The Netscape implementation always allowed multiple ranges in a single selection, for instance so the user could select a column of a table However, multi-range selections proved to be an unpleasant corner case that web developers didn't know about and even Gecko developers rarely handled correctly. Other browser engines never implemented the feature, and clamped selections to a single range in various incompatible fashions.
This specification follows non-Gecko engines in restricting selections to at most one range,
but the API was still originally designed for selections with arbitrary numbers of ranges.
This explains oddities like the coexistence of removeRange()
and removeAllRanges()
,
and a getRangeAt()
method that takes an integer argument that must always be zero.
All of the members of the Selection
interface are defined
in terms of operations on the range
object (if any) represented by the object.
These operations can raise exceptions, as defined for the Range
([DOM4]) interface;
this can therefore result in the members of the Selection
interface raising exceptions as well,
in addition to any explicitly called out below.
We need to ensure we can only add ranges that are in the document associated with the selection.
3. Extensions to Other Interfaces
This specification extends several interfaces to provide entry points to the interfaces defined in this specification.
Extensions to Document
interface
partial interface Document {
Selection
? getSelection ();
};
3.1 Methods
getSelection
-
The method must return the selection associated with context object if the context object has an associated browsing context, and it must return
null
otherwise.No parameters.Return type:
, nullableSelection
If we create a Document object with no browsing context
(say via document.implementation.createHTMLDocument("") and call
getSelection() on it),
IE9 seems to return a different Selection object.
Firefox 12.0a1 and Opera Next 12.00 alpha return the same object as for the current window.
Chrome 17 dev returns null.
See discussion.
There's no meaningful selection associated with such a document, so we follow WebKit and require returning null
.
Extensions to Window
interface
partial interface Window {
Selection
? getSelection ();
};
3.2 Methods
getSelection
-
The method must invoke and return the result of
getSelection()
on the context object'sdocument
([HTML5]) property.No parameters.Return type:
, nullableSelection
4. User Interactions
The user agent should allow the user to change the selection associated with the active document (defined in [HTML5]). If the user makes any modification to a selection, the user agent must create a new range with suitable start and end of the range ([DOM4]) and associate the selection with this new range (not modify the existing range).
The user agent must not make a selection empty if it was not already empty in response to any user actions (e.g. clicking on a non-editable region).
See bug 15470. IE9 and Opera Next 12.00 alpha allow the user to reset the range to null after the fact by clicking somewhere; Firefox 12.0a1 and Chrome 17 dev do not. I follow Gecko/WebKit, because it lessens the chance of getRangeAt(0) throwing.
Define select
and selectionchange
events.
A. Acknowledgements
Many thanks to
Aryeh Gregor, who is the original author of this specification as well as HTML Editing API specification.
Contributors to the HTML Editing API specification - Ehsan Akhgari, Tab Atkins, Mathias Bynens, Tim Down, Markus Ernst, Daniel Glazman, Tali Gregor (née Fuss), Stig Halvorsen, Jeff Harris, Ian Hickson, Cameron Heavon-Jones, Anne van Kesteren, Alfonso Martínez de Lizarrondo, Glenn Maynard, Ms2ger, Robert O'Callahan, Julie Parent, Simon Pieters, Michael A. Puls II, Rich Schwerdtfeger, Jonas Sicking, Henri Sivonen, Smylers, Hallvord R. M. Steen, Roland Steiner, Annie Sullivan, timeless, Ojan Vafai, Brett Zamir, and Boris Zbarsky for their feedback, participation, or other helpful contributions
- Robin Berjon for making our lives so much easier with his cool tool.
B. References
B.1 Informative references
- [DOM4]
- Anne van Kesteren; Aryeh Gregor; Ms2ger; Alex Russell; Robin Berjon. W3C DOM4. 4 February 2014. W3C Last Call Working Draft. URL: https://www.w3.org/TR/dom/
- [HTML5]
- Robin Berjon; Steve Faulkner; Travis Leithead; Erika Doyle Navara; Edward O'Connor; Silvia Pfeiffer. HTML5. 4 February 2014. W3C Candidate Recommendation. URL: https://www.w3.org/TR/html5/