The Page menu, Help, and the manual from the tooltips
The page and view commands are in a menu under "Page", with grey titles over the display and size sections. Help, at the far right, shows a note for now; with Ctrl (Command on a Mac) held it shows the explanations of every command in one text, alphabetical, wrapped at 80 columns, a shared name prefixed by its context. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8ecf3e94a6
commit
35c4f434f3
5 files changed
+419
-55
No files matched your search
+86
-27
@@ -35,6 +35,7 @@ import { TextEditor } from './text_editor.js';
|
||||
import { enableTooltips } from './tooltip.js';
|
||||
import { guardSelection, inField } from './selection_guard.js';
|
||||
import { QUIET_ZONE, qrModules, qrPath } from './qr_code.js';
|
||||
import { manualText } from './manual.js';
|
||||
|
||||
/**
|
||||
* The colours in the toolbar: black, red, green, blue, white (Andy's
|
||||
@@ -629,14 +630,12 @@ async function enterSession(invitationText, name, notebook) {
|
||||
};
|
||||
new ResizeObserver(watchToolbar).observe(toolbar);
|
||||
watchToolbar();
|
||||
// The drawing group keeps the width of its widest state, the pen's
|
||||
// colours and widths, so that the page-and-view group, centred between
|
||||
// it and the session group, stays put when the tool changes (Andy,
|
||||
// 2026-10-11). The floor rises if the group ever grows, as when a
|
||||
// colour is added.
|
||||
// The settings (colours, widths, font and size) keep their widest
|
||||
// room likewise, so that Eraser, Hide and Undo, which stand after them
|
||||
// since 2026-10-11, do not jump when a tool shows fewer settings.
|
||||
// The drawing group and the settings (colours, widths, font and size)
|
||||
// keep the width of their widest state, so that Eraser, Hide and Undo,
|
||||
// which stand after the settings since 2026-10-11, do not jump when a
|
||||
// tool shows fewer settings (the drawing group's floor also kept the
|
||||
// middle group still while there was one, until 2026-10-14). The
|
||||
// floor rises if a group ever grows, as when a colour is added.
|
||||
for (const part of [element('drawing'), element('settings')]) {
|
||||
let widest = 0;
|
||||
new ResizeObserver(() => {
|
||||
@@ -1053,6 +1052,30 @@ async function enterSession(invitationText, name, notebook) {
|
||||
};
|
||||
// The host's Fit and A4 reach everyone (Andy, 2026-10-08); a guest's
|
||||
// are the guest's own.
|
||||
// The page and view commands live in the Page menu (Andy, 2026-10-14:
|
||||
// seldom used, and the one row is precious); Help is a note for now.
|
||||
attachPopup(element('page_menu_button'), element('page_menu'));
|
||||
// Ctrl-click (Command on a Mac) on Help: the tooltips of every command,
|
||||
// gathered as one text (Andy, 2026-10-14). Registered before the
|
||||
// popup's handler, which it then stops.
|
||||
const manualDialog = /** @type {HTMLDialogElement} */ (element('manual_dialog'));
|
||||
element('help').addEventListener('click', (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey)) return;
|
||||
event.stopImmediatePropagation();
|
||||
element('manual_text').textContent = manualText(document);
|
||||
manualDialog.showModal();
|
||||
});
|
||||
element('manual_close').addEventListener('click', () => manualDialog.close());
|
||||
element('copy_manual').addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(element('manual_text').textContent ?? '');
|
||||
element('copy_manual').textContent = 'Copied';
|
||||
setTimeout(() => { element('copy_manual').textContent = 'Copy'; }, 1500);
|
||||
} catch (error) {
|
||||
log(`The manual could not be copied: ${error}`);
|
||||
}
|
||||
});
|
||||
attachPopup(element('help'), element('help_popup'));
|
||||
element('zoom_fit').addEventListener('click', () => {
|
||||
renderer.fit();
|
||||
session.sendViewCommand('fit');
|
||||
@@ -1277,6 +1300,59 @@ function press(group, chosen) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The closers of every popup, so that opening one closes the others. @type {Set<() => void>} */
|
||||
const popupClosers = new Set();
|
||||
|
||||
/**
|
||||
* A popup of Kokkoja's own below a toolbar button (the colour palette,
|
||||
* the Page menu, the Help note; no system menu or dialog, 2026-10-14).
|
||||
* The button's press opens it below the button, left edges aligned, or
|
||||
* right edges when it would run past the window; a press anywhere else,
|
||||
* Escape, or a press on an item marked `.menu_item` closes it.
|
||||
* @param {HTMLElement} button
|
||||
* @param {HTMLElement} popup
|
||||
* @param {() => void} [onOpen] Fill the popup before it shows.
|
||||
*/
|
||||
export function attachPopup(button, popup, onOpen) {
|
||||
const close = () => {
|
||||
popup.hidden = true;
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
popupClosers.add(close);
|
||||
button.addEventListener('click', () => {
|
||||
if (!popup.hidden) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
// One popup at a time (a keyboard's Enter opens without the press
|
||||
// that would close the other).
|
||||
for (const other of popupClosers) other();
|
||||
onOpen?.();
|
||||
const box = button.getBoundingClientRect();
|
||||
// Measured at the window's left edge: placed near the right edge, an
|
||||
// absolute box shrinks to the room left and its text wraps.
|
||||
popup.style.left = '0px';
|
||||
popup.style.top = `${box.bottom + 6}px`;
|
||||
popup.hidden = false;
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
const width = popup.getBoundingClientRect().width;
|
||||
const fits = box.left + width <= window.innerWidth - 8;
|
||||
popup.style.left = `${fits ? box.left : Math.max(8, box.right - width)}px`;
|
||||
});
|
||||
popup.addEventListener('click', (event) => {
|
||||
const item = /** @type {Element} */ (event.target).closest('.menu_item');
|
||||
// A label's click reaches here twice, once as the input's; close on the second.
|
||||
if (item && !(event.target instanceof HTMLLabelElement)) close();
|
||||
});
|
||||
document.addEventListener('pointerdown', (event) => {
|
||||
const target = /** @type {Node} */ (event.target);
|
||||
if (!popup.hidden && !popup.contains(target) && !button.contains(target)) close();
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && !popup.hidden) close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.color Initial color.
|
||||
@@ -1374,25 +1450,7 @@ function buildToolbar(options) {
|
||||
palette.append(button);
|
||||
}
|
||||
};
|
||||
more.addEventListener('click', () => {
|
||||
if (!palette.hidden) {
|
||||
palette.hidden = true;
|
||||
return;
|
||||
}
|
||||
showPalette();
|
||||
const box = more.getBoundingClientRect();
|
||||
palette.style.left = `${box.left}px`;
|
||||
palette.style.top = `${box.bottom + 6}px`;
|
||||
palette.hidden = false;
|
||||
});
|
||||
document.addEventListener('pointerdown', (event) => {
|
||||
if (!palette.hidden && !palette.contains(/** @type {Node} */ (event.target)) && event.target !== more) {
|
||||
palette.hidden = true;
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') palette.hidden = true;
|
||||
});
|
||||
attachPopup(more, palette, showPalette);
|
||||
|
||||
const widths = element('widths');
|
||||
for (const width of WIDTHS) {
|
||||
@@ -1466,6 +1524,7 @@ function showParticipants(session, renderer) {
|
||||
const visible = document.createElement('input');
|
||||
visible.type = 'checkbox';
|
||||
visible.checked = !renderer.hidden.has(participant.id);
|
||||
visible.setAttribute('aria-label', 'Shown');
|
||||
visible.setAttribute('data-help', 'Show or hide this participant’s writing on your screen only.');
|
||||
visible.addEventListener('change', () => renderer.setLayerVisible(participant.id, visible.checked));
|
||||
label.append(visible, ` ${participant.name}`);
|
||||
|
||||
Reference in new issue
Block a user