// @ts-check /** * The notebook chooser: the "Notebook" button's dialog, which shows the * notebooks and the pages kept in them, and loads the chosen page. */ import { listKeptPages, listNotebooks, readPage } from './notebook.js'; /** * @param {string} id * @returns {HTMLElement} */ function element(id) { return /** @type {HTMLElement} */ (document.getElementById(id)); } /** * @param {string} pageName * @returns {string} "2026_10_02_01.svg" shown as "2026-10-02 (1)". */ function pageTitle(pageName) { const match = /^(\d{4})_(\d{2})_(\d{2})_(\d+)\.svg$/.exec(pageName); return match ? `${match[1]}-${match[2]}-${match[3]} (${Number(match[4])})` : pageName; } export class NotebookChooser { /** * @param {(notebook: string, pageName: string, text: string) => void} onChosen * Called with the chosen page file. */ constructor(onChosen) { this.onChosen = onChosen; this.dialog = /** @type {HTMLDialogElement} */ (element('notebook_chooser')); this.notebook = ''; /** Counts openings, so that pictures fetched for an earlier one are dropped. */ this.opening = 0; /** @type {string[]} */ this.pictureAddresses = []; element('chooser_close').addEventListener('click', () => this.dialog.close()); this.dialog.addEventListener('close', () => this.forgetPictures()); } /** * Show the notebooks, or the pages of the only notebook. * @param {string} [notebook] Open on this notebook. */ async open(notebook) { this.opening += 1; this.dialog.showModal(); element('chooser_message').textContent = ''; element('chooser_pages').replaceChildren(); // Nothing stale while the lists load. try { const { notebooks } = await listNotebooks(); this.showNotebooks(notebooks); // The notebook being written in is chosen already, its pages shown; // the list is always there to see (Andy, 2026-10-06). const chosen = notebook && notebooks.includes(notebook) ? notebook : notebooks.length === 1 ? notebooks[0] : ''; await this.showPages(chosen); } catch (error) { element('chooser_message').textContent = error instanceof Error ? error.message : String(error); } } /** @param {string[]} notebooks */ showNotebooks(notebooks) { const list = element('chooser_notebooks'); list.replaceChildren(); for (const notebook of notebooks) { const button = document.createElement('button'); button.type = 'button'; button.textContent = notebook; button.addEventListener('click', () => { this.showPages(notebook); }); list.append(button); } } /** @param {string} notebook */ async showPages(notebook) { this.notebook = notebook; for (const button of element('chooser_notebooks').querySelectorAll('button')) { button.setAttribute('aria-pressed', String(button.textContent === notebook)); } const grid = element('chooser_pages'); grid.replaceChildren(); this.forgetPictures(); if (!notebook) { element('chooser_message').textContent = 'Choose a notebook.'; return; } const opening = this.opening += 1; const pages = await listKeptPages(notebook); if (opening !== this.opening) return; element('chooser_message').textContent = pages.length ? '' : 'This notebook has no kept pages.'; /** @type {Map} */ const pictures = new Map(); for (const pageName of pages) { const card = document.createElement('button'); card.type = 'button'; card.className = 'page_card'; const picture = document.createElement('img'); picture.alt = ''; pictures.set(pageName, picture); const title = document.createElement('span'); title.textContent = pageTitle(pageName); card.append(picture, title); card.addEventListener('click', async () => { const text = await readPage(notebook, pageName); if (text === null) return; this.dialog.close(); this.onChosen(notebook, pageName, text); }); grid.append(card); } // The pictures are the page files themselves, fetched one by one. for (const [pageName, picture] of pictures) { const text = await readPage(notebook, pageName); if (opening !== this.opening) return; if (text === null) continue; const address = URL.createObjectURL(new Blob([text], { type: 'image/svg+xml' })); this.pictureAddresses.push(address); picture.src = address; } } forgetPictures() { for (const address of this.pictureAddresses) URL.revokeObjectURL(address); this.pictureAddresses = []; } }