diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index b3a40b5..a75fe57 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -86,6 +86,28 @@ let alignDropdown = $state(false); let insertDropdown = $state(false); + function scrollEditorBodyToBottom(source: HTMLElement | null | undefined = editorElement) { + const editorBody = source?.closest('.editor-body') as HTMLElement | null; + if (!editorBody) return; + editorBody.scrollTop = editorBody.scrollHeight; + requestAnimationFrame(() => { + editorBody.scrollTop = editorBody.scrollHeight; + }); + } + + function handleSourceCtrlEnd(event: KeyboardEvent) { + if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.key !== 'End') return false; + event.preventDefault(); + const ta = sourceElement; + ta.focus({ preventScroll: true }); + ta.setSelectionRange(sourceContent.length, sourceContent.length); + ta.scrollTop = ta.scrollHeight; + requestAnimationFrame(() => { + ta.scrollTop = ta.scrollHeight; + }); + return true; + } + function closeAllDropdowns() { headingDropdown = false; colorDropdown = false; @@ -585,6 +607,30 @@ }, }); + const CtrlEndScrollPastEnd = Extension.create({ + name: 'ctrlEndScrollPastEnd', + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('ctrlEndScrollPastEnd'), + props: { + handleDOMEvents: { + keydown(view, event) { + if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.key !== 'End') return false; + event.preventDefault(); + const tr = view.state.tr.setSelection(TextSelection.atEnd(view.state.doc)); + view.dispatch(tr); + (view.dom as HTMLElement).focus({ preventScroll: true }); + scrollEditorBodyToBottom(view.dom as HTMLElement); + return true; + }, + }, + }, + }), + ]; + }, + }); + const PageBreak = TiptapNode.create({ name: 'pageBreak', group: 'block', @@ -3115,6 +3161,7 @@ TextAlign.configure({ types: ['heading', 'paragraph'] }).extend({ addKeyboardShortcuts: () => ({}), }), + CtrlEndScrollPastEnd, HeadingShortcuts, SlashCommands, MoveLineShortcuts, @@ -4584,6 +4631,7 @@ pushSourceHistoryDebounced(); }} onkeydown={(e) => { + if (handleSourceCtrlEnd(e)) return; const mod = e.ctrlKey || e.metaKey; if (e.key === 'Enter' && e.shiftKey && !mod) { e.preventDefault(); @@ -4641,6 +4689,7 @@ pushSourceHistoryDebounced(); }} onkeydown={(e) => { + if (handleSourceCtrlEnd(e)) return; const mod = e.ctrlKey || e.metaKey; // Shift+Enter: insert two trailing spaces + newline for markdown hard break if (e.key === 'Enter' && e.shiftKey && !mod) { @@ -6667,7 +6716,7 @@ line-height: 1.3; resize: none; outline: none; - padding: 0; + padding: 0 0 var(--editor-scroll-past-end, 65vh); margin: 0; user-select: text; /* Wrap long lines instead of horizontal-scrolling (matches mobile). (issue #100) */ @@ -6725,6 +6774,11 @@ overflow: hidden; } + .editor-container:not(.mobile) :global(.tiptap-wrapper .tiptap) { + min-height: calc(100% + var(--editor-scroll-past-end, 65vh)); + padding-bottom: var(--editor-scroll-past-end, 65vh); + } + :global(.tiptap-wrapper .tiptap p) { margin: 0 0 0.75em; line-height: var(--editor-line-height, 1.65);