@altnovatrail/canvas 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +59 -2
- package/lib/core/boxes.d.ts +54 -0
- package/lib/core/boxes.js +116 -0
- package/lib/core/clock.d.ts +25 -0
- package/lib/core/clock.js +39 -0
- package/lib/core/crop.d.ts +87 -0
- package/lib/core/crop.js +227 -0
- package/lib/core/eraser.d.ts +42 -0
- package/lib/core/eraser.js +127 -0
- package/lib/core/example.d.ts +66 -0
- package/lib/core/example.js +116 -0
- package/lib/core/fields.d.ts +29 -0
- package/lib/core/fields.js +78 -0
- package/lib/core/geometry.d.ts +38 -0
- package/lib/core/geometry.js +107 -0
- package/lib/core/grid.d.ts +30 -0
- package/lib/core/grid.js +47 -0
- package/lib/core/groups.d.ts +55 -0
- package/lib/core/groups.js +172 -0
- package/lib/core/hands.d.ts +120 -0
- package/lib/core/hands.js +273 -0
- package/lib/core/history.d.ts +42 -0
- package/lib/core/history.js +228 -0
- package/lib/core/index.d.ts +12 -0
- package/lib/core/index.js +15 -0
- package/lib/core/ink.d.ts +1 -0
- package/lib/core/ink.js +5 -0
- package/lib/core/manifest.d.ts +22 -0
- package/lib/core/manifest.js +50 -0
- package/lib/core/marks.d.ts +145 -0
- package/lib/core/marks.js +454 -0
- package/lib/core/outline.d.ts +22 -0
- package/lib/core/outline.js +50 -0
- package/lib/core/page.d.ts +62 -0
- package/lib/core/page.js +309 -0
- package/lib/core/png.d.ts +13 -0
- package/lib/core/png.js +231 -0
- package/lib/core/raster.d.ts +14 -0
- package/lib/core/raster.js +72 -0
- package/lib/core/recorder.d.ts +73 -0
- package/lib/core/recorder.js +127 -0
- package/lib/core/screen.d.ts +35 -0
- package/lib/core/screen.js +36 -0
- package/lib/core/sets.d.ts +10 -0
- package/lib/core/sets.js +31 -0
- package/lib/core/sheet.d.ts +9 -0
- package/lib/core/sheet.js +6 -0
- package/lib/core/submission.d.ts +41 -0
- package/lib/core/submission.js +63 -0
- package/lib/core/task.d.ts +214 -0
- package/lib/core/task.js +343 -0
- package/lib/core/widths.d.ts +57 -0
- package/lib/core/widths.js +218 -0
- package/lib/core/work.d.ts +77 -0
- package/lib/core/work.js +293 -0
- package/lib/element/attributes.d.ts +11 -0
- package/lib/element/attributes.js +22 -0
- package/lib/element/index.d.ts +219 -0
- package/lib/element/index.js +1898 -0
- package/lib/element/labels.d.ts +41 -0
- package/lib/element/labels.js +74 -0
- package/lib/element/parts.d.ts +63 -0
- package/lib/element/parts.js +191 -0
- package/lib/element/pictures.d.ts +45 -0
- package/lib/element/pictures.js +167 -0
- package/lib/element/probe.d.ts +33 -0
- package/lib/element/probe.js +18 -0
- package/lib/element/session.d.ts +3 -0
- package/lib/element/session.js +7 -0
- package/lib/element/statement.d.ts +35 -0
- package/lib/element/statement.js +116 -0
- package/lib/element/tools.d.ts +64 -0
- package/lib/element/tools.js +254 -0
- package/package.json +41 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PicturePart, StatementPart } from "../core/task.js";
|
|
2
|
+
import type { Pictures } from "./pictures.js";
|
|
3
|
+
/** The statement of a task above the sheet, as the element shows it. */
|
|
4
|
+
export declare class Statement {
|
|
5
|
+
#private;
|
|
6
|
+
/**
|
|
7
|
+
* The statement's element: the paragraphs and the pictures, in the module's explanation language,
|
|
8
|
+
* or in the element's own without a manifest (R32).
|
|
9
|
+
*/
|
|
10
|
+
readonly element: HTMLDivElement;
|
|
11
|
+
/** A statement whose pictures the element's pictures give their files. */
|
|
12
|
+
constructor(pictures: Pictures);
|
|
13
|
+
/**
|
|
14
|
+
* The language the statement is in, a BCP 47 tag, or `undefined` for the element's own, which
|
|
15
|
+
* the statement inherits. A new language changes the attribute alone: the paragraphs and the
|
|
16
|
+
* pictures stay as they are.
|
|
17
|
+
*/
|
|
18
|
+
set lang(lang: string | undefined);
|
|
19
|
+
/**
|
|
20
|
+
* The statement of a task, in place of the last, whose pictures are taken off. A picture whose
|
|
21
|
+
* file has come shows it at once: the element names the new statement's pictures first.
|
|
22
|
+
*/
|
|
23
|
+
show(statement: readonly StatementPart[]): void;
|
|
24
|
+
}
|
|
25
|
+
/** A picture beside the sheet in its frame, and what takes it off when the frame leaves. */
|
|
26
|
+
export interface Framed {
|
|
27
|
+
readonly frame: HTMLElement;
|
|
28
|
+
readonly off: () => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A picture beside the sheet in a frame of its size, placed with the element's pictures: a
|
|
32
|
+
* statement's, and a worked example's in its list of parts (R42). The frame shows the picture once
|
|
33
|
+
* its file has come and is decoded, and what it shows in words till then, or if it never does.
|
|
34
|
+
*/
|
|
35
|
+
export declare function framed(pictures: Pictures, part: PicturePart): Framed;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The statement of a task, above the sheet, in the element's own scrolling (R23): its paragraphs
|
|
2
|
+
// as text and its pictures from the consumer's media, which the element's pictures load (R42).
|
|
3
|
+
// Each picture keeps a frame of its own size, so that nothing moves under the pen when it comes,
|
|
4
|
+
// and the frame shows what the picture shows in words until it comes, or if it never does. This
|
|
5
|
+
// module is no entry point of the package.
|
|
6
|
+
/** The statement of a task above the sheet, as the element shows it. */
|
|
7
|
+
export class Statement {
|
|
8
|
+
/**
|
|
9
|
+
* The statement's element: the paragraphs and the pictures, in the module's explanation language,
|
|
10
|
+
* or in the element's own without a manifest (R32).
|
|
11
|
+
*/
|
|
12
|
+
element = document.createElement("div");
|
|
13
|
+
#pictures;
|
|
14
|
+
/** What takes the statement's pictures off, once another statement takes its place. */
|
|
15
|
+
#off = [];
|
|
16
|
+
/** A statement whose pictures the element's pictures give their files. */
|
|
17
|
+
constructor(pictures) {
|
|
18
|
+
this.#pictures = pictures;
|
|
19
|
+
this.element.className = "statement";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The language the statement is in, a BCP 47 tag, or `undefined` for the element's own, which
|
|
23
|
+
* the statement inherits. A new language changes the attribute alone: the paragraphs and the
|
|
24
|
+
* pictures stay as they are.
|
|
25
|
+
*/
|
|
26
|
+
set lang(lang) {
|
|
27
|
+
if (lang === undefined)
|
|
28
|
+
this.element.removeAttribute("lang");
|
|
29
|
+
else
|
|
30
|
+
this.element.lang = lang;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The statement of a task, in place of the last, whose pictures are taken off. A picture whose
|
|
34
|
+
* file has come shows it at once: the element names the new statement's pictures first.
|
|
35
|
+
*/
|
|
36
|
+
show(statement) {
|
|
37
|
+
for (const off of this.#off)
|
|
38
|
+
off();
|
|
39
|
+
const off = [];
|
|
40
|
+
const parts = statement.map((part) => {
|
|
41
|
+
if ("text" in part) {
|
|
42
|
+
const paragraph = document.createElement("p");
|
|
43
|
+
// Text, never markup: a module is data (R02 spec).
|
|
44
|
+
paragraph.textContent = part.text;
|
|
45
|
+
return paragraph;
|
|
46
|
+
}
|
|
47
|
+
const picture = framed(this.#pictures, part);
|
|
48
|
+
off.push(picture.off);
|
|
49
|
+
return picture.frame;
|
|
50
|
+
});
|
|
51
|
+
this.element.replaceChildren(...parts);
|
|
52
|
+
this.#off = off;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A picture beside the sheet in a frame of its size, placed with the element's pictures: a
|
|
57
|
+
* statement's, and a worked example's in its list of parts (R42). The frame shows the picture once
|
|
58
|
+
* its file has come and is decoded, and what it shows in words till then, or if it never does.
|
|
59
|
+
*/
|
|
60
|
+
export function framed(pictures, part) {
|
|
61
|
+
const frame = document.createElement("div");
|
|
62
|
+
frame.className = "picture";
|
|
63
|
+
frame.style.width = `${part.width}px`;
|
|
64
|
+
frame.style.aspectRatio = `${part.width} / ${part.height}`;
|
|
65
|
+
fillFrame(frame, part);
|
|
66
|
+
/** The address the frame is to show, until it goes. */
|
|
67
|
+
let shown;
|
|
68
|
+
const off = pictures.place(part.picture, {
|
|
69
|
+
show(url) {
|
|
70
|
+
shown = url;
|
|
71
|
+
const image = document.createElement("img");
|
|
72
|
+
image.alt = part.alt;
|
|
73
|
+
image.src = url;
|
|
74
|
+
const swap = () => {
|
|
75
|
+
frame.removeAttribute("role");
|
|
76
|
+
frame.removeAttribute("aria-label");
|
|
77
|
+
frame.classList.add("shown");
|
|
78
|
+
frame.replaceChildren(image);
|
|
79
|
+
};
|
|
80
|
+
// A browser that keeps the decoded address at hand has the picture whole at once, as
|
|
81
|
+
// Chromium does; another decodes it again, and the frame shows its words till then (T64).
|
|
82
|
+
if (image.complete && image.naturalWidth > 0) {
|
|
83
|
+
swap();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
image.decode().then(() => {
|
|
87
|
+
if (shown === url)
|
|
88
|
+
swap();
|
|
89
|
+
},
|
|
90
|
+
// The address went while it was decoded: the frame keeps its words.
|
|
91
|
+
() => undefined);
|
|
92
|
+
},
|
|
93
|
+
hide() {
|
|
94
|
+
shown = undefined;
|
|
95
|
+
fillFrame(frame, part);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return { frame, off };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A frame without its picture shows what the picture shows in words, and is read as the picture.
|
|
102
|
+
* One that only decorates shows nothing and is not read.
|
|
103
|
+
*/
|
|
104
|
+
function fillFrame(frame, part) {
|
|
105
|
+
frame.classList.remove("shown");
|
|
106
|
+
if (part.alt === "") {
|
|
107
|
+
frame.classList.add("decorative");
|
|
108
|
+
frame.removeAttribute("role");
|
|
109
|
+
frame.removeAttribute("aria-label");
|
|
110
|
+
frame.replaceChildren();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
frame.setAttribute("role", "img");
|
|
114
|
+
frame.setAttribute("aria-label", part.alt);
|
|
115
|
+
frame.textContent = part.alt;
|
|
116
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Tool } from "../core/task.js";
|
|
2
|
+
/**
|
|
3
|
+
* What the row asks of the element: a tool the learner chose — perhaps the one in use — an undo, a
|
|
4
|
+
* clearing, a scroll on down the sheet, for `onward`, or back up it, the finger to write again,
|
|
5
|
+
* out of pen mode, and on a worked example the next part of its demo, or the whole demo hidden or
|
|
6
|
+
* shown (R42).
|
|
7
|
+
*/
|
|
8
|
+
export interface ToolActions {
|
|
9
|
+
readonly choose: (tool: Tool) => void;
|
|
10
|
+
readonly undo: () => void;
|
|
11
|
+
readonly clear: () => void;
|
|
12
|
+
readonly upOrDown: (onward: boolean) => void;
|
|
13
|
+
readonly fingers: () => void;
|
|
14
|
+
readonly next: () => void;
|
|
15
|
+
readonly toggle: () => void;
|
|
16
|
+
}
|
|
17
|
+
/** What Next and Demo control, by their IDs in the element's shadow root (R42). */
|
|
18
|
+
export interface DemoControls {
|
|
19
|
+
/** The region that tells of the part Next opens. */
|
|
20
|
+
readonly status: string;
|
|
21
|
+
/** The list of the open parts, which Demo hides and shows. */
|
|
22
|
+
readonly parts: string;
|
|
23
|
+
}
|
|
24
|
+
/** What the row is of, and is named by: a sheet, a workbook or a worked example (R36, R42). */
|
|
25
|
+
export type RowKind = "sheet" | "workbook" | "example";
|
|
26
|
+
/**
|
|
27
|
+
* What the row shows: what it is of, the tools the task allows, the one in use, whether undo and
|
|
28
|
+
* clearing have anything to do, whether the finger may be asked to write again, and on a worked
|
|
29
|
+
* example whether Next has a part to open and whether the demo is shown (R42).
|
|
30
|
+
*/
|
|
31
|
+
export interface ToolState {
|
|
32
|
+
readonly kind: RowKind;
|
|
33
|
+
readonly tools: readonly Tool[];
|
|
34
|
+
readonly tool: Tool;
|
|
35
|
+
readonly undoable: boolean;
|
|
36
|
+
readonly clearable: boolean;
|
|
37
|
+
readonly finger: boolean;
|
|
38
|
+
readonly opens: boolean;
|
|
39
|
+
readonly expanded: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** The row of the element's own buttons, as the element shows it. */
|
|
42
|
+
export declare class Tools {
|
|
43
|
+
#private;
|
|
44
|
+
/** The row: a toolbar, named in the element's language. */
|
|
45
|
+
readonly element: HTMLDivElement;
|
|
46
|
+
constructor(actions: ToolActions, controls: DemoControls);
|
|
47
|
+
/**
|
|
48
|
+
* The row as the sheet now is. Only what has changed is written. A worked example's row has no
|
|
49
|
+
* tools of writing, no undo and no clearing, for the learner does not write on its sheet, and has
|
|
50
|
+
* Next, unavailable while the demo is hidden or open whole, and Demo, expanded while the demo is
|
|
51
|
+
* shown (R42).
|
|
52
|
+
*/
|
|
53
|
+
show({ kind, tools, tool, undoable, clearable, finger, opens, expanded }: ToolState): void;
|
|
54
|
+
/**
|
|
55
|
+
* Whether anything scrolls back up the sheet, for Up, and on down it, for Down: a button with
|
|
56
|
+
* nowhere to go stands unavailable. Only what has changed is written.
|
|
57
|
+
*/
|
|
58
|
+
edges(above: boolean, below: boolean): void;
|
|
59
|
+
/**
|
|
60
|
+
* The labels in the language of a tag — the element's `lang` — and the row's own `lang` theirs,
|
|
61
|
+
* so that a screen reader reads them in the language they are in.
|
|
62
|
+
*/
|
|
63
|
+
label(tag: string | null): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// The element's own row of buttons (R28): the tools the task allows — the pen and the eraser — and
|
|
2
|
+
// undo and clearing. It is a toolbar as WAI-ARIA's Authoring Practices have one: one stop of Tab,
|
|
3
|
+
// the arrow keys from button to button, and the tools a group of radio buttons that the arrows go
|
|
4
|
+
// through without choosing one. Undo and Clear with nothing to take back or to clear stay where the
|
|
5
|
+
// focus can reach them, marked unavailable: a last undo from the keyboard leaves the focus where it
|
|
6
|
+
// was. Up and Down scroll the sheet with a single pointer, and stand unavailable at the end they
|
|
7
|
+
// scroll to (R33). In pen mode a last button lets a finger write again (R29). The labels are in
|
|
8
|
+
// the element's language (labels.ts), and the row is named by the canvas kind: the sheet or the
|
|
9
|
+
// workbook (R36). A lesson's worked example names it too, and has no tools of writing in it: the
|
|
10
|
+
// learner does not write on its sheet. Its first buttons are its demo's, Next, which opens the next
|
|
11
|
+
// part, and Demo, a disclosure button that hides the whole demo and shows it again (R42). A button
|
|
12
|
+
// that goes gives its stop of Tab and the focus to the first button shown. This module is no entry
|
|
13
|
+
// point of the package.
|
|
14
|
+
import { SVG, write } from "./attributes.js";
|
|
15
|
+
import { labelsFor } from "./labels.js";
|
|
16
|
+
/** The row of the element's own buttons, as the element shows it. */
|
|
17
|
+
export class Tools {
|
|
18
|
+
/** The row: a toolbar, named in the element's language. */
|
|
19
|
+
element = document.createElement("div");
|
|
20
|
+
/** Next and Demo, a worked example's first buttons, kept together on a narrow screen (R42). */
|
|
21
|
+
#demoPair = document.createElement("div");
|
|
22
|
+
#next = plainButton();
|
|
23
|
+
#demo = plainButton();
|
|
24
|
+
#demoLabel = document.createElement("span");
|
|
25
|
+
#group = document.createElement("div");
|
|
26
|
+
#pen = radioButton();
|
|
27
|
+
#eraser = radioButton();
|
|
28
|
+
#undo = plainButton();
|
|
29
|
+
#clear = plainButton();
|
|
30
|
+
/** Up and Down, a pair that a row wrapping on a narrow screen keeps together. */
|
|
31
|
+
#pair = document.createElement("div");
|
|
32
|
+
#up = plainButton();
|
|
33
|
+
#down = plainButton();
|
|
34
|
+
#finger = plainButton();
|
|
35
|
+
/** The buttons in the order the arrow keys go through them: the row's own. */
|
|
36
|
+
#buttons = [
|
|
37
|
+
this.#next,
|
|
38
|
+
this.#demo,
|
|
39
|
+
this.#pen,
|
|
40
|
+
this.#eraser,
|
|
41
|
+
this.#undo,
|
|
42
|
+
this.#clear,
|
|
43
|
+
this.#up,
|
|
44
|
+
this.#down,
|
|
45
|
+
this.#finger,
|
|
46
|
+
];
|
|
47
|
+
/** The language of the labels shown. */
|
|
48
|
+
#lang;
|
|
49
|
+
/** What the row is of, named so. */
|
|
50
|
+
#kind = "sheet";
|
|
51
|
+
/** Whether a button of the row has had the focus: Tab comes to the first button until one has. */
|
|
52
|
+
#chosen = false;
|
|
53
|
+
constructor(actions, controls) {
|
|
54
|
+
this.element.className = "tools";
|
|
55
|
+
this.element.setAttribute("role", "toolbar");
|
|
56
|
+
this.#demoPair.className = "demo";
|
|
57
|
+
// Next and Demo control what they do not hold: the region that tells of a part, and the list.
|
|
58
|
+
this.#next.setAttribute("aria-controls", controls.status);
|
|
59
|
+
this.#demo.setAttribute("aria-controls", controls.parts);
|
|
60
|
+
this.#demo.setAttribute("aria-expanded", "true");
|
|
61
|
+
this.#demo.append(triangle(), this.#demoLabel);
|
|
62
|
+
this.#demoPair.append(this.#next, this.#demo);
|
|
63
|
+
this.#group.className = "choice";
|
|
64
|
+
this.#group.setAttribute("role", "radiogroup");
|
|
65
|
+
this.#group.append(this.#pen, this.#eraser);
|
|
66
|
+
this.#pair.className = "updown";
|
|
67
|
+
this.#pair.append(this.#up, this.#down);
|
|
68
|
+
// The finger's button comes last: coming and going, it moves no other button of the row.
|
|
69
|
+
this.element.append(this.#demoPair, this.#group, this.#undo, this.#clear, this.#pair, this.#finger);
|
|
70
|
+
// A worked example's buttons and the finger's come with what they are for.
|
|
71
|
+
for (const hidden of [this.#demoPair, this.#next, this.#demo, this.#finger]) {
|
|
72
|
+
hidden.hidden = true;
|
|
73
|
+
}
|
|
74
|
+
// Tab comes to the first button shown, until another has had the focus.
|
|
75
|
+
this.#stopAt(this.#first());
|
|
76
|
+
// The element decides whether a choice is a change: the tool in use, chosen again, is none.
|
|
77
|
+
this.#pen.addEventListener("click", () => actions.choose("pen"));
|
|
78
|
+
this.#eraser.addEventListener("click", () => actions.choose("eraser"));
|
|
79
|
+
const doing = (button, act) => {
|
|
80
|
+
// aria-disabled stops no click: the button does nothing while it stands unavailable.
|
|
81
|
+
button.addEventListener("click", () => {
|
|
82
|
+
if (button.getAttribute("aria-disabled") !== "true")
|
|
83
|
+
act();
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
doing(this.#undo, actions.undo);
|
|
87
|
+
doing(this.#clear, actions.clear);
|
|
88
|
+
doing(this.#next, actions.next);
|
|
89
|
+
this.#demo.addEventListener("click", () => actions.toggle());
|
|
90
|
+
// Up and Down ask the element anew at every press, unavailable or not: a scroll the element did
|
|
91
|
+
// not hear may have left either standing as it no longer is.
|
|
92
|
+
this.#up.addEventListener("click", () => actions.upOrDown(false));
|
|
93
|
+
this.#down.addEventListener("click", () => actions.upOrDown(true));
|
|
94
|
+
this.#finger.addEventListener("click", () => actions.fingers());
|
|
95
|
+
this.element.addEventListener("keydown", (event) => this.#key(event));
|
|
96
|
+
// The button that had the focus last is the one Tab comes back to.
|
|
97
|
+
this.element.addEventListener("focusin", (event) => {
|
|
98
|
+
const focused = this.#buttons.find((button) => button === event.target);
|
|
99
|
+
if (focused === undefined)
|
|
100
|
+
return;
|
|
101
|
+
this.#chosen = true;
|
|
102
|
+
this.#stopAt(focused);
|
|
103
|
+
});
|
|
104
|
+
this.label(null);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The row as the sheet now is. Only what has changed is written. A worked example's row has no
|
|
108
|
+
* tools of writing, no undo and no clearing, for the learner does not write on its sheet, and has
|
|
109
|
+
* Next, unavailable while the demo is hidden or open whole, and Demo, expanded while the demo is
|
|
110
|
+
* shown (R42).
|
|
111
|
+
*/
|
|
112
|
+
show({ kind, tools, tool, undoable, clearable, finger, opens, expanded }) {
|
|
113
|
+
if (kind !== this.#kind) {
|
|
114
|
+
this.#kind = kind;
|
|
115
|
+
this.#name();
|
|
116
|
+
}
|
|
117
|
+
const writes = kind !== "example";
|
|
118
|
+
this.#reveal([
|
|
119
|
+
[this.#demoPair, !writes],
|
|
120
|
+
[this.#next, !writes],
|
|
121
|
+
[this.#demo, !writes],
|
|
122
|
+
[this.#group, writes],
|
|
123
|
+
[this.#pen, writes],
|
|
124
|
+
[this.#eraser, writes && tools.includes("eraser")],
|
|
125
|
+
[this.#undo, writes],
|
|
126
|
+
[this.#clear, writes],
|
|
127
|
+
[this.#finger, writes && finger],
|
|
128
|
+
]);
|
|
129
|
+
write(this.#pen, "aria-checked", String(tool === "pen"));
|
|
130
|
+
write(this.#eraser, "aria-checked", String(tool === "eraser"));
|
|
131
|
+
write(this.#undo, "aria-disabled", undoable ? null : "true");
|
|
132
|
+
write(this.#clear, "aria-disabled", clearable ? null : "true");
|
|
133
|
+
write(this.#next, "aria-disabled", opens ? null : "true");
|
|
134
|
+
write(this.#demo, "aria-expanded", String(expanded));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether anything scrolls back up the sheet, for Up, and on down it, for Down: a button with
|
|
138
|
+
* nowhere to go stands unavailable. Only what has changed is written.
|
|
139
|
+
*/
|
|
140
|
+
edges(above, below) {
|
|
141
|
+
write(this.#up, "aria-disabled", above ? null : "true");
|
|
142
|
+
write(this.#down, "aria-disabled", below ? null : "true");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The labels in the language of a tag — the element's `lang` — and the row's own `lang` theirs,
|
|
146
|
+
* so that a screen reader reads them in the language they are in.
|
|
147
|
+
*/
|
|
148
|
+
label(tag) {
|
|
149
|
+
const { lang, labels } = labelsFor(tag);
|
|
150
|
+
if (lang === this.#lang)
|
|
151
|
+
return;
|
|
152
|
+
this.#lang = lang;
|
|
153
|
+
this.element.lang = lang;
|
|
154
|
+
this.#name();
|
|
155
|
+
this.#group.setAttribute("aria-label", labels.tool);
|
|
156
|
+
this.#next.textContent = labels.next;
|
|
157
|
+
this.#demoLabel.textContent = labels.demo;
|
|
158
|
+
this.#pen.textContent = labels.pen;
|
|
159
|
+
this.#eraser.textContent = labels.eraser;
|
|
160
|
+
this.#undo.textContent = labels.undo;
|
|
161
|
+
this.#clear.textContent = labels.clear;
|
|
162
|
+
this.#up.textContent = labels.up;
|
|
163
|
+
this.#down.textContent = labels.down;
|
|
164
|
+
this.#finger.textContent = labels.finger;
|
|
165
|
+
}
|
|
166
|
+
/** The row's name: the canvas kind's, in the labels' language. */
|
|
167
|
+
#name() {
|
|
168
|
+
const { labels } = labelsFor(this.#lang ?? null);
|
|
169
|
+
write(this.element, "aria-label", labels[this.#kind]);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The arrow keys move the focus to the next button or the one before, round the row, and Home
|
|
173
|
+
* and End to the first and the last: nothing is chosen, and no other key is the row's.
|
|
174
|
+
*/
|
|
175
|
+
#key(event) {
|
|
176
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
177
|
+
return;
|
|
178
|
+
const shown = this.#shown();
|
|
179
|
+
const at = shown.indexOf(event.target);
|
|
180
|
+
if (at < 0)
|
|
181
|
+
return;
|
|
182
|
+
let next;
|
|
183
|
+
if (event.key === "ArrowRight")
|
|
184
|
+
next = shown[(at + 1) % shown.length];
|
|
185
|
+
else if (event.key === "ArrowLeft")
|
|
186
|
+
next = shown[(at - 1 + shown.length) % shown.length];
|
|
187
|
+
else if (event.key === "Home")
|
|
188
|
+
next = shown[0];
|
|
189
|
+
else if (event.key === "End")
|
|
190
|
+
next = shown.at(-1);
|
|
191
|
+
else
|
|
192
|
+
return;
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
next?.focus();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Buttons come or go, all of them at once, and then a button that went gives its stop of Tab and
|
|
198
|
+
* the focus to the first button shown: the pen on a sheet, Next on a worked example (R28, R42). Till
|
|
199
|
+
* a button has had the focus, the stop is the first button shown, whichever it is now. Nothing is
|
|
200
|
+
* read or written while every button stays as it is: the row is shown anew after every stroke.
|
|
201
|
+
*/
|
|
202
|
+
#reveal(wanted) {
|
|
203
|
+
if (wanted.every(([element, shown]) => element.hidden !== shown))
|
|
204
|
+
return;
|
|
205
|
+
// Read before a button hides: a hidden one loses the focus at the next update of the style.
|
|
206
|
+
const focused = this.#buttons.find((button) => button.matches(":focus"));
|
|
207
|
+
for (const [element, shown] of wanted)
|
|
208
|
+
element.hidden = !shown;
|
|
209
|
+
const first = this.#first();
|
|
210
|
+
if (!this.#chosen || this.#buttons.some((button) => button.tabIndex === 0 && button.hidden)) {
|
|
211
|
+
this.#stopAt(first);
|
|
212
|
+
}
|
|
213
|
+
if (focused?.hidden)
|
|
214
|
+
first.focus();
|
|
215
|
+
}
|
|
216
|
+
/** The buttons the row shows, in its order. */
|
|
217
|
+
#shown() {
|
|
218
|
+
return this.#buttons.filter((button) => !button.hidden);
|
|
219
|
+
}
|
|
220
|
+
/** The first button the row shows: Up and Down are on every row (R33). */
|
|
221
|
+
#first() {
|
|
222
|
+
return this.#shown()[0] ?? this.#up;
|
|
223
|
+
}
|
|
224
|
+
/** The one button Tab comes to: the arrow keys reach the others. */
|
|
225
|
+
#stopAt(stop) {
|
|
226
|
+
for (const button of this.#buttons)
|
|
227
|
+
button.tabIndex = button === stop ? 0 : -1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** A button of a tool: a radio button of the group, a button still, which the arrows leave alone. */
|
|
231
|
+
function radioButton() {
|
|
232
|
+
const button = plainButton();
|
|
233
|
+
button.setAttribute("role", "radio");
|
|
234
|
+
button.setAttribute("aria-checked", "false");
|
|
235
|
+
return button;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The triangle of a disclosure button, pointing down while what it shows is shown: a picture that
|
|
239
|
+
* says nothing, for aria-expanded says it (R42).
|
|
240
|
+
*/
|
|
241
|
+
function triangle() {
|
|
242
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
243
|
+
svg.setAttribute("viewBox", "0 0 10 10");
|
|
244
|
+
svg.setAttribute("aria-hidden", "true");
|
|
245
|
+
const path = document.createElementNS(SVG, "path");
|
|
246
|
+
path.setAttribute("d", "M1 3h8L5 8z");
|
|
247
|
+
svg.append(path);
|
|
248
|
+
return svg;
|
|
249
|
+
}
|
|
250
|
+
function plainButton() {
|
|
251
|
+
const button = document.createElement("button");
|
|
252
|
+
button.type = "button";
|
|
253
|
+
return button;
|
|
254
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altnovatrail/canvas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NovaTrail's canvases: the workplace of a task, where the learner writes, draws and types.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/altnovatrail/canvas.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"lib"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./lib/element/index.d.ts",
|
|
17
|
+
"default": "./lib/element/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./core": {
|
|
20
|
+
"types": "./lib/core/index.d.ts",
|
|
21
|
+
"default": "./lib/core/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"./lib/element/index.js"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@altnovatrail/spec": "0.6.2",
|
|
29
|
+
"perfect-freehand": "1.2.3",
|
|
30
|
+
"rbush": "4.0.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "2.6.4",
|
|
34
|
+
"@types/node": "24.19.2",
|
|
35
|
+
"@types/rbush": "4.0.0",
|
|
36
|
+
"@vitest/browser-playwright": "5.0.5",
|
|
37
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
38
|
+
"playwright": "1.64.1",
|
|
39
|
+
"typescript": "7.0.2",
|
|
40
|
+
"vite": "8.3.5",
|
|
41
|
+
"vitest": "5.0.5"
|
|
42
|
+
}
|
|
43
|
+
}
|