+
New
Image
Notebooks
+ 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
{
+ 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);
if (text === null) return;
this.dialog.close();
diff --git a/client/js/main.js b/client/js/main.js
index 0edf2fa..cffb10d 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -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.
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
// while it is chosen puts the pen back in hand.
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.
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
// chooser of our own, which begins where the notebooks are.
diff --git a/client/js/tooltip.js b/client/js/tooltip.js
index 139ec12..a8c8e1e 100644
--- a/client/js/tooltip.js
+++ b/client/js/tooltip.js
@@ -9,6 +9,8 @@
* are covered too, since the listeners are on the document.
*/
+import { trace } from './log.js';
+
const DELAY_MS = 2000;
const GAP_PX = 6;
@@ -55,6 +57,9 @@ function hide() {
}
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) => {
const element = helpElement(event.target);
if (!element) {
@@ -63,9 +68,17 @@ export function enableTooltips() {
}
if (element === shownFor) return;
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
// acting on the element, and a list it opens would be covered (Andy,
// 2026-10-07, the notebook list). Focus from the keyboard does.
diff --git a/docs/guest.md b/docs/guest.md
index 553b37e..704075d 100644
--- a/docs/guest.md
+++ b/docs/guest.md
@@ -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
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,
-your notebook, and **Session**, which opens a window with the
-participants and the invitation. Below it the page has the whole width of the window. Every button
+**Notebooks**, your notebook, and **Session**, which opens a window with
+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
explanation appears beneath it.
@@ -216,6 +216,14 @@ mouse delivers.
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.
+**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
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;
one wider than the page widens it. To put an image on a page of its
own, press **New** first.
-* **Notebooks** starts a new page for everyone from a page kept in one of
- your notebooks, with everything that was written on it. A chooser
- shows your notebooks and small pictures of their kept pages. The kept
- page itself is not changed: the session works on a copy. The same
- 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.
+* **Notebooks**, which everyone has, now also starts a new page for
+ everyone from a page kept in one of your notebooks, with everything
+ that was written on it: choose the page in the chooser. The kept page
+ itself is not changed: the session works on a copy.
* **Hide**, among the tools, hides part of the page for everyone: drag
open a rectangle and whatever lies under it, writing included, is
covered in white, marked by blue corners so that nobody writes there
diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py
index 5b8c56f..1427df2 100644
--- a/tests/test_end_to_end.py
+++ b/tests/test_end_to_end.py
@@ -851,11 +851,11 @@ class EndToEndTest(unittest.TestCase):
# Allowed by the host, the guest puts an image on the page (the
# 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)
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Guest"), "true")
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")
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")
@@ -1182,7 +1182,7 @@ class EndToEndTest(unittest.TestCase):
# after the right is withdrawn, and cannot remove the host's.
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "true")
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")
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")
@@ -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.assertEqual(firefox.run(TOGGLE_PERMISSION, "Viewer"), "false")
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")
own = added["images"][2]
firefox.run(ERASER_AT, own["x"] + 100, own["y"] + 50, 5)
@@ -1431,9 +1431,9 @@ class EndToEndTest(unittest.TestCase):
"return document.getElementById('notice').textContent;"))
# 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)
- 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
# 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.wait_until(lambda s: s["page"]["number"] == 2 and s["image"] and s["image"]["ready"],
"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)
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "true")
firefox.switch_to(student_tab)
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")
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")
@@ -1981,7 +1981,7 @@ class EndToEndTest(unittest.TestCase):
self.assertEqual(firefox.run(TOGGLE_PERMISSION, "Student"), "false")
firefox.switch_to(student_tab)
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")
firefox.run("window.kokkoja.session.newPage();")
time.sleep(1)
@@ -2241,6 +2241,21 @@ class EndToEndTest(unittest.TestCase):
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")
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
# shown at the scale at which one sheet fits the window.
self.assertEqual(firefox.run(VIEW), long_view)