diff --git a/js/app/packages/block-channel/component/MarkdownArea.tsx b/js/app/packages/block-channel/component/MarkdownArea.tsx index 4e1402644..90980115f 100644 --- a/js/app/packages/block-channel/component/MarkdownArea.tsx +++ b/js/app/packages/block-channel/component/MarkdownArea.tsx @@ -55,6 +55,7 @@ import { type LexicalEditor, type TextFormatType, } from 'lexical'; + import { type Accessor, createEffect, @@ -283,7 +284,8 @@ function MarkdownArea(props: MarkdownAreaProps & ConsumableMarkdownAreaProps) { autoRegister( editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { + (e) => { + if (!e) return false; // TODO (seamus) : This is hacky. If we got a props.onEnter,then shift+enter becomes // the new "regular enter", so we delete the shiftKey and pass along to lexical. if (e.shiftKey) { diff --git a/js/app/packages/block-md/component/TitleEditor.tsx b/js/app/packages/block-md/component/TitleEditor.tsx index 972a06bfa..f2926330a 100644 --- a/js/app/packages/block-md/component/TitleEditor.tsx +++ b/js/app/packages/block-md/component/TitleEditor.tsx @@ -61,7 +61,8 @@ function titleNavigationPlugin( // Press enter in the title editor. titleEditor.registerCommand( KEY_ENTER_COMMAND, - (event: KeyboardEvent) => { + (event) => { + if (!event) return false; if (ignoreArrows()) return true; event?.preventDefault(); // Prepend a new paragraph to the main editor. diff --git a/js/app/packages/core/component/AI/component/input/useChatMarkdownArea.tsx b/js/app/packages/core/component/AI/component/input/useChatMarkdownArea.tsx index 0a9f9378f..60db6e734 100644 --- a/js/app/packages/core/component/AI/component/input/useChatMarkdownArea.tsx +++ b/js/app/packages/core/component/AI/component/input/useChatMarkdownArea.tsx @@ -373,7 +373,8 @@ function MarkdownArea( autoRegister( editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { + (e) => { + if (!e) return false; // TODO (seamus) : This is hacky. If we got a props.onEnter,then shift+enter becomes // the new "", so we delete the shiftKey and pass along to lexical. if (e.shiftKey) { diff --git a/js/app/packages/core/component/LexicalMarkdown/component/core/MarkdownTextarea.tsx b/js/app/packages/core/component/LexicalMarkdown/component/core/MarkdownTextarea.tsx index fac7db44c..82e118e13 100644 --- a/js/app/packages/core/component/LexicalMarkdown/component/core/MarkdownTextarea.tsx +++ b/js/app/packages/core/component/LexicalMarkdown/component/core/MarkdownTextarea.tsx @@ -202,7 +202,8 @@ export function MarkdownTextarea(props: MarkdownTextareaProps) { if (onEnter == null) return; cleanupEnterListener = editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { + (e) => { + if (!e) return false; // TODO (seamus) : This is hacky. If we got a props.onEnter,then shift+enter becomes // the new "regular enter", so we delete the shiftKey and pass along to lexical. if (e.altKey && e.shiftKey) { diff --git a/js/app/packages/core/component/LexicalMarkdown/component/misc/MentionsTextarea.tsx b/js/app/packages/core/component/LexicalMarkdown/component/misc/MentionsTextarea.tsx index 25c14126b..43ab5b375 100644 --- a/js/app/packages/core/component/LexicalMarkdown/component/misc/MentionsTextarea.tsx +++ b/js/app/packages/core/component/LexicalMarkdown/component/misc/MentionsTextarea.tsx @@ -117,8 +117,8 @@ export function MentionsTextarea(props: MentionsTextareaProps) { cleanupEnterListener(); cleanupEnterListener = editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { - e.preventDefault(); + (e) => { + e?.preventDefault(); return true; }, // Run at HIGH here so that the mentions menu can run at CRITICAL diff --git a/js/app/packages/core/component/LexicalMarkdown/plugins/checklist/checklistPlugin.ts b/js/app/packages/core/component/LexicalMarkdown/plugins/checklist/checklistPlugin.ts index dab1867dc..5e20ebe27 100644 --- a/js/app/packages/core/component/LexicalMarkdown/plugins/checklist/checklistPlugin.ts +++ b/js/app/packages/core/component/LexicalMarkdown/plugins/checklist/checklistPlugin.ts @@ -181,7 +181,8 @@ function registerChecklistPlugin(editor: LexicalEditor) { registerMouseEvents(editor), editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { + (e) => { + if (!e) return false; // Meta+Enter toggles check state. const isCommand = IS_MAC ? e.metaKey : e.ctrlKey; if (isCommand && !e.shiftKey) { diff --git a/js/app/packages/core/component/LexicalMarkdown/plugins/keyboard-shortcuts/keyboardShortcutsPlugin.ts b/js/app/packages/core/component/LexicalMarkdown/plugins/keyboard-shortcuts/keyboardShortcutsPlugin.ts index 26f24348c..aea6d38f4 100644 --- a/js/app/packages/core/component/LexicalMarkdown/plugins/keyboard-shortcuts/keyboardShortcutsPlugin.ts +++ b/js/app/packages/core/component/LexicalMarkdown/plugins/keyboard-shortcuts/keyboardShortcutsPlugin.ts @@ -93,7 +93,7 @@ export function keyboardShortcutsPlugin(props: KeyboardShortcutPluginProps) { export const DefaultShortcuts: Shortcut[] = [ { - label: META_OR_CTRL + '+shift+x', + label: `${META_OR_CTRL}+shift+x`, test: (e) => { return ( e.code === 'KeyX' && e.shiftKey && metaOrCtrl(e.metaKey, e.ctrlKey) @@ -105,7 +105,7 @@ export const DefaultShortcuts: Shortcut[] = [ priority: 0, }, { - label: META_OR_CTRL + 'e', + label: `${META_OR_CTRL}+e`, test: (e) => { return e.code === 'KeyE' && metaOrCtrl(e.metaKey, e.ctrlKey); }, @@ -115,7 +115,7 @@ export const DefaultShortcuts: Shortcut[] = [ priority: 0, }, { - label: META_OR_CTRL + '+shift+h', + label: `${META_OR_CTRL}+shift+h`, test: (e) => { return e.code === 'KeyH' && metaOrCtrl(e.metaKey, e.ctrlKey); }, diff --git a/js/app/packages/core/component/LexicalMarkdown/plugins/markdown-shortcuts/markdownShortcutsPlugin.ts b/js/app/packages/core/component/LexicalMarkdown/plugins/markdown-shortcuts/markdownShortcutsPlugin.ts index 793cbc2b2..8ae2fda43 100644 --- a/js/app/packages/core/component/LexicalMarkdown/plugins/markdown-shortcuts/markdownShortcutsPlugin.ts +++ b/js/app/packages/core/component/LexicalMarkdown/plugins/markdown-shortcuts/markdownShortcutsPlugin.ts @@ -48,7 +48,7 @@ function registerMarkdownShortcutsPlugins( registerMarkdownShortcuts(editor, transformers), editor.registerCommand( KEY_ENTER_COMMAND, - (e: KeyboardEvent) => { + (e) => { const selection = $getSelection(); if (!$isRangeSelection(selection)) return false; const anchor = selection.anchor; @@ -67,12 +67,12 @@ function registerMarkdownShortcutsPlugins( if (transformer.type === 'multiline-element') { transformer.replace(parent, [node], match, null, null, false); node.remove(); - e.preventDefault(); + e?.preventDefault(); return true; } else if (transformer.type === 'element') { transformer.replace(parent, [node], match, false); node.remove(); - e.preventDefault(); + e?.preventDefault(); return true; } } diff --git a/js/app/packages/core/component/LexicalMarkdown/plugins/normalize-enter/normalizeEnterPlugin.ts b/js/app/packages/core/component/LexicalMarkdown/plugins/normalize-enter/normalizeEnterPlugin.ts index 783da9fe3..044d362ae 100644 --- a/js/app/packages/core/component/LexicalMarkdown/plugins/normalize-enter/normalizeEnterPlugin.ts +++ b/js/app/packages/core/component/LexicalMarkdown/plugins/normalize-enter/normalizeEnterPlugin.ts @@ -3,13 +3,19 @@ import { mergeRegister } from '@lexical/utils'; import { $createParagraphNode, $getSelection, + $isElementNode, + $isLineBreakNode, + $isParagraphNode, $isRangeSelection, + $isRootNode, + COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, type ElementNode, KEY_ENTER_COMMAND, type LexicalEditor, type RangeSelection, } from 'lexical'; +import { isEmptyOrMatches } from '../../utils'; function $testSelectionPosition( selection: RangeSelection, @@ -22,6 +28,42 @@ function $testSelectionPosition( ); } +/** + * Returns true if the selection is at the start of an empty paragraph or is + * directly preceded by a line break. + */ +export function $isAtStartOfEmptyParagraph(): boolean { + const selection = $getSelection(); + if (!$isRangeSelection(selection) || !selection.isCollapsed()) { + return false; + } + + const node = selection.anchor.getNode(); + const parentElement = node.getParent(); + + // check for preceding line break node + if (selection.anchor.type === 'element') { + if ($isElementNode(node)) { + const offsetNode = node.getChildAtIndex(selection.anchor.offset); + if ($isLineBreakNode(offsetNode)) { + return true; + } + } + } + + if (selection.anchor.offset !== 0) { + return false; + } + + if ($isParagraphNode(parentElement)) { + return isEmptyOrMatches(parentElement.getTextContent().trim(), /^$/); + } + if ($isRootNode(parentElement)) { + return isEmptyOrMatches(node.getTextContent().trim(), /^$/); + } + return false; +} + /** * Normalize enter at start of block elements. * QuoteNode - mirrors heading node behavior and notion. @@ -56,7 +98,8 @@ function registerNormalizeEnterPlugin(editor: LexicalEditor) { return mergeRegister( editor.registerCommand( KEY_ENTER_COMMAND, - (event: KeyboardEvent) => { + (event) => { + if (!event) return false; if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) { return false; } @@ -65,6 +108,21 @@ function registerNormalizeEnterPlugin(editor: LexicalEditor) { return res; }, COMMAND_PRIORITY_NORMAL + ), + + editor.registerCommand( + KEY_ENTER_COMMAND, + (e) => { + if (e?.shiftKey) { + if ($isAtStartOfEmptyParagraph()) { + e.preventDefault(); + editor.dispatchCommand(KEY_ENTER_COMMAND, null); + return true; + } + } + return false; + }, + COMMAND_PRIORITY_LOW ) ); }