@pie-players/pie-players-shared 0.3.73 → 0.3.74

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.
Files changed (123) hide show
  1. package/README.md +27 -1
  2. package/dist/components/vendor/nds/README.md +7 -5
  3. package/dist/config/profile.d.ts +0 -2
  4. package/dist/config/profile.js +2 -8
  5. package/dist/i18n/index.d.ts +2 -3
  6. package/dist/i18n/index.js +2 -3
  7. package/dist/i18n/messages/en-US.d.ts +10 -0
  8. package/dist/i18n/messages/en-US.js +12 -2
  9. package/dist/i18n/messages/nl-NL.d.ts +6 -0
  10. package/dist/i18n/messages/nl-NL.js +8 -2
  11. package/dist/i18n/provider.d.ts +4 -5
  12. package/dist/i18n/provider.js +4 -5
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +3 -1
  15. package/dist/instrumentation/index.d.ts +5 -4
  16. package/dist/instrumentation/index.js +5 -4
  17. package/dist/instrumentation/new-relic-agent.d.ts +20 -0
  18. package/dist/instrumentation/new-relic-agent.js +24 -0
  19. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +5 -4
  20. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +7 -7
  21. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +21 -32
  22. package/dist/instrumentation/types.d.ts +7 -3
  23. package/dist/loader-config.d.ts +16 -6
  24. package/dist/loaders/controller-loading.d.ts +14 -0
  25. package/dist/loaders/controller-loading.js +12 -0
  26. package/dist/loaders/defaults.d.ts +10 -0
  27. package/dist/loaders/defaults.js +10 -0
  28. package/dist/loaders/element-loader.d.ts +20 -8
  29. package/dist/loaders/element-loader.js +102 -36
  30. package/dist/loaders/element-package-policy.d.ts +2 -0
  31. package/dist/loaders/element-package-policy.js +2 -1
  32. package/dist/loaders/esm-adapter.d.ts +18 -0
  33. package/dist/loaders/esm-adapter.js +617 -102
  34. package/dist/loaders/iife-adapter.d.ts +0 -5
  35. package/dist/loaders/iife-adapter.js +3 -9
  36. package/dist/loaders/index.d.ts +10 -3
  37. package/dist/loaders/index.js +8 -3
  38. package/dist/loaders/mathjax-version-conflict.d.ts +17 -0
  39. package/dist/loaders/mathjax-version-conflict.js +51 -0
  40. package/dist/loaders/module-shim.d.ts +12 -0
  41. package/dist/loaders/module-shim.js +12 -0
  42. package/dist/loaders/preloaded-alignment.d.ts +24 -0
  43. package/dist/loaders/preloaded-alignment.js +57 -0
  44. package/dist/loaders/preloaded-authored-tags.d.ts +26 -0
  45. package/dist/loaders/preloaded-authored-tags.js +69 -0
  46. package/dist/loaders/preloaded-registration.d.ts +56 -0
  47. package/dist/loaders/preloaded-registration.js +129 -0
  48. package/dist/media/index.d.ts +69 -0
  49. package/dist/media/index.js +194 -0
  50. package/dist/pie/authoring.d.ts +1 -1
  51. package/dist/pie/authoring.js +25 -18
  52. package/dist/pie/element-announcements.d.ts +39 -0
  53. package/dist/pie/element-announcements.js +67 -0
  54. package/dist/pie/element-observer.d.ts +3 -1
  55. package/dist/pie/element-observer.js +1 -0
  56. package/dist/pie/index.d.ts +4 -4
  57. package/dist/pie/index.js +5 -3
  58. package/dist/pie/initialization.js +13 -22
  59. package/dist/pie/initialize-element.d.ts +3 -0
  60. package/dist/pie/initialize-element.js +3 -4
  61. package/dist/pie/instrumentation-event-map.js +2 -9
  62. package/dist/pie/instrumentation-provider-resolution.d.ts +2 -1
  63. package/dist/pie/instrumentation-provider-resolution.js +0 -1
  64. package/dist/pie/item-session-contract.d.ts +8 -0
  65. package/dist/pie/item-session-contract.js +30 -9
  66. package/dist/pie/markup-math.d.ts +20 -0
  67. package/dist/pie/markup-math.js +118 -0
  68. package/dist/pie/math-rendering.d.ts +10 -21
  69. package/dist/pie/math-rendering.js +50 -35
  70. package/dist/pie/registry.d.ts +13 -1
  71. package/dist/pie/registry.js +39 -1
  72. package/dist/pie/resource-monitor.d.ts +6 -33
  73. package/dist/pie/resource-monitor.js +14 -213
  74. package/dist/pie/scoring.d.ts +11 -2
  75. package/dist/pie/scoring.js +18 -16
  76. package/dist/pie/session-commit.d.ts +16 -0
  77. package/dist/pie/session-commit.js +33 -5
  78. package/dist/pie/session-snapshot.d.ts +80 -0
  79. package/dist/pie/session-snapshot.js +166 -0
  80. package/dist/pie/tag-names.d.ts +1 -0
  81. package/dist/pie/tag-names.js +1 -0
  82. package/dist/pie/types.d.ts +4 -5
  83. package/dist/pie/types.js +1 -0
  84. package/dist/pie/updates.d.ts +2 -2
  85. package/dist/pie/updates.js +33 -15
  86. package/dist/pie/versioned-tag.d.ts +4 -0
  87. package/dist/pie/versioned-tag.js +4 -0
  88. package/dist/security/index.d.ts +2 -3
  89. package/dist/security/index.js +2 -3
  90. package/dist/security/sanitize-item-markup.d.ts +0 -2
  91. package/dist/security/sanitize-item-markup.js +0 -4
  92. package/dist/security/sanitize-svg-icon.d.ts +0 -2
  93. package/dist/security/sanitize-svg-icon.js +0 -5
  94. package/dist/security/wrap-overwide.d.ts +5 -0
  95. package/dist/security/wrap-overwide.js +16 -0
  96. package/dist/timed-media/policy.js +5 -16
  97. package/dist/types/index.d.ts +30 -311
  98. package/dist/types/index.js +0 -68
  99. package/dist/ui/attribute-coercion.d.ts +12 -0
  100. package/dist/ui/attribute-coercion.js +33 -0
  101. package/dist/ui/content-styles.d.ts +18 -16
  102. package/dist/ui/content-styles.js +67 -36
  103. package/dist/ui/overlay-containment.d.ts +31 -0
  104. package/dist/ui/overlay-containment.js +63 -0
  105. package/dist/ui/overlay-placement.d.ts +110 -0
  106. package/dist/ui/overlay-placement.js +222 -0
  107. package/dist/ui/pointer-gesture.d.ts +32 -0
  108. package/dist/ui/pointer-gesture.js +56 -0
  109. package/dist/ui/pointer-rotate.d.ts +29 -0
  110. package/dist/ui/pointer-rotate.js +39 -0
  111. package/package.json +21 -17
  112. package/dist/pie/component-context.d.ts +0 -21
  113. package/dist/pie/component-context.js +0 -29
  114. package/dist/pie/player-initializer.d.ts +0 -54
  115. package/dist/pie/player-initializer.js +0 -122
  116. package/dist/security/wrap-model-rich-content.d.ts +0 -1
  117. package/dist/security/wrap-model-rich-content.js +0 -40
  118. package/dist/server/npm-auth-env.d.ts +0 -13
  119. package/dist/server/npm-auth-env.js +0 -64
  120. package/dist/server/npm-registry.d.ts +0 -7
  121. package/dist/server/npm-registry.js +0 -59
  122. package/dist/ui/debug-panel-persistence.d.ts +0 -17
  123. package/dist/ui/debug-panel-persistence.js +0 -37
