fix(browse): editor lifecycle — dispose on switch, guard unsaved edits, kill leaks
The markdown/YAML preview editors were never disposed when switching to a non-editor file: dispose() was only called from inside the same plugin's render(), so md→PDF/image/YAML overwrote the pane via innerHTML and leaked the Toast UI instance, its DOM, and document-level resizer drag listeners. Unsaved edits were also discarded silently on any file switch (including arrow-key auto-preview), and debounced change handlers could resolve after an editor was disposed and write the wrong file's dirty/hash state. preview.js now owns editor lifecycle centrally in renderInline: - disposeEditors() up front before replacing the pane (fixes the leak for every md/yaml → anything switch). - dirty guard: deliberate switches (click/Enter/menu) confirm before discarding; auto previews (keyboard cursor walking the tree, opts.auto) leave the dirty editor in place rather than nagging per keystroke; re-selecting the file already being edited is a no-op. - a renderSeq token bails late-arriving loads so a slow file can't paint stale content into the pane after a newer selection. - clearPreview() exposed and used by rescope (events.js) and popstate (app.js) so those resets dispose the editor instead of leaking it. - beforeunload warns when an editor is dirty at page exit. preview-markdown.js: per-mount AbortController wired into the resizer document listeners so dispose() detaches them even mid-drag; debounced change/save/convert handlers guard `currentInstance !== instance` so a disposed editor's callbacks can't corrupt the active file; expose isDirty()/currentNode(). preview-yaml.js: track dirty/node state, guard the change handler the same way, expose dispose()/isDirty()/currentNode(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f211d748f
commit
cfb2fab401
5 changed files with 184 additions and 25 deletions
|
|
@ -139,8 +139,14 @@
|
||||||
window.app.state.lastPreviewedNodeId = null;
|
window.app.state.lastPreviewedNodeId = null;
|
||||||
tree.setRoot(es);
|
tree.setRoot(es);
|
||||||
tree.render();
|
tree.render();
|
||||||
|
// Route through clearPreview so a live editor is disposed
|
||||||
|
// (not leaked) when back/forward swaps scope.
|
||||||
|
var pmod = window.app.modules.preview;
|
||||||
|
if (pmod && pmod.clearPreview) pmod.clearPreview();
|
||||||
|
else {
|
||||||
var previewBody = document.getElementById('previewBody');
|
var previewBody = document.getElementById('previewBody');
|
||||||
if (previewBody) previewBody.innerHTML = '';
|
if (previewBody) previewBody.innerHTML = '';
|
||||||
|
}
|
||||||
var previewTitle = document.getElementById('previewTitle');
|
var previewTitle = document.getElementById('previewTitle');
|
||||||
if (previewTitle) previewTitle.textContent = 'No file selected';
|
if (previewTitle) previewTitle.textContent = 'No file selected';
|
||||||
// Reapply view mode for the new URL (incoming/ → grid, etc).
|
// Reapply view mode for the new URL (incoming/ → grid, etc).
|
||||||
|
|
|
||||||
|
|
@ -449,7 +449,10 @@
|
||||||
// selection-only; their preview is "expand to see inside".
|
// selection-only; their preview is "expand to see inside".
|
||||||
if (nextNode && !nextNode.isDir && !nextNode.isZip
|
if (nextNode && !nextNode.isDir && !nextNode.isZip
|
||||||
&& previewModule) {
|
&& previewModule) {
|
||||||
previewModule.showFilePreview(nextNode);
|
// auto:true — keyboard cursor walking the tree. If an
|
||||||
|
// editor has unsaved edits, the preview module leaves it
|
||||||
|
// in place rather than prompting on every keystroke.
|
||||||
|
previewModule.showFilePreview(nextNode, { auto: true });
|
||||||
state.lastPreviewedNodeId = nextId;
|
state.lastPreviewedNodeId = nextId;
|
||||||
}
|
}
|
||||||
// Scroll the now-selected row into view.
|
// Scroll the now-selected row into view.
|
||||||
|
|
@ -1408,9 +1411,14 @@
|
||||||
tree.setRoot(entries);
|
tree.setRoot(entries);
|
||||||
tree.render();
|
tree.render();
|
||||||
// Reset the preview pane so the user sees an "empty selection"
|
// Reset the preview pane so the user sees an "empty selection"
|
||||||
// state at the new scope instead of the previous file.
|
// state at the new scope instead of the previous file. Route
|
||||||
|
// through clearPreview so a live editor is disposed (not leaked).
|
||||||
|
var pmod = previewMod();
|
||||||
|
if (pmod && pmod.clearPreview) pmod.clearPreview();
|
||||||
|
else {
|
||||||
var previewBody = document.getElementById('previewBody');
|
var previewBody = document.getElementById('previewBody');
|
||||||
if (previewBody) previewBody.innerHTML = '';
|
if (previewBody) previewBody.innerHTML = '';
|
||||||
|
}
|
||||||
var previewTitle = document.getElementById('previewTitle');
|
var previewTitle = document.getElementById('previewTitle');
|
||||||
if (previewTitle) previewTitle.textContent = 'No file selected';
|
if (previewTitle) previewTitle.textContent = 'No file selected';
|
||||||
var previewMeta = document.getElementById('previewMeta');
|
var previewMeta = document.getElementById('previewMeta');
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispose() {
|
function dispose() {
|
||||||
if (currentInstance && currentInstance.editor) {
|
if (currentInstance) {
|
||||||
|
// Tear down the document-level resizer drag listeners (added
|
||||||
|
// lazily on mousedown). They're normally removed on mouseup,
|
||||||
|
// but a dispose mid-drag — or any switch away — would otherwise
|
||||||
|
// strand them pointing at the dead shell. The AbortController
|
||||||
|
// removes whatever is still attached in one call.
|
||||||
|
if (currentInstance.ac) {
|
||||||
|
try { currentInstance.ac.abort(); } catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (currentInstance.editor) {
|
||||||
try { currentInstance.editor.destroy(); } catch (_) { /* ignore */ }
|
try { currentInstance.editor.destroy(); } catch (_) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
currentInstance = null;
|
currentInstance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDirty() {
|
||||||
|
return !!(currentInstance && currentInstance.dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentNode() {
|
||||||
|
return currentInstance ? currentInstance.node : null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Front matter ────────────────────────────────────────────────────────
|
// ── Front matter ────────────────────────────────────────────────────────
|
||||||
// Lightweight YAML front-matter parser. Same envelope as mdedit's:
|
// Lightweight YAML front-matter parser. Same envelope as mdedit's:
|
||||||
// `---\n…\n---\n`, key:value lines, simple `[a, b, c]` arrays.
|
// `---\n…\n---\n`, key:value lines, simple `[a, b, c]` arrays.
|
||||||
|
|
@ -564,15 +582,20 @@
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
currentInstance = {
|
// One AbortController per mount — wired into the document-level
|
||||||
|
// resizer listeners below so dispose() can detach them all at once.
|
||||||
|
var ac = new AbortController();
|
||||||
|
var instance = {
|
||||||
editor: editor,
|
editor: editor,
|
||||||
container: container,
|
container: container,
|
||||||
dirty: false,
|
dirty: false,
|
||||||
node: node,
|
node: node,
|
||||||
hash: initialHash,
|
hash: initialHash,
|
||||||
tocEl: tocBody,
|
tocEl: tocBody,
|
||||||
fmEl: fmTextarea
|
fmEl: fmTextarea,
|
||||||
|
ac: ac
|
||||||
};
|
};
|
||||||
|
currentInstance = instance;
|
||||||
|
|
||||||
if (!writableMode) {
|
if (!writableMode) {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
|
@ -609,8 +632,8 @@
|
||||||
resizer.classList.add('is-dragging');
|
resizer.classList.add('is-dragging');
|
||||||
startX = e.clientX;
|
startX = e.clientX;
|
||||||
startW = sidebar.getBoundingClientRect().width;
|
startW = sidebar.getBoundingClientRect().width;
|
||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove, { signal: ac.signal });
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp, { signal: ac.signal });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
resizer.addEventListener('keydown', function (e) {
|
resizer.addEventListener('keydown', function (e) {
|
||||||
|
|
@ -654,8 +677,8 @@
|
||||||
fmResizer.classList.add('is-dragging');
|
fmResizer.classList.add('is-dragging');
|
||||||
startY = e.clientY;
|
startY = e.clientY;
|
||||||
startH = fmSection.getBoundingClientRect().height;
|
startH = fmSection.getBoundingClientRect().height;
|
||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove, { signal: ac.signal });
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp, { signal: ac.signal });
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
fmResizer.addEventListener('keydown', function (e) {
|
fmResizer.addEventListener('keydown', function (e) {
|
||||||
|
|
@ -670,7 +693,8 @@
|
||||||
|
|
||||||
// ── Change tracking + auto-rerender ────────────────────────────────
|
// ── Change tracking + auto-rerender ────────────────────────────────
|
||||||
function markDirty(isDirty) {
|
function markDirty(isDirty) {
|
||||||
currentInstance.dirty = isDirty;
|
if (currentInstance !== instance) return; // editor replaced
|
||||||
|
instance.dirty = isDirty;
|
||||||
// Re-read canSave at every transition, not via a closure-captured
|
// Re-read canSave at every transition, not via a closure-captured
|
||||||
// value, so the gate reflects current write authority — see the
|
// value, so the gate reflects current write authority — see the
|
||||||
// matching pattern in preview-yaml.js.
|
// matching pattern in preview-yaml.js.
|
||||||
|
|
@ -678,29 +702,40 @@
|
||||||
dirtyEl.textContent = isDirty ? '● modified' : '';
|
dirtyEl.textContent = isDirty ? '● modified' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The debounced handlers can resolve AFTER this editor was disposed
|
||||||
|
// and a new file mounted (the timer + the await both outlive the
|
||||||
|
// switch). Bail when we're no longer the active instance so we never
|
||||||
|
// call into a destroyed Toast UI editor or write the wrong file's
|
||||||
|
// dirty/hash state.
|
||||||
var onChange = debounce(async function () {
|
var onChange = debounce(async function () {
|
||||||
|
if (currentInstance !== instance) return;
|
||||||
var body = editor.getMarkdown();
|
var body = editor.getMarkdown();
|
||||||
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
||||||
markDirty(h !== currentInstance.hash);
|
if (currentInstance !== instance) return;
|
||||||
|
markDirty(h !== instance.hash);
|
||||||
renderToc(tocBody, body, editor);
|
renderToc(tocBody, body, editor);
|
||||||
}, 250);
|
}, 250);
|
||||||
editor.on('change', onChange);
|
editor.on('change', onChange);
|
||||||
|
|
||||||
var onFmChange = debounce(async function () {
|
var onFmChange = debounce(async function () {
|
||||||
|
if (currentInstance !== instance) return;
|
||||||
var body = editor.getMarkdown();
|
var body = editor.getMarkdown();
|
||||||
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
var h = await hashContent(assembleContent(fmTextarea.value, body));
|
||||||
markDirty(h !== currentInstance.hash);
|
if (currentInstance !== instance) return;
|
||||||
|
markDirty(h !== instance.hash);
|
||||||
}, 250);
|
}, 250);
|
||||||
fmTextarea.addEventListener('input', onFmChange);
|
fmTextarea.addEventListener('input', onFmChange);
|
||||||
|
|
||||||
// ── Save ───────────────────────────────────────────────────────────
|
// ── Save ───────────────────────────────────────────────────────────
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!currentInstance.dirty || !canSave(node)) return;
|
if (currentInstance !== instance) return;
|
||||||
|
if (!instance.dirty || !canSave(node)) return;
|
||||||
var content = assembleContent(fmTextarea.value, editor.getMarkdown());
|
var content = assembleContent(fmTextarea.value, editor.getMarkdown());
|
||||||
try {
|
try {
|
||||||
statusEl.textContent = 'Saving…';
|
statusEl.textContent = 'Saving…';
|
||||||
await saveContent(node, content);
|
await saveContent(node, content);
|
||||||
currentInstance.hash = await hashContent(content);
|
if (currentInstance !== instance) return; // switched away mid-save
|
||||||
|
instance.hash = await hashContent(content);
|
||||||
markDirty(false);
|
markDirty(false);
|
||||||
statusEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
|
statusEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
|
||||||
if (window.zddc && window.zddc.toast) {
|
if (window.zddc && window.zddc.toast) {
|
||||||
|
|
@ -732,7 +767,7 @@
|
||||||
convertBtns.forEach(function (a) {
|
convertBtns.forEach(function (a) {
|
||||||
a.addEventListener('click', async function (e) {
|
a.addEventListener('click', async function (e) {
|
||||||
var fmt = a.dataset.fmt;
|
var fmt = a.dataset.fmt;
|
||||||
if (!currentInstance.dirty) {
|
if (!instance.dirty) {
|
||||||
// Clean — let the browser handle the click. The
|
// Clean — let the browser handle the click. The
|
||||||
// server's response (DOCX/HTML/PDF bytes, 422,
|
// server's response (DOCX/HTML/PDF bytes, 422,
|
||||||
// 503, etc.) lands in whatever target the user
|
// 503, etc.) lands in whatever target the user
|
||||||
|
|
@ -751,7 +786,7 @@
|
||||||
}
|
}
|
||||||
statusEl.textContent = 'Saving before download…';
|
statusEl.textContent = 'Saving before download…';
|
||||||
try { await save(); } catch (_) { /* save() surfaces its own error */ }
|
try { await save(); } catch (_) { /* save() surfaces its own error */ }
|
||||||
if (currentInstance.dirty) return; // save failed; toast already shown
|
if (currentInstance !== instance || instance.dirty) return; // save failed / switched away
|
||||||
statusEl.textContent = 'Downloading ' + fmt.toUpperCase() + '…';
|
statusEl.textContent = 'Downloading ' + fmt.toUpperCase() + '…';
|
||||||
// Re-trigger the click. dirty=false now so the handler
|
// Re-trigger the click. dirty=false now so the handler
|
||||||
// exits early on the second pass and the browser
|
// exits early on the second pass and the browser
|
||||||
|
|
@ -763,6 +798,8 @@
|
||||||
|
|
||||||
window.app.modules.markdown = {
|
window.app.modules.markdown = {
|
||||||
render: render,
|
render: render,
|
||||||
dispose: dispose
|
dispose: dispose,
|
||||||
|
isDirty: isDirty,
|
||||||
|
currentNode: currentNode
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -378,12 +378,24 @@
|
||||||
// ── Mount ───────────────────────────────────────────────────────────────
|
// ── Mount ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
var currentEditor = null;
|
var currentEditor = null;
|
||||||
|
var currentDirty = false;
|
||||||
|
var currentNodeRef = null;
|
||||||
|
|
||||||
function dispose() {
|
function dispose() {
|
||||||
// CM doesn't have an explicit destroy(); GC handles it once
|
// CM doesn't have an explicit destroy(); GC handles it once
|
||||||
// the host element is removed. Clear our reference so a stale
|
// the host element is removed. Clear our reference so a stale
|
||||||
// editor doesn't keep handlers alive.
|
// editor doesn't keep handlers alive.
|
||||||
currentEditor = null;
|
currentEditor = null;
|
||||||
|
currentDirty = false;
|
||||||
|
currentNodeRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirty() {
|
||||||
|
return currentDirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentNode() {
|
||||||
|
return currentNodeRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function render(node, container, ctx) {
|
async function render(node, container, ctx) {
|
||||||
|
|
@ -499,6 +511,8 @@
|
||||||
// Force an initial lint pass now that _zddcNode is set.
|
// Force an initial lint pass now that _zddcNode is set.
|
||||||
editor.performLint();
|
editor.performLint();
|
||||||
currentEditor = editor;
|
currentEditor = editor;
|
||||||
|
currentNodeRef = node;
|
||||||
|
currentDirty = false;
|
||||||
|
|
||||||
if (!writable) {
|
if (!writable) {
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
|
@ -514,12 +528,16 @@
|
||||||
var initialHash = await hashContent(text);
|
var initialHash = await hashContent(text);
|
||||||
|
|
||||||
function markDirty(isDirty) {
|
function markDirty(isDirty) {
|
||||||
|
if (currentEditor !== editor) return; // editor replaced
|
||||||
|
currentDirty = isDirty;
|
||||||
saveBtn.disabled = !isDirty || !canSave(node);
|
saveBtn.disabled = !isDirty || !canSave(node);
|
||||||
dirtyEl.textContent = isDirty ? '● modified' : '';
|
dirtyEl.textContent = isDirty ? '● modified' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.on('change', async function () {
|
editor.on('change', async function () {
|
||||||
|
if (currentEditor !== editor) return; // switched away
|
||||||
var h = await hashContent(editor.getValue());
|
var h = await hashContent(editor.getValue());
|
||||||
|
if (currentEditor !== editor) return; // replaced during await
|
||||||
markDirty(h !== initialHash);
|
markDirty(h !== initialHash);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -564,6 +582,9 @@
|
||||||
|
|
||||||
window.app.modules.yamledit = {
|
window.app.modules.yamledit = {
|
||||||
handles: handles,
|
handles: handles,
|
||||||
render: render
|
render: render,
|
||||||
|
dispose: dispose,
|
||||||
|
isDirty: isDirty,
|
||||||
|
currentNode: currentNode
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,62 @@
|
||||||
return { url: URL.createObjectURL(blob), fromServer: false };
|
return { url: URL.createObjectURL(blob), fromServer: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Editor lifecycle helpers ─────────────────────────────────────────────
|
||||||
|
// The markdown and YAML plugins each mount a long-lived editor into the
|
||||||
|
// preview pane. Switching files (or clearing the pane) must dispose the
|
||||||
|
// live editor first — otherwise the Toast UI instance, its DOM, and its
|
||||||
|
// document-level resizer listeners leak when we overwrite the container.
|
||||||
|
|
||||||
|
function editorModules() {
|
||||||
|
var m = window.app.modules;
|
||||||
|
return [m.markdown, m.yamledit].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeEditors() {
|
||||||
|
editorModules().forEach(function (mod) {
|
||||||
|
if (mod.dispose) { try { mod.dispose(); } catch (_) { /* ignore */ } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The editor module (if any) holding unsaved edits, else null.
|
||||||
|
function dirtyEditor() {
|
||||||
|
var mods = editorModules();
|
||||||
|
for (var i = 0; i < mods.length; i++) {
|
||||||
|
if (mods[i].isDirty && mods[i].isDirty()) return mods[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function samePreviewNode(a, b) {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
if (a === b) return true;
|
||||||
|
if (a.url && b.url) return a.url === b.url;
|
||||||
|
return a.name === b.name && a.parentId === b.parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down any live editor and blank the pane. Used by callers that
|
||||||
|
// reset the preview directly (rescope, popstate) so they don't leak the
|
||||||
|
// editor or strand its dirty state.
|
||||||
|
function clearPreview() {
|
||||||
|
disposeEditors();
|
||||||
|
var container = document.getElementById('previewBody');
|
||||||
|
if (container) container.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn before a full page unload (reload / close / external nav) drops
|
||||||
|
// unsaved editor changes. SPA-internal switches are guarded in
|
||||||
|
// renderInline; this catches the browser-level exit.
|
||||||
|
window.addEventListener('beforeunload', function (e) {
|
||||||
|
if (dirtyEditor()) { e.preventDefault(); e.returnValue = ''; }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Inline rendering ────────────────────────────────────────────────────
|
// ── Inline rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Bumped on every renderInline entry; a render that loses the race
|
||||||
|
// (a newer selection started while its bytes were in flight) bails
|
||||||
|
// before writing stale content into the shared pane.
|
||||||
|
var renderSeq = 0;
|
||||||
|
|
||||||
function renderEmpty(container, msg) {
|
function renderEmpty(container, msg) {
|
||||||
container.innerHTML = '<div class="preview-empty">' + escapeHtml(msg) + '</div>';
|
container.innerHTML = '<div class="preview-empty">' + escapeHtml(msg) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -87,13 +141,37 @@
|
||||||
+ escapeHtml(msg) + '</div>';
|
+ escapeHtml(msg) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderInline(node) {
|
async function renderInline(node, opts) {
|
||||||
|
opts = opts || {};
|
||||||
var container = document.getElementById('previewBody');
|
var container = document.getElementById('previewBody');
|
||||||
var titleEl = document.getElementById('previewTitle');
|
var titleEl = document.getElementById('previewTitle');
|
||||||
var metaEl = document.getElementById('previewMeta');
|
var metaEl = document.getElementById('previewMeta');
|
||||||
var popoutBtn = document.getElementById('previewPopout');
|
var popoutBtn = document.getElementById('previewPopout');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// Guard unsaved editor edits before we tear the editor down.
|
||||||
|
var dm = dirtyEditor();
|
||||||
|
if (dm) {
|
||||||
|
var cur = dm.currentNode ? dm.currentNode() : null;
|
||||||
|
if (samePreviewNode(cur, node)) {
|
||||||
|
// Re-selecting the file we're already editing — don't reload
|
||||||
|
// and clobber the in-progress edits.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.auto) {
|
||||||
|
// Keyboard/auto preview (cursor walking the tree): leave the
|
||||||
|
// dirty editor in place rather than prompting on every key.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var label = cur ? cur.name : 'this file';
|
||||||
|
if (!window.confirm('Discard unsaved changes to ' + label + '?')) return;
|
||||||
|
}
|
||||||
|
// Safe to replace the pane now: dispose any live editor so its
|
||||||
|
// instance + document-level listeners don't leak.
|
||||||
|
disposeEditors();
|
||||||
|
|
||||||
|
var seq = ++renderSeq;
|
||||||
|
|
||||||
if (titleEl) titleEl.textContent = node.name;
|
if (titleEl) titleEl.textContent = node.name;
|
||||||
if (metaEl) {
|
if (metaEl) {
|
||||||
var meta = [];
|
var meta = [];
|
||||||
|
|
@ -134,6 +212,7 @@
|
||||||
if (ext === 'pdf' || ext === 'html' || ext === 'htm') {
|
if (ext === 'pdf' || ext === 'html' || ext === 'htm') {
|
||||||
try {
|
try {
|
||||||
var info = await getBlobUrl(node);
|
var info = await getBlobUrl(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
var sandbox = (ext === 'pdf') ? '' : ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"';
|
var sandbox = (ext === 'pdf') ? '' : ' sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"';
|
||||||
container.innerHTML = '<iframe class="preview-iframe" src="' + escapeHtml(info.url) + '"' + sandbox + '></iframe>';
|
container.innerHTML = '<iframe class="preview-iframe" src="' + escapeHtml(info.url) + '"' + sandbox + '></iframe>';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -146,6 +225,7 @@
|
||||||
if (preview && preview.isImage(ext) && !preview.isTiff(ext)) {
|
if (preview && preview.isImage(ext) && !preview.isTiff(ext)) {
|
||||||
try {
|
try {
|
||||||
var imgInfo = await getBlobUrl(node);
|
var imgInfo = await getBlobUrl(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
container.innerHTML = '<img class="preview-image" alt="' + escapeHtml(node.name)
|
container.innerHTML = '<img class="preview-image" alt="' + escapeHtml(node.name)
|
||||||
+ '" src="' + escapeHtml(imgInfo.url) + '">';
|
+ '" src="' + escapeHtml(imgInfo.url) + '">';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -157,6 +237,7 @@
|
||||||
if (preview && preview.isTiff(ext)) {
|
if (preview && preview.isTiff(ext)) {
|
||||||
try {
|
try {
|
||||||
var tiffBuf = await getArrayBuffer(node);
|
var tiffBuf = await getArrayBuffer(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
await preview.renderTiff(document, container, tiffBuf, { fileName: node.name });
|
await preview.renderTiff(document, container, tiffBuf, { fileName: node.name });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -168,6 +249,7 @@
|
||||||
if (preview && preview.isZip(ext)) {
|
if (preview && preview.isZip(ext)) {
|
||||||
try {
|
try {
|
||||||
var zipBuf = await getArrayBuffer(node);
|
var zipBuf = await getArrayBuffer(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
await preview.renderZipListing(document, container, zipBuf, { fileName: node.name });
|
await preview.renderZipListing(document, container, zipBuf, { fileName: node.name });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -182,6 +264,7 @@
|
||||||
if (preview && preview.isOffice(ext)) {
|
if (preview && preview.isOffice(ext)) {
|
||||||
try {
|
try {
|
||||||
var officeBuf = await getArrayBuffer(node);
|
var officeBuf = await getArrayBuffer(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
if (ext === 'docx') {
|
if (ext === 'docx') {
|
||||||
await preview.renderDocx(document, container, officeBuf, { fileName: node.name });
|
await preview.renderDocx(document, container, officeBuf, { fileName: node.name });
|
||||||
|
|
@ -197,6 +280,7 @@
|
||||||
if (preview && preview.isText(ext)) {
|
if (preview && preview.isText(ext)) {
|
||||||
try {
|
try {
|
||||||
var txtBuf = await getArrayBuffer(node);
|
var txtBuf = await getArrayBuffer(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
var text = new TextDecoder('utf-8', { fatal: false }).decode(txtBuf);
|
var text = new TextDecoder('utf-8', { fatal: false }).decode(txtBuf);
|
||||||
var MAX = 200000;
|
var MAX = 200000;
|
||||||
if (text.length > MAX) {
|
if (text.length > MAX) {
|
||||||
|
|
@ -217,6 +301,7 @@
|
||||||
// Unknown type — offer a download link.
|
// Unknown type — offer a download link.
|
||||||
try {
|
try {
|
||||||
var fallbackInfo = await getBlobUrl(node);
|
var fallbackInfo = await getBlobUrl(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<div class="preview-empty">'
|
'<div class="preview-empty">'
|
||||||
+ 'No inline preview for <code>.' + escapeHtml(ext) + '</code>. '
|
+ 'No inline preview for <code>.' + escapeHtml(ext) + '</code>. '
|
||||||
|
|
@ -358,11 +443,13 @@
|
||||||
if (node.isDir) return;
|
if (node.isDir) return;
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
if (opts.popup) return renderInPopup(node);
|
if (opts.popup) return renderInPopup(node);
|
||||||
return renderInline(node);
|
return renderInline(node, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.app.modules.preview = {
|
window.app.modules.preview = {
|
||||||
showFilePreview: showFilePreview,
|
showFilePreview: showFilePreview,
|
||||||
|
// Tear down any live editor + blank the pane (rescope / popstate).
|
||||||
|
clearPreview: clearPreview,
|
||||||
// Expose for the markdown plugin so it can read file bytes.
|
// Expose for the markdown plugin so it can read file bytes.
|
||||||
getArrayBuffer: getArrayBuffer
|
getArrayBuffer: getArrayBuffer
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue