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();
|
||||
var container = document.getElementById('previewBody');
|
||||
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
|
||||
|
|
@ -141,6 +144,41 @@
|
|||
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 ────────────────────────────────────────────────────
|
||||
|
||||
// Bumped on every renderInline entry; a render that loses the race
|
||||
|
|
@ -169,9 +207,10 @@
|
|||
var dm = dirtyEditor();
|
||||
if (dm) {
|
||||
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
|
||||
// and clobber the in-progress edits.
|
||||
// and clobber the in-progress edits. (A deliberate view toggle
|
||||
// falls through to the discard prompt below.)
|
||||
return;
|
||||
}
|
||||
if (opts.auto) {
|
||||
|
|
@ -199,6 +238,32 @@
|
|||
|
||||
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.
|
||||
if ((ext === 'md' || ext === 'markdown') &&
|
||||
window.app.modules.markdown &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue