Skip to content

Commit

Permalink
feat(RichTextEditor): add BlockquotePlugin to support multiline quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
olafsulich committed Jan 7, 2025
1 parent 66e9f31 commit edd234d
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 9 deletions.
5 changes: 2 additions & 3 deletions src/script/components/RichTextEditor/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {FormatToolbar} from './components/FormatToolbar/FormatToolbar';
import {EmojiNode} from './nodes/EmojiNode';
import {MentionNode} from './nodes/MentionNode';
import {AutoFocusPlugin} from './plugins/AutoFocusPlugin';
import {BlockquotePlugin} from './plugins/BlockquotePlugin/BlockquotePlugin';
import {CodeHighlightPlugin} from './plugins/CodeHighlightPlugin/CodeHighlightPlugin';
import {DraftStatePlugin} from './plugins/DraftStatePlugin';
import {EditedMessagePlugin} from './plugins/EditedMessagePlugin/EditedMessagePlugin';
Expand Down Expand Up @@ -259,8 +260,8 @@ export const RichTextEditor = ({
<EmojiPickerPlugin openStateRef={emojiPickerOpen} />
<HistoryPlugin />
<ListPlugin />
<BlockquotePlugin />
{replaceEmojis && <ReplaceEmojiPlugin />}

<ReplaceCarriageReturnPlugin />
<MarkdownShortcutPlugin transformers={markdownTransformers} />
<CodeHighlightPlugin />
Expand All @@ -269,13 +270,11 @@ export const RichTextEditor = ({
placeholder={<Placeholder text={placeholder} hasLocalEphemeralTimer={hasLocalEphemeralTimer} />}
ErrorBoundary={LexicalErrorBoundary}
/>

<ClearEditorPlugin />
<MentionsPlugin
onSearch={search => (typeof search === 'string' ? getMentionCandidates(search) : [])}
openStateRef={mentionsOpen}
/>

<OnChangePlugin onChange={handleChange} ignoreSelectionChange />

<SendPlugin
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {useEffect} from 'react';

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$isQuoteNode} from '@lexical/rich-text';
import {
COMMAND_PRIORITY_LOW,
KEY_ENTER_COMMAND,
$getSelection,
$isRangeSelection,
KEY_BACKSPACE_COMMAND,
$isLineBreakNode,
INSERT_PARAGRAPH_COMMAND,
INSERT_LINE_BREAK_COMMAND,
LexicalEditor,
} from 'lexical';

export const BlockquotePlugin = () => {

Check failure on line 36 in src/script/components/RichTextEditor/plugins/BlockquotePlugin/BlockquotePlugin.tsx

View workflow job for this annotation

GitHub Actions / test

Function expression, which lacks return-type annotation, implicitly has an 'any' return type.
const [editor] = useLexicalComposerContext();

useEffect(() => {
return registerBlockquoteEnterCommand(editor);
}, [editor]);

useEffect(() => {
return registerBlockquoteBackspaceCommand(editor);
}, [editor]);

return null;
};

/**
* Because we use a custom Shift + Enter command (see SendPlugin.tsx), we need to register a custom Shify + Enter command for the blockquote.
* By default our Shift + Enter adds a new paragraph, which escapes the blockquote, prevents for adding multiline quotes.
* This command will add a new line break instead of a new paragraph, which will keep the blockquote.
*/
const registerBlockquoteEnterCommand = (editor: LexicalEditor) => {
return editor.registerCommand(
KEY_ENTER_COMMAND,
event => {
if (!event) {
return false;
}

const selection = $getSelection();

if (!$isRangeSelection(selection)) {
return false;
}

const anchorNode = selection.anchor.getNode();
const quoteBlock = anchorNode.getParent();

if (!$isQuoteNode(quoteBlock) || !$isQuoteNode(anchorNode)) {
return false;
}

if (event.shiftKey) {
event.preventDefault();
editor.update(() => {
editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, true);
});
return true;
}

event.preventDefault();

return true;
},
COMMAND_PRIORITY_LOW,
);
};

/**
* Because we use a custom Shift + Enter for the blockquotes, we no longer have an abilitiy to escape a blockquote by pressing Shift + Enter (cause the above command adds a new line break).
* This command will remove the last line break in the blockquote and add a new paragraph, which will escape the blockquote.
*/
const registerBlockquoteBackspaceCommand = (editor: LexicalEditor) => {
return editor.registerCommand(
KEY_BACKSPACE_COMMAND,
event => {
const selection = $getSelection();

if (!$isRangeSelection(selection)) {
return false;
}

event.preventDefault();

const anchorNode = selection.anchor.getNode();
const quoteBlock = anchorNode.getParent();

if (!$isQuoteNode(quoteBlock) && !$isQuoteNode(anchorNode)) {
return false;
}

if (!('getChildren' in anchorNode)) {
return false;
}

const children = anchorNode.getChildren();

const lastChild = children?.[children.length - 1];

const isLastChildLineBreakNode = $isLineBreakNode(lastChild);

if (!isLastChildLineBreakNode) {
return false;
}

editor.update(() => {
lastChild.remove();
editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
});

return true;
},
COMMAND_PRIORITY_LOW,
);
};
11 changes: 5 additions & 6 deletions src/script/components/RichTextEditor/plugins/SendPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -39,17 +39,16 @@ export function SendPlugin({onSend}: Props): null {
return false;
}

// Mimic the "Enter" behavior when a user press "Shift + Enter"
// It's useful for the rich text editor, especially when creating lists
if (event.shiftKey) {
const messageFormatButtonsEnabled = Config.getConfig().FEATURE.ENABLE_MESSAGE_FORMAT_BUTTONS;

if (messageFormatButtonsEnabled) {
event.preventDefault();
return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
editor.update(() => {
editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
});
return true;
}

return true;
}

// When sending a message with "Enter", we want to prevent the default behavior (new line)
Expand Down

0 comments on commit edd234d

Please sign in to comment.