@@ -23,9 +23,8 @@
23
23
  const MARKER_ATTRIBUTE = "data-pie-content-styles";
24
24
  /**
25
25
  * Declared by `components.css` itself, so it is observable no matter how the
26
- * stylesheet arrived — our injection or a host import. Diagnostics only: it
27
- * tells a host that opted out and then shipped nothing, and it spots a host copy
28
- * sitting alongside ours.
26
+ * stylesheet arrived — our injection or a host import. A host copy carrying it
27
+ * makes the player stand down, and an opted-out host with no copy is warned.
29
28
  */
30
29
  const SENTINEL_PROPERTY = "--pie-content-styles";
31
30
  // Svelte's dev-mode custom-element reset expands `all: unset` into individual
@@ -100,7 +99,16 @@ export function contentStylesPresent() {
100
99
  return countContentStyleSheets({ excludeInstalled: false }) > 0;
101
100
  }
102
101
  /**
103
- * Installs `cssText` as a document-level stylesheet, once per document.
102
+ * Installs `cssText` as a document-level stylesheet, once per document, unless
103
+ * the host already supplies one.
104
+ *
105
+ * A host copy is recognised by the sentinel wherever it sits, including one
106
+ * confined to the host's player subtree with `@scope`. Such a host scoped its
107
+ * copy because the stylesheet's bare element selectors would otherwise reach
108
+ * its own UI, so a global copy from the player would reintroduce exactly that.
109
+ * A host copy can also land after this runs — a lazily injected `<style>`, a
110
+ * `<link>` still loading — so the installed copy keeps watching and removes
111
+ * itself when one appears, as it does when the host sets the opt-out late.
104
112
  *
105
113
  * The stylesheet is **prepended** to `<head>` and deliberately left out of a
106
114
  * cascade layer. Prepending reproduces the placement hosts were told to use by
@@ -126,14 +134,58 @@ export function installContentStyles(cssText, source) {
126
134
  // the same stylesheet. Duplicating it would be harmless but pointless.
127
135
  return "already-installed";
128
136
  }
137
+ if (countHostContentStyleSheets() > 0)
138
+ return "host-supplied";
129
139
  if (!cssText)
130
140
  return "no-document";
131
141
  const style = document.createElement("style");
132
142
  style.setAttribute(MARKER_ATTRIBUTE, source);
133
143
  style.textContent = cssText;
134
144
  document.head.prepend(style);
145
+ watchForHostOwnership(style);
135
146
  return "installed";
136
147
  }
148
+ let stopWatchingHostOwnership = null;
149
+ const watchForHostOwnership = (installed) => {
150
+ stopWatchingHostOwnership?.();
151
+ if (typeof MutationObserver === "undefined")
152
+ return;
153
+ const stop = () => {
154
+ observer.disconnect();
155
+ document.removeEventListener("load", onLoad, true);
156
+ if (stopWatchingHostOwnership === stop)
157
+ stopWatchingHostOwnership = null;
158
+ };
159
+ const reconcile = () => {
160
+ if (!installed.isConnected) {
161
+ stop();
162
+ return;
163
+ }
164
+ if (contentStylesOptedOut() || countHostContentStyleSheets() > 0) {
165
+ installed.remove();
166
+ stop();
167
+ }
168
+ };
169
+ const observer = new MutationObserver((records) => {
170
+ const relevant = records.some((record) => record.type === "attributes" ||
171
+ Array.from(record.addedNodes).some((node) => node.nodeName === "STYLE" || node.nodeName === "LINK"));
172
+ if (relevant)
173
+ reconcile();
174
+ });
175
+ // `load` does not bubble, so a capturing listener is what sees a host
176
+ // `<link>` finish, including one already in flight when this runs.
177
+ const onLoad = (event) => {
178
+ if (event.target?.nodeName === "LINK")
179
+ reconcile();
180
+ };
181
+ observer.observe(document.head, { childList: true });
182
+ observer.observe(document.documentElement, {
183
+ attributes: true,
184
+ attributeFilter: [OPT_OUT_ATTRIBUTE],
185
+ });
186
+ document.addEventListener("load", onLoad, true);
187
+ stopWatchingHostOwnership = stop;
188
+ };
137
189
  /**
138
190
  * Counts content stylesheets in the document, detected by the sentinel property
139
191
  * rather than by URL, so a copy arriving as a `<link>`, a bundler-injected
@@ -141,8 +193,8 @@ export function installContentStyles(cssText, source) {
141
193
  * count to copies the host loaded itself.
142
194
  *
143
195
  * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy
144
- * served from another origin therefore reads as absent. That only costs a
145
- * diagnostic, never correctness.
196
+ * served from another origin therefore reads as absent, and the player installs
197
+ * its own copy alongside it. Such a host sets the opt-out attribute instead.
146
198
  */
