Skip to content

Commit 8af8fb3

Browse files
committed
feat(mdviewer): add reload support and source-line-based scroll restore
- Reload button clears file cache and re-renders, restoring scroll position and edit mode - Scroll save/restore now uses data-source-line elements instead of pixel positions, making it immune to image loading layout shifts
1 parent 2ef5f03 commit 8af8fb3

File tree

4 files changed

+125
-8
lines changed

4 files changed

+125
-8
lines changed

src-mdviewer/src/bridge.js

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let _syncId = 0;
1313
let _lastReceivedSyncId = -1;
1414
let _suppressContentChange = false;
1515
let _baseURL = "";
16+
let _pendingReloadScroll = null; // { filePath, scrollPos, editMode } for scroll restore after reload
1617

1718
/**
1819
* Check if a URL is absolute (not relative to the document).
@@ -126,6 +127,9 @@ export function initBridge() {
126127
case "MDVIEWR_CLOSE_FILE":
127128
handleCloseFile(data);
128129
break;
130+
case "MDVIEWR_RELOAD_FILE":
131+
handleReloadFile(data);
132+
break;
129133
case "MDVIEWR_SET_THEME":
130134
handleSetTheme(data);
131135
break;
@@ -362,12 +366,45 @@ function handleSwitchFile(data) {
362366
docCache.createEntry(filePath, markdown, parseResult);
363367
docCache.switchTo(filePath);
364368

365-
setState({
366-
currentContent: markdown,
367-
parseResult: parseResult
368-
});
369+
// Restore scroll position from reload if applicable
370+
if (_pendingReloadScroll && _pendingReloadScroll.filePath === filePath) {
371+
const entry = docCache.getEntry(filePath);
372+
if (entry) {
373+
entry._scrollSourceLine = _pendingReloadScroll.scrollSourceLine;
374+
entry._editMode = _pendingReloadScroll.editMode;
375+
}
376+
const restoreEditMode = _pendingReloadScroll.editMode;
377+
_pendingReloadScroll = null;
378+
379+
setState({
380+
currentContent: markdown,
381+
parseResult: parseResult
382+
});
383+
emit("file:rendered", parseResult);
384+
385+
// Scroll to source line element after render
386+
if (entry && entry._scrollSourceLine) {
387+
requestAnimationFrame(() => {
388+
const els = entry.dom.querySelectorAll("[data-source-line]");
389+
for (const el of els) {
390+
if (parseInt(el.getAttribute("data-source-line"), 10) === entry._scrollSourceLine) {
391+
el.scrollIntoView({ behavior: "instant", block: "start" });
392+
break;
393+
}
394+
}
395+
});
396+
}
369397

370-
emit("file:rendered", parseResult);
398+
if (restoreEditMode) {
399+
setState({ editMode: true });
400+
}
401+
} else {
402+
setState({
403+
currentContent: markdown,
404+
parseResult: parseResult
405+
});
406+
emit("file:rendered", parseResult);
407+
}
371408
}
372409

373410
_suppressContentChange = false;
@@ -395,6 +432,35 @@ function handleCloseFile(data) {
395432
}
396433
}
397434

435+
/**
436+
* Reload a specific file: save scroll position, clear its cache entry,
437+
* so the next SWITCH_FILE will re-render from scratch.
438+
*/
439+
function handleReloadFile(data) {
440+
const { filePath } = data;
441+
if (!filePath) return;
442+
443+
const entry = docCache.getEntry(filePath);
444+
const savedScrollPos = entry ? entry.scrollPos : 0;
445+
const wasEditMode = entry ? entry._editMode : false;
446+
447+
// If this is the active file, save current scroll
448+
if (docCache.getActiveFilePath() === filePath) {
449+
docCache.saveActiveScrollPos();
450+
const activeEntry = docCache.getEntry(filePath);
451+
if (activeEntry) {
452+
const scrollSourceLine = activeEntry._scrollSourceLine;
453+
if (getState().editMode) {
454+
setState({ editMode: false });
455+
}
456+
docCache.removeEntry(filePath);
457+
_pendingReloadScroll = { filePath, scrollSourceLine, editMode: wasEditMode };
458+
}
459+
} else {
460+
docCache.removeEntry(filePath);
461+
}
462+
}
463+
398464
// --- Theme, edit mode, locale ---
399465

