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
This commit is contained in:
MF
2026-06-15 18:47:05 +02:00
committed by ArkHost
parent a1404062c5
commit a94c33edaf
+55 -1
View File
@@ -86,6 +86,28 @@
let alignDropdown = $state(false); let alignDropdown = $state(false);
let insertDropdown = $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() { function closeAllDropdowns() {
headingDropdown = false; headingDropdown = false;
colorDropdown = 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({ const PageBreak = TiptapNode.create({
name: 'pageBreak', name: 'pageBreak',
group: 'block', group: 'block',
@@ -3115,6 +3161,7 @@
TextAlign.configure({ types: ['heading', 'paragraph'] }).extend({ TextAlign.configure({ types: ['heading', 'paragraph'] }).extend({
addKeyboardShortcuts: () => ({}), addKeyboardShortcuts: () => ({}),
}), }),
CtrlEndScrollPastEnd,
HeadingShortcuts, HeadingShortcuts,
SlashCommands, SlashCommands,
MoveLineShortcuts, MoveLineShortcuts,
@@ -4584,6 +4631,7 @@
pushSourceHistoryDebounced(); pushSourceHistoryDebounced();
}} }}
onkeydown={(e) => { onkeydown={(e) => {
if (handleSourceCtrlEnd(e)) return;
const mod = e.ctrlKey || e.metaKey; const mod = e.ctrlKey || e.metaKey;
if (e.key === 'Enter' && e.shiftKey && !mod) { if (e.key === 'Enter' && e.shiftKey && !mod) {
e.preventDefault(); e.preventDefault();
@@ -4641,6 +4689,7 @@
pushSourceHistoryDebounced(); pushSourceHistoryDebounced();
}} }}
onkeydown={(e) => { onkeydown={(e) => {
if (handleSourceCtrlEnd(e)) return;
const mod = e.ctrlKey || e.metaKey; const mod = e.ctrlKey || e.metaKey;
// Shift+Enter: insert two trailing spaces + newline for markdown hard break // Shift+Enter: insert two trailing spaces + newline for markdown hard break
if (e.key === 'Enter' && e.shiftKey && !mod) { if (e.key === 'Enter' && e.shiftKey && !mod) {
@@ -6667,7 +6716,7 @@
line-height: 1.3; line-height: 1.3;
resize: none; resize: none;
outline: none; outline: none;
padding: 0; padding: 0 0 var(--editor-scroll-past-end, 65vh);
margin: 0; margin: 0;
user-select: text; user-select: text;
/* Wrap long lines instead of horizontal-scrolling (matches mobile). (issue #100) */ /* Wrap long lines instead of horizontal-scrolling (matches mobile). (issue #100) */
@@ -6725,6 +6774,11 @@
overflow: hidden; 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) { :global(.tiptap-wrapper .tiptap p) {
margin: 0 0 0.75em; margin: 0 0 0.75em;
line-height: var(--editor-line-height, 1.65); line-height: var(--editor-line-height, 1.65);