Re: contentEditable=minimal

On Tue, May 27, 2014 at 11:01 AM, Robin Berjon <robin@w3.org> wrote:

> On 25/05/2014 20:40 , Piotr Koszuliński wrote:
>
>>     Making some things unselectable might also be useful. IE has
>>     unselectable, there's also -moz-user-select and friends. But this is
>>     small fries for later I'd reckon.
>>
>> There are also nested non-editable islands. We built very important
>> feature based on them - http://ckeditor.com/demo#widgets. Currently we
>> block their selection by preventing mousedown and we handle left/right
>> arrows. But cancelling selectionchange would allow us to control more
>> cases in a cleaner way.
>>
>
> I'd be curious to know what your take is on the best way to expose this.
> IE has an unselectable attribute, whereas Gecko and WebKit have a CSS
> property. In this thread we've been talking about using cancellable events
> for this (or if not cancellable, ones in which the selection can be
> modified on the fly).
>
> On instinct I would tend to think that this not a great usage of CSS, it's
> much more tied to behaviour at a lower level. But it sort of is one of
> those borderline things (as many of the properties that initially came from
> the CSS UI module).
>
> The scriptable option that we're consider is good in that it enables
> arbitrary cases, but it could be interesting to support a number of cases
> out of the box with a simpler (for developers) approach.
>
> Let's imagine the following DOM:
>
> <div contenteditable=minimal>
>   <p>blah blah blah</p>
>   <div class=widget unselectable>...</div>
>   <p>blah blah blah</p>
> </div>
>
> If the cursor is at the beginning of the first p, you hold Shift, and
> click at the end of the second p, we could imagine that you'd get a
> Selection with two Ranges (one for each p) and not containing the
> unselectable widget. I *think* that's the most desirable default behaviour,
> and it's also one that can be pretty painful to control through script. In
> that sense an unselectable attribute would make sense. (I reckon that
> setting the widget to be cE=false would have the same effect, but it is
> nevertheless an orthogonal property.)
>
> WDYT?
>
>
My previous email might be confusing because I mentioned only that CKEditor
handles mousedown event. Actually, this is a tip of an iceberg :). I think
that it may be an interesting case, because we were forced to overwrite
most of browser behaviours when implementing the widget system. Also, this
case is so complex that it's easier to start from what we experienced
rather than saying that if X and Y will be possible, then our needs will be
satisfied. I hope you forgive me this laziness :)

The main requirements for the widget system were:

1. User must not be able to modify a non-editable part of widget.
2. Widget must be selectable as a whole (like an image).
 3. Widget may contain editable fragments (nested editables).
4. There are block and inline widgets (surprisingly important requirement).
5. Widget may contain other widgets in its nested editables. So widgets may
be nested multiple times.
6. Widget must be selectable by click, arrow keys, etc. It also has to be
deletable by backspace/delete and copyable by CTRL+C.

This is a structure that we used:

<div contenteditable=true> <!-- editor's main editable -->
   <p>foo</p>
  <div contenteditable=false> <!-- widget wrapper -->
    <p contenteditable=true>nested editable</p>
    <p>non-editable content</p>
    <div contenteditable=true>
       <p>another nested editable</p>
    </div>
  </div>
  <p>foo2 <span contenteditable=false>inline widget</span></p>
</div>

Why did we choose contenteditable=false? At the beginning it was simply
intuitive. We don't fully control the interaction methods (mouse, keyboard,
selection), so let browser do that. Then we overwrote most of the
interaction methods so currently the attribute is not so crucial. However,
there's one aspect of interaction which we cannot control and I'm not sure
if we'll ever be able - dynamically changing selection (its visual and
logical aspect) around block widgets, their nested editables and a special
case that complicates all this - blocks may be floated. Let's try to
imagine how we could handle selection without using contenteditable=false.

<p>foo</p>
<div class="widget">widget</div>
<p>bar</p>

TC1:
User clicks main editable's margin, on the side of the block widget. If he
clicks closer to the top of the widget, we would like to place caret at the
end of preceding editable block (foo^), and when closer to bottom, then in
the following block (^bar). Without checking dimensions, that would not be
doable, because browser would suggest a selection inside <div>widget</div>.
On the other hand, we could consider always selecting entire widget if
selection was proposed inside it. This seems to be a good option,
especially taking into account that we may have multiple block widgets one
after another, and then you cannot place selection between them anyway.

