Text Editing Part 3/3 Keyboard Input and Select/Copy/Paste

Turns out, this is not needed.

To get the algorithm working right, a trick is needed to get steps 2 and 3 in the right order, and for step 3 to be performed immediately after 2 (no good to wait for the animation frame).

The answer is already there in mweiss/elm-rte-editor. The trick is to use 2 custom elements and to structure them like this:

<elm-editor>
   <div contenteditable="true">...</div>
   <selection-state row="5" col="10"/>
</elm-editor>

Now the Elm virtual-dom will render changes to the content editable first, and then changes to the selection-state. When the attributes of the selection state are updated, it can run some code right away to fix up the caret position.

This is dependant on the execution order of the Elm virtual-dom algorithm. On the other hand, that is a slow moving target and probably unlikely to suddenly change and work backwards anyway.

Thanks to @tibastral for prompting me to re-think this on Slack, and of course @mweiss for figuring it all out a while ago.

1 Like

I now have algorithms for translating between text editor (row, col) and the browser Selection, which is abstracted as to a path + offset format (List Int, Int).

The path describes a path from a parent node elm-editor, down to some point within a text node in the DOM. So [1,2,1,0], means follow child 1, then child 2, then child 1, then child 0. On offset of 10, would then mean step 10 characters into the text node found at the end of that path.

Iā€™m not 100% sure yet, but I think mweiss/elm-rte-toolkit did something super clever here. Because you need to write your HTML using its own ElementNode model, for reasons described above, it is also able to figure out these paths itself. So even though you write a view to customize your editable content, cursor position maintenance is automatic?

The situation is simpler for my text editor, since I know the DOM structure. It may get trickier later on, depending on how customizable the view is in the completed work. That is, if I let users write their own arbitrary elm/html Html views, they will also need to supply a correct (row, col) <-> (List Int, Int) coordinate mapping. Other options would be:

  • Steal another trick from elm-rte-toolkit and only allow customization through an alternate HTML DSL, that is able to figure out the mapping itself.
  • Only allow very limited customization, such as setting color, bold/italic, and so on.

Was hoping to avoid both of those optionsā€¦ anyway, a decision for later.

===

I am also thinking about cursor control on tablets/phones. Doing most cursor control through keyboard events at the moment, intercepted in Elm. I think that may not work well away from Desktop PCs, since virtual keyboards may have their own ideas about cursor control. I could let the browser do all cursor control, and map back into (row, col) on the Elm side. I think the browsers cursor contol sucks some of the time thoughā€¦ Perhaps both ways need to be implemented, selected at runtime by feature flags depending on the browser and device being used.

Will park this exploration for the time being, and just do Elm side keyboard events for this spike.

===

In the demo at the moment, I am displaying 2 cursors. One is the browsers cursor, and the other is an Elm rendered custom cursor. You can see they track each other well, and if you enter some " to get syntax highlighting, that creates more child nodes on the line, so the path + offset logic does some work there to keep them in sync.

Lots of ways you can split the cursors at the moment, such as scrolling down or clicking with mouse. Iā€™m working on cleaning that up now:

This is all improved now. You can scroll to the end of a long document and type and the cursors are all ligned up. You can click with the mouse to set the cursor position. Seems to work reasonably well on Chrome and Firefox on my Linux box.

Tried it on my phone and weird stuff happens??!! Not that Iā€™ll be editing much code on my phone, but I guess a tablet is just about feasable, and its going to be nice to have device support as wide and solid as it can be. I have no Apple devices to test on. At any rate, it feels like fixing all the glitches is going to be a question of digging into the vagaries of browser contenteditable implementations and ironing them out with little bug fixes - that is, a lot of work!

Now for cut/copy/pasteā€¦

===

One thing I ended up doing is distinguishing between selection change events that result from the Elm side changing the selection versus user input that the browser is handling. For example, I want the browser to handle setting the selection on a mouse click, otherwise I would need to know the character width of the text to calculate how to set the cursor position from Elm. So I set a flag on mousedown and clear it on mouseup and check this on selection change. If set the selection change event passes up a flag to indicate that this is a ā€˜control eventā€™ as opposed to it just repeating a selection change that was instructed to it by the Elm side.

I track all selection change events with a ā€˜tracking cursorā€™, which just means that I save them in the model, but donā€™t really use this for anything. The real cursor position is held in a ā€˜control cursorā€™ field in the model. If the control cursor is moved, say by pressing an arrow key, there will be a brief time when the tracking and control cursors do not align. The selection change event should be reflected back from the custom element, and the tracking cursor brought back into alignment.

I am thinking this could be useful for automated testing, since it gives me a way to detect when the browsers concept of where the cursor is differs from the Elm codeā€™s idea of where it is - if they diverge for more than some short time interval, say. It may be possible to do some end-to-end automated device testing with this:

1 Like

Iā€™ve been trying to work out the details of how to handle selection combined with virtual scrolling. To that end, Iā€™ve modelled it as a state machine:

When the browser selection changes to not collapsed, the Elm side changes the cursor to a RegionCursor, which models the same thing but in text editor coordinates (row, col), rather than by DOM node + offset. The start of the selection will always be whatever start coordinate this gives. This will only be cleared if the selection is cleared.

As the browser selection changes, the selection start corrdinate will remain the same, but the end will be updated. The control cursor will be updated to a region that is the same as this selection, but clipped to the current virtual scroll viewing window. That will be passed back to the custom element to update the browser selection to.

In this way, the browser selection will always exist within the virtual scrolling window. The modelled selection can range outside of this, as the scroll position is moved.

If the browser selection is changed to collapsed, or cleared altogether, the state will change back to NotSelecting. A new selection with a different start point could now be made.

Seems reasonable, now to implement!

2 Likes

The demo now has the interaction between selected text regions and the virtual scrolling working:

Bit glitchy in places, for example if you make the selection by doing click-and-hold then move the mouse up, so your selection is upside down, its not right. I have not taking upside-downess into consideration in the clipping algorithm yet.

Anyway, the idea is you can select an area of text, then use the scroll bar to move away from that, far enough that the selected text falls outside the virtual scroll area that is rendered (which itself extends +/- 1 page around the visible text window). When you scroll back again, your selection will be restored. when the selection is partially outside the rendered virtual scroll area, it will be clipped to that area, so the browser renders it correctly.

Internally, the Elm model knows where your selection started and ended, regardless of how it is being rendered on screen. So if I now implement copy/cut/paste operations, I will know the correct area of text to work with.

===

Another think I notice, if you click and drag the mouse off the bottom of the window, and let it scroll down to select a large area of text - then you try scrolling around that selected text, the selection highlight flickers a lot. This is due to the clipping constantly resetting the browser selection. There are also other ways in which the browser selection becomes invisible during selection.

Might be better to render the selection highlight in the Elm view to overcome this.

Ok, I figured out what this is about.

The selection position is given in DOM node+offset coordinates. Start with a clipped selection range of child node 1 to child node 20 on a page that is 20 lines long. When the page is scrolled, the new clipped selection range can still be child 1 to child 20, even though it actually means a different part of the document. Same numbers, different meaning. And I was de-duplicating updates to the selection, where it does not really change. I probably just need to put something else in the selection object that is being passed down to the custom element - like the scroll top position, to ensure that the selection is always updated when the view is scrolled.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.