Kokkoja as of 2026-10-10: the teacher's requests, the Session window, notices, the QR code, the license
This commit is contained in:
1 parent
95c80d04c9
commit
1d087af633
32 files changed
+4917
-890
No files matched your search
@@ -0,0 +1,79 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* No selection of text that the stylesheet declares unselectable.
|
||||
*
|
||||
* Safari on iOS and iPadOS sometimes selected the save status at the
|
||||
* toolbar's right end when a stroke began with an Apple Pencil on Andy's
|
||||
* iPad (2026-10-10), no hand on the glass: WebKit may take a Pencil drag
|
||||
* for a selection gesture, and the selection snaps to the nearest text.
|
||||
* The prefixed -webkit-user-select: none made it rarer, not gone, so
|
||||
* WebKit's gesture path does not fully respect the property. Two
|
||||
* guards, both inert where the browser already does the right thing:
|
||||
* a selection that is about to start in unselectable text is refused
|
||||
* (selectstart), and one that WebKit makes there anyway is cleared the
|
||||
* moment it appears (selectionchange). Each is logged, in the console
|
||||
* once per place and in the record every time, as evidence of which
|
||||
* path the browser took. Selections inside fields
|
||||
* (the invitation, a text block being typed) are left alone: a field's
|
||||
* own selection reaches document.getSelection() in some browsers as a
|
||||
* range on the field's parent, which may be unselectable.
|
||||
*/
|
||||
import { log, trace } from './log.js';
|
||||
|
||||
/**
|
||||
* @param {Node | null | undefined} node
|
||||
* @returns {boolean} Whether the stylesheet forbids selecting it.
|
||||
*/
|
||||
function unselectable(node) {
|
||||
const element = node instanceof Element ? node : node?.parentElement;
|
||||
if (!element) return false;
|
||||
const style = getComputedStyle(element);
|
||||
const value = style.getPropertyValue('-webkit-user-select') ||
|
||||
style.getPropertyValue('user-select');
|
||||
return value === 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node | null | undefined} node
|
||||
* @returns {string} The element, for the log.
|
||||
*/
|
||||
function describe(node) {
|
||||
const element = node instanceof Element ? node : node?.parentElement;
|
||||
if (!element) return 'nothing';
|
||||
const name = element.tagName.toLowerCase();
|
||||
if (element.id) return `${name}#${element.id}`;
|
||||
return element.className ? `${name}.${element.className}` : name;
|
||||
}
|
||||
|
||||
/** Whether the selection belongs to a focused text field. */
|
||||
function inField() {
|
||||
const active = document.activeElement;
|
||||
return active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ||
|
||||
(active instanceof HTMLElement && active.isContentEditable);
|
||||
}
|
||||
|
||||
export function guardSelection() {
|
||||
/** @type {Set<string>} */
|
||||
const logged = new Set();
|
||||
/** @param {string} what */
|
||||
function once(what) {
|
||||
if (logged.has(what)) return trace(what);
|
||||
logged.add(what);
|
||||
log(what);
|
||||
}
|
||||
|
||||
document.addEventListener('selectstart', (event) => {
|
||||
if (inField() || !(event.target instanceof Node) || !unselectable(event.target)) return;
|
||||
event.preventDefault();
|
||||
once(`Refused a selection starting in unselectable ${describe(event.target)}`);
|
||||
});
|
||||
|
||||
document.addEventListener('selectionchange', () => {
|
||||
const selection = document.getSelection();
|
||||
if (!selection || selection.isCollapsed || inField()) return;
|
||||
if (!unselectable(selection.anchorNode) && !unselectable(selection.focusNode)) return;
|
||||
const where = describe(selection.anchorNode);
|
||||
selection.removeAllRanges();
|
||||
once(`Cleared a selection the browser made in unselectable ${where}`);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user