400466
function handleSetTheme(data) {

src-mdviewer/src/core/doc-cache.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,18 @@ export function switchTo(filePath) {
148148

149149
activeFilePath = filePath;
150150

151-
// Restore scroll position after layout
151+
// Restore scroll position after layout using source-line element if available
152152
requestAnimationFrame(() => {
153+
if (entry._scrollSourceLine) {
154+
const elements = entry.dom.querySelectorAll("[data-source-line]");
155+
for (const el of elements) {
156+
if (parseInt(el.getAttribute("data-source-line"), 10) === entry._scrollSourceLine) {
157+
el.scrollIntoView({ behavior: "instant", block: "start" });
158+
return;
159+
}
160+
}
161+
}
162+
// Fallback to pixel position
153163
viewerContainer.scrollTop = entry.scrollPos;
154164
});
155165

@@ -158,12 +168,30 @@ export function switchTo(filePath) {
158168

159169
/**
160170
* Save the current scroll position for the active document.
171+
* Saves both pixel position and the source line visible at the top of the viewport,
172+
* so scroll can be restored even when images haven't loaded yet.
161173
*/
162174
export function saveActiveScrollPos() {
163175
if (!activeFilePath) return;
164176
const entry = cache.get(activeFilePath);
165-
if (entry) {
166-
entry.scrollPos = viewerContainer.scrollTop;
177+
if (!entry) return;
178+
179+
entry.scrollPos = viewerContainer.scrollTop;
180+
181+
// Find the source line element closest to the top of the viewport
182+
const elements = entry.dom.querySelectorAll("[data-source-line]");
183+
const containerTop = viewerContainer.getBoundingClientRect().top;
184+
let bestEl = null;
185+
let bestDist = Infinity;
186+
for (const el of elements) {
187+
const dist = Math.abs(el.getBoundingClientRect().top - containerTop);
188+
if (dist < bestDist) {
189+
bestDist = dist;
190+
bestEl = el;
191+
}
192+
}
193+
if (bestEl) {
194+
entry._scrollSourceLine = parseInt(bestEl.getAttribute("data-source-line"), 10);
167195
}
168196
}
169197

src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,26 @@ define(function (require, exports, module) {
593593
return _doc._masterEditor._codeMirror;
594594
}
595595

596+
/**
597+
* Clear the current file's cache in the mdviewer iframe and force re-render.
598+
* Called on reload button click.
599+
*/
600+
function reloadCurrentFile() {
601+
if (!_active || !_iframeReady || !_doc) {
602+
return;
603+
}
604+
const iframeWindow = _getIframeWindow();
605+
if (!iframeWindow) {
606+
return;
607+
}
608+
iframeWindow.postMessage({
609+
type: "MDVIEWR_RELOAD_FILE",
610+
filePath: _doc.file.fullPath
611+
}, "*");
612+
}
613+
596614
exports.activate = activate;
597615
exports.deactivate = deactivate;
598616
exports.isActive = isActive;
617+
exports.reloadCurrentFile = reloadCurrentFile;
599618
});

src/extensionsIntegrated/Phoenix-live-preview/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,10 @@ define(function (require, exports, module) {
865865

866866
if (_isMdviewrActive) {
867867
// Mdviewr iframe already loaded, just update the sync for the new document
868+
if (force) {
869+
// Reload: clear this file's cache and force re-render
870+
MarkdownSync.reloadCurrentFile();
871+
}
868872
MarkdownSync.activate(currentDoc, $iframe, baseURL);
869873
return;
870874
}

0 commit comments

Comments
 (0)