147
199
  const countContentStyleSheets = ({ excludeInstalled, }) => {
148
200
  // Walks the owning elements rather than document.styleSheets: the marker
@@ -176,18 +228,10 @@ const countHostContentStyleSheets = () => countContentStyleSheets({ excludeInsta
176
228
  let auditWarningIssued = false;
177
229
  const pendingChecks = [];
178
230
  /**
179
- * Reports, once per page, the two ways content styling can go wrong. Neither is
180
- * detectable synchronously — a host copy may still be in flight as an async
181
- * `<link>` — so the check is deferred.
182
- *
183
- * - **Missing**: the host opted out and then loaded nothing, so authored content
184
- * renders unstyled. This is the failure the old host-import contract produced
185
- * silently.
186
- * - **Duplicated**: the host still imports `components.css` itself *and* the
187
- * player installed a copy. Rendering is correct while the two agree, but the
188
- * host's copy loads later and therefore wins ties at equal specificity — so a
189
- * host copy pinned to an older `@pie-players/pie-theme` silently overrides the
190
- * player's newer rules. Harmless today, a confusing override tomorrow.
231
+ * Reports, once per page, a host that opted out and then loaded no content
232
+ * stylesheet, so authored content renders unstyled — the failure the old
233
+ * host-import contract produced silently. Not detectable synchronously — a host
234
+ * copy may still be in flight as an async `<link>` — so the check is deferred.
191
235
  */
192
236
  export function auditContentStyles(source) {
193
237
  if (!isBrowser() || auditWarningIssued)
@@ -195,9 +239,7 @@ export function auditContentStyles(source) {
195
239
  const check = () => {
196
240
  if (auditWarningIssued)
197
241
  return;
198
- if (contentStylesOptedOut()) {
199
- if (contentStylesPresent())
200
- return;
242
+ if (contentStylesOptedOut() && !contentStylesPresent()) {
201
243
  auditWarningIssued = true;
202
244
  console.warn(`[${source}] No PIE content stylesheet found. This document sets ` +
203
245
  `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}", so ${source} did not install ` +
@@ -206,19 +248,6 @@ export function auditContentStyles(source) {
206
248
  `families, answer-eliminator styles) will render unstyled. Either ` +
207
249
  `import "@pie-players/pie-theme/components.css" in the host app, or ` +
208
250
  `drop the ${OPT_OUT_ATTRIBUTE} attribute to let the player install it.`);
209
- return;
210
- }
211
- if (countHostContentStyleSheets() > 0) {
212
- auditWarningIssued = true;
213
- console.warn(`[${source}] The PIE content stylesheet is loaded twice: ${source} ` +
214
- `installs it, and this host also imports ` +
215
- `"@pie-players/pie-theme/components.css". Rendering is unaffected ` +
216
- `while both copies match, but the host copy loads later and wins ` +
217
- `ties at equal specificity, so a copy pinned to an older ` +
218
- `@pie-players/pie-theme will silently override newer player styles. ` +
219
- `Remove the host import, or set ` +
220
- `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}" on <html> to own the ` +
221
- `stylesheet deliberately.`);
222
251
  }
223
252
  };
224
253
  // A host stylesheet can land well after the module graph evaluates, and a
@@ -240,10 +269,12 @@ export function auditContentStyles(source) {
240
269
  }
241
270
  }
242
271
  /**
243
- * Test-only: clears the once-per-page warning latch and cancels any pending
244
- * deferred check, so a timer scheduled by one test cannot warn during the next.
272
+ * Test-only: clears the once-per-page warning latch, cancels any pending
273
+ * deferred check and stops watching for a host copy, so state from one test
274
+ * cannot act during the next.
245
275
  */
246
276
  export function resetContentStylesWarningForTesting() {
277
+ stopWatchingHostOwnership?.();
247
278
  auditWarningIssued = false;
248
279
  for (const handle of pendingChecks)
249
280
  clearTimeout(handle);
@@ -36,6 +36,37 @@ export declare function resolveContainingBlockRect(element: HTMLElement | undefi
36
36
  * than clamping to an inverted range.
37
37
  */
38
38
  export declare function clampOffsetWithinBlock(offset: Point, box: Size, block: Size, gutter?: number): Point;
39
+ /**
40
+ * Clamps a translate offset applied to a box centred in its containing block so
41
+ * that at least `visible` pixels of the box stay inside the block on each axis.
42
+ *
43
+ * For an overlay that has to overhang its block to be useful — a ruler lined up
44
+ * against content near a card's edge — without ever being dragged out of reach
45
+ * behind that edge. A box or block narrower than `visible` keeps all of the
46
+ * smaller extent.
47
+ */
48
+ export declare function clampOffsetOverlappingBlock(offset: Point, box: Size, block: Size, visible: number): Point;
49
+ /** The axis-aligned extent of a `size` box turned by `degrees` about its centre. */
50
+ export declare function rotatedExtent(size: Size, degrees: number): Size;
51
+ /**
52
+ * How far a `size` box's centre moves when the box turns by `degrees` about
53
+ * `origin`, given in the box's own coordinates from its top-left corner.
54
+ *
55
+ * Zero for a box turned about its centre. An overlay turned about another
56
+ * point adds this to its translate offset to get the offset of its rotated
57
+ * extent's centre, which is what the centred clamps above bound.
58
+ */
59
+ export declare function rotationCentreShift(size: Size, origin: Point, degrees: number): Point;
60
+ /**
61
+ * Where to centre a control strip docked past the bottom edge of a `box`
62
+ * turned by `degrees`, in the box's own coordinates from its top-left corner.
63
+ *
64
+ * The strip is drawn counter-rotated so it stays level on screen. Its reach
65
+ * along the box's downward normal is its projection onto that normal, which
66
+ * grows from its height at no turn to its width at a quarter turn, so a strip
67
+ * placed that far out clears the box at any rotation.
68
+ */
69
+ export declare function uprightDockCentre(box: Size, strip: Size, degrees: number, gap: number): Point;
39
70
  /**
40
71
  * Clamps an absolute centre point, in containing-block coordinates, so the box
41
72
  * it positions stays inside that block.
@@ -49,6 +49,69 @@ export function clampOffsetWithinBlock(offset, box, block, gutter = DEFAULT_CONT
49
49
  y: clampAxis(offset.y, box.height, block.height),
50
50
  };
51
51
  }
52
+ /**
53
+ * Clamps a translate offset applied to a box centred in its containing block so
54
+ * that at least `visible` pixels of the box stay inside the block on each axis.
55
+ *
56
+ * For an overlay that has to overhang its block to be useful — a ruler lined up
57
+ * against content near a card's edge — without ever being dragged out of reach
58
+ * behind that edge. A box or block narrower than `visible` keeps all of the
59
+ * smaller extent.
60
+ */
61
+ export function clampOffsetOverlappingBlock(offset, box, block, visible) {
62
+ const clampAxis = (value, boxExtent, blockExtent) => {
63
+ const keep = Math.min(visible, boxExtent, blockExtent);
64
+ const travel = blockExtent / 2 + boxExtent / 2 - keep;
65
+ return Math.max(-travel, Math.min(travel, value));
66
+ };
67
+ return {
68
+ x: clampAxis(offset.x, box.width, block.width),
69
+ y: clampAxis(offset.y, box.height, block.height),
70
+ };
71
+ }
72
+ /** The axis-aligned extent of a `size` box turned by `degrees` about its centre. */
73
+ export function rotatedExtent(size, degrees) {
74
+ const radians = (degrees * Math.PI) / 180;
75
+ const cos = Math.abs(Math.cos(radians));
76
+ const sin = Math.abs(Math.sin(radians));
77
+ return {
78
+ width: size.width * cos + size.height * sin,
79
+ height: size.width * sin + size.height * cos,
80
+ };
81
+ }
82
+ /**
83
+ * How far a `size` box's centre moves when the box turns by `degrees` about
84
+ * `origin`, given in the box's own coordinates from its top-left corner.
85
+ *
86
+ * Zero for a box turned about its centre. An overlay turned about another
87
+ * point adds this to its translate offset to get the offset of its rotated
88
+ * extent's centre, which is what the centred clamps above bound.
89
+ */
90
+ export function rotationCentreShift(size, origin, degrees) {
91
+ const radians = (degrees * Math.PI) / 180;
92
+ const cos = Math.cos(radians);
93
+ const sin = Math.sin(radians);
94
+ const dx = size.width / 2 - origin.x;
95
+ const dy = size.height / 2 - origin.y;
96
+ return {
97
+ x: dx * cos - dy * sin - dx,
98
+ y: dx * sin + dy * cos - dy,
99
+ };
100
+ }
101
+ /**
102
+ * Where to centre a control strip docked past the bottom edge of a `box`
103
+ * turned by `degrees`, in the box's own coordinates from its top-left corner.
104
+ *
105
+ * The strip is drawn counter-rotated so it stays level on screen. Its reach
106
+ * along the box's downward normal is its projection onto that normal, which
107
+ * grows from its height at no turn to its width at a quarter turn, so a strip
108
+ * placed that far out clears the box at any rotation.
109
+ */
110
+ export function uprightDockCentre(box, strip, degrees, gap) {
111
+ const radians = (degrees * Math.PI) / 180;
112
+ const reach = strip.width * Math.abs(Math.sin(radians)) + strip.height * Math.abs(Math.cos(radians));
113
+ return { x: box.width / 2, y: box.height + gap + reach / 2 };
114
+ }
52
115
  /**
53
116
  * Clamps an absolute centre point, in containing-block coordinates, so the box
54
117
  * it positions stays inside that block.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Placement of a tool overlay the learner moves and turns: the ruler and the
3
+ * protractor. One controller holds where the tool sits and is the only writer of
4
+ * its `style.transform`, so a drag, an arrow key and a tap control always agree
5
+ * about where the tool is.
6
+ */
7
+ import { type Point, type Size } from "./overlay-containment.js";
8
+ /** An offset from the tool's centred position, and its turn about its pivot. */
9
+ export interface OverlayPlacement {
10
+ x: number;
11
+ y: number;
12
+ rotation: number;
13
+ }
14
+ /** Pixels an arrow key or move button shifts the tool. */
15
+ export declare const OVERLAY_PLACEMENT_MOVE_STEP = 10;
16
+ /** Degrees Shift+arrow turns the tool. */
17
+ export declare const OVERLAY_PLACEMENT_ROTATE_STEP = 5;
18
+ /** Degrees Page Up/Down turns the tool. */
19
+ export declare const OVERLAY_PLACEMENT_FINE_ROTATE_STEP = 1;
20
+ /** Each move, with its tap control's glyph and label and its announcement. */
21
+ export declare const OVERLAY_PLACEMENT_NUDGES: {
22
+ readonly left: {
23
+ readonly dx: -1;
24
+ readonly dy: 0;
25
+ readonly glyph: "←";
26
+ readonly labelKey: "toolkit.window.moveLeftA11y";
27
+ readonly announceKey: "toolkit.announce.movedLeft";
28
+ };
29
+ readonly up: {
30
+ readonly dx: 0;
31
+ readonly dy: -1;
32
+ readonly glyph: "↑";
33
+ readonly labelKey: "toolkit.window.moveUpA11y";
34
+ readonly announceKey: "toolkit.announce.movedUp";
35
+ };
36
+ readonly down: {
37
+ readonly dx: 0;
38
+ readonly dy: 1;
39
+ readonly glyph: "↓";
40
+ readonly labelKey: "toolkit.window.moveDownA11y";
41
+ readonly announceKey: "toolkit.announce.movedDown";
42
+ };
43
+ readonly right: {
44
+ readonly dx: 1;
45
+ readonly dy: 0;
46
+ readonly glyph: "→";
47
+ readonly labelKey: "toolkit.window.moveRightA11y";
48
+ readonly announceKey: "toolkit.announce.movedRight";
49
+ };
50
+ };
51
+ export type OverlayPlacementNudge = keyof typeof OVERLAY_PLACEMENT_NUDGES;
52
+ /** The interface-catalog keys a move or turn is announced with. */
53
+ export type OverlayPlacementAnnouncement = (typeof OVERLAY_PLACEMENT_NUDGES)[OverlayPlacementNudge]["announceKey"] | "toolkit.announce.rotatedTo";
54
+ /** The tap controls' rotations, the keyboard's two steps each way. */
55
+ export declare const OVERLAY_PLACEMENT_ROTATIONS: readonly [number, number, 1, 5];
56
+ export interface OverlayPlacementOptions {
57
+ /** The tool's positioned element, which receives the transform. */
58
+ getElement: () => HTMLElement | undefined;
59
+ /** The tap controls, docked level past the tool's bottom edge. */
60
+ getControls: () => HTMLElement | undefined;
61
+ /**
62
+ * The point the tool turns about, from its top-left corner at no turn.
63
+ * Defaults to its centre, and must match the stylesheet's `transform-origin`.
64
+ */
65
+ pivot?: (size: Size) => Point;
66
+ /**
67
+ * That pivot on screen when a pointer rotation starts, or `undefined` to
68
+ * decline it. Defaults to the centre of the tool's bounding box, which a turn
69
+ * about the centre leaves in place.
70
+ */
71
+ getScreenPivot?: () => Point | undefined;
72
+ /** Raises the tool above its peers when a gesture starts or it is shown. */
73
+ bringToFront?: (element: HTMLElement) => void;
74
+ /** Speaks a move or turn; the tool resolves the key in its interface catalog. */
75
+ announce: (key: OverlayPlacementAnnouncement, params: Record<string, number>) => void;
76
+ }
77
+ export interface OverlayPlacementController {
78
+ readonly placement: OverlayPlacement;
79
+ /**
80
+ * Writes `next` to `style.transform`, first keeping part of the turned tool
81
+ * inside the box it is positioned against. The bound lets the tool overhang:
82
+ * an item card is barely wider than a ruler, and full containment would pin
83
+ * its zero mark to the card's edge. The card clips what overhangs, and 100px
84
+ * stays in view to grab it back.
85
+ */
86
+ apply: (next: OverlayPlacement) => void;
87
+ /** Raises the tool and re-applies its placement, as a fresh reveal does. */
88
+ show: () => void;
89
+ /** Ends any gesture and returns the tool to its centred, upright position. */
90
+ reset: () => void;
91
+ /** Starts a drag from a press on the tool. */
92
+ startDrag: (event: PointerEvent) => void;
93
+ /** Starts a turn from a press on `handle`, which must not also start a drag. */
94
+ startRotate: (event: PointerEvent, handle: HTMLElement) => void;
95
+ /** Moves the tool one step, as an arrow key or a move button does. */
96
+ nudge: (direction: OverlayPlacementNudge) => void;
97
+ /** Turns the tool by `degrees`, clockwise when positive, to a whole degree. */
98
+ rotateBy: (degrees: number) => void;
99
+ /**
100
+ * Arrows move, Shift+arrows turn 5°, Page Up/Down turn 1°. Returns whether
101
+ * it handled the key, having prevented its default.
102
+ */
103
+ handleKeyDown: (event: KeyboardEvent) => boolean;
104
+ /**
105
+ * Re-applies the placement on window resize, which can leave the tool outside
106
+ * its shrunken containing block. Returns a cleanup that also ends any gesture.
107
+ */
108
+ connect: () => () => void;
109
+ }
110
+ export declare function createOverlayPlacement(options: OverlayPlacementOptions): OverlayPlacementController;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Placement of a tool overlay the learner moves and turns: the ruler and the
3
+ * protractor. One controller holds where the tool sits and is the only writer of
4
+ * its `style.transform`, so a drag, an arrow key and a tap control always agree
5
+ * about where the tool is.
6
+ */
7
+ import { clampOffsetOverlappingBlock, resolveContainingBlockRect, rotatedExtent, rotationCentreShift, uprightDockCentre, } from "./overlay-containment.js";
8
+ import { createPointerDragController } from "./pointer-drag.js";
9
+ import { createPointerGesture } from "./pointer-gesture.js";
10
+ import { createPointerRotateController } from "./pointer-rotate.js";
11
+ /** Pixels an arrow key or move button shifts the tool. */
12
+ export const OVERLAY_PLACEMENT_MOVE_STEP = 10;
13
+ /** Degrees Shift+arrow turns the tool. */
14
+ export const OVERLAY_PLACEMENT_ROTATE_STEP = 5;
15
+ /** Degrees Page Up/Down turns the tool. */
16
+ export const OVERLAY_PLACEMENT_FINE_ROTATE_STEP = 1;
17
+ /** Each move, with its tap control's glyph and label and its announcement. */
18
+ export const OVERLAY_PLACEMENT_NUDGES = {
19
+ left: {
20
+ dx: -1,
21
+ dy: 0,
22
+ glyph: "←",
23
+ labelKey: "toolkit.window.moveLeftA11y",
24
+ announceKey: "toolkit.announce.movedLeft",
25
+ },
26
+ up: {
27
+ dx: 0,
28
+ dy: -1,
29
+ glyph: "↑",
30
+ labelKey: "toolkit.window.moveUpA11y",
31
+ announceKey: "toolkit.announce.movedUp",
32
+ },
33
+ down: {
34
+ dx: 0,
35
+ dy: 1,
36
+ glyph: "↓",
37
+ labelKey: "toolkit.window.moveDownA11y",
38
+ announceKey: "toolkit.announce.movedDown",
39
+ },
40
+ right: {
41
+ dx: 1,
42
+ dy: 0,
43
+ glyph: "→",
44
+ labelKey: "toolkit.window.moveRightA11y",
45
+ announceKey: "toolkit.announce.movedRight",
46
+ },
47
+ };
48
+ /** The tap controls' rotations, the keyboard's two steps each way. */
49
+ export const OVERLAY_PLACEMENT_ROTATIONS = [
50
+ -OVERLAY_PLACEMENT_ROTATE_STEP,
51
+ -OVERLAY_PLACEMENT_FINE_ROTATE_STEP,
52
+ OVERLAY_PLACEMENT_FINE_ROTATE_STEP,
53
+ OVERLAY_PLACEMENT_ROTATE_STEP,
54
+ ];
55
+ const ARROW_KEYS = {
56
+ ArrowLeft: "left",
57
+ ArrowUp: "up",
58
+ ArrowDown: "down",
59
+ ArrowRight: "right",
60
+ };
61
+ /** Pixels of the tool kept inside its containing block on each axis. */
62
+ const MIN_VISIBLE = 100;
63
+ /** Space between the tool's bottom edge and its tap controls. */
64
+ const CONTROLS_GAP = 8;
65
+ export function createOverlayPlacement(options) {
66
+ let placement = { x: 0, y: 0, rotation: 0 };
67
+ const sizeOf = (element) => ({
68
+ width: element.offsetWidth,
69
+ height: element.offsetHeight,
70
+ });
71
+ function dockControls(element) {
72
+ const controls = options.getControls();
73
+ if (!controls)
74
+ return;
75
+ // Counter-rotated, so each arrow points the way its button moves the tool.
76
+ const centre = uprightDockCentre(sizeOf(element), sizeOf(controls), placement.rotation, CONTROLS_GAP);
77
+ controls.style.transform = `translate(${centre.x}px, ${centre.y}px) translate(-50%, -50%) rotate(${-placement.rotation}deg)`;
78
+ }
79
+ function apply(next) {
80
+ const element = options.getElement();
81
+ if (!element)
82
+ return;
83
+ const block = resolveContainingBlockRect(element);
84
+ const size = sizeOf(element);
85
+ // A tool turning off its centre has its turned extent centred off its
86
+ // translate offset by this shift; the clamp bounds that centre.
87
+ const shift = options.pivot
88
+ ? rotationCentreShift(size, options.pivot(size), next.rotation)
89
+ : { x: 0, y: 0 };
90
+ const centre = { x: next.x + shift.x, y: next.y + shift.y };
91
+ const bounded = block
92
+ ? clampOffsetOverlappingBlock(centre, rotatedExtent(size, next.rotation), block, MIN_VISIBLE)
93
+ : centre;
94
+ placement = {
95
+ x: bounded.x - shift.x,
96
+ y: bounded.y - shift.y,
97
+ rotation: next.rotation,
98
+ };
99
+ element.style.transform = `translate(-50%, -50%) translate(${placement.x}px, ${placement.y}px) rotate(${placement.rotation}deg)`;
100
+ dockControls(element);
101
+ }
102
+ const dragController = createPointerDragController({
103
+ getPosition: () => ({ x: placement.x, y: placement.y }),
104
+ setPosition: (next) => apply({ ...placement, ...next }),
105
+ });
106
+ const rotateController = createPointerRotateController({
107
+ getRotation: () => placement.rotation,
108
+ setRotation: (rotation) => apply({ ...placement, rotation }),
109
+ });
110
+ const gesture = createPointerGesture({
111
+ onMove: (event) => {
112
+ dragController.handlePointerMove(event);
113
+ rotateController.handlePointerMove(event);
114
+ },
115
+ onEnd: () => {
116
+ dragController.endDragging();
117
+ rotateController.endRotating();
118
+ },
119
+ });
120
+ /**
121
+ * Claims a press for a drag or turn. The claim's `preventDefault` suppresses
122
+ * the press's default focus, so focus the tool here: the arrow-key
123
+ * alternatives act on it.
124
+ */
125
+ function claim(event, element, target) {
126
+ if (!gesture.begin(event, target))
127
+ return false;
128
+ element.focus({ preventScroll: true });
129
+ return true;
130
+ }
131
+ const screenPivot = () => {
132
+ if (options.getScreenPivot)
133
+ return options.getScreenPivot();
134
+ const rect = options.getElement()?.getBoundingClientRect();
135
+ return (rect && { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
136
+ };
137
+ function nudge(direction) {
138
+ const { dx, dy, announceKey } = OVERLAY_PLACEMENT_NUDGES[direction];
139
+ apply({
140
+ x: placement.x + dx * OVERLAY_PLACEMENT_MOVE_STEP,
141
+ y: placement.y + dy * OVERLAY_PLACEMENT_MOVE_STEP,
142
+ rotation: placement.rotation,
143
+ });
144
+ options.announce(announceKey, {
145
+ position: Math.round(dx ? placement.x : placement.y),
146
+ });
147
+ }
148
+ function rotateBy(degrees) {
149
+ const rotation = (((Math.round(placement.rotation) + degrees) % 360) + 360) % 360;
150
+ apply({ ...placement, rotation });
151
+ options.announce("toolkit.announce.rotatedTo", { degrees: rotation });
152
+ }
153
+ const reapply = () => apply(placement);
154
+ return {
155
+ get placement() {
156
+ return placement;
157
+ },
158
+ apply,
159
+ show() {
160
+ const element = options.getElement();
161
+ if (!element)
162
+ return;
163
+ options.bringToFront?.(element);
164
+ reapply();
165
+ },
166
+ reset() {
167
+ gesture.release();
168
+ placement = { x: 0, y: 0, rotation: 0 };
169
+ },
170
+ startDrag(event) {
171
+ const element = options.getElement();
172
+ if (!element)
173
+ return;
174
+ options.bringToFront?.(element);
175
+ if (!claim(event, element, element))
176
+ return;
177
+ dragController.startDragging(event, element);
178
+ },
179
+ startRotate(event, handle) {
180
+ event.stopPropagation();
181
+ const element = options.getElement();
182
+ const pivot = screenPivot();
183
+ if (!element || !pivot)
184
+ return;
185
+ options.bringToFront?.(element);
186
+ if (!claim(event, element, handle))
187
+ return;
188
+ rotateController.startRotating(event, handle, pivot);
189
+ },
190
+ nudge,
191
+ rotateBy,
192
+ handleKeyDown(event) {
193
+ const direction = ARROW_KEYS[event.key];
194
+ if (direction && event.shiftKey) {
195
+ rotateBy(direction === "up" || direction === "left"
196
+ ? -OVERLAY_PLACEMENT_ROTATE_STEP
197
+ : OVERLAY_PLACEMENT_ROTATE_STEP);
198
+ }
199
+ else if (direction) {
200
+ nudge(direction);
201
+ }
202
+ else if (event.key === "PageUp") {
203
+ rotateBy(-OVERLAY_PLACEMENT_FINE_ROTATE_STEP);
204
+ }
205
+ else if (event.key === "PageDown") {
206
+ rotateBy(OVERLAY_PLACEMENT_FINE_ROTATE_STEP);
207
+ }
208
+ else {
209
+ return false;
210
+ }
211
+ event.preventDefault();
212
+ return true;
213
+ },
214
+ connect() {
215
+ window.addEventListener("resize", reapply);
216
+ return () => {
217
+ gesture.release();
218
+ window.removeEventListener("resize", reapply);
219
+ };
220
+ },
221
+ };
222
+ }
@@ -0,0 +1,32 @@
1
+ export interface PointerGestureArgs {
2
+ /** Receives every move of the pointer that began the gesture. */
3
+ onMove: (event: PointerEvent) => void;
4
+ /** Called once when the gesture ends, however it ends. */
5
+ onEnd: () => void;
6
+ }
7
+ export interface PointerGesture {
8
+ isActive: () => boolean;
9
+ /**
10
+ * Claims a pointerdown for a gesture on `target`, returning whether it did.
11
+ * Declines a non-primary button, and any pointer while another gesture is
12
+ * active, so a second finger landing mid-drag neither moves nor pinches.
13
+ * The caller starts its drag or rotate controller after a claim, which
14
+ * takes the pointer capture this listens on.
15
+ */
16
+ begin: (event: PointerEvent, target: HTMLElement) => boolean;
17
+ /** Ends the active gesture, if any, and releases its pointer capture. */
18
+ release: () => void;
19
+ }
20
+ /**
21
+ * The lifecycle of one pointer gesture on a tool overlay: which pointer drives
22
+ * it, the listeners on the element that captured it, and every way it can end.
23
+ * `pointercancel` and `lostpointercapture` end it as well as `pointerup`,
24
+ * because iPadOS cancels a touch it takes over for a system gesture, and a
25
+ * gesture that waited for `pointerup` would stay stuck to the next touch.
26
+ *
27
+ * The claim calls `preventDefault`, which keeps a touch from scrolling or
28
+ * zooming the page and a mouse press from selecting text, and also suppresses
29
+ * the press's default focus: a caller whose keyboard alternatives act on a
30
+ * focused element focuses it itself.
31
+ */
32
+ export declare function createPointerGesture(args: PointerGestureArgs): PointerGesture;