@pie-players/pie-default-tool-loaders 0.3.66 → 0.3.68
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/dist/content-alternates.d.ts +20 -0
- package/dist/content-alternates.js +22 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/packaged-capability-composition.d.ts +6 -6
- package/dist/packaged-capability-composition.js +59 -11
- package/dist/registrations/accessibility-tools.js +28 -105
- package/dist/registrations/audio-transcript.js +7 -1
- package/dist/registrations/calculator.js +40 -17
- package/dist/registrations/dictionary-tools.d.ts +75 -0
- package/dist/registrations/dictionary-tools.js +235 -0
- package/dist/registrations/interaction-tools.d.ts +0 -7
- package/dist/registrations/interaction-tools.js +4 -60
- package/dist/registrations/measurement-tools.js +13 -62
- package/dist/registrations/overlay-toolbar-render.d.ts +43 -0
- package/dist/registrations/overlay-toolbar-render.js +75 -0
- package/dist/registrations/selection-actions.d.ts +24 -0
- package/dist/registrations/selection-actions.js +73 -0
- package/dist/registrations/subject-specific-tools.js +21 -82
- package/dist/registrations/tts.js +2 -0
- package/package.json +16 -14
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictionary Tool Registrations
|
|
3
|
+
*
|
|
4
|
+
* Word lookup and picture (symbol) lookup, each a floating panel opened from the
|
|
5
|
+
* toolbar.
|
|
6
|
+
*
|
|
7
|
+
* Neither declares a universal support id. A dictionary is a granted accommodation,
|
|
8
|
+
* not a universal affordance: on a vocabulary item it is construct-relevant, so
|
|
9
|
+
* handing it to every learner by default would change what the item measures. A
|
|
10
|
+
* programme grants it through the PNP like any other accommodation.
|
|
11
|
+
*
|
|
12
|
+
* Both panels also accept a `term` from whatever selection affordance a host offers,
|
|
13
|
+
* but neither depends on one. A sighted keyboard-only learner cannot originate a text
|
|
14
|
+
* selection in non-editable content — Chromium does not extend one with Shift+Arrow
|
|
15
|
+
* there without caret browsing, an OS toggle absent on mobile — so a selection-only
|
|
16
|
+
* dictionary is unreachable for them. The panel's own field is the keyboard route.
|
|
17
|
+
*/
|
|
18
|
+
import { createScopedVisibilityBinding, createToolElement, hasReadableText, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
19
|
+
/**
|
|
20
|
+
* Apply the host's per-tool render params to the panel.
|
|
21
|
+
*
|
|
22
|
+
* `lookup` wins over `endpoint` when a host supplies both, matching the element's own
|
|
23
|
+
* precedence: a host that already has a client should not also have to describe its
|
|
24
|
+
* HTTP shape. Absent both, the panel renders its unconfigured state — PIE ships no
|
|
25
|
+
* dictionary endpoint, because the corpus behind one is licensed per programme.
|
|
26
|
+
*
|
|
27
|
+
* An `endpoint` alone is the whole configuration for the common case: the panel calls
|
|
28
|
+
* it on the assessment's own session, so a route already behind that session answers
|
|
29
|
+
* without a token. `headers` and `credentials` are the seam for a host authorising some
|
|
30
|
+
* other way, and neither is required.
|
|
31
|
+
*/
|
|
32
|
+
function applyLookupParams(element, toolId, toolbarContext,
|
|
33
|
+
/** A language variant's own corpus language; see {@link DictionaryVariantOptions}. */
|
|
34
|
+
lookupLanguage) {
|
|
35
|
+
const params = toolbarContext.getToolRenderParams?.(toolId) ?? {};
|
|
36
|
+
if (typeof params.endpoint === "string")
|
|
37
|
+
element.endpoint = params.endpoint;
|
|
38
|
+
if (typeof params.lookup === "function")
|
|
39
|
+
element.lookup = params.lookup;
|
|
40
|
+
if (typeof params.headers === "function")
|
|
41
|
+
element.headers = params.headers;
|
|
42
|
+
if (typeof params.credentials === "string")
|
|
43
|
+
element.credentials = params.credentials;
|
|
44
|
+
// The selection door: a gateway acting on the learner's selection requests this
|
|
45
|
+
// tool with the words in `term`, and the toolbar layers that over the host's own
|
|
46
|
+
// params. The toolbar stamps every request with a neutral `toolRequestId` — core
|
|
47
|
+
// names no tool's concepts — and this layer, which knows both, hands it to the panel
|
|
48
|
+
// as the identity of its term. That is what makes reapplying an unchanged term on a
|
|
49
|
+
// later sync free while a second request for the same word still re-runs.
|
|
50
|
+
if (typeof params.term === "string" && params.term)
|
|
51
|
+
element.term = params.term;
|
|
52
|
+
if (typeof params.toolRequestId === "string" ||
|
|
53
|
+
typeof params.toolRequestId === "number") {
|
|
54
|
+
element.termRequestId = params.toolRequestId;
|
|
55
|
+
}
|
|
56
|
+
// Resolution order: what the host named for this tool, then the capability's own
|
|
57
|
+
// corpus language if it is a language variant, then the toolbar's scope. A variant
|
|
58
|
+
// exists precisely to look words up in a language the surrounding content is not in —
|
|
59
|
+
// a Spanish gloss for an English passage — so its language outranks the scope's, and a
|
|
60
|
+
// host that names one anyway still wins.
|
|
61
|
+
const language = typeof params.language === "string"
|
|
62
|
+
? params.language
|
|
63
|
+
: (lookupLanguage ?? toolbarContext.language);
|
|
64
|
+
if (typeof language === "string" && language)
|
|
65
|
+
element.language = language;
|
|
66
|
+
}
|
|
67
|
+
function renderDictionaryPanel(args) {
|
|
68
|
+
const { registration, context, toolbarContext } = args;
|
|
69
|
+
const visibility = createScopedVisibilityBinding(registration.toolId, toolbarContext);
|
|
70
|
+
const button = {
|
|
71
|
+
toolId: registration.toolId,
|
|
72
|
+
label: registration.name,
|
|
73
|
+
icon: typeof registration.icon === "function"
|
|
74
|
+
? registration.icon(context)
|
|
75
|
+
: registration.icon,
|
|
76
|
+
disabled: false,
|
|
77
|
+
ariaLabel: toolbarContext.i18n.t(args.buttonA11yKey),
|
|
78
|
+
tooltip: toolbarContext.i18n.t(args.tooltipKey),
|
|
79
|
+
onClick: () => toolbarContext.toggleTool(registration.toolId),
|
|
80
|
+
active: visibility.isActive(),
|
|
81
|
+
};
|
|
82
|
+
const componentOverrides = toolbarContext.componentOverrides ??
|
|
83
|
+
{};
|
|
84
|
+
const overlay = createToolElement(registration.toolId, context, toolbarContext, componentOverrides);
|
|
85
|
+
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
86
|
+
applyLookupParams(overlay, registration.toolId, toolbarContext, args.lookupLanguage);
|
|
87
|
+
return {
|
|
88
|
+
toolId: registration.toolId,
|
|
89
|
+
button,
|
|
90
|
+
elements: [
|
|
91
|
+
{
|
|
92
|
+
element: overlay,
|
|
93
|
+
mount: "after-buttons",
|
|
94
|
+
shell: {
|
|
95
|
+
title: toolbarContext.i18n.t(args.shellTitleKey),
|
|
96
|
+
draggable: true,
|
|
97
|
+
resizable: true,
|
|
98
|
+
closeable: true,
|
|
99
|
+
initialWidth: args.initialWidth,
|
|
100
|
+
initialHeight: args.initialHeight,
|
|
101
|
+
minWidth: args.minWidth,
|
|
102
|
+
minHeight: args.minHeight,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
// Re-read params on sync rather than closing over them: a host calling
|
|
107
|
+
// `updateToolConfig` mid-session changes the endpoint without remounting, and a
|
|
108
|
+
// remount would discard the learner's current lookup.
|
|
109
|
+
sync: () => {
|
|
110
|
+
syncButtonAndOverlayVisibility({
|
|
111
|
+
button,
|
|
112
|
+
overlay,
|
|
113
|
+
isActive: visibility.isActive,
|
|
114
|
+
});
|
|
115
|
+
applyLookupParams(overlay, registration.toolId, toolbarContext, args.lookupLanguage);
|
|
116
|
+
},
|
|
117
|
+
subscribeActive: visibility.subscribeActive,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Dictionary tool registration
|
|
122
|
+
*
|
|
123
|
+
* Word lookup from a host-supplied service, in one language. Call this again with another
|
|
124
|
+
* `toolId`, `pnpSupportIds` and `lookupLanguage` to offer a second language beside it.
|
|
125
|
+
*/
|
|
126
|
+
export function createDictionaryToolRegistration(options = {}) {
|
|
127
|
+
const toolId = options.toolId ?? "dictionary";
|
|
128
|
+
const prefix = options.messageKeyPrefix ?? `tools.${toolId}`;
|
|
129
|
+
return {
|
|
130
|
+
toolId,
|
|
131
|
+
name: options.name ?? "Dictionary",
|
|
132
|
+
description: options.description ?? "Look up word definitions",
|
|
133
|
+
nameKey: `${prefix}.name`,
|
|
134
|
+
descriptionKey: `${prefix}.description`,
|
|
135
|
+
icon: "book-open",
|
|
136
|
+
// Text can appear at any of these; the panel itself floats at section scope.
|
|
137
|
+
supportedLevels: ["section", "item", "passage", "rubric"],
|
|
138
|
+
// PNP support IDs
|
|
139
|
+
// Maps to AfA PNP 3.0 / QTI 3.0 dictionary support, plus the common variants a
|
|
140
|
+
// host profile is likely to carry.
|
|
141
|
+
pnpSupportIds: options.pnpSupportIds ?? [
|
|
142
|
+
"dictionary", // Canonical id
|
|
143
|
+
"englishDictionary", // Common variant
|
|
144
|
+
"glossary", // Common variant
|
|
145
|
+
"definitions", // Common variant
|
|
146
|
+
],
|
|
147
|
+
/** Pass 2: a dictionary is relevant wherever there is text to look words up from. */
|
|
148
|
+
isVisibleInContext(context) {
|
|
149
|
+
return hasReadableText(context);
|
|
150
|
+
},
|
|
151
|
+
renderToolbar(context, toolbarContext) {
|
|
152
|
+
return renderDictionaryPanel({
|
|
153
|
+
registration: this,
|
|
154
|
+
context,
|
|
155
|
+
toolbarContext,
|
|
156
|
+
buttonA11yKey: `${prefix}.buttonA11y`,
|
|
157
|
+
tooltipKey: `${prefix}.tooltip`,
|
|
158
|
+
shellTitleKey: `${prefix}.name`,
|
|
159
|
+
lookupLanguage: options.lookupLanguage,
|
|
160
|
+
initialWidth: 420,
|
|
161
|
+
initialHeight: 380,
|
|
162
|
+
minWidth: 300,
|
|
163
|
+
minHeight: 240,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The content-following dictionary every deployment gets. */
|
|
169
|
+
export const dictionaryToolRegistration = createDictionaryToolRegistration();
|
|
170
|
+
/**
|
|
171
|
+
* Picture Dictionary tool registration
|
|
172
|
+
*
|
|
173
|
+
* Symbol or picture lookup from a host-supplied service. Sized wider than the word
|
|
174
|
+
* dictionary because its results are a grid rather than a column of text.
|
|
175
|
+
*/
|
|
176
|
+
export function createPictureDictionaryToolRegistration(options = {}) {
|
|
177
|
+
const toolId = options.toolId ?? "pictureDictionary";
|
|
178
|
+
const prefix = options.messageKeyPrefix ?? `tools.${toolId}`;
|
|
179
|
+
return {
|
|
180
|
+
toolId,
|
|
181
|
+
name: options.name ?? "Picture Dictionary",
|
|
182
|
+
description: options.description ?? "Look up pictures for words",
|
|
183
|
+
nameKey: `${prefix}.name`,
|
|
184
|
+
descriptionKey: `${prefix}.description`,
|
|
185
|
+
icon: "photo",
|
|
186
|
+
supportedLevels: ["section", "item", "passage", "rubric"],
|
|
187
|
+
// PNP support IDs
|
|
188
|
+
// AfA PNP 3.0 names an illustrated equivalent of a glossary; the variants cover
|
|
189
|
+
// what host profiles call it in practice.
|
|
190
|
+
pnpSupportIds: options.pnpSupportIds ?? [
|
|
191
|
+
"pictureDictionary", // Canonical id
|
|
192
|
+
"illustratedGlossary", // Common variant
|
|
193
|
+
"symbolDictionary", // Common variant
|
|
194
|
+
"pictureSupport", // Common variant
|
|
195
|
+
],
|
|
196
|
+
isVisibleInContext(context) {
|
|
197
|
+
return hasReadableText(context);
|
|
198
|
+
},
|
|
199
|
+
renderToolbar(context, toolbarContext) {
|
|
200
|
+
return renderDictionaryPanel({
|
|
201
|
+
registration: this,
|
|
202
|
+
context,
|
|
203
|
+
toolbarContext,
|
|
204
|
+
buttonA11yKey: `${prefix}.buttonA11y`,
|
|
205
|
+
tooltipKey: `${prefix}.tooltip`,
|
|
206
|
+
shellTitleKey: `${prefix}.name`,
|
|
207
|
+
lookupLanguage: options.lookupLanguage,
|
|
208
|
+
initialWidth: 520,
|
|
209
|
+
initialHeight: 460,
|
|
210
|
+
minWidth: 320,
|
|
211
|
+
minHeight: 280,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** The content-following picture dictionary every deployment gets. */
|
|
217
|
+
export const pictureDictionaryToolRegistration = createPictureDictionaryToolRegistration();
|
|
218
|
+
/**
|
|
219
|
+
* Spanish variants, packaged because a Spanish gloss for English content is a named
|
|
220
|
+
* accommodation rather than a hypothetical. Neither appears unless a PNP grants it.
|
|
221
|
+
*/
|
|
222
|
+
export const spanishDictionaryToolRegistration = createDictionaryToolRegistration({
|
|
223
|
+
toolId: "dictionarySpanish",
|
|
224
|
+
name: "Spanish Dictionary",
|
|
225
|
+
description: "Look up word definitions in Spanish",
|
|
226
|
+
pnpSupportIds: ["spanishDictionary", "spanishGlossary"],
|
|
227
|
+
lookupLanguage: "es",
|
|
228
|
+
});
|
|
229
|
+
export const spanishPictureDictionaryToolRegistration = createPictureDictionaryToolRegistration({
|
|
230
|
+
toolId: "pictureDictionarySpanish",
|
|
231
|
+
name: "Spanish Picture Dictionary",
|
|
232
|
+
description: "Look up pictures for words in Spanish",
|
|
233
|
+
pnpSupportIds: ["spanishPictureDictionary", "spanishIllustratedGlossary"],
|
|
234
|
+
lookupLanguage: "es",
|
|
235
|
+
});
|
|
@@ -18,10 +18,3 @@ import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools
|
|
|
18
18
|
* Only appears on multiple-choice style questions.
|
|
19
19
|
*/
|
|
20
20
|
export declare const answerEliminatorToolRegistration: ToolRegistration;
|
|
21
|
-
/**
|
|
22
|
-
* Highlighter tool registration
|
|
23
|
-
*
|
|
24
|
-
* Allows students to highlight text in passages and questions.
|
|
25
|
-
* Appears on items with readable text content.
|
|
26
|
-
*/
|
|
27
|
-
export declare const highlighterToolRegistration: ToolRegistration;
|
|
@@ -23,6 +23,8 @@ export const answerEliminatorToolRegistration = {
|
|
|
23
23
|
toolId: "answerEliminator",
|
|
24
24
|
name: "Answer Eliminator",
|
|
25
25
|
description: "Strike through answer choices",
|
|
26
|
+
nameKey: "tools.answerEliminator.name",
|
|
27
|
+
descriptionKey: "tools.answerEliminator.description",
|
|
26
28
|
icon: "strikethrough",
|
|
27
29
|
// Answer eliminator appears at item level only
|
|
28
30
|
supportedLevels: ["item"],
|
|
@@ -52,8 +54,8 @@ export const answerEliminatorToolRegistration = {
|
|
|
52
54
|
label: this.name,
|
|
53
55
|
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z"/></svg>',
|
|
54
56
|
disabled: false,
|
|
55
|
-
ariaLabel: "
|
|
56
|
-
tooltip: "
|
|
57
|
+
ariaLabel: toolbarContext.i18n.t("tools.answerEliminator.buttonA11y"),
|
|
58
|
+
tooltip: toolbarContext.i18n.t("tools.answerEliminator.tooltip"),
|
|
57
59
|
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
58
60
|
active: visibility.isActive(),
|
|
59
61
|
};
|
|
@@ -83,61 +85,3 @@ export const answerEliminatorToolRegistration = {
|
|
|
83
85
|
};
|
|
84
86
|
},
|
|
85
87
|
};
|
|
86
|
-
/**
|
|
87
|
-
* Highlighter tool registration
|
|
88
|
-
*
|
|
89
|
-
* Allows students to highlight text in passages and questions.
|
|
90
|
-
* Appears on items with readable text content.
|
|
91
|
-
*/
|
|
92
|
-
export const highlighterToolRegistration = {
|
|
93
|
-
toolId: "highlighter",
|
|
94
|
-
name: "Highlighter",
|
|
95
|
-
description: "Highlight text",
|
|
96
|
-
icon: "highlighter",
|
|
97
|
-
activation: "toolbar-toggle",
|
|
98
|
-
// Highlighter appears at passage, rubric, item, and element levels
|
|
99
|
-
supportedLevels: ["passage", "rubric", "item", "element"],
|
|
100
|
-
// PNP support IDs
|
|
101
|
-
pnpSupportIds: ["highlighter", "textHighlight", "annotation"],
|
|
102
|
-
/**
|
|
103
|
-
* Pass 2: Highlighter is relevant when readable text is available
|
|
104
|
-
*/
|
|
105
|
-
isVisibleInContext(context) {
|
|
106
|
-
return hasReadableText(context);
|
|
107
|
-
},
|
|
108
|
-
renderToolbar(context, toolbarContext) {
|
|
109
|
-
const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
|
|
110
|
-
const button = {
|
|
111
|
-
toolId: this.toolId,
|
|
112
|
-
label: this.name,
|
|
113
|
-
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
114
|
-
disabled: false,
|
|
115
|
-
ariaLabel: "Highlighter - Highlight text",
|
|
116
|
-
tooltip: "Highlight",
|
|
117
|
-
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
118
|
-
active: visibility.isActive(),
|
|
119
|
-
};
|
|
120
|
-
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
121
|
-
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
122
|
-
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
123
|
-
return {
|
|
124
|
-
toolId: this.toolId,
|
|
125
|
-
button,
|
|
126
|
-
elements: [{ element: overlay, mount: "after-buttons" }],
|
|
127
|
-
sync: () => {
|
|
128
|
-
syncButtonAndOverlayVisibility({
|
|
129
|
-
button,
|
|
130
|
-
overlay,
|
|
131
|
-
isActive: visibility.isActive,
|
|
132
|
-
onActiveChange: (active) => {
|
|
133
|
-
overlay.enabled = active;
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
if (toolbarContext.ttsService) {
|
|
137
|
-
overlay.ttsService = toolbarContext.ttsService;
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
subscribeActive: visibility.subscribeActive,
|
|
141
|
-
};
|
|
142
|
-
},
|
|
143
|
-
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { hasMathContent } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
11
11
|
import { createToolElement, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
12
12
|
import { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
13
|
+
import { renderOverlayToolbar } from "./overlay-toolbar-render.js";
|
|
13
14
|
/**
|
|
14
15
|
* Ruler tool registration
|
|
15
16
|
*
|
|
@@ -20,6 +21,8 @@ export const rulerToolRegistration = {
|
|
|
20
21
|
toolId: "ruler",
|
|
21
22
|
name: "Ruler",
|
|
22
23
|
description: "On-screen ruler for measurements",
|
|
24
|
+
nameKey: "tools.ruler.name",
|
|
25
|
+
descriptionKey: "tools.ruler.description",
|
|
23
26
|
icon: "ruler",
|
|
24
27
|
// Ruler typically appears at section/item/element level
|
|
25
28
|
supportedLevels: ["section", "item", "element"],
|
|
@@ -36,37 +39,10 @@ export const rulerToolRegistration = {
|
|
|
36
39
|
return hasMathContent(context);
|
|
37
40
|
},
|
|
38
41
|
renderToolbar(context, toolbarContext) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
44
|
-
disabled: false,
|
|
45
|
-
ariaLabel: "Open ruler tool",
|
|
46
|
-
tooltip: "Ruler",
|
|
47
|
-
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
48
|
-
active: visibility.isActive(),
|
|
49
|
-
};
|
|
50
|
-
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
51
|
-
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
52
|
-
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
53
|
-
applyOverlaySurface(overlay, "frameless");
|
|
54
|
-
return {
|
|
55
|
-
toolId: this.toolId,
|
|
56
|
-
button,
|
|
57
|
-
elements: [{ element: overlay, mount: "after-buttons" }],
|
|
58
|
-
sync: () => {
|
|
59
|
-
syncButtonAndOverlayVisibility({
|
|
60
|
-
button,
|
|
61
|
-
overlay,
|
|
62
|
-
isActive: visibility.isActive,
|
|
63
|
-
});
|
|
64
|
-
if (toolbarContext.toolkitCoordinator) {
|
|
65
|
-
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
subscribeActive: visibility.subscribeActive,
|
|
69
|
-
};
|
|
42
|
+
return renderOverlayToolbar(this, context, toolbarContext, {
|
|
43
|
+
surface: "frameless",
|
|
44
|
+
handsOverCoordinator: true,
|
|
45
|
+
});
|
|
70
46
|
},
|
|
71
47
|
};
|
|
72
48
|
/**
|
|
@@ -79,6 +55,8 @@ export const protractorToolRegistration = {
|
|
|
79
55
|
toolId: "protractor",
|
|
80
56
|
name: "Protractor",
|
|
81
57
|
description: "On-screen protractor for angle measurements",
|
|
58
|
+
nameKey: "tools.protractor.name",
|
|
59
|
+
descriptionKey: "tools.protractor.description",
|
|
82
60
|
icon: "protractor",
|
|
83
61
|
// Protractor typically appears at section/item/element level
|
|
84
62
|
supportedLevels: ["section", "item", "element"],
|
|
@@ -95,36 +73,9 @@ export const protractorToolRegistration = {
|
|
|
95
73
|
return hasMathContent(context);
|
|
96
74
|
},
|
|
97
75
|
renderToolbar(context, toolbarContext) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
|
|
103
|
-
disabled: false,
|
|
104
|
-
ariaLabel: "Open protractor tool",
|
|
105
|
-
tooltip: "Protractor",
|
|
106
|
-
onClick: () => toolbarContext.toggleTool(this.toolId),
|
|
107
|
-
active: visibility.isActive(),
|
|
108
|
-
};
|
|
109
|
-
const componentOverrides = toolbarContext.componentOverrides ?? {};
|
|
110
|
-
const overlay = createToolElement(this.toolId, context, toolbarContext, componentOverrides);
|
|
111
|
-
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
112
|
-
applyOverlaySurface(overlay, "frameless");
|
|
113
|
-
return {
|
|
114
|
-
toolId: this.toolId,
|
|
115
|
-
button,
|
|
116
|
-
elements: [{ element: overlay, mount: "after-buttons" }],
|
|
117
|
-
sync: () => {
|
|
118
|
-
syncButtonAndOverlayVisibility({
|
|
119
|
-
button,
|
|
120
|
-
overlay,
|
|
121
|
-
isActive: visibility.isActive,
|
|
122
|
-
});
|
|
123
|
-
if (toolbarContext.toolkitCoordinator) {
|
|
124
|
-
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
subscribeActive: visibility.subscribeActive,
|
|
128
|
-
};
|
|
76
|
+
return renderOverlayToolbar(this, context, toolbarContext, {
|
|
77
|
+
surface: "frameless",
|
|
78
|
+
handsOverCoordinator: true,
|
|
79
|
+
});
|
|
129
80
|
},
|
|
130
81
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `renderToolbar` shape shared by every packaged capability that puts one
|
|
3
|
+
* overlay behind one toolbar button.
|
|
4
|
+
*
|
|
5
|
+
* Seven registrations had this inline. What actually varies between them is the
|
|
6
|
+
* three options below and nothing else: whether the overlay paints its own chrome
|
|
7
|
+
* or takes a draggable window, that window's size, and whether the overlay needs
|
|
8
|
+
* the coordinator re-handed on each sync. Everything else — the visibility
|
|
9
|
+
* binding, the button, the component-override lookup, the `tool-id` attribute, the
|
|
10
|
+
* sync — was identical, which is why one rule landing in three spellings was
|
|
11
|
+
* possible: the shell title was `resolveToolRegistrationName` in two files,
|
|
12
|
+
* `displayName` in a third and a raw `t(key)` in a fourth.
|
|
13
|
+
*
|
|
14
|
+
* Catalog keys are derived from `toolId`, which held at all seven sites:
|
|
15
|
+
* `tools.<toolId>.buttonA11y` and `tools.<toolId>.tooltip`. A capability needing a
|
|
16
|
+
* different prefix — the dictionary variants, which compose several capabilities
|
|
17
|
+
* off one element — keeps its own renderer.
|
|
18
|
+
*/
|
|
19
|
+
import { type ToolContext, type ToolRegistration, type ToolToolbarRenderResult, type ToolbarContext } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
20
|
+
/** A window's geometry. Absent for overlays that paint their own chrome. */
|
|
21
|
+
export interface OverlayToolShell {
|
|
22
|
+
resizable: boolean;
|
|
23
|
+
initialWidth: number;
|
|
24
|
+
initialHeight: number;
|
|
25
|
+
minWidth: number;
|
|
26
|
+
minHeight: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RenderOverlayToolbarOptions {
|
|
29
|
+
/**
|
|
30
|
+
* `"frameless"` for an overlay that draws its own surface — a ruler or a line
|
|
31
|
+
* reader sits on the content rather than in a panel.
|
|
32
|
+
*/
|
|
33
|
+
surface?: "frameless";
|
|
34
|
+
/** Present when the capability opens in a draggable, closeable window. */
|
|
35
|
+
shell?: OverlayToolShell;
|
|
36
|
+
/**
|
|
37
|
+
* Re-hand the toolkit coordinator on every sync. Overlays that take part in
|
|
38
|
+
* stacking or visibility restore need it; the two that only render their own
|
|
39
|
+
* model do not, and giving them a property they ignore is not free to assume.
|
|
40
|
+
*/
|
|
41
|
+
handsOverCoordinator?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function renderOverlayToolbar(registration: ToolRegistration, context: ToolContext, toolbarContext: ToolbarContext, options?: RenderOverlayToolbarOptions): ToolToolbarRenderResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `renderToolbar` shape shared by every packaged capability that puts one
|
|
3
|
+
* overlay behind one toolbar button.
|
|
4
|
+
*
|
|
5
|
+
* Seven registrations had this inline. What actually varies between them is the
|
|
6
|
+
* three options below and nothing else: whether the overlay paints its own chrome
|
|
7
|
+
* or takes a draggable window, that window's size, and whether the overlay needs
|
|
8
|
+
* the coordinator re-handed on each sync. Everything else — the visibility
|
|
9
|
+
* binding, the button, the component-override lookup, the `tool-id` attribute, the
|
|
10
|
+
* sync — was identical, which is why one rule landing in three spellings was
|
|
11
|
+
* possible: the shell title was `resolveToolRegistrationName` in two files,
|
|
12
|
+
* `displayName` in a third and a raw `t(key)` in a fourth.
|
|
13
|
+
*
|
|
14
|
+
* Catalog keys are derived from `toolId`, which held at all seven sites:
|
|
15
|
+
* `tools.<toolId>.buttonA11y` and `tools.<toolId>.tooltip`. A capability needing a
|
|
16
|
+
* different prefix — the dictionary variants, which compose several capabilities
|
|
17
|
+
* off one element — keeps its own renderer.
|
|
18
|
+
*/
|
|
19
|
+
import { applyOverlaySurface, createScopedVisibilityBinding, createToolElement, resolveToolRegistrationName, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
20
|
+
export function renderOverlayToolbar(registration, context, toolbarContext, options = {}) {
|
|
21
|
+
const { toolId } = registration;
|
|
22
|
+
const visibility = createScopedVisibilityBinding(toolId, toolbarContext);
|
|
23
|
+
const button = {
|
|
24
|
+
toolId,
|
|
25
|
+
label: registration.name,
|
|
26
|
+
icon: typeof registration.icon === "function"
|
|
27
|
+
? registration.icon(context)
|
|
28
|
+
: registration.icon,
|
|
29
|
+
disabled: false,
|
|
30
|
+
ariaLabel: toolbarContext.i18n.t(`tools.${toolId}.buttonA11y`),
|
|
31
|
+
tooltip: toolbarContext.i18n.t(`tools.${toolId}.tooltip`),
|
|
32
|
+
onClick: () => toolbarContext.toggleTool(toolId),
|
|
33
|
+
active: visibility.isActive(),
|
|
34
|
+
};
|
|
35
|
+
const componentOverrides = toolbarContext.componentOverrides ??
|
|
36
|
+
{};
|
|
37
|
+
const overlay = createToolElement(toolId, context, toolbarContext, componentOverrides);
|
|
38
|
+
overlay.setAttribute("tool-id", visibility.fullToolId);
|
|
39
|
+
if (options.surface === "frameless") {
|
|
40
|
+
applyOverlaySurface(overlay, "frameless");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
toolId,
|
|
44
|
+
button,
|
|
45
|
+
elements: [
|
|
46
|
+
{
|
|
47
|
+
element: overlay,
|
|
48
|
+
mount: "after-buttons",
|
|
49
|
+
...(options.shell
|
|
50
|
+
? {
|
|
51
|
+
shell: {
|
|
52
|
+
// Through `nameKey`, so a window's title tracks the interface
|
|
53
|
+
// locale the way its button already does.
|
|
54
|
+
title: resolveToolRegistrationName(registration, toolbarContext.i18n),
|
|
55
|
+
draggable: true,
|
|
56
|
+
closeable: true,
|
|
57
|
+
...options.shell,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
sync: () => {
|
|
64
|
+
syncButtonAndOverlayVisibility({
|
|
65
|
+
button,
|
|
66
|
+
overlay,
|
|
67
|
+
isActive: visibility.isActive,
|
|
68
|
+
});
|
|
69
|
+
if (options.handsOverCoordinator && toolbarContext.toolkitCoordinator) {
|
|
70
|
+
overlay.toolkitCoordinator = toolbarContext.toolkitCoordinator;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
subscribeActive: visibility.subscribeActive,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which capabilities appear as actions on the learner's current selection.
|
|
3
|
+
*
|
|
4
|
+
* The one place that pairs a selection gateway to the tools it can open. The gateway
|
|
5
|
+
* renders what it is handed and names nothing; the tools know nothing about
|
|
6
|
+
* selections. Composition is the only layer allowed to know both, which is why this
|
|
7
|
+
* list lives here rather than in either of them.
|
|
8
|
+
*
|
|
9
|
+
* A selection is a shortcut, never the only way in. Chromium does not extend a
|
|
10
|
+
* selection with Shift+Arrow in non-editable content unless caret browsing is on —
|
|
11
|
+
* an OS toggle that does not exist on mobile — so a sighted keyboard-only learner
|
|
12
|
+
* cannot originate one at all. Every tool named here also opens from the toolbar and
|
|
13
|
+
* carries its own term field.
|
|
14
|
+
*/
|
|
15
|
+
import type { ToolSelectionAction, ToolSurfaceServices } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
16
|
+
/**
|
|
17
|
+
* Actions for the gateway to render, given the services it was handed.
|
|
18
|
+
*
|
|
19
|
+
* Rebuilt on every sync rather than captured once: a host calling
|
|
20
|
+
* `updateAssessment` mid-session swaps the coordinator without remounting the
|
|
21
|
+
* gateway, and an action closing over the previous one would open a tool in a
|
|
22
|
+
* session the learner has left.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSelectionActions(services: ToolSurfaceServices): ToolSelectionAction[];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which capabilities appear as actions on the learner's current selection.
|
|
3
|
+
*
|
|
4
|
+
* The one place that pairs a selection gateway to the tools it can open. The gateway
|
|
5
|
+
* renders what it is handed and names nothing; the tools know nothing about
|
|
6
|
+
* selections. Composition is the only layer allowed to know both, which is why this
|
|
7
|
+
* list lives here rather than in either of them.
|
|
8
|
+
*
|
|
9
|
+
* A selection is a shortcut, never the only way in. Chromium does not extend a
|
|
10
|
+
* selection with Shift+Arrow in non-editable content unless caret browsing is on —
|
|
11
|
+
* an OS toggle that does not exist on mobile — so a sighted keyboard-only learner
|
|
12
|
+
* cannot originate one at all. Every tool named here also opens from the toolbar and
|
|
13
|
+
* carries its own term field.
|
|
14
|
+
*/
|
|
15
|
+
import { resolveFallbackToolIcon } from "@pie-players/pie-assessment-toolkit/tools/internal";
|
|
16
|
+
function asRequestCapable(coordinator) {
|
|
17
|
+
const candidate = coordinator;
|
|
18
|
+
if (typeof candidate?.canRequestTool !== "function")
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof candidate.requestTool !== "function")
|
|
21
|
+
return null;
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
const SELECTION_ACTION_SPECS = [
|
|
25
|
+
{
|
|
26
|
+
toolId: "dictionary",
|
|
27
|
+
label: "Look up selected text in dictionary",
|
|
28
|
+
tooltip: "Look up",
|
|
29
|
+
icon: "book-open",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
toolId: "pictureDictionary",
|
|
33
|
+
label: "Show pictures for selected text",
|
|
34
|
+
tooltip: "Show pictures",
|
|
35
|
+
icon: "photo",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Actions for the gateway to render, given the services it was handed.
|
|
40
|
+
*
|
|
41
|
+
* Rebuilt on every sync rather than captured once: a host calling
|
|
42
|
+
* `updateAssessment` mid-session swaps the coordinator without remounting the
|
|
43
|
+
* gateway, and an action closing over the previous one would open a tool in a
|
|
44
|
+
* session the learner has left.
|
|
45
|
+
*/
|
|
46
|
+
export function buildSelectionActions(services) {
|
|
47
|
+
const coordinator = asRequestCapable(services.toolkitCoordinator);
|
|
48
|
+
if (!coordinator)
|
|
49
|
+
return [];
|
|
50
|
+
return SELECTION_ACTION_SPECS.map((spec) => {
|
|
51
|
+
const iconSvg = resolveFallbackToolIcon(spec.icon) ?? undefined;
|
|
52
|
+
return {
|
|
53
|
+
id: spec.toolId,
|
|
54
|
+
label: spec.label,
|
|
55
|
+
tooltip: spec.tooltip,
|
|
56
|
+
iconSvg,
|
|
57
|
+
// Asked per selection. A tool the PNP does not grant, or that no toolbar in
|
|
58
|
+
// this section hosts, is absent rather than present and inert.
|
|
59
|
+
isAvailable: () => coordinator.canRequestTool(spec.toolId),
|
|
60
|
+
run: (selection) => {
|
|
61
|
+
const term = selection.text.trim();
|
|
62
|
+
if (!term)
|
|
63
|
+
return;
|
|
64
|
+
// Both panels read the selected text from `term`. No level is named, so the
|
|
65
|
+
// request takes whichever toolbar hosts the tool, preferring section scope.
|
|
66
|
+
coordinator.requestTool({
|
|
67
|
+
toolId: spec.toolId,
|
|
68
|
+
params: { term },
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|