feat(browse): render previewable files (.html) safely, with a Source⇄Preview toggle
Renderable types (.html/.htm) now show RENDERED by default again — but in a sandboxed iframe (no scripts, no same-origin) so arbitrary HTML is safe — and a header toggle flips to the CodeMirror source view (and back). Non-renderable text files open straight in CodeMirror as before; PDF stays an iframe; markdown keeps its own rendered/source toggle. - RENDERABLE map + per-node view mode (rendered default; remembers only the last-toggled node, so switching files resets). Toggle button sits by Pop out. - The same-node render guard now allows a deliberate toggle through (prompting to discard if the source editor is dirty); clearPreview hides the toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1bd73b1512
commit
af16b14a52
1 changed files with 67 additions and 2 deletions
|
|
@ -132,6 +132,9 @@
|
||||||
disposeEditors();
|
disposeEditors();
|
||||||
var container = document.getElementById('previewBody');
|
var container = document.getElementById('previewBody');
|
||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
|
toggleTargetNode = null;
|
||||||
|
var tb = document.getElementById('previewViewToggle');
|
||||||
|
if (tb) tb.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn before a full page unload (reload / close / external nav) drops
|
// Warn before a full page unload (reload / close / external nav) drops
|
||||||
|
|
@ -141,6 +144,41 @@
|
||||||
if (dirtyEditor()) { e.preventDefault(); e.returnValue = ''; }
|
if (dirtyEditor()) { e.preventDefault(); e.returnValue = ''; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Rendered ⇄ Source toggle ─────────────────────────────────────────────
|
||||||
|
// Some types we can RENDER, not just edit (.html). Those show rendered by
|
||||||
|
// default (sandboxed — no scripts, no same-origin) with a toggle to the
|
||||||
|
// CodeMirror source view. Markdown has its own rendered/source toggle, so
|
||||||
|
// it's not here. Extend RENDERABLE to add more (svg already previews as an
|
||||||
|
// image; csv could render as a table later).
|
||||||
|
var RENDERABLE = { html: 1, htm: 1 };
|
||||||
|
function isRenderable(ext) { return !!RENDERABLE[(ext || '').toLowerCase()]; }
|
||||||
|
function nodeKey(node) { return (node && (node.url || node.name)) || ''; }
|
||||||
|
// Per-node mode; 'rendered' is the default. Only the node the user last
|
||||||
|
// toggled is remembered, so switching files resets to rendered.
|
||||||
|
var viewToggle = { key: null, mode: 'rendered' };
|
||||||
|
var toggleTargetNode = null;
|
||||||
|
function effectiveMode(node) {
|
||||||
|
return (viewToggle.key && viewToggle.key === nodeKey(node)) ? viewToggle.mode : 'rendered';
|
||||||
|
}
|
||||||
|
function ensureViewToggleBtn() {
|
||||||
|
var btn = document.getElementById('previewViewToggle');
|
||||||
|
if (btn) return btn;
|
||||||
|
var popout = document.getElementById('previewPopout');
|
||||||
|
if (!popout || !popout.parentNode) return null;
|
||||||
|
btn = document.createElement('button');
|
||||||
|
btn.id = 'previewViewToggle';
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'btn btn-sm btn-secondary hidden';
|
||||||
|
popout.parentNode.insertBefore(btn, popout);
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (!toggleTargetNode) return;
|
||||||
|
var next = effectiveMode(toggleTargetNode) === 'rendered' ? 'source' : 'rendered';
|
||||||
|
viewToggle = { key: nodeKey(toggleTargetNode), mode: next };
|
||||||
|
renderInline(toggleTargetNode, { toggle: true });
|
||||||
|
});
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Inline rendering ────────────────────────────────────────────────────
|
// ── Inline rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Bumped on every renderInline entry; a render that loses the race
|
// Bumped on every renderInline entry; a render that loses the race
|
||||||
|
|
@ -169,9 +207,10 @@
|
||||||
var dm = dirtyEditor();
|
var dm = dirtyEditor();
|
||||||
if (dm) {
|
if (dm) {
|
||||||
var cur = dm.currentNode ? dm.currentNode() : null;
|
var cur = dm.currentNode ? dm.currentNode() : null;
|
||||||
if (samePreviewNode(cur, node)) {
|
if (samePreviewNode(cur, node) && !opts.toggle) {
|
||||||
// Re-selecting the file we're already editing — don't reload
|
// Re-selecting the file we're already editing — don't reload
|
||||||
// and clobber the in-progress edits.
|
// and clobber the in-progress edits. (A deliberate view toggle
|
||||||
|
// falls through to the discard prompt below.)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (opts.auto) {
|
if (opts.auto) {
|
||||||
|
|
@ -199,6 +238,32 @@
|
||||||
|
|
||||||
var ext = (node.ext || '').toLowerCase();
|
var ext = (node.ext || '').toLowerCase();
|
||||||
|
|
||||||
|
// Rendered ⇄ Source toggle button — shown only for renderable types.
|
||||||
|
var toggleBtn = ensureViewToggleBtn();
|
||||||
|
if (toggleBtn) {
|
||||||
|
if (isRenderable(ext)) {
|
||||||
|
toggleTargetNode = node;
|
||||||
|
toggleBtn.classList.remove('hidden');
|
||||||
|
toggleBtn.textContent = effectiveMode(node) === 'rendered' ? '⟨⟩ Source' : '◱ Preview';
|
||||||
|
} else {
|
||||||
|
toggleBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderable types (.html) — show rendered by default, sandboxed for
|
||||||
|
// safety (no scripts, no same-origin). The toggle flips to source.
|
||||||
|
if (isRenderable(ext) && effectiveMode(node) === 'rendered') {
|
||||||
|
try {
|
||||||
|
var rinfo = await getBlobUrl(node);
|
||||||
|
if (seq !== renderSeq) return;
|
||||||
|
container.innerHTML = '<iframe class="preview-iframe" sandbox src="'
|
||||||
|
+ escapeHtml(rinfo.url) + '"></iframe>';
|
||||||
|
} catch (e) {
|
||||||
|
renderError(container, e.message || String(e));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown plugin (if loaded) takes over for .md / .markdown.
|
// Markdown plugin (if loaded) takes over for .md / .markdown.
|
||||||
if ((ext === 'md' || ext === 'markdown') &&
|
if ((ext === 'md' || ext === 'markdown') &&
|
||||||
window.app.modules.markdown &&
|
window.app.modules.markdown &&
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue