152 lines
4.4 KiB
JavaScript
152 lines
4.4 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* A notebook picker: a list of the notebooks with "New notebook…" as its
|
|
* last entry, which asks for the name in a field shown in the list's
|
|
* place (not in a browser dialog). Used on the start screen and in the
|
|
* session's panel. A notebook exists once a page is written into it,
|
|
* so making one is naming it.
|
|
*/
|
|
|
|
import { listNotebooks } from './notebook.js';
|
|
|
|
/** The notebook used when none is chosen: the one for whatever has no notebook of its own. */
|
|
export const DEFAULT_NOTEBOOK = 'General';
|
|
|
|
/** The value of the "New notebook…" entry; no notebook can be named so. */
|
|
const NEW = '/new';
|
|
|
|
/** What the local program accepts as a notebook name (NAME in kokkoja.py). */
|
|
const NAME_ALLOWED = /^[^\x00-\x1f/\\:*?"<>|.][^\x00-\x1f/\\:*?"<>|]{0,119}$/;
|
|
const NAME_REFUSED = 'A notebook name cannot contain / \\ : * ? " < > | and cannot begin with a dot.';
|
|
|
|
export class NotebookPicker {
|
|
/**
|
|
* @param {HTMLSelectElement} select
|
|
* @param {HTMLInputElement} entry The field for a new name.
|
|
* @param {(message: string) => void} say Explains a refused name; '' clears.
|
|
* @param {(notebook: string) => void} onChosen The choice changed.
|
|
*/
|
|
constructor(select, entry, say, onChosen) {
|
|
this.select = select;
|
|
this.entry = entry;
|
|
this.say = say;
|
|
this.onChosen = onChosen;
|
|
/** The choice before "New notebook…" was picked, to return to. */
|
|
this.previous = '';
|
|
|
|
select.addEventListener('change', () => {
|
|
if (select.value === NEW) this.beginNew();
|
|
else onChosen(select.value);
|
|
});
|
|
entry.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
this.confirm();
|
|
} else if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
this.cancel();
|
|
}
|
|
});
|
|
// Leaving the field with a usable name confirms it; with none, gives up.
|
|
entry.addEventListener('blur', () => {
|
|
if (entry.hidden) return;
|
|
if (NAME_ALLOWED.test(entry.value.trim())) this.confirm();
|
|
else this.cancel();
|
|
});
|
|
}
|
|
|
|
/** @returns {string} The chosen notebook. */
|
|
get value() {
|
|
return this.select.value === NEW ? this.previous : this.select.value;
|
|
}
|
|
|
|
/**
|
|
* List the notebooks, with the given one chosen (added to the list if
|
|
* it is not there yet, since a notebook exists once a page is written).
|
|
* @param {string} current
|
|
*/
|
|
async show(current) {
|
|
let names = /** @type {string[]} */ ([]);
|
|
try {
|
|
names = (await listNotebooks()).notebooks;
|
|
} catch {
|
|
// Without the list, the current notebook alone is offered.
|
|
}
|
|
this.fill(names, current);
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} names
|
|
* @param {string} current
|
|
*/
|
|
fill(names, current) {
|
|
const all = [...new Set([...names, current])].filter(Boolean).sort((a, b) => a.localeCompare(b));
|
|
this.select.replaceChildren();
|
|
for (const name of all) {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
option.textContent = name;
|
|
this.select.append(option);
|
|
}
|
|
const make = document.createElement('option');
|
|
make.value = NEW;
|
|
make.textContent = 'New notebook…';
|
|
this.select.append(make);
|
|
this.select.value = current;
|
|
}
|
|
|
|
beginNew() {
|
|
this.previous = this.previous || this.select.options[0]?.value || '';
|
|
this.say('');
|
|
this.select.hidden = true;
|
|
this.entry.value = '';
|
|
this.entry.hidden = false;
|
|
this.entry.focus();
|
|
}
|
|
|
|
confirm() {
|
|
const name = this.entry.value.trim();
|
|
if (!NAME_ALLOWED.test(name)) {
|
|
this.say(name ? NAME_REFUSED : '');
|
|
if (!name) this.cancel();
|
|
return;
|
|
}
|
|
this.say('');
|
|
if (![...this.select.options].some((option) => option.value === name)) {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
option.textContent = name;
|
|
this.select.insertBefore(option, this.select.options[this.select.options.length - 1]);
|
|
}
|
|
this.select.value = name;
|
|
this.previous = name;
|
|
this.endEntry();
|
|
this.onChosen(name);
|
|
}
|
|
|
|
cancel() {
|
|
this.select.value = this.previous;
|
|
this.endEntry();
|
|
}
|
|
|
|
endEntry() {
|
|
this.entry.hidden = true;
|
|
this.select.hidden = false;
|
|
}
|
|
|
|
/**
|
|
* The choice, made from outside; it is also the one to return to if
|
|
* "New notebook…" is given up.
|
|
* @param {string} name
|
|
*/
|
|
set current(name) {
|
|
this.previous = name;
|
|
this.select.value = name;
|
|
}
|
|
|
|
/** @returns {string} */
|
|
get current() {
|
|
return this.value;
|
|
}
|
|
}
|