Triple-click selection and Selection API issue
See original GitHub issueDescription Hello! We are currently facing a bug with triple-click selection. When we triple-click on a word, the expectation is that the whole text block gets selected, a paragraph for example. The problem here is that for 2 of the 3 browsers I tested (it happens on Chrome and Edge, not on Firefox), Slate will also select the following (or part of the following) sibling node.
You can see recordings for both behaviours, the first for Chrome (bug) and the second on Firefox (no bug).
Recording
Steps To reproduce the behavior:
- Go to https://www.slatejs.org/examples/images
- Triple-click on the first paragraph
- On Chrome the image gets selected as well, not on Firefox
Expectation The expectation would be to have the selection only on the text block that was actually targeted, and not the following one.
Environment
- Slate Version: latest
- Operating System: macOS 10.15.7
- Browser: Chrome, Edge
Context From my understanding, there is two things that feel are part of the issue.
-
The first might be because of the way
offsets
are considered within a selection. At this line: https://github.com/ianstormtaylor/slate/blob/e042ebd4a2539aeabf0996e8f1a1dad1be5860c0/packages/slate-react/src/utils/dom.ts#L110offset
is compared tochildNodes.length
But according to the Selection API the offset represents the number of characters selected, not the number of child nodes, so this is not comparing the same things at all. -
The second thing is, if the value of
focusOffset
is 0, it means there is nothing selected in thefocusNode
and therefore, this node should be removed from the selection range. Currently, maybe because of the first point, Slate keeps digging into thisfocusNode
to find a selectable child and include it in the range. On top of it, iffocusNode
is avoid
element, it can create more problems, as Slate is not supposed to handle the content of avoid
element.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:6 (3 by maintainers)
Top GitHub Comments
I’ve been investigating / trying to fix this (see comments on #4492) and wanted to record some findings:
The basic issue as I understand it is that the DOM selection API supports two kinds of selection endpoint:
but Slate selections support only text endpoints.
When a DOM selection has an element endpoint, Slate looks for a corresponding text endpoint (
ReactEditor.toSlatePoint
). In most cases, this text endpoint is after the original element endpoint; so when the focus of a forward selection is an element endpoint, the Slate selection is “hanging”: its focus is at the beginning of the following block.Browsers vary on exactly what actions produce element endpoints, but they are generally actions that sensibly mean “select the whole element”:
All of these generate element endpoints (in most cases) in the browsers I’ve tested (Chrome, Firefox, Safari, all on a Mac), and in most cases the element endpoints turn into a hanging selection in Slate.
It’s good to have hanging selections! There’s no other way in Slate to represent the selection of a whole block, and there are lots of actions that sensibly work on a whole block: e.g. if you triple-click a paragraph or shift-arrow-down a paragraph and hit delete, it’s sensible for the whole paragraph to be deleted, not just the text within it. (I haven’t tested a lot of rich text editors on this but it seems pretty standard.)
But hanging selections have some bugs:
First, browsers differ in exactly how they generate selection endpoints for the actions above:
DOM.normalizeDOMPoint
searches backwards for a text node when the offset is past the last child).(I feel like these differences are not really a problem—applications should do something sensible with both hanging and non-hanging selections; so inconsistencies in when hanging / non-hanging selections are generated don’t really matter. Just my opinion!)
Second, applications don’t always handle hanging selections correctly:
Range.unhang
before checking the nodes. I think this is a bug in the example—it should properly handle both hanging and non-hanging selections. (Note that you can’t currently reproduce this in Chrome due to #4455 / #4512; it’s reproducible in Safari with a triple-click or shift-arrow-down, or in Firefox with shift-arrow-down.)@bytrangle made an attempt at fixing these bugs with #4455 / #4512. I’ve been looking into alternative fixes because @bytrangle’s fixes have caused other bugs (#4492), because they address Chrome only (the issue is more widespread), and because they fix the bugs by getting rid of hanging selections (which I think is a mistake; hanging ranges are useful).
I’ve been working on a branch that addresses these bugs by always looking for the text endpoint inside the DOM selection, never outside. This works without browser-specific fixups, but it also gets rid of hanging selections (as above I think this is a mistake). I’d rather address the bugs by fixing the examples where needed, and by understanding / fixing the behavior of hanging selections inside void nodes. What do you think? @ianstormtaylor would love your input here.
So first of all, to not mix everything below, there is different things at play here:
DOM Node
andSlate Node
, which are not the same.Selection
fromSelection API
and theSlate Selection
, different things as well.I will try to keep these names.
If you do a single click on you paragraph, for example between the letters
o
andl
of the wordbold
, theSelection
object (from Selection API) will contain this (not only, but I keep the useful part here)Here, the two
text
areDOM Nodes
representing the wordbold
and the2
is after how many characters of that word you clicked. Based on that, Slate will return the first validSlate Node
in which thistext
is. In our case, the full paragraph. It will then assign it to theSlate Selection
.Now if you do the “click-and-drag”, your
Selection
object (from API) will be:For Slate, it’s the same as before, it will get the first valid
Slate Node
that contains your selection, in this case still the same paragraph, give it to theSlate Selection
.And for the triple click:
Since here the Selection API returns the text of the quote as a focusNode
(DOM Node)
, Slate detects that this is not part of the paragraph you “triple-selected”, and will therefore add the Quote element to yourSlate Selection
. And this last part will trigger all the issues we noticed, like the trigger ofisBlockActive
and the image issue.The problem here is that since the focusOffset is at 0, it means that no characters from the focusNode is actually selected, so Slate should not be giving that
DOM Node
to theSlate Selection
. To make the connection with my initial issue, this is not a problem with Firefox because unlike Chrome, it already removes this “unselected node” before returning the selection, so Slate doesn’t even know about it