diff --git a/src/code-viewer.html b/src/code-viewer.html index d2e0ce339..56da3d235 100644 --- a/src/code-viewer.html +++ b/src/code-viewer.html @@ -16,7 +16,10 @@
diff --git a/src/css/code-viewer.css b/src/css/code-viewer.css index 0d296c886..774fa69ef 100644 --- a/src/css/code-viewer.css +++ b/src/css/code-viewer.css @@ -23,6 +23,16 @@ body { #header:focus-within #pastURLs { display: flex; } +#currentURL { + display: flex; + gap: 0.5rem; + } +#currentURL > .fa-icon { + padding: 0 0.5rem; + } +#currentURL > .fa-icon:hover { + background-color: var(--surface-3); + } #pastURLs { background-color: var(--surface-0); border: 1px solid var(--border-1); diff --git a/src/js/code-viewer.js b/src/js/code-viewer.js index aff962af8..6eecacd58 100644 --- a/src/js/code-viewer.js +++ b/src/js/code-viewer.js @@ -30,9 +30,9 @@ import { getActualTheme } from './theme.js'; /******************************************************************************/ -const urlToTextMap = new Map(); +const urlToDocMap = new Map(); const params = new URLSearchParams(document.location.search); -let fromURL = ''; +let currentURL = ''; const cmEditor = new CodeMirror(qs$('#content'), { autofocus: true, @@ -88,12 +88,11 @@ cmEditor.addOverlay({ }, }); +urlToDocMap.set('', cmEditor.getDoc()); + /******************************************************************************/ async function fetchResource(url) { - if ( urlToTextMap.has(url) ) { - return urlToTextMap.get(url); - } let response, text; try { response = await fetch(url); @@ -103,34 +102,46 @@ async function fetchResource(url) { } let mime = response.headers.get('Content-Type') || ''; mime = mime.replace(/\s*;.*$/, '').trim(); + const options = { + 'end_with_newline': true, + 'indent_size': 2, + 'html': { + 'js': { + 'indent_size': 4, + }, + }, + 'js': { + 'indent_size': 4, + 'preserve-newlines': true, + }, + }; switch ( mime ) { case 'text/css': - text = beautifier.css(text, { indent_size: 2 }); + text = beautifier.css(text, options); break; case 'text/html': case 'application/xhtml+xml': case 'application/xml': case 'image/svg+xml': - text = beautifier.html(text, { indent_size: 2 }); + text = beautifier.html(text, options); break; case 'text/javascript': case 'application/javascript': case 'application/x-javascript': - text = beautifier.js(text, { indent_size: 4 }); + text = beautifier.js(text, options); break; case 'application/json': - text = beautifier.js(text, { indent_size: 2 }); + text = beautifier.js(text, options); break; default: break; } - urlToTextMap.set(url, { mime, text }); return { mime, text }; } /******************************************************************************/ -function updatePastURLs(url) { +function addPastURLs(url) { const list = qs$('#pastURLs'); let current; for ( let i = 0; i < list.children.length; i++ ) { @@ -139,6 +150,7 @@ function updatePastURLs(url) { if ( span.textContent !== url ) { continue; } current = span; } + if ( url === '' ) { return; } if ( current === undefined ) { current = document.createElement('span'); current.textContent = url; @@ -149,47 +161,92 @@ function updatePastURLs(url) { /******************************************************************************/ +function setInputURL(url) { + const input = qs$('#header input[type="url"]'); + if ( url === input.value ) { return; } + dom.attr(input, 'value', url); + input.value = url; +} + +/******************************************************************************/ + async function setURL(resourceURL) { // For convenience, remove potentially existing quotes around the URL if ( /^(["']).+\1$/.test(resourceURL) ) { resourceURL = resourceURL.slice(1, -1); } - const input = qs$('#header input[type="url"]'); - let to; - try { - to = new URL(resourceURL, fromURL || undefined); - } catch(ex) { + let afterURL; + if ( resourceURL !== '' ) { + try { + const url = new URL(resourceURL, currentURL || undefined); + url.hash = ''; + afterURL = url.href; + } catch(ex) { + } + if ( afterURL === undefined ) { return; } + } else { + afterURL = ''; } - if ( to === undefined ) { return; } - if ( /^https?:\/\/./.test(to.href) === false ) { return; } - if ( to.href === fromURL ) { return; } - let r; - try { - r = await fetchResource(to.href); - } catch(reason) { + if ( afterURL !== '' && /^https?:\/\/./.test(afterURL) === false ) { + return; } - if ( r === undefined ) { return; } - fromURL = to.href; - dom.attr(input, 'value', to.href); - input.value = to; + if ( afterURL === currentURL ) { + if ( afterURL !== resourceURL ) { + setInputURL(afterURL); + } + return; + } + let afterDoc = urlToDocMap.get(afterURL); + if ( afterDoc === undefined ) { + const r = await fetchResource(afterURL) || { mime: '', text: '' }; + afterDoc = new CodeMirror.Doc(r.text, r.mime || ''); + } + urlToDocMap.set(currentURL, cmEditor.swapDoc(afterDoc)); + currentURL = afterURL; + setInputURL(afterURL); const a = qs$('.cm-search-widget .sourceURL'); - dom.attr(a, 'href', to); - dom.attr(a, 'title', to); - cmEditor.setOption('mode', r.mime || ''); - cmEditor.setValue(r.text); - updatePastURLs(to.href); + dom.attr(a, 'href', afterURL); + dom.attr(a, 'title', afterURL); + addPastURLs(afterURL); // For unknown reasons, calling focus() synchronously does not work... vAPI.setTimeout(( ) => { cmEditor.focus(); }, 1); } /******************************************************************************/ +function removeURL(url) { + if ( url === '' ) { return; } + const list = qs$('#pastURLs'); + let foundAt = -1; + for ( let i = 0; i < list.children.length; i++ ) { + const span = list.children[i]; + if ( span.textContent !== url ) { continue; } + foundAt = i; + } + if ( foundAt === -1 ) { return; } + list.children[foundAt].remove(); + if ( foundAt >= list.children.length ) { + foundAt = list.children.length - 1; + } + const afterURL = foundAt !== -1 + ? list.children[foundAt].textContent + : ''; + setURL(afterURL); + urlToDocMap.delete(url); +} + +/******************************************************************************/ + setURL(params.get('url')); dom.on('#header input[type="url"]', 'change', ev => { setURL(ev.target.value); }); +dom.on('#removeURL', 'click', ( ) => { + removeURL(qs$('#header input[type="url"]').value); +}); + dom.on('#pastURLs', 'mousedown', 'span', ev => { setURL(ev.target.textContent); });