From 10291bafe0a72a3ed492324b5b32b2e12d6ab029 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 23 Aug 2019 17:37:58 +0100 Subject: [PATCH 1/8] add support for selecting ranges in the editor model, and replacing them this to support finding emoticons and replacing them with an emoji --- src/editor/model.js | 39 +++++++++++------ src/editor/position.js | 38 +++++++++++++++++ src/editor/range.js | 53 +++++++++++++++++++++++ test/editor/range-test.js | 88 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 12 deletions(-) create mode 100644 src/editor/position.js create mode 100644 src/editor/range.js create mode 100644 test/editor/range-test.js diff --git a/src/editor/model.js b/src/editor/model.js index 2f1e5218d8..c5b80247f6 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -16,6 +16,8 @@ limitations under the License. */ import {diffAtCaret, diffDeletion} from "./diff"; +import DocumentPosition from "./position"; +import Range from "./range"; export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { @@ -197,7 +199,7 @@ export default class EditorModel { this._updateCallback(pos); } - _mergeAdjacentParts(docPos) { + _mergeAdjacentParts() { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -339,19 +341,32 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } -} -class DocumentPosition { - constructor(index, offset) { - this._index = index; - this._offset = offset; + startRange(position) { + return new Range(this, position); } - get index() { - return this._index; - } - - get offset() { - return this._offset; + // called from Range.replace + replaceRange(startPosition, endPosition, parts) { + const newStartPartIndex = this._splitAt(startPosition); + const idxDiff = newStartPartIndex - startPosition.index; + // if both position are in the same part, and we split it at start position, + // the offset of the end position needs to be decreased by the offset of the start position + const removedOffset = startPosition.index === endPosition.index ? startPosition.offset : 0; + const adjustedEndPosition = new DocumentPosition( + endPosition.index + idxDiff, + endPosition.offset - removedOffset, + ); + const newEndPartIndex = this._splitAt(adjustedEndPosition); + for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { + this._removePart(i); + } + let insertIdx = newStartPartIndex; + for (const part of parts) { + this._insertPart(insertIdx, part); + insertIdx += 1; + } + this._mergeAdjacentParts(); + this._updateCallback(); } } diff --git a/src/editor/position.js b/src/editor/position.js new file mode 100644 index 0000000000..c771482012 --- /dev/null +++ b/src/editor/position.js @@ -0,0 +1,38 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export default class DocumentPosition { + constructor(index, offset) { + this._index = index; + this._offset = offset; + } + + get index() { + return this._index; + } + + get offset() { + return this._offset; + } + + compare(otherPos) { + if (this._index === otherPos._index) { + return this._offset - otherPos._offset; + } else { + return this._index - otherPos._index; + } + } +} diff --git a/src/editor/range.js b/src/editor/range.js new file mode 100644 index 0000000000..e2ecc5d12b --- /dev/null +++ b/src/editor/range.js @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export default class Range { + constructor(model, startPosition, endPosition = startPosition) { + this._model = model; + this._start = startPosition; + this._end = endPosition; + } + + moveStart(delta) { + this._start = this._start.forwardsWhile(this._model, () => { + delta -= 1; + return delta >= 0; + }); + } + + expandBackwardsWhile(predicate) { + this._start = this._start.backwardsWhile(this._model, predicate); + } + + get text() { + let text = ""; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + const t = part.text.substring(startIdx, endIdx); + text = text + t; + }); + return text; + } + + replace(parts) { + const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); + let oldLength = 0; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + oldLength += endIdx - startIdx; + }); + this._model.replaceRange(this._start, this._end, parts); + return newLength - oldLength; + } +} diff --git a/test/editor/range-test.js b/test/editor/range-test.js new file mode 100644 index 0000000000..5a95da952d --- /dev/null +++ b/test/editor/range-test.js @@ -0,0 +1,88 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +const pillChannel = "#riot-dev:matrix.org"; + +describe('editor/range', function() { + it('range on empty model', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([], pc, renderer); + const range = model.startRange(model.positionForOffset(0, true)); // after "world" + let called = false; + range.expandBackwardsWhile(chr => { + called = true; + return true; + }); + expect(called).toBe(false); + expect(range.text).toBe(""); + }); + it('range replace within a part', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello world!!!!")], pc, renderer); + const range = model.startRange(model.positionForOffset(11)); // after "world" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("world"); + range.replace([pc.roomPill(pillChannel)]); + console.log({parts: JSON.stringify(model.serializeParts())}); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("hello "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe("!!!!"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); + it('range replace across parts', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("try to re"), + pc.plain("pla"), + pc.plain("ce "), + pc.plain("me"), + ], pc, renderer); + const range = model.startRange(model.positionForOffset(14)); // after "replace" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("replace"); + console.log("range.text", {text: range.text}); + range.replace([pc.roomPill(pillChannel)]); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("try to "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe(" me"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); +}); From 0e65f71a375fbdee100828dcd84cd6c22a448796 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:00:56 +0200 Subject: [PATCH 2/8] support incrementing/decrementing doc positions with predicate --- src/editor/position.js | 69 +++++++++++++++++++++++++++++++ test/editor/position-test.js | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 test/editor/position-test.js diff --git a/src/editor/position.js b/src/editor/position.js index c771482012..5dcb31fe65 100644 --- a/src/editor/position.js +++ b/src/editor/position.js @@ -35,4 +35,73 @@ export default class DocumentPosition { return this._index - otherPos._index; } } + + iteratePartsBetween(other, model, callback) { + if (this.index === -1 || other.index === -1) { + return; + } + const [startPos, endPos] = this.compare(other) < 0 ? [this, other] : [other, this]; + if (startPos.index === endPos.index) { + callback(model.parts[this.index], startPos.offset, endPos.offset); + } else { + const firstPart = model.parts[startPos.index]; + callback(firstPart, startPos.offset, firstPart.text.length); + for (let i = startPos.index + 1; i < endPos.index; ++i) { + const part = model.parts[i]; + callback(part, 0, part.text.length); + } + const lastPart = model.parts[endPos.index]; + callback(lastPart, 0, endPos.offset); + } + } + + forwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const {parts} = model; + while (index < parts.length) { + const part = parts[index]; + while (offset < part.text.length) { + if (!predicate(index, offset, part)) { + return new DocumentPosition(index, offset); + } + offset += 1; + } + // end reached + if (index === (parts.length - 1)) { + return new DocumentPosition(index, offset); + } else { + index += 1; + offset = 0; + } + } + } + + backwardsWhile(model, predicate) { + if (this.index === -1) { + return this; + } + + let {index, offset} = this; + const parts = model.parts; + while (index >= 0) { + const part = parts[index]; + while (offset > 0) { + if (!predicate(index, offset - 1, part)) { + return new DocumentPosition(index, offset); + } + offset -= 1; + } + // start reached + if (index === 0) { + return new DocumentPosition(index, offset); + } else { + index -= 1; + offset = parts[index].text.length; + } + } + } } diff --git a/test/editor/position-test.js b/test/editor/position-test.js new file mode 100644 index 0000000000..7ac4284c60 --- /dev/null +++ b/test/editor/position-test.js @@ -0,0 +1,80 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +describe('editor/position', function() { + it('move first position backward in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.backwardsWhile(model, () => true); + expect(pos).toBe(pos2); + }); + it('move first position forwards in empty model', function() { + const model = new EditorModel([], createPartCreator(), createRenderer()); + const pos = model.positionForOffset(0, true); + const pos2 = pos.forwardsWhile(() => true); + expect(pos).toBe(pos2); + }); + it('move forwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(1); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); + it('move forwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.forwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(1); + expect(pos2.offset).toBe(2); + }); + it('move backwards within one part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello")], pc, createRenderer()); + const pos = model.positionForOffset(4); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(1); + }); + it('move backwards crossing to other part', function() { + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello"), pc.plain(" world")], pc, createRenderer()); + const pos = model.positionForOffset(7); + let n = 3; + const pos2 = pos.backwardsWhile(model, () => { n -= 1; return n >= 0; }); + expect(pos2.index).toBe(0); + expect(pos2.offset).toBe(4); + }); +}); From f8f0e77bdefbc8cbc2de0def3d78c5de0eec0123 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:09:46 +0200 Subject: [PATCH 3/8] add transform step during editor model update --- src/editor/model.js | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index c5b80247f6..5584627688 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,15 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; + /** + * @callback TransformCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. + This is used to adjust the caret position. + */ + export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { this._parts = parts; @@ -26,7 +35,19 @@ export default class EditorModel { this._activePartIdx = null; this._autoComplete = null; this._autoCompletePartIdx = null; + this._transformCallback = null; this.setUpdateCallback(updateCallback); + this._updateInProgress = false; + } + + /** Set a callback for the transformation step. + * While processing an update, right before calling the update callback, + * a transform callback can be called, which serves to do modifications + * on the model that can span multiple parts. Also see `startRange()`. + * @param {TransformCallback} transformCallback + */ + setTransformCallback(transformCallback) { + this._transformCallback = transformCallback; } setUpdateCallback(updateCallback) { @@ -133,6 +154,7 @@ export default class EditorModel { } update(newValue, inputType, caret) { + this._updateInProgress = true; const diff = this._diff(newValue, inputType, caret); const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; @@ -147,11 +169,23 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - const newPosition = this.positionForOffset(caretOffset, true); + let newPosition = this.positionForOffset(caretOffset, true); this._setActivePart(newPosition, canOpenAutoComplete); + if (this._transformCallback) { + const transformAddedLen = this._transform(newPosition, inputType, diff); + if (transformAddedLen !== 0) { + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); + } + } + this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); } + _transform(newPosition, inputType, diff) { + const result = this._transformCallback(newPosition, inputType, diff); + return Number.isFinite(result) ? result : 0; + } + _setActivePart(pos, canOpenAutoComplete) { const {index} = pos; const part = this._parts[index]; @@ -367,6 +401,8 @@ export default class EditorModel { insertIdx += 1; } this._mergeAdjacentParts(); - this._updateCallback(); + if (!this._updateInProgress) { + this._updateCallback(); + } } } From 4fd4ad41c127061c7b5440f72dc3fcd3eae721ee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:02 +0200 Subject: [PATCH 4/8] improve editor model documentation --- src/editor/model.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/editor/model.js b/src/editor/model.js index 5584627688..689b657f05 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -19,6 +19,13 @@ import {diffAtCaret, diffDeletion} from "./diff"; import DocumentPosition from "./position"; import Range from "./range"; +/** + * @callback ModelCallback + * @param {DocumentPosition?} caretPosition the position where the caret should be position + * @param {string?} inputType the inputType of the DOM input event + * @param {object?} diff an object with `removed` and `added` strings + */ + /** * @callback TransformCallback * @param {DocumentPosition?} caretPosition the position where the caret should be position @@ -50,6 +57,9 @@ export default class EditorModel { this._transformCallback = transformCallback; } + /** Set a callback for rerendering the model after it has been updated. + * @param {ModelCallback} updateCallback + */ setUpdateCallback(updateCallback) { this._updateCallback = updateCallback; } @@ -376,6 +386,11 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } + /** + * Starts a range, which can span across multiple parts, to find and replace text. + * @param {DocumentPosition} position where to start the range + * @return {Range} + */ startRange(position) { return new Range(this, position); } From 0273795f5db86c416a2e40978c5b853a3fae26c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 26 Aug 2019 16:10:26 +0200 Subject: [PATCH 5/8] add transform step to composer to auto-replace emoticons with emoji --- .../views/rooms/BasicMessageComposer.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index e4179d9c3b..780f39f9e7 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -25,6 +25,11 @@ import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SettingsStore from "../../../settings/SettingsStore"; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; + +const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const IS_MAC = navigator.platform.indexOf("Mac") !== -1; @@ -70,6 +75,28 @@ export default class BasicMessageEditor extends React.Component { this._modifiedFlag = false; } + _replaceEmoticon = (caret, inputType, diff) => { + const {model} = this.props; + const range = model.startRange(caret); + // expand range max 8 characters backwards from caret + let n = 8; + range.expandBackwardsWhile((index, offset) => { + const part = model.parts[index]; + n -= 1; + return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + }); + const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); + if (emoticonMatch) { + const query = emoticonMatch[1].toLowerCase().replace("-", ""); + const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); + if (data) { + // + 1 because index is reported without preceding space + range.moveStart(emoticonMatch.index + 1); + return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); + } + } + } + _updateEditorState = (caret, inputType, diff) => { renderModel(this._editorRef, this.props.model); if (caret) { @@ -262,6 +289,9 @@ export default class BasicMessageEditor extends React.Component { componentDidMount() { const model = this.props.model; model.setUpdateCallback(this._updateEditorState); + if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { + model.setTransformCallback(this._replaceEmoticon); + } const partCreator = model.partCreator; // TODO: does this allow us to get rid of EditorStateTransfer? // not really, but we could not serialize the parts, and just change the autoCompleter From 5c28b57681e0ff75194a15cd180000f3886e481c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:49:22 +0200 Subject: [PATCH 6/8] always recalculate position after doing transform step as the amount of characters might not have changed, parts may still have been merged, removed or added which requires a new position. --- src/editor/model.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index 689b657f05..d0f1be7158 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -183,9 +183,7 @@ export default class EditorModel { this._setActivePart(newPosition, canOpenAutoComplete); if (this._transformCallback) { const transformAddedLen = this._transform(newPosition, inputType, diff); - if (transformAddedLen !== 0) { - newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); - } + newPosition = this.positionForOffset(caretOffset + transformAddedLen, true); } this._updateInProgress = false; this._updateCallback(newPosition, inputType, diff); From 56606a46f4b120065957ca020122e7da6edc5bc2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:50:36 +0200 Subject: [PATCH 7/8] don't assume preceding space for emoticon at start of document also add more inline comments to explain what is going on --- src/components/views/rooms/BasicMessageComposer.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 780f39f9e7..662167b714 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -78,7 +78,8 @@ export default class BasicMessageEditor extends React.Component { _replaceEmoticon = (caret, inputType, diff) => { const {model} = this.props; const range = model.startRange(caret); - // expand range max 8 characters backwards from caret + // expand range max 8 characters backwards from caret, + // as a space to look for an emoticon let n = 8; range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; @@ -90,8 +91,14 @@ export default class BasicMessageEditor extends React.Component { const query = emoticonMatch[1].toLowerCase().replace("-", ""); const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); if (data) { - // + 1 because index is reported without preceding space - range.moveStart(emoticonMatch.index + 1); + const hasPrecedingSpace = emoticonMatch[0][0] === " "; + // we need the range to only comprise of the emoticon + // because we'll replace the whole range with an emoji, + // so move the start forward to the start of the emoticon. + // Take + 1 because index is reported without the possible preceding space. + range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0)); + // this returns the amount of added/removed characters during the replace + // so the caret position can be adjusted. return range.replace([this.props.model.partCreator.plain(data.unicode + " ")]); } } From f10e1d76549b1cf7972ec951741521088739ebd3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 27 Aug 2019 09:54:13 +0200 Subject: [PATCH 8/8] fix jsdoc comments --- src/editor/model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/editor/model.js b/src/editor/model.js index d0f1be7158..9d129afa69 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -32,7 +32,7 @@ import Range from "./range"; * @param {string?} inputType the inputType of the DOM input event * @param {object?} diff an object with `removed` and `added` strings * @return {Number?} addedLen how many characters were added/removed (-) before the caret during the transformation step. - This is used to adjust the caret position. + * This is used to adjust the caret position. */ export default class EditorModel { @@ -47,7 +47,8 @@ export default class EditorModel { this._updateInProgress = false; } - /** Set a callback for the transformation step. + /** + * Set a callback for the transformation step. * While processing an update, right before calling the update callback, * a transform callback can be called, which serves to do modifications * on the model that can span multiple parts. Also see `startRange()`. @@ -57,7 +58,8 @@ export default class EditorModel { this._transformCallback = transformCallback; } - /** Set a callback for rerendering the model after it has been updated. + /** + * Set a callback for rerendering the model after it has been updated. * @param {ModelCallback} updateCallback */ setUpdateCallback(updateCallback) {