Everyone has the Notebooks chooser

From the development repository at 1ccf212.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-11 17:34:24 +02:00
1 parent ba6791455b
commit bc3c9bb886
6 files changed
+73 -25

No files matched your search

+2 -2
View File
@@ -87,13 +87,13 @@
</label> </label>
</div> </div>
<div class="toolbar_right"> <div class="toolbar_right">
<div class="group" id="host_tools" hidden> <div class="group" id="host_tools">
<button id="new_page" type="button" <button id="new_page" type="button"
data-help="Start a new blank page for everyone. The current page is kept in each notebook.">New</button> data-help="Start a new blank page for everyone. The current page is kept in each notebook.">New</button>
<button id="load_image" type="button" <button id="load_image" type="button"
data-help="Put an image file, such as a scanned exercise, on the page for everyone: below the images already there, centred, at its own size. Rest the pen on an image to move it, or near a corner to scale it; it comes to the front. A tap of the eraser on an image removes it; writing on it stays.">Image</button> data-help="Put an image file, such as a scanned exercise, on the page for everyone: below the images already there, centred, at its own size. Rest the pen on an image to move it, or near a corner to scale it; it comes to the front. A tap of the eraser on an image removes it; writing on it stays.">Image</button>
<button id="load_notebook_page" type="button" <button id="load_notebook_page" type="button"
data-help="Start a new page for everyone from a page kept in a notebook, with everything on it. The current page is kept in each notebook.">Notebooks</button> data-help="Your notebooks and the pages kept in them, to look at or to delete. The host and co-hosts can start a new page for everyone from a kept page, with everything on it; the current page is then kept in each notebook.">Notebooks</button>
</div> </div>
<div class="group" aria-label="Notebook"> <div class="group" aria-label="Notebook">
<select id="session_notebook" class="notebook_pick" aria-label="Current notebook" <select id="session_notebook" class="notebook_pick" aria-label="Current notebook"
+13 -1
View File
@@ -15,6 +15,10 @@
* a Trash the entries are moved there, and the button says so; * a Trash the entries are moved there, and the button says so;
* in the browser they are gone for good, and the button says that. The * in the browser they are gone for good, and the button says that. The
* notebook being written in cannot be marked: its mark explains. * notebook being written in cannot be marked: its mark explains.
* Everyone has the chooser (Andy, 2026-10-11), so that a guest can see
* and clear what their notebook holds, in a browser above all; loading
* a page into the session is for the host and co-hosts, and a card says
* so to anyone else.
*/ */
import { discardNotebook, discardPage, listKeptPages, listNotebooks, readPage } from './notebook.js'; import { discardNotebook, discardPage, listKeptPages, listNotebooks, readPage } from './notebook.js';
@@ -75,6 +79,8 @@ export class NotebookChooser {
this.notebook = ''; this.notebook = '';
/** The notebook being written in, which cannot be deleted while it is. */ /** The notebook being written in, which cannot be deleted while it is. */
this.writing = ''; this.writing = '';
/** Whether this participant may load a page into the session. */
this.mayLoad = true;
/** Whether deletions go to a Trash (the local program's) or are for good. */ /** Whether deletions go to a Trash (the local program's) or are for good. */
this.trash = false; this.trash = false;
/** Counts openings, so that pictures fetched for an earlier one are dropped. */ /** Counts openings, so that pictures fetched for an earlier one are dropped. */
@@ -99,10 +105,12 @@ export class NotebookChooser {
/** /**
* Show the notebooks, or the pages of the only notebook. * Show the notebooks, or the pages of the only notebook.
* @param {string} [notebook] Open on this notebook: the one being written in. * @param {string} [notebook] Open on this notebook: the one being written in.
* @param {boolean} [mayLoad] Whether a page may be loaded into the session.
*/ */
async open(notebook) { async open(notebook, mayLoad = true) {
this.opening += 1; this.opening += 1;
this.writing = notebook ?? ''; this.writing = notebook ?? '';
this.mayLoad = mayLoad;
this.clearMarks(); this.clearMarks();
this.dialog.showModal(); this.dialog.showModal();
element('chooser_message').textContent = ''; element('chooser_message').textContent = '';
@@ -190,6 +198,10 @@ export class NotebookChooser {
title.textContent = pageTitle(pageName); title.textContent = pageTitle(pageName);
card.append(picture, title); card.append(picture, title);
card.addEventListener('click', async () => { card.addEventListener('click', async () => {
if (!this.mayLoad) {
this.say('Only the host and co-hosts can start a page for everyone from a kept page.');
return;
}
const text = await readPage(notebook, pageName); const text = await readPage(notebook, pageName);
if (text === null) return; if (text === null) return;
this.dialog.close(); this.dialog.close();
+5 -2
View File
@@ -1053,7 +1053,10 @@ async function enterSession(invitationText, name, notebook) {
// The host drives: the host starts new pages, and may let a guest do so. // The host drives: the host starts new pages, and may let a guest do so.
function showHostTools() { function showHostTools() {
element('host_tools').hidden = !session.mayMakePages(); // New and Image are the host's; Notebooks, in the same group so that
// nothing moves for the host, is everyone's (the chooser reserves
// loading).
for (const id of ['new_page', 'load_image']) element(id).hidden = !session.mayMakePages();
// The Hide tool is for the host and co-hosts; losing the right // The Hide tool is for the host and co-hosts; losing the right
// while it is chosen puts the pen back in hand. // while it is chosen puts the pen back in hand.
const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]')); const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]'));
@@ -1095,7 +1098,7 @@ async function enterSession(invitationText, name, notebook) {
// A deleted notebook leaves the toolbar's list. // A deleted notebook leaves the toolbar's list.
showNotebooks(sessionPicker).catch(() => {}); showNotebooks(sessionPicker).catch(() => {});
}); });
element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook); }); element('load_notebook_page').addEventListener('click', () => { chooser.open(writer.notebook, session.mayMakePages()); });
// Image files are chosen, and pictures of the page exported, through a // Image files are chosen, and pictures of the page exported, through a
// chooser of our own, which begins where the notebooks are. // chooser of our own, which begins where the notebooks are.
+15 -2
View File
@@ -9,6 +9,8 @@
* are covered too, since the listeners are on the document. * are covered too, since the listeners are on the document.
*/ */
import { trace } from './log.js';
const DELAY_MS = 2000; const DELAY_MS = 2000;
const GAP_PX = 6; const GAP_PX = 6;
@@ -55,6 +57,9 @@ function hide() {
} }
export function enableTooltips() { export function enableTooltips() {
// Traced into the Session window's log (2026-10-11: Andy's Wacom pen
// hovering over a button on Hollis showed no tooltip, and the log is
// the way to see what the browser delivers for a hovering pen).
document.addEventListener('pointerover', (event) => { document.addEventListener('pointerover', (event) => {
const element = helpElement(event.target); const element = helpElement(event.target);
if (!element) { if (!element) {
@@ -63,9 +68,17 @@ export function enableTooltips() {
} }
if (element === shownFor) return; if (element === shownFor) return;
hide(); hide();
timer = setTimeout(() => { timer = null; show(element); }, DELAY_MS); trace(`Hover: ${event.pointerType} over ${element.textContent?.trim() || element.id}`);
timer = setTimeout(() => {
timer = null;
trace(`Tooltip: ${element.textContent?.trim() || element.id}`);
show(element);
}, DELAY_MS);
});
document.addEventListener('pointerleave', (event) => {
if (timer !== null || shownFor) trace(`Hover: ${event.pointerType} left the document`);
hide();
}); });
document.addEventListener('pointerleave', hide);
// Focus that comes from a click brings no tooltip: the user is already // Focus that comes from a click brings no tooltip: the user is already
// acting on the element, and a list it opens would be covered (Andy, // acting on the element, and a list it opens would be covered (Andy,
// 2026-10-07, the notebook list). Focus from the keyboard does. // 2026-10-07, the notebook list). Focus from the keyboard does.
+14 -9
View File
@@ -51,8 +51,8 @@ place in it waits for the host to return.
Across the top is the toolbar, one row holding everything in three Across the top is the toolbar, one row holding everything in three
groups: on the left the tools, their settings and **Undo**; in the groups: on the left the tools, their settings and **Undo**; in the
middle the page and the view; on the right the host's page buttons, middle the page and the view; on the right the host's page buttons,
your notebook, and **Session**, which opens a window with the **Notebooks**, your notebook, and **Session**, which opens a window with
participants and the invitation. Below it the page has the whole width of the window. Every button the participants and the invitation. Below it the page has the whole width of the window. Every button
explains itself: rest the pointer on it for two seconds and its explains itself: rest the pointer on it for two seconds and its
explanation appears beneath it. explanation appears beneath it.
@@ -216,6 +216,14 @@ mouse delivers.
Every page of the session is saved for you, as it changes, without your Every page of the session is saved for you, as it changes, without your
doing anything. Where it is saved depends on how you joined. doing anything. Where it is saved depends on how you joined.
**Notebooks** in the toolbar opens a chooser that shows your notebooks
and small pictures of the pages kept in them, wherever they are kept.
There you can delete pages and notebooks you no longer want: mark them
with the × at their left, then press **Delete** twice. In the browser
a deleted page is gone for good; with the Kokkoja program it goes to the
Trash. Starting a page for everyone from a kept page is the host's
business, and a co-host's.
### In the browser ### In the browser
When you joined by opening the link, your notebook is kept inside the When you joined by opening the link, your notebook is kept inside the
@@ -300,13 +308,10 @@ right of your toolbar:
size, centred, 5 mm below the image above or at the top of the page; size, centred, 5 mm below the image above or at the top of the page;
one wider than the page widens it. To put an image on a page of its one wider than the page widens it. To put an image on a page of its
own, press **New** first. own, press **New** first.
* **Notebooks** starts a new page for everyone from a page kept in one of * **Notebooks**, which everyone has, now also starts a new page for
your notebooks, with everything that was written on it. A chooser everyone from a page kept in one of your notebooks, with everything
shows your notebooks and small pictures of their kept pages. The kept that was written on it: choose the page in the chooser. The kept page
page itself is not changed: the session works on a copy. The same itself is not changed: the session works on a copy.
chooser deletes pages and notebooks of yours, marked with the × at
their left and then the **Delete** button, pressed twice; the host's part says
more.
* **Hide**, among the tools, hides part of the page for everyone: drag * **Hide**, among the tools, hides part of the page for everyone: drag
open a rectangle and whatever lies under it, writing included, is open a rectangle and whatever lies under it, writing included, is
covered in white, marked by blue corners so that nobody writes there covered in white, marked by blue corners so that nobody writes there
+24 -9
View File
@@ -851,11 +851,11 @@ class EndToEndTest(unittest.TestCase):
# Allowed by the host, the guest puts an image on the page (the # Allowed by the host, the guest puts an image on the page (the
# file would come from the browser's own chooser). # file would come from the browser's own chooser).
self.assertTrue(firefox.run("return document.getElementById('host_tools').hidden;")) self.assertTrue(firefox.run("return document.getElementById('new_page').hidden;"))
firefox.switch_to(host_tab) firefox.switch_to(host_tab)
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Guest"), "true") self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Guest"), "true")
firefox.switch_to(guest_tab) firefox.switch_to(guest_tab)
self.wait_until(lambda s: not firefox.run("return document.getElementById('host_tools').hidden;"), self.wait_until(lambda s: not firefox.run("return document.getElementById('new_page').hidden;"),
"the guest to be allowed to start pages") "the guest to be allowed to start pages")
firefox.run_until_done(LOAD_IMAGE, 300, 400, "png", 0, False) firefox.run_until_done(LOAD_IMAGE, 300, 400, "png", 0, False)
guest = self.wait_until(lambda s: s["image"] and s["image"]["ready"], "the guest's image") guest = self.wait_until(lambda s: s["image"] and s["image"]["ready"], "the guest's image")
@@ -1182,7 +1182,7 @@ class EndToEndTest(unittest.TestCase):
# after the right is withdrawn, and cannot remove the host's. # after the right is withdrawn, and cannot remove the host's.
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "true") self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "true")
firefox.switch_to(guest_tab) firefox.switch_to(guest_tab)
self.wait_until(lambda s: not firefox.run("return document.getElementById('host_tools').hidden;"), self.wait_until(lambda s: not firefox.run("return document.getElementById('new_page').hidden;"),
"the viewer to be allowed") "the viewer to be allowed")
firefox.run_until_done(LOAD_IMAGE, 200, 100, "png", 0, False) firefox.run_until_done(LOAD_IMAGE, 200, 100, "png", 0, False)
added = self.wait_until(lambda s: len(s["images"]) == 3, "the viewer's image") added = self.wait_until(lambda s: len(s["images"]) == 3, "the viewer's image")
@@ -1190,7 +1190,7 @@ class EndToEndTest(unittest.TestCase):
self.wait_until(lambda s: len(s["images"]) == 3, "the viewer's image to reach the host") self.wait_until(lambda s: len(s["images"]) == 3, "the viewer's image to reach the host")
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "false") self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "false")
firefox.switch_to(guest_tab) firefox.switch_to(guest_tab)
self.wait_until(lambda s: firefox.run("return document.getElementById('host_tools').hidden;"), self.wait_until(lambda s: firefox.run("return document.getElementById('new_page').hidden;"),
"the right to be withdrawn") "the right to be withdrawn")
own = added["images"][2] own = added["images"][2]
firefox.run(ERASER_AT, own["x"] + 100, own["y"] + 50, 5) firefox.run(ERASER_AT, own["x"] + 100, own["y"] + 50, 5)
@@ -1431,9 +1431,9 @@ class EndToEndTest(unittest.TestCase):
"return document.getElementById('notice').textContent;")) "return document.getElementById('notice').textContent;"))
# Only the host can start a new page. # Only the host can start a new page.
self.assertTrue(firefox.run("return document.getElementById('host_tools').hidden;")) self.assertTrue(firefox.run("return document.getElementById('new_page').hidden;"))
firefox.switch_to(teacher_tab) firefox.switch_to(teacher_tab)
self.assertFalse(firefox.run("return document.getElementById('host_tools').hidden;")) self.assertFalse(firefox.run("return document.getElementById('new_page').hidden;"))
# The host starts a new page. Everyone moves to it, and everyone's # The host starts a new page. Everyone moves to it, and everyone's
# old page is kept without anyone asking: the teacher's now, the # old page is kept without anyone asking: the teacher's now, the
@@ -1963,12 +1963,12 @@ class EndToEndTest(unittest.TestCase):
self.enter_name("Student", "join_session") self.enter_name("Student", "join_session")
self.wait_until(lambda s: s["page"]["number"] == 2 and s["image"] and s["image"]["ready"], self.wait_until(lambda s: s["page"]["number"] == 2 and s["image"] and s["image"]["ready"],
"the student to reach the large image page") "the student to reach the large image page")
self.assertTrue(firefox.run("return document.getElementById('host_tools').hidden;")) self.assertTrue(firefox.run("return document.getElementById('new_page').hidden;"))
firefox.switch_to(teacher_tab) firefox.switch_to(teacher_tab)
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "true") self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "true")
firefox.switch_to(student_tab) firefox.switch_to(student_tab)
self.wait_until( self.wait_until(
lambda s: not firefox.run("return document.getElementById('host_tools').hidden;"), lambda s: not firefox.run("return document.getElementById('new_page').hidden;"),
"the student to be allowed to start pages") "the student to be allowed to start pages")
firefox.run_until_done(LOAD_IMAGE, 300, 400, "png", 0, True) firefox.run_until_done(LOAD_IMAGE, 300, 400, "png", 0, True)
student = self.wait_until(lambda s: s["page"]["number"] == 3, "the student's image page") student = self.wait_until(lambda s: s["page"]["number"] == 3, "the student's image page")
@@ -1981,7 +1981,7 @@ class EndToEndTest(unittest.TestCase):
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "false") self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "false")
firefox.switch_to(student_tab) firefox.switch_to(student_tab)
self.wait_until( self.wait_until(
lambda s: firefox.run("return document.getElementById('host_tools').hidden;"), lambda s: firefox.run("return document.getElementById('new_page').hidden;"),
"the student's permission to be withdrawn") "the student's permission to be withdrawn")
firefox.run("window.kokkoja.session.newPage();") firefox.run("window.kokkoja.session.newPage();")
time.sleep(1) time.sleep(1)
@@ -2241,6 +2241,21 @@ class EndToEndTest(unittest.TestCase):
return [...document.getElementById('session_notebook').options].map((o) => o.textContent); return [...document.getElementById('session_notebook').options].map((o) => o.textContent);
"""), lambda names: "Other notebook" not in names, "the toolbar's list to lose the notebook") """), lambda names: "Other notebook" not in names, "the toolbar's list to lose the notebook")
firefox.run("document.getElementById('chooser_close').click();") firefox.run("document.getElementById('chooser_close').click();")
# A guest has the chooser too, to look at and clear their own
# notebook; a card tells them that loading is the host's.
firefox.switch_to(student_tab)
number = self.state()["page"]["number"]
firefox.run("document.getElementById('load_notebook_page').click();")
self.wait_until_true(lambda: firefox.run("""
return document.querySelectorAll('#chooser_pages .page_card').length;
"""), lambda n: n >= 1, "the student's chooser to list kept pages")
firefox.run("document.querySelector('#chooser_pages .page_card').click();")
self.assertIn("Only the host and co-hosts", firefox.run("return document.getElementById('chooser_message').textContent;"))
self.assertTrue(firefox.run("return document.getElementById('notebook_chooser').open;"))
self.assertEqual(self.state()["page"]["number"], number)
firefox.run("document.getElementById('chooser_close').click();")
firefox.switch_to(teacher_tab)
# A page that its images have made longer than a sheet is first # A page that its images have made longer than a sheet is first
# shown at the scale at which one sheet fits the window. # shown at the scale at which one sheet fits the window.
self.assertEqual(firefox.run(VIEW), long_view) self.assertEqual(firefox.run(VIEW), long_view)