From 30fd027adfaa0394b95da615e991df0703b6af01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 2 Aug 2019 12:19:46 +0200 Subject: [PATCH 1/4] document editor --- docs/ciderEditor.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/ciderEditor.md diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md new file mode 100644 index 0000000000..019f4cb8b1 --- /dev/null +++ b/docs/ciderEditor.md @@ -0,0 +1,25 @@ +# 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, ... so 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. + +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. From 995ae41e67dc02bd4e83c81ed4c670fd1e2c7ce5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 2 Aug 2019 13:40:16 +0200 Subject: [PATCH 2/4] add line breaks --- docs/ciderEditor.md | 57 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 019f4cb8b1..93751b590a 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -1,25 +1,62 @@ # 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. +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. +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, ... so 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`. +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, ... +so 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. +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. +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. +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 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. +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. +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. -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. +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. From ad776fbfca8559747c3dc9f6c040c09ebcfee3c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 2 Aug 2019 13:45:35 +0200 Subject: [PATCH 3/4] describe all reasons why we need a custom textify algorithm --- docs/ciderEditor.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index 93751b590a..ae08d3a445 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -16,8 +16,10 @@ The parts are then reconciled with the DOM. 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, ... -so doesn't use `innerText`, `textContent` or anything similar. +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`. From dae6fae3d666e8b4434498972845041b00031de6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 2 Aug 2019 13:45:52 +0200 Subject: [PATCH 4/4] describe caret nodes --- docs/ciderEditor.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index ae08d3a445..2448be852a 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -55,6 +55,13 @@ 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