module-inspector.ts (5081B)
1 import type { SourceEditor } from './source-editor.js'; 2 import { importSites } from './import-folds.js'; 3 import { loadLibraries, renderLibraryExports } from './library-browser.js'; 4 5 export function moduleInspector(input: SourceEditor, local: () => { label: string; manifest: string; names: string[] }[]) { 6 const card = document.createElement('section'); card.className = 'definition-card module-card'; card.hidden = true; 7 card.setAttribute('aria-label', 'Imported module'); 8 const heading = document.createElement('div'); heading.className = 'definition-card-controls'; 9 const title = document.createElement('strong'), close = document.createElement('button'); 10 close.type = 'button'; close.textContent = '×'; close.className = 'definition-card-close'; close.setAttribute('aria-label', 'Close module inspection'); 11 const search = document.createElement('input'); search.type = 'search'; search.placeholder = 'Filter definitions…'; search.setAttribute('aria-label', 'Search module definitions'); 12 const list = document.createElement('div'); list.className = 'module-exports'; 13 const status = document.createElement('p'); status.className = 'hint'; status.setAttribute('role', 'status'); 14 const identity = document.createElement('div'); identity.className = 'inspection-title'; 15 const kind = document.createElement('span'); kind.className = 'inspection-kind'; kind.textContent = 'Imported module'; 16 const description = document.createElement('p'); description.className = 'module-description'; description.hidden = true; 17 identity.append(kind, title); heading.append(identity, close); card.append(heading, description, search, list, status); input.dom.append(card); 18 let selected: {hash: string; from: number} | undefined, names: string[] = [], revision = 0; 19 function hide(): void { revision++; selected = undefined; card.hidden = true; } 20 function position(): void { 21 if (!selected) return; 22 const site = importSites(input.view.state).find(site => site.from === selected!.from && site.hash === selected!.hash); 23 if (!site || input.view.state.doc.lineAt(input.view.state.selection.main.head).number !== site.line) { hide(); return; } 24 const rect = input.view.coordsAtPos(site.to), height = input.dom.clientHeight; 25 card.style.maxHeight = `${Math.max(80, Math.min(height - 16, innerHeight * .75, 400))}px`; 26 const top = rect ? rect.bottom - input.dom.getBoundingClientRect().top + 4 : 8; 27 card.style.top = `${Math.max(8, Math.min(top, height - card.offsetHeight - 8))}px`; 28 } 29 function render(): void { 30 renderLibraryExports(list, names, search.value, name => { 31 void navigator.clipboard.writeText(name).then(() => { status.textContent = 'Definition name copied.'; }, () => { status.textContent = name; }); 32 }); 33 position(); 34 } 35 async function show(target: HTMLElement): Promise<void> { 36 const hash = target.dataset.importHash; 37 const site = importSites(input.view.state).find(site => site.hash === hash && site.line === input.view.state.doc.lineAt(input.view.state.selection.main.head).number); 38 if (!site) return; 39 const current = ++revision; 40 selected = { hash: site.hash, from: site.from }; names = []; search.value = ''; list.replaceChildren(); description.hidden = true; description.textContent = ''; 41 title.textContent = 'Imported module'; status.textContent = 'Loading module definitions…'; card.hidden = false; position(); search.focus({preventScroll:true}); 42 try { 43 const nearby = local().find(row => row.manifest === hash); 44 const published = nearby ? undefined : (await loadLibraries()).find(row => row.manifest === hash); 45 if (current !== revision) return; 46 title.textContent = nearby?.label ?? published?.title ?? 'Imported module'; 47 description.textContent = published?.description ?? (nearby ? 'Local module' : ''); description.hidden = !description.textContent; 48 names = nearby?.names ?? published?.names ?? []; 49 status.textContent = nearby || published ? `${names.length} definitions · Click a name to copy it.` : 'This module is not in the library index or local modules.'; 50 render(); 51 } catch (error) { if (current === revision) status.textContent = `Could not load module definitions: ${String(error)}`; } 52 } 53 input.dom.addEventListener('click', event => { 54 const target = (event.target as Element).closest<HTMLElement>('.import-hash-full'); 55 if (target && event.detail <= 1) void show(target); 56 }); 57 input.dom.addEventListener('keydown', event => { 58 const target = (event.target as Element).closest<HTMLElement>('.import-hash-full'); 59 if (target && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); event.stopPropagation(); void show(target); } 60 }, true); 61 function dismiss(): void { hide(); input.focus(); } 62 close.onclick = dismiss; 63 card.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); dismiss(); } }); 64 search.oninput = render; input.onLayout(position); input.onChange(hide); 65 new ResizeObserver(position).observe(card); 66 return { hide }; 67 }