diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index 39d24b1bab..38cfff9b65 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -22,9 +22,11 @@ import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; import {setCaretPosition} from '../../../editor/caret'; import { + replaceRangeAndExpandSelection, + formatRangeAsQuote, formatInline, } from '../../../editor/operations'; -import {getCaretOffsetAndText, getRangeForSelection, getSelectionOffsetAndText} from '../../../editor/dom'; +import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; import Autocomplete from '../rooms/Autocomplete'; import {autoCompleteCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; @@ -427,37 +429,6 @@ export default class BasicMessageEditor extends React.Component { return caretPosition; } - _replaceSelection(callback) { - const selection = document.getSelection(); - if (selection.isCollapsed) { - return; - } - const focusOffset = getSelectionOffsetAndText( - this._editorRef, - selection.focusNode, - selection.focusOffset, - ).offset; - const anchorOffset = getSelectionOffsetAndText( - this._editorRef, - selection.anchorNode, - selection.anchorOffset, - ).offset; - const {model} = this.props; - const focusPosition = focusOffset.asPosition(model); - const anchorPosition = anchorOffset.asPosition(model); - const range = model.startRange(focusPosition, anchorPosition); - const firstPosition = focusPosition.compare(anchorPosition) < 0 ? focusPosition : anchorPosition; - - model.transform(() => { - const oldLen = range.length; - const newParts = callback(range); - const addedLen = range.replace(newParts); - const lastOffset = firstPosition.asOffset(model); - lastOffset.offset += oldLen + addedLen; - return lastOffset.asPosition(model); - }); - } - _wrapSelectionAsInline(prefix, suffix = prefix) { const range = getRangeForSelection( this._editorRef, @@ -479,30 +450,11 @@ export default class BasicMessageEditor extends React.Component { } _formatQuote = () => { - const {model} = this.props; - const {partCreator} = this.props.model; - this._replaceSelection(range => { - const parts = range.parts; - parts.splice(0, 0, partCreator.plain("> ")); - const startsWithPartial = range.start.offset !== 0; - const isFirstPart = range.start.index === 0; - const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; - // prepend a newline if there is more text before the range on this line - if (startsWithPartial || (!isFirstPart && !previousIsNewline)) { - parts.splice(0, 0, partCreator.newline()); - } - // start at position 1 to make sure we skip the potentially inserted newline above, - // as we already inserted a quote sign for it above - for (let i = 1; i < parts.length; ++i) { - const part = parts[i]; - if (part.type === "newline") { - parts.splice(i + 1, 0, partCreator.plain("> ")); - } - } - parts.push(partCreator.newline()); - parts.push(partCreator.newline()); - return parts; - }); + const range = getRangeForSelection( + this._editorRef, + this.props.model, + document.getSelection()); + formatRangeAsQuote(range); } _formatCodeBlock = () => { diff --git a/src/editor/dom.js b/src/editor/dom.js index 03ee7f2cfc..e2a65be6ff 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -45,7 +45,7 @@ export function getCaretOffsetAndText(editor, sel) { return {caret: offset, text}; } -export function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { +function getSelectionOffsetAndText(editor, selectionNode, selectionOffset) { // sometimes selectionNode is an element, and then selectionOffset means // the index of a child element ... - 1 🤷 if (selectionNode.nodeType === Node.ELEMENT_NODE && selectionOffset !== 0) { diff --git a/src/editor/offset.js b/src/editor/offset.js index c638640f6f..785f16bc6d 100644 --- a/src/editor/offset.js +++ b/src/editor/offset.js @@ -23,4 +23,8 @@ export default class DocumentOffset { asPosition(model) { return model.positionForOffset(this.offset, this.atNodeEnd); } + + add(delta, atNodeEnd = false) { + return new DocumentOffset(this.offset + delta, atNodeEnd); + } } diff --git a/src/editor/operations.js b/src/editor/operations.js index 4f0757948a..be979c275a 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -29,6 +29,44 @@ export function replaceRangeAndExpandSelection(model, range, newParts) { }); } +export function rangeStartsAtBeginningOfLine(range) { + const {model} = range; + const startsWithPartial = range.start.offset !== 0; + const isFirstPart = range.start.index === 0; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + return !startsWithPartial && (isFirstPart || previousIsNewline); +} + +export function rangeEndsAtEndOfLine(range) { + const {model} = range; + const lastPart = model.parts[range.end.index]; + const endsWithPartial = range.end.offset !== lastPart.length; + const isLastPart = range.end.index === model.parts.length - 1; + const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline"; + return !endsWithPartial && (isLastPart || nextIsNewline); +} + +export function formatRangeAsQuote(range) { + const {model, parts} = range; + const {partCreator} = model; + for (let i = 0; i < parts.length; ++i) { + const part = parts[i]; + if (part.type === "newline") { + parts.splice(i + 1, 0, partCreator.plain("> ")); + } + } + parts.unshift(partCreator.plain("> ")); + if (!rangeStartsAtBeginningOfLine(range)) { + parts.unshift(partCreator.newline()); + } + if (rangeEndsAtEndOfLine(range)) { + parts.push(partCreator.newline()); + } + + parts.push(partCreator.newline()); + replaceRangeAndExpandSelection(model, range, parts); +} + export function formatInline(range, prefix, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; diff --git a/src/editor/range.js b/src/editor/range.js index 2163076515..0739cd7842 100644 --- a/src/editor/range.js +++ b/src/editor/range.js @@ -33,6 +33,10 @@ export default class Range { this._start = this._start.backwardsWhile(this._model, predicate); } + get model() { + return this._model; + } + get text() { let text = ""; this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => {