Fix regression around pasting links (#8537)

* Fix regression around pasting links

* Add tests
This commit is contained in:
Michael Telatynski 2022-05-09 13:39:32 +01:00 committed by GitHub
parent 7e21be06d0
commit 674aec4050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 29 deletions

View file

@ -28,7 +28,7 @@ import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineF
from '../../../editor/operations';
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
import { getAutoCompleteCreator, Type } from '../../../editor/parts';
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
@ -92,7 +92,7 @@ function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
interface IProps {
model: EditorModel;
room: Room;
threadId: string;
threadId?: string;
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
@ -333,28 +333,29 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
event.preventDefault(); // we always handle the paste ourselves
if (this.props.onPaste && this.props.onPaste(event, this.props.model)) {
if (this.props.onPaste?.(event, this.props.model)) {
// to prevent double handling, allow props.onPaste to skip internal onPaste
return true;
}
const { model } = this.props;
const { partCreator } = model;
const plainText = event.clipboardData.getData("text/plain");
const partsText = event.clipboardData.getData("application/x-element-composer");
let parts;
let parts: Part[];
if (partsText) {
const serializedTextParts = JSON.parse(partsText);
const deserializedParts = serializedTextParts.map(p => partCreator.deserializePart(p));
parts = deserializedParts;
parts = serializedTextParts.map(p => partCreator.deserializePart(p));
} else {
const text = event.clipboardData.getData("text/plain");
parts = parsePlainTextMessage(text, partCreator, { shouldEscape: false });
parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false });
}
const textToInsert = event.clipboardData.getData("text/plain");
this.modifiedFlag = true;
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection());
if (textToInsert && linkify.test(textToInsert)) {
formatRangeAsLink(range, textToInsert);
if (plainText && range.length > 0 && linkify.test(plainText)) {
formatRangeAsLink(range, plainText);
} else {
replaceRangeAndMoveCaret(range, parts);
}

View file

@ -17,6 +17,8 @@ limitations under the License.
import { CARET_NODE_CHAR, isCaretNode } from "./render";
import DocumentOffset from "./offset";
import EditorModel from "./model";
import Range from "./range";
type Predicate = (node: Node) => boolean;
type Callback = (node: Node) => void;
@ -122,7 +124,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
let foundNode = false;
let text = "";
function enterNodeCallback(node) {
function enterNodeCallback(node: HTMLElement) {
if (!foundNode) {
if (node === selectionNode) {
foundNode = true;
@ -148,12 +150,12 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
return true;
}
function leaveNodeCallback(node) {
function leaveNodeCallback(node: HTMLElement) {
// if this is not the last DIV (which are only used as line containers atm)
// we don't just check if there is a nextSibling because sometimes the caret ends up
// after the last DIV and it creates a newline if you type then,
// whereas you just want it to be appended to the current line
if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
if (node.tagName === "DIV" && (<HTMLElement>node.nextSibling)?.tagName === "DIV") {
text += "\n";
if (!foundNode) {
offsetToNode += 1;
@ -167,7 +169,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
}
// get text value of text node, ignoring ZWS if it's a caret node
function getTextNodeValue(node) {
function getTextNodeValue(node: Node): string {
const nodeText = node.nodeValue;
// filter out ZWS for caret nodes
if (isCaretNode(node.parentElement)) {
@ -184,7 +186,7 @@ function getTextNodeValue(node) {
}
}
export function getRangeForSelection(editor, model, selection) {
export function getRangeForSelection(editor: HTMLDivElement, model: EditorModel, selection: Selection): Range {
const focusOffset = getSelectionOffsetAndText(
editor,
selection.focusNode,

View file

@ -219,14 +219,12 @@ export function formatRangeAsCode(range: Range): void {
export function formatRangeAsLink(range: Range, text?: string) {
const { model } = range;
const { partCreator } = model;
const linkRegex = /\[(.*?)\]\(.*?\)/g;
const linkRegex = /\[(.*?)]\(.*?\)/g;
const isFormattedAsLink = linkRegex.test(range.text);
if (isFormattedAsLink) {
const linkDescription = range.text.replace(linkRegex, "$1");
const newParts = [partCreator.plain(linkDescription)];
const prefixLength = 1;
const suffixLength = range.length - (linkDescription.length + 2);
replaceRangeAndAutoAdjustCaret(range, newParts, true, prefixLength, suffixLength);
replaceRangeAndMoveCaret(range, newParts, 0);
} else {
// We set offset to -1 here so that the caret lands between the brackets
replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1);