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:
1 parent
ba6791455b
commit
bc3c9bb886
6 files changed
+73
-25
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user