diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md new file mode 100644 index 0000000000..2448be852a --- /dev/null +++ b/docs/ciderEditor.md @@ -0,0 +1,71 @@ +# The CIDER (Contenteditable-Input-Diff-Error-Reconcile) editor + +The CIDER editor is a custom editor written for Riot. +Most of the code can be found in the `/editor/` directory of the `matrix-react-sdk` project. +It is used to power the composer to edit messages, +and will soon be used as the main composer to send messages as well. + +## High-level overview. + +The editor is backed by a model that contains parts. +A part has some text and a type (plain text, pill, ...). When typing in the editor, +the model validates the input and updates the parts. +The parts are then reconciled with the DOM. + +## Inner workings + +When typing in the `contenteditable` element, the `input` event fires and +the DOM of the editor is turned into a string. The way this is done has +some logic to it to deal with adding newlines for block elements, to make sure +the caret offset is calculated in the same way as the content string, and the ignore +caret nodes (more on that later). +For these reasons it doesn't use `innerText`, `textContent` or anything similar. +The model addresses any content in the editor within as an offset within this string. +The caret position is thus also converted from a position in the DOM tree +to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. + +Once the content string and caret offset is calculated, it is passed to the `update()` +method of the model. The model first calculates the same content string its current parts, +basically just concatenating their text. It then looks for differences between +the current and the new content string. The diffing algorithm is very basic, +and assumes there is only one change around the caret offset, +so this should be very inexpensive. See `diff.js` for details. + +The result of the diffing is the strings that was added and/or removed from +the current content. These differences are then applied to the parts, +where parts can apply validation logic to these changes. + +For example, if you type an @ in some plain text, the plain text part rejects +that character, and this character is then presented to the part creator, +which will turn it into a pill candidate part. +Pill candidate parts are what opens the auto completion, and upon picking a completion, +replace themselves with an actual pill which can't be edited anymore. + +The diffing is needed to preserve state in the parts apart from their text +(which is the only thing the model receives from the DOM), e.g. to build +the model incrementally. Any text that didn't change is assumed +to leave the parts it intersects alone. + +The benefit of this is that we can use the `input` event, which is broadly supported, +to find changes in the editor. We don't have to rely on keyboard events, +which relate poorly to text input or changes. + +Once the parts of the model are updated, the DOM of the editor is then reconciled +with the new model state, see `renderModel` in `render.js` for this. +If the model didn't reject the input and didn't make any additional changes, +this won't make any changes to the DOM at all, and should thus be fairly efficient. + +For the browser to allow the user to place the caret between two pills, +or between a pill and the start and end of the line, we need some extra DOM nodes. +These DOM nodes are called caret nodes, and contain an invisble character, so +the caret can be placed into them. The model is unaware of caret nodes, and they +are only added to the DOM during the render phase. Likewise, when calculating +the content string, caret nodes need to be ignored, as they would confuse the model. + +As part of the reconciliation, the caret position is also adjusted to any changes +the model made to the input. The caret is passed around in two formats. +The model receives the caret *offset* within the content string (which includes +an atNodeEnd flag to make it unambiguous if it is at a part and or the next part start). +The model converts this to a caret *position* internally, which has a partIndex +and an offset within the part text, which is more natural to work with. +From there on, the caret *position* is used, also during reconciliation.