remember scroll
This commit is contained in:
parent
a7995edace
commit
984e10e30f
|
|
@ -6,6 +6,7 @@
|
||||||
- **Rich markdown editor** with live preview
|
- **Rich markdown editor** with live preview
|
||||||
- **Three view modes**: Edit, Split, Preview
|
- **Three view modes**: Edit, Split, Preview
|
||||||
- **Auto-save** - Never lose your work
|
- **Auto-save** - Never lose your work
|
||||||
|
- **Resume where you left off** - Editor scroll position is remembered per note within the session, so switching back to a long note returns you to the same spot
|
||||||
- **Undo/Redo** - Ctrl+Z / Ctrl+Y support
|
- **Undo/Redo** - Ctrl+Z / Ctrl+Y support
|
||||||
- **Note templates** - Create notes from templates with dynamic placeholders
|
- **Note templates** - Create notes from templates with dynamic placeholders
|
||||||
- **Syntax highlighting** for code blocks (50+ languages)
|
- **Syntax highlighting** for code blocks (50+ languages)
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,11 @@ function noteApp() {
|
||||||
isUndoRedo: false,
|
isUndoRedo: false,
|
||||||
hasPendingHistoryChanges: false,
|
hasPendingHistoryChanges: false,
|
||||||
|
|
||||||
|
// Per-note editor scroll position, keyed by note path. Captured when leaving a note in
|
||||||
|
// loadNote() and restored when returning to the same note in the same session. Stays
|
||||||
|
// in memory only — cleared on page reload, intentionally (matches typical editor UX).
|
||||||
|
noteScrollPositions: {},
|
||||||
|
|
||||||
// Stats plugin state
|
// Stats plugin state
|
||||||
statsPluginEnabled: false,
|
statsPluginEnabled: false,
|
||||||
noteStats: null,
|
noteStats: null,
|
||||||
|
|
@ -730,9 +735,15 @@ function noteApp() {
|
||||||
// Watch view mode changes and auto-save
|
// Watch view mode changes and auto-save
|
||||||
this.$watch('viewMode', (newValue) => {
|
this.$watch('viewMode', (newValue) => {
|
||||||
this.saveViewMode();
|
this.saveViewMode();
|
||||||
// Scroll to top when switching modes
|
// Re-apply the saved scroll position to whichever pane just became visible.
|
||||||
|
// x-show toggles display:none/block; an overflow:auto element resets its
|
||||||
|
// scrollTop on that transition, so the previously-hidden pane lands at 0
|
||||||
|
// unless we explicitly restore. $nextTick waits for Alpine to flush the
|
||||||
|
// x-show change, the rAF then waits for layout so scrollHeight is valid.
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollToTop();
|
requestAnimationFrame(() => {
|
||||||
|
this._restoreNoteScroll();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1151,7 +1162,12 @@ function noteApp() {
|
||||||
}, 50); // 50ms debounce
|
}, 50); // 50ms debounce
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sync overlay scroll with textarea
|
// Sync overlay scroll with textarea. Per-note scroll restoration is captured separately
|
||||||
|
// inside the _editorScrollHandler / _previewScrollHandler in setupScrollSync(), where
|
||||||
|
// the existing isScrolling guard ensures we only record user-driven scrolls (not the
|
||||||
|
// programmatic mirrors between panes), and where we have access to the pane the user
|
||||||
|
// actually scrolled — important because the textarea's scroll event never fires in
|
||||||
|
// preview-only mode (the textarea is display:none).
|
||||||
syncOverlayScroll() {
|
syncOverlayScroll() {
|
||||||
const textarea = document.getElementById('note-editor');
|
const textarea = document.getElementById('note-editor');
|
||||||
const overlay = document.getElementById('syntax-overlay');
|
const overlay = document.getElementById('syntax-overlay');
|
||||||
|
|
@ -4008,6 +4024,10 @@ function noteApp() {
|
||||||
this._cachedRenderedHTML = '';
|
this._cachedRenderedHTML = '';
|
||||||
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
this._initializedVideoSources = new Set(); // Clear video cache for new note
|
||||||
this.noteContent = data.content;
|
this.noteContent = data.content;
|
||||||
|
// Note: scroll restoration happens later in the post-$nextTick block below
|
||||||
|
// (where the existing scrollToTop call used to live), via _restoreNoteScroll().
|
||||||
|
// Doing it there means a single, unified restore path instead of two racing
|
||||||
|
// ones. See the post-$nextTick block for the full timing rationale.
|
||||||
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
this.currentNoteName = notePath.split('/').pop().replace('.md', '');
|
||||||
if (this.currentMediaType === 'drawing') {
|
if (this.currentMediaType === 'drawing') {
|
||||||
this._drawingDisconnectResizeObserver();
|
this._drawingDisconnectResizeObserver();
|
||||||
|
|
@ -4078,7 +4098,18 @@ function noteApp() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.refreshDOMCache();
|
this.refreshDOMCache();
|
||||||
this.setupScrollSync();
|
this.setupScrollSync();
|
||||||
this.scrollToTop();
|
// First pass: editor is ready (textarea reflows synchronously). Preview
|
||||||
|
// may still have scrollHeight=0 because the markdown render pipeline
|
||||||
|
// (marked + MathJax + Mermaid + syntax highlighting) is async.
|
||||||
|
this._restoreNoteScroll();
|
||||||
|
// Second pass on a later frame: by now the preview pane has typically
|
||||||
|
// rendered and laid out, so its scrollTop write actually sticks. The
|
||||||
|
// helper is idempotent — re-running for the editor is a no-op.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this._restoreNoteScroll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Apply or clear search highlighting
|
// Apply or clear search highlighting
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
|
|
@ -5877,6 +5908,14 @@ function noteApp() {
|
||||||
const scrollPercentage = editor.scrollTop / scrollableHeight;
|
const scrollPercentage = editor.scrollTop / scrollableHeight;
|
||||||
const previewScrollableHeight = preview.scrollHeight - preview.clientHeight;
|
const previewScrollableHeight = preview.scrollHeight - preview.clientHeight;
|
||||||
|
|
||||||
|
// Remember the user-driven scroll position as a percentage so it restores
|
||||||
|
// correctly across view modes (edit / split / preview) where editor and
|
||||||
|
// preview have wildly different scrollHeights. Saving after the isScrolling
|
||||||
|
// guard means we only record real user input, never the programmatic mirror.
|
||||||
|
if (this.currentNote) {
|
||||||
|
this.noteScrollPositions[this.currentNote] = scrollPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
if (previewScrollableHeight > 0) {
|
if (previewScrollableHeight > 0) {
|
||||||
this.isScrolling = true;
|
this.isScrolling = true;
|
||||||
preview.scrollTop = scrollPercentage * previewScrollableHeight;
|
preview.scrollTop = scrollPercentage * previewScrollableHeight;
|
||||||
|
|
@ -5895,6 +5934,12 @@ function noteApp() {
|
||||||
const scrollPercentage = preview.scrollTop / scrollableHeight;
|
const scrollPercentage = preview.scrollTop / scrollableHeight;
|
||||||
const editorScrollableHeight = editor.scrollHeight - editor.clientHeight;
|
const editorScrollableHeight = editor.scrollHeight - editor.clientHeight;
|
||||||
|
|
||||||
|
// Capture preview-driven scrolls too — this is the only path that fires in
|
||||||
|
// preview-only mode, where the textarea is display:none and never scrolls.
|
||||||
|
if (this.currentNote) {
|
||||||
|
this.noteScrollPositions[this.currentNote] = scrollPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
if (editorScrollableHeight > 0) {
|
if (editorScrollableHeight > 0) {
|
||||||
this.isScrolling = true;
|
this.isScrolling = true;
|
||||||
editor.scrollTop = scrollPercentage * editorScrollableHeight;
|
editor.scrollTop = scrollPercentage * editorScrollableHeight;
|
||||||
|
|
@ -6387,6 +6432,40 @@ function noteApp() {
|
||||||
localStorage.setItem('editorWidth', this.editorWidth.toString());
|
localStorage.setItem('editorWidth', this.editorWidth.toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the saved scroll percentage for the current note in both panes. Called from
|
||||||
|
* loadNote() (after the new content has been laid out) and from the viewMode watcher
|
||||||
|
* (browsers reset scrollTop to 0 when an overflow:auto element transitions from
|
||||||
|
* display:none back to display:block via x-show, so newly-visible panes need
|
||||||
|
* re-positioning). Storing/applying a percentage means the same logical position is
|
||||||
|
* preserved across mode toggles even though editor and preview have different
|
||||||
|
* scrollHeights. Idempotent: safe to call multiple times. The isScrolling flag
|
||||||
|
* prevents the existing scroll-sync handlers from echoing these programmatic writes.
|
||||||
|
*/
|
||||||
|
_restoreNoteScroll() {
|
||||||
|
if (!this.currentNote || this.currentMedia) return;
|
||||||
|
const pct = this.noteScrollPositions[this.currentNote] || 0;
|
||||||
|
if (!this._domCache.editor || !this._domCache.previewContainer) {
|
||||||
|
this.refreshDOMCache();
|
||||||
|
}
|
||||||
|
const editor = this._domCache.editor;
|
||||||
|
const preview = this._domCache.previewContainer;
|
||||||
|
if (editor) {
|
||||||
|
const scrollable = editor.scrollHeight - editor.clientHeight;
|
||||||
|
if (scrollable > 0) {
|
||||||
|
this.isScrolling = true;
|
||||||
|
editor.scrollTop = pct * scrollable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preview) {
|
||||||
|
const scrollable = preview.scrollHeight - preview.clientHeight;
|
||||||
|
if (scrollable > 0) {
|
||||||
|
this.isScrolling = true;
|
||||||
|
preview.scrollTop = pct * scrollable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Scroll to top of editor and preview
|
// Scroll to top of editor and preview
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
// Disable scroll sync temporarily to prevent interference
|
// Disable scroll sync temporarily to prevent interference
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue