From a94c33edaf976e1376420b2ba03fb39ffd16877e Mon Sep 17 00:00:00 2001 From: MF Date: Mon, 15 Jun 2026 18:47:05 +0200 Subject: [PATCH] Scroll Past End (#116) Adds editor scroll room after the final line so users can focus the active writing area without inserting blank lines. Ctrl+End moves to the document end and scrolls into that workspace; plain End keeps native line-end behavior. Presentation-only except cursor movement; does not mutate note content, mark dirty, or affect autosave. Reviewed-on: https://codeberg.org/ArkHost/HelixNotes/pulls/116 --- src/lib/components/Editor.svelte | 56 +++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) 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);