File size: 3,025 Bytes
39371ea b74c172 39371ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | import { canHighlight, editorLanguage } from './editor-language.mjs';
export function createEditorUI(element) {
const input = element.querySelector('#editor'), highlight = element.querySelector('#editor-highlight');
let path = '', revision = 0, timer, watchdog, worker, active = false, composing = false, queued;
function syncScroll() {
// Match the textarea's actual viewport, excluding either native scrollbar.
highlight.style.width = input.clientWidth + 'px';
highlight.style.height = input.clientHeight + 'px';
highlight.scrollTop = input.scrollTop;
highlight.scrollLeft = input.scrollLeft;
}
function plain() { element.classList.remove('is-highlighted'); }
function stopWorker() {
clearTimeout(watchdog);
worker?.terminate(); worker = undefined; active = false;
}
function send() {
if (active || !queued) return;
const job = queued; queued = undefined;
try {
if (!worker) {
worker = new Worker(new URL('./editor-highlight.worker.mjs', import.meta.url), { type: 'module' });
worker.onmessage = ({ data }) => {
clearTimeout(watchdog); active = false;
if (data.id === revision && data.html !== null) {
// Prism escapes file contents before adding its own token spans.
highlight.innerHTML = data.html + '\n';
syncScroll(); element.classList.add('is-highlighted');
}
send();
};
worker.onerror = () => { stopWorker(); queued = undefined; };
}
active = true;
worker.postMessage(job);
// A pathological regex can only occupy this worker, and is terminated.
watchdog = setTimeout(() => { stopWorker(); send(); }, 1500);
} catch { stopWorker(); }
}
function refresh(delay = 0) {
revision++; clearTimeout(timer); queued = undefined; plain();
const language = editorLanguage(path), text = input.value;
element.dataset.language = language ?? 'plain';
if (composing || !text || !language || !canHighlight(text)) return;
const id = revision;
timer = setTimeout(() => { queued = { id, text, language }; send(); }, delay);
}
input.addEventListener('input', () => refresh(80));
input.addEventListener('compositionstart', () => { composing = true; refresh(); });
input.addEventListener('compositionend', () => { composing = false; refresh(80); });
input.addEventListener('scroll', syncScroll, { passive: true });
const resize = new ResizeObserver(syncScroll); resize.observe(input);
return {
setFile(nextPath, text, placeholder = nextPath ? 'Empty file.' : 'Select a file to inspect or edit it.') {
input.placeholder = placeholder;
if (path === nextPath && input.value === text) return;
const changed = path !== nextPath;
path = nextPath;
if (input.value !== text) input.value = text;
if (changed) { input.scrollTop = 0; input.scrollLeft = 0; }
refresh();
},
dispose() { clearTimeout(timer); stopWorker(); resize.disconnect(); },
};
}
|