diff --git a/browse/js/preview.js b/browse/js/preview.js index 385a497..bc6eb07 100644 --- a/browse/js/preview.js +++ b/browse/js/preview.js @@ -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 = ''; + } 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 &&