TC2:
Another aspect is selection normalisation. When we make a decision that
entire block widget should be selected we want to be able to anchor the
selection in widget's parent element:
<p>foo</p>[<div>widget</div>]<p>foo</p>. Since browser does not know the
semantics of our widget, will it preserve the selection exactly were we put
it? Because if it makes a decision to move it closer to text nodes, then we
hit a wall.

TC3:
And how will browser render the selection? For our case it should select
the widget like an image, not like a text (whole area should be marked, not
only lines of text). Why? For usability reasons - this will indicate that
this block is non-editable. And additionally the block may be empty and
have size set by CSS and then its selection will mark it if it marks only
text.

TC4: <p>f<span>widget</span>bar</p>
Now user wants to select "f" by mouse. He starts at "[f" and drags to the
right. When he reaches "[f<span>]widget" we need to make a decision -
select entire widget or right trim selection? If we start selecting entire
widget too quickly we'll make it very hard to select only "f" by mouse or
simply impossible if browser will normalise selection (move its right
boundary to the <span>). When we delay selecting entire widget, then we
have a problem on the right end: "[f<span>widget]</span>bar".

TC5: <p><span>widget</span></p>
If the normalisation happens at left or right end, then clicking a margin
of editable will never make a collapsed selection outside widget.

TC6: <p>foo<span>wid get</span>bar</p>
User doubleclicks "foo", browser proposes [foo<span>wid]. Should we select
"foo" or the widget? We would need to know the click target. And what if it
was "f<b>o</b>o"?

TC7: widget with nested editable (see the simple box example on
http://ckeditor.com/demo#widgets)
When user clicks border of the box, he intends to select the widget. But in
such case browser would propose a selection in the nested editable. Again,
we would need to check the click target.

As you can imagine, there will be more cases. Floated content, nested
editables, complex DOM structures. Most of the TCs might be handled in
better or worse way, but I think it's clear that it will be better if
browsers handle that. Imagine that all this would be done by JavaScript on
every mousemove (when making selection by dragging) :D.

Now, should the behaviour come from CSS or attributes? First of all, I
think that there are two separate behaviours involved. Being non-editable
and being selectable (and how that selection looks). The first part, being
non-editable, is more about semantics, so for me it should be handled like
today - using the contenteditable attribute. Second part, selection,
includes 3 options: normal, none, like an image. The problem with
unselectable style/property is that in our case we want the content to be
selectable. If user selects all content of editor and copy, widgets should
be copied too. So currently existing unselectable style/attribute is not an
option for us. I think that non-editable content can't be selected as we
would like it to be by default, so there'd need to exist a new option. For
now, I was trying to add a style to selected widget, but this is hard,
because a layer needs to be rendered over the widget and it visually
conflicts with native selection, so this is not a good solution either.


Besides handling selection changes, I want to emphasise one more problem -
where the selection engine allows us to place the selection. Inline widgets
works pretty well - we are able to place caret before, between and after
non-editable inline elements. Only selecting whole elements was a problem.
The bigger problem, though, is with block widgets, because it was neither
possible to select them, nor place selection between them, nor at the
beginning of editable if its contents start with widget. The ultimate edge
case is editable which contains only a non-editable block :D.

We worked around this by moving the real selection to a hidden container
inside editable. We call this a fake selection, because what you actually
see when you click a widget is a CSS-based outline and selection is kept
somewhere outside of the viewport. Thanks to that we are able to select
every element we want, because we remember what's selected. Then we
overwrote most methods for changing selection, so we control how the widget
may be unselected. This works pretty well and with more controllable
selectionchange event could be even nicer. However, it only works for a
single element selections. It's not possible to select two widgets this way.

Therefore selection engines could be improved to handle selection around
non-editable elements. Two aspects could be fixed:
1. Selection containing non-editable element(s) should be preserved when
made from API (and should be doable manually): <p>foo</p>[<div
contenteditable="false">foo</div>]<p>foo</p>
2. To avoid problems with selection beside non-editable blocks (because
it's tricky), when user clicks a non-editable block or next to it, it could
be selected. Actually, we went further and we select widgets when
navigating with arrow keys, deleting, etc., because this indicates better
where is the selection. That's especially important when there's a floated
content, because collapsed selection poorly indicates what's going on.

-- 
Piotrek Koszuliński
CKEditor JavaScript Lead Developer

Received on Wednesday, 28 May 2014 07:23:14 UTC