@pie-players/pie-section-player 0.2.12 → 0.2.13
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/README.md +28 -568
- package/dist/component-definitions.d.ts +0 -3
- package/dist/component-definitions.d.ts.map +1 -1
- package/dist/components/section-player-vertical-element.d.ts +2 -0
- package/dist/components/section-player-vertical-element.d.ts.map +1 -0
- package/dist/components/shared/composition.d.ts +9 -0
- package/dist/components/shared/composition.d.ts.map +1 -0
- package/dist/components/shared/player-action.d.ts +18 -0
- package/dist/components/shared/player-action.d.ts.map +1 -0
- package/dist/components/shared/player-preload.d.ts +37 -0
- package/dist/components/shared/player-preload.d.ts.map +1 -0
- package/dist/components/shared/section-player-runtime.d.ts +104 -0
- package/dist/components/shared/section-player-runtime.d.ts.map +1 -0
- package/dist/components/shared/section-player-view-state.d.ts +24 -0
- package/dist/components/shared/section-player-view-state.d.ts.map +1 -0
- package/dist/controllers/SectionContentService.d.ts.map +1 -1
- package/dist/controllers/SectionController.d.ts +5 -1
- package/dist/controllers/SectionController.d.ts.map +1 -1
- package/dist/controllers/SectionSessionService.d.ts +0 -1
- package/dist/controllers/SectionSessionService.d.ts.map +1 -1
- package/dist/controllers/toolkit-section-contracts.d.ts +2 -28
- package/dist/controllers/toolkit-section-contracts.d.ts.map +1 -1
- package/dist/controllers/types.d.ts +28 -1
- package/dist/controllers/types.d.ts.map +1 -1
- package/dist/pie-item-player-B1iGN63e.js +6189 -0
- package/dist/pie-section-player.d.ts +0 -8
- package/dist/pie-section-player.d.ts.map +1 -1
- package/dist/pie-section-player.js +56558 -11
- package/dist/player-preload-CQVG0Bih.js +705 -0
- package/dist/utils/player-preload.d.ts +2 -0
- package/dist/utils/player-preload.d.ts.map +1 -0
- package/dist/utils/player-preload.js +8 -0
- package/package.json +23 -32
- package/src/components/ItemShellElement.svelte +10 -1
- package/src/components/PieSectionPlayerBaseElement.svelte +21 -78
- package/src/components/PieSectionPlayerSplitPaneElement.svelte +236 -295
- package/src/components/PieSectionPlayerVerticalElement.svelte +424 -0
- package/src/components/shared/SectionItemCard.svelte +92 -0
- package/src/components/shared/SectionPassageCard.svelte +88 -0
- package/dist/ItemRenderer-MsjF_Beu.js +0 -467
- package/dist/PieItemModeLayoutElement-D7oTzA9T.js +0 -316
- package/dist/PieSplitPanelLayoutElement-GUtJ_NlF.js +0 -246
- package/dist/PieVerticalLayoutElement-BoA3FO5g.js +0 -194
- package/dist/controllers/SectionToolkitService.d.ts +0 -24
- package/dist/controllers/SectionToolkitService.d.ts.map +0 -1
- package/dist/controllers/SessionPersistenceStrategy.d.ts +0 -15
- package/dist/controllers/SessionPersistenceStrategy.d.ts.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/pie-section-player-DJ5NcwdT.js +0 -17078
- package/dist/runtime/runtime-event-guards.d.ts +0 -4
- package/dist/runtime/runtime-event-guards.d.ts.map +0 -1
- package/src/PieSectionPlayer.svelte +0 -826
- package/src/components/ItemModeLayout.svelte +0 -172
- package/src/components/ItemNavigation.svelte +0 -96
- package/src/components/ItemPlayerBridge.svelte +0 -110
- package/src/components/ItemRenderer.svelte +0 -248
- package/src/components/ItemShell.svelte +0 -86
- package/src/components/layout-elements/PieItemModeLayoutElement.svelte +0 -47
- package/src/components/layout-elements/PieSplitPanelLayoutElement.svelte +0 -62
- package/src/components/layout-elements/PieVerticalLayoutElement.svelte +0 -41
- package/src/components/layouts/SplitPanelLayout.svelte +0 -385
- package/src/components/layouts/VerticalLayout.svelte +0 -193
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
pie-section-player Custom Element
|
|
3
|
-
|
|
4
|
-
A web component for rendering QTI 3.0 assessment sections with passages and items.
|
|
5
|
-
Supports two modes based on keepTogether attribute:
|
|
6
|
-
- keepTogether=true: Page mode (all items visible with passages)
|
|
7
|
-
- keepTogether=false: Item mode (one item at a time)
|
|
8
|
-
|
|
9
|
-
Usage:
|
|
10
|
-
<pie-section-player
|
|
11
|
-
section='{"identifier":"section-1","keepTogether":true,...}'
|
|
12
|
-
mode="gather"
|
|
13
|
-
view="candidate"
|
|
14
|
-
bundle-host="https://cdn.pie.org">
|
|
15
|
-
</pie-section-player>
|
|
16
|
-
|
|
17
|
-
Events:
|
|
18
|
-
- section-loaded: Fired when section is loaded and ready
|
|
19
|
-
- item-changed: Fired when current item changes (item mode only)
|
|
20
|
-
- section-complete: Fired when all items completed
|
|
21
|
-
- player-error: Fired on errors
|
|
22
|
-
- toolkit-coordinator-ready: Fired when coordinator is resolved
|
|
23
|
-
-->
|
|
24
|
-
<svelte:options
|
|
25
|
-
customElement={{
|
|
26
|
-
tag: "pie-section-player",
|
|
27
|
-
shadow: "open",
|
|
28
|
-
props: {
|
|
29
|
-
// Core props
|
|
30
|
-
section: { attribute: "section", type: "Object" },
|
|
31
|
-
env: { attribute: "env", type: "Object" },
|
|
32
|
-
view: { attribute: "view", type: "String" },
|
|
33
|
-
layout: { attribute: "layout", type: "String" },
|
|
34
|
-
|
|
35
|
-
// Styling
|
|
36
|
-
customClassName: { attribute: "custom-class-name", type: "String" },
|
|
37
|
-
|
|
38
|
-
// Tools toolbar position
|
|
39
|
-
toolbarPosition: { attribute: "toolbar-position", type: "String" },
|
|
40
|
-
showToolbar: { attribute: "show-toolbar", type: "Boolean" },
|
|
41
|
-
|
|
42
|
-
// Debug
|
|
43
|
-
debug: { attribute: "debug", type: "String" },
|
|
44
|
-
|
|
45
|
-
// Toolkit coordinator (JS property, not attribute)
|
|
46
|
-
toolkitCoordinator: { type: "Object", reflect: false },
|
|
47
|
-
// Host-provided web component definitions
|
|
48
|
-
layoutDefinitions: { type: "Object", reflect: false },
|
|
49
|
-
},
|
|
50
|
-
}}
|
|
51
|
-
/>
|
|
52
|
-
|
|
53
|
-
<script lang="ts">
|
|
54
|
-
import {
|
|
55
|
-
assessmentToolkitRuntimeContext,
|
|
56
|
-
ToolkitCoordinator,
|
|
57
|
-
type AssessmentToolkitRuntimeContext,
|
|
58
|
-
} from "@pie-players/pie-assessment-toolkit";
|
|
59
|
-
import { ContextProvider, ContextRoot } from "@pie-players/pie-context";
|
|
60
|
-
import {
|
|
61
|
-
DEFAULT_LAYOUT_DEFINITIONS,
|
|
62
|
-
DEFAULT_PLAYER_DEFINITIONS,
|
|
63
|
-
mergeComponentDefinitions,
|
|
64
|
-
type ComponentDefinition,
|
|
65
|
-
} from "./component-definitions.js";
|
|
66
|
-
import {
|
|
67
|
-
type ElementLoaderInterface,
|
|
68
|
-
IifeElementLoader,
|
|
69
|
-
} from "@pie-players/pie-players-shared";
|
|
70
|
-
import type {
|
|
71
|
-
ItemEntity,
|
|
72
|
-
PassageEntity,
|
|
73
|
-
AssessmentSection,
|
|
74
|
-
RubricBlock,
|
|
75
|
-
} from "@pie-players/pie-players-shared";
|
|
76
|
-
import { onMount } from "svelte";
|
|
77
|
-
import { SectionController } from "./controllers/SectionController.js";
|
|
78
|
-
import { SectionToolkitService } from "./controllers/SectionToolkitService.js";
|
|
79
|
-
import type { SectionCompositionModel, SectionViewModel } from "./controllers/types.js";
|
|
80
|
-
|
|
81
|
-
type SectionPlayerRuntimeContext = AssessmentToolkitRuntimeContext & {
|
|
82
|
-
reportSessionChanged?: (itemId: string, detail: unknown) => void;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const isBrowser = typeof window !== "undefined";
|
|
86
|
-
|
|
87
|
-
// Props
|
|
88
|
-
let {
|
|
89
|
-
section = null as AssessmentSection | null,
|
|
90
|
-
env = { mode: "gather", role: "student" } as {
|
|
91
|
-
mode: "gather" | "view" | "evaluate" | "author";
|
|
92
|
-
role: "student" | "instructor";
|
|
93
|
-
},
|
|
94
|
-
view = "candidate" as
|
|
95
|
-
| "candidate"
|
|
96
|
-
| "scorer"
|
|
97
|
-
| "author"
|
|
98
|
-
| "proctor"
|
|
99
|
-
| "testConstructor"
|
|
100
|
-
| "tutor",
|
|
101
|
-
layout = "split-panel",
|
|
102
|
-
customClassName = "",
|
|
103
|
-
toolbarPosition = "right" as "top" | "right" | "bottom" | "left" | "none",
|
|
104
|
-
showToolbar = true,
|
|
105
|
-
debug = "" as string | boolean,
|
|
106
|
-
|
|
107
|
-
// Toolkit coordinator (host may provide; section player creates one lazily if absent)
|
|
108
|
-
toolkitCoordinator = null as ToolkitCoordinator | null,
|
|
109
|
-
layoutDefinitions = {} as Partial<Record<string, ComponentDefinition>>,
|
|
110
|
-
|
|
111
|
-
// Event handlers
|
|
112
|
-
onsessionchanged = null as ((detail: any) => void) | null,
|
|
113
|
-
} = $props();
|
|
114
|
-
|
|
115
|
-
type ToolkitCoordinatorReadyDetail = {
|
|
116
|
-
toolkitCoordinator: ToolkitCoordinator;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const EMPTY_VIEW_MODEL: SectionViewModel = {
|
|
120
|
-
passages: [],
|
|
121
|
-
items: [],
|
|
122
|
-
rubricBlocks: [],
|
|
123
|
-
instructions: [],
|
|
124
|
-
adapterItemRefs: [],
|
|
125
|
-
currentItemIndex: 0,
|
|
126
|
-
isPageMode: false,
|
|
127
|
-
};
|
|
128
|
-
const EMPTY_COMPOSITION_MODEL: SectionCompositionModel = {
|
|
129
|
-
section: null,
|
|
130
|
-
assessmentItemRefs: [],
|
|
131
|
-
passages: [],
|
|
132
|
-
items: [],
|
|
133
|
-
rubricBlocks: [],
|
|
134
|
-
instructions: [],
|
|
135
|
-
currentItemIndex: 0,
|
|
136
|
-
currentItem: null,
|
|
137
|
-
isPageMode: false,
|
|
138
|
-
itemSessionsByItemId: {},
|
|
139
|
-
testAttemptSession: null,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
let ownedToolkitCoordinator: ToolkitCoordinator | null = null;
|
|
143
|
-
let fallbackAssessmentId: string | null = null;
|
|
144
|
-
let fallbackSectionId: string | null = null;
|
|
145
|
-
let lastNotifiedCoordinator = $state<ToolkitCoordinator | null>(null);
|
|
146
|
-
let sectionController = $state<SectionController | null>(null);
|
|
147
|
-
let sectionControllerVersion = $state(0);
|
|
148
|
-
const sectionToolkitService = new SectionToolkitService();
|
|
149
|
-
let lastLoadedSignature = $state<string | null>(null);
|
|
150
|
-
let lastControllerInputKey = $state<string | null>(null);
|
|
151
|
-
let lastControllerCoordinator = $state<ToolkitCoordinator | null>(null);
|
|
152
|
-
|
|
153
|
-
function getFallbackAssessmentId(): string {
|
|
154
|
-
if (!fallbackAssessmentId) {
|
|
155
|
-
fallbackAssessmentId = `anon_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
156
|
-
}
|
|
157
|
-
return fallbackAssessmentId;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function getFallbackSectionId(): string {
|
|
161
|
-
if (!fallbackSectionId) {
|
|
162
|
-
// Use deterministic fallback so host widgets can resolve the same controller key.
|
|
163
|
-
fallbackSectionId = `section-${assessmentId || "default"}`;
|
|
164
|
-
}
|
|
165
|
-
return fallbackSectionId;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const preferredAssessmentId = $derived.by(() => {
|
|
169
|
-
if (section?.identifier) return section.identifier;
|
|
170
|
-
return null;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
function ensureOwnedCoordinator(): ToolkitCoordinator {
|
|
174
|
-
if (!ownedToolkitCoordinator) {
|
|
175
|
-
ownedToolkitCoordinator = new ToolkitCoordinator({
|
|
176
|
-
assessmentId: preferredAssessmentId ?? getFallbackAssessmentId(),
|
|
177
|
-
lazyInit: true,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
return ownedToolkitCoordinator;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const coordinator = $derived.by(
|
|
184
|
-
() => (toolkitCoordinator as ToolkitCoordinator | null) ?? ensureOwnedCoordinator(),
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
$effect(() => {
|
|
188
|
-
if (!coordinator || coordinator === lastNotifiedCoordinator) return;
|
|
189
|
-
lastNotifiedCoordinator = coordinator;
|
|
190
|
-
const detail: ToolkitCoordinatorReadyDetail = {
|
|
191
|
-
toolkitCoordinator: coordinator,
|
|
192
|
-
};
|
|
193
|
-
emitSectionEvent("toolkit-coordinator-ready", detail);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// Extract services from coordinator
|
|
197
|
-
const services = $derived.by(() => coordinator.getServiceBundle());
|
|
198
|
-
const assessmentId = $derived(coordinator.assessmentId);
|
|
199
|
-
|
|
200
|
-
// Generate or extract sectionId
|
|
201
|
-
const sectionId = $derived.by(() => {
|
|
202
|
-
if (section?.identifier) return section.identifier;
|
|
203
|
-
return getFallbackSectionId();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// State
|
|
207
|
-
let isLoading = $state(false);
|
|
208
|
-
let error = $state<string | null>(null);
|
|
209
|
-
|
|
210
|
-
// Element loading state
|
|
211
|
-
let elementsLoaded = $state(false);
|
|
212
|
-
|
|
213
|
-
// TTS error state
|
|
214
|
-
let ttsError = $state<string | null>(null);
|
|
215
|
-
|
|
216
|
-
let pageLayoutElement = $state<HTMLElement | null>(null);
|
|
217
|
-
let rootElement = $state<HTMLElement | null>(null);
|
|
218
|
-
let runtimeContextProvider: ContextProvider<
|
|
219
|
-
typeof assessmentToolkitRuntimeContext
|
|
220
|
-
> | null = null;
|
|
221
|
-
let runtimeContextRoot: ContextRoot | null = null;
|
|
222
|
-
|
|
223
|
-
function getHostElement(): HTMLElement | null {
|
|
224
|
-
if (!rootElement) return null;
|
|
225
|
-
const rootNode = rootElement.getRootNode();
|
|
226
|
-
if (rootNode && "host" in rootNode) {
|
|
227
|
-
return (rootNode as ShadowRoot).host as HTMLElement;
|
|
228
|
-
}
|
|
229
|
-
return rootElement.parentElement as HTMLElement | null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function emitSectionEvent(name: string, detail: unknown): void {
|
|
233
|
-
const event = new CustomEvent(name, {
|
|
234
|
-
detail,
|
|
235
|
-
bubbles: true,
|
|
236
|
-
composed: true,
|
|
237
|
-
});
|
|
238
|
-
const host = getHostElement();
|
|
239
|
-
if (host) {
|
|
240
|
-
host.dispatchEvent(event);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
dispatchEvent(event);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function bumpControllerState(): void {
|
|
247
|
-
sectionControllerVersion += 1;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Extract mode from env for convenience
|
|
251
|
-
let mode = $derived(env.mode);
|
|
252
|
-
let runtimeContextValue = $derived.by(
|
|
253
|
-
(): SectionPlayerRuntimeContext => ({
|
|
254
|
-
toolkitCoordinator: coordinator,
|
|
255
|
-
toolCoordinator: coordinator.toolCoordinator,
|
|
256
|
-
ttsService: services.ttsService,
|
|
257
|
-
highlightCoordinator: services.highlightCoordinator,
|
|
258
|
-
catalogResolver: services.catalogResolver,
|
|
259
|
-
elementToolStateStore: services.elementToolStateStore,
|
|
260
|
-
assessmentId,
|
|
261
|
-
sectionId,
|
|
262
|
-
itemPlayer: {
|
|
263
|
-
type: "iife",
|
|
264
|
-
tagName: resolvedPlayerTag,
|
|
265
|
-
isDefault: true,
|
|
266
|
-
},
|
|
267
|
-
reportSessionChanged: (itemId: string, detail: unknown) =>
|
|
268
|
-
handleItemSessionChanged(itemId, detail),
|
|
269
|
-
}),
|
|
270
|
-
);
|
|
271
|
-
let resolvedPlayerDefinition = $derived.by(
|
|
272
|
-
() => DEFAULT_PLAYER_DEFINITIONS["iife"],
|
|
273
|
-
);
|
|
274
|
-
let resolvedPlayerTag = $derived(
|
|
275
|
-
resolvedPlayerDefinition?.tagName || "pie-iife-player",
|
|
276
|
-
);
|
|
277
|
-
let mergedLayoutDefinitions = $derived.by(() =>
|
|
278
|
-
mergeComponentDefinitions(DEFAULT_LAYOUT_DEFINITIONS, layoutDefinitions),
|
|
279
|
-
);
|
|
280
|
-
let resolvedLayout = $derived(layout);
|
|
281
|
-
let resolvedLayoutDefinition = $derived.by(
|
|
282
|
-
() =>
|
|
283
|
-
mergedLayoutDefinitions[resolvedLayout] ||
|
|
284
|
-
mergedLayoutDefinitions["split-panel"],
|
|
285
|
-
);
|
|
286
|
-
let resolvedLayoutTag = $derived(
|
|
287
|
-
resolvedLayoutDefinition?.tagName || "pie-split-panel-layout",
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
const controllerViewModel = $derived.by(() => {
|
|
291
|
-
sectionControllerVersion;
|
|
292
|
-
return sectionController?.getViewModel() || EMPTY_VIEW_MODEL;
|
|
293
|
-
});
|
|
294
|
-
let passages = $derived(controllerViewModel.passages);
|
|
295
|
-
let items = $derived(controllerViewModel.items);
|
|
296
|
-
let rubricBlocks = $derived(controllerViewModel.rubricBlocks);
|
|
297
|
-
let currentItemIndex = $derived(controllerViewModel.currentItemIndex);
|
|
298
|
-
let isPageMode = $derived(controllerViewModel.isPageMode);
|
|
299
|
-
let compositionModel = $derived.by(() => {
|
|
300
|
-
sectionControllerVersion;
|
|
301
|
-
return sectionController?.getCompositionModel() || EMPTY_COMPOSITION_MODEL;
|
|
302
|
-
});
|
|
303
|
-
const navigationState = $derived.by(() => {
|
|
304
|
-
sectionControllerVersion;
|
|
305
|
-
return (
|
|
306
|
-
sectionController?.getNavigationState(isLoading) || {
|
|
307
|
-
currentIndex: currentItemIndex,
|
|
308
|
-
totalItems: items.length,
|
|
309
|
-
canNext: !isPageMode && currentItemIndex < items.length - 1,
|
|
310
|
-
canPrevious: !isPageMode && currentItemIndex > 0,
|
|
311
|
-
isLoading,
|
|
312
|
-
}
|
|
313
|
-
);
|
|
314
|
-
});
|
|
315
|
-
let canNavigateNext = $derived(navigationState.canNext);
|
|
316
|
-
let canNavigatePrevious = $derived(navigationState.canPrevious);
|
|
317
|
-
// Navigate to item (item mode only)
|
|
318
|
-
function navigateToItem(index: number): void {
|
|
319
|
-
if (!sectionController) return;
|
|
320
|
-
const result = sectionController.navigateToItem(index);
|
|
321
|
-
if (!result) return;
|
|
322
|
-
bumpControllerState();
|
|
323
|
-
emitSectionEvent("item-changed", result.eventDetail);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Public navigation methods are intentionally intra-section (item mode only).
|
|
327
|
-
// Cross-section/page navigation belongs to the higher-level assessment player.
|
|
328
|
-
export function navigateNext() {
|
|
329
|
-
if (canNavigateNext) {
|
|
330
|
-
navigateToItem(currentItemIndex + 1);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function navigatePrevious() {
|
|
335
|
-
if (canNavigatePrevious) {
|
|
336
|
-
navigateToItem(currentItemIndex - 1);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function getNavigationState() {
|
|
341
|
-
return navigationState;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Lifecycle
|
|
345
|
-
onMount(() => {
|
|
346
|
-
if (isBrowser) {
|
|
347
|
-
import("@pie-players/pie-section-tools-toolbar").catch((err) => {
|
|
348
|
-
console.error(
|
|
349
|
-
"[PieSectionPlayer] Failed to load section tools toolbar:",
|
|
350
|
-
err,
|
|
351
|
-
);
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
$effect(() => {
|
|
357
|
-
const resolvedCoordinator = coordinator;
|
|
358
|
-
const inputSection = section;
|
|
359
|
-
const inputView = view;
|
|
360
|
-
const inputAssessmentId = assessmentId;
|
|
361
|
-
const inputSectionId = sectionId;
|
|
362
|
-
|
|
363
|
-
let cancelled = false;
|
|
364
|
-
const inputKey = inputSection
|
|
365
|
-
? `${inputAssessmentId}:${inputSectionId}:${inputView}:${inputSection.identifier || ""}`
|
|
366
|
-
: null;
|
|
367
|
-
|
|
368
|
-
if (!inputSection) {
|
|
369
|
-
sectionController = null;
|
|
370
|
-
lastControllerInputKey = null;
|
|
371
|
-
lastControllerCoordinator = null;
|
|
372
|
-
bumpControllerState();
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (
|
|
377
|
-
sectionController &&
|
|
378
|
-
inputKey &&
|
|
379
|
-
lastControllerInputKey === inputKey &&
|
|
380
|
-
lastControllerCoordinator === resolvedCoordinator
|
|
381
|
-
) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
void sectionToolkitService
|
|
386
|
-
.resolveSectionController<SectionController>({
|
|
387
|
-
coordinator: resolvedCoordinator,
|
|
388
|
-
sectionId: inputSectionId,
|
|
389
|
-
input: {
|
|
390
|
-
section: inputSection,
|
|
391
|
-
view: inputView,
|
|
392
|
-
assessmentId: inputAssessmentId,
|
|
393
|
-
sectionId: inputSectionId,
|
|
394
|
-
},
|
|
395
|
-
createDefaultController: () => new SectionController(),
|
|
396
|
-
})
|
|
397
|
-
.then((controller) => {
|
|
398
|
-
if (cancelled) return;
|
|
399
|
-
sectionController = controller;
|
|
400
|
-
lastControllerInputKey = inputKey;
|
|
401
|
-
lastControllerCoordinator = resolvedCoordinator;
|
|
402
|
-
bumpControllerState();
|
|
403
|
-
})
|
|
404
|
-
.catch((err) => {
|
|
405
|
-
if (cancelled) return;
|
|
406
|
-
error = String(err instanceof Error ? err.message : err);
|
|
407
|
-
console.error("[PieSectionPlayer] Failed to resolve section controller:", err);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
return () => {
|
|
411
|
-
cancelled = true;
|
|
412
|
-
};
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
$effect(() => {
|
|
416
|
-
const resolvedCoordinator = coordinator;
|
|
417
|
-
const resolvedSectionId = sectionId;
|
|
418
|
-
return () => {
|
|
419
|
-
void sectionToolkitService.disposeSectionController({
|
|
420
|
-
coordinator: resolvedCoordinator,
|
|
421
|
-
sectionId: resolvedSectionId,
|
|
422
|
-
});
|
|
423
|
-
};
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
$effect(() => {
|
|
427
|
-
if (!section || !sectionController) return;
|
|
428
|
-
const signature =
|
|
429
|
-
`${assessmentId}:${sectionId}:${view}:` +
|
|
430
|
-
`${items.length}:${passages.length}:${isPageMode}`;
|
|
431
|
-
if (signature === lastLoadedSignature) return;
|
|
432
|
-
lastLoadedSignature = signature;
|
|
433
|
-
emitSectionEvent("section-loaded", sectionController.getSectionLoadedEventDetail());
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
// Ensure selected page-mode layout web component is registered.
|
|
437
|
-
$effect(() => {
|
|
438
|
-
resolvedLayoutDefinition?.ensureDefined?.().catch((err) => {
|
|
439
|
-
console.error("[PieSectionPlayer] Failed to load layout component:", err);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Ensure selected player web component is registered.
|
|
444
|
-
$effect(() => {
|
|
445
|
-
resolvedPlayerDefinition?.ensureDefined?.().catch((err) => {
|
|
446
|
-
console.error("[PieSectionPlayer] Failed to load player component:", err);
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// Element pre-loading effect (loads all unique elements before rendering items)
|
|
451
|
-
$effect(() => {
|
|
452
|
-
if (!section) {
|
|
453
|
-
elementsLoaded = false;
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Collect all renderables needing element preloading:
|
|
458
|
-
// - passages (stimulus rubric blocks / linked passages)
|
|
459
|
-
// - assessment items
|
|
460
|
-
// - rubric blocks with PIE passage configs (e.g. instructions/rubrics)
|
|
461
|
-
const additionalRubricPassages = (rubricBlocks || [])
|
|
462
|
-
.map((rb) => rb?.passage)
|
|
463
|
-
.filter((p): p is PassageEntity => !!p && !!p.config);
|
|
464
|
-
const allItems: ItemEntity[] = [...passages, ...items, ...additionalRubricPassages];
|
|
465
|
-
|
|
466
|
-
if (allItems.length === 0) {
|
|
467
|
-
elementsLoaded = true;
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Ensure item renderers do not mount until required PIE bundles are available.
|
|
472
|
-
elementsLoaded = false;
|
|
473
|
-
|
|
474
|
-
const effectiveBundleHost = String(
|
|
475
|
-
resolvedPlayerDefinition?.attributes?.["bundle-host"] || "",
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
// Create the loader from the fixed IIFE player definition.
|
|
479
|
-
let loader: ElementLoaderInterface | null = null;
|
|
480
|
-
|
|
481
|
-
if (effectiveBundleHost) {
|
|
482
|
-
loader = new IifeElementLoader({
|
|
483
|
-
bundleHost: effectiveBundleHost,
|
|
484
|
-
debugEnabled: () => !!debug,
|
|
485
|
-
});
|
|
486
|
-
} else {
|
|
487
|
-
console.warn("[PieSectionPlayer] Missing bundle-host for IIFE element preloader.");
|
|
488
|
-
elementsLoaded = true;
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Load all elements upfront
|
|
493
|
-
loader
|
|
494
|
-
.loadFromItems(allItems, {
|
|
495
|
-
view: mode === "author" ? "author" : "delivery",
|
|
496
|
-
needsControllers: true,
|
|
497
|
-
})
|
|
498
|
-
.then(() => {
|
|
499
|
-
elementsLoaded = true;
|
|
500
|
-
console.log(
|
|
501
|
-
`[PieSectionPlayer] Loaded elements for ${allItems.length} items`,
|
|
502
|
-
);
|
|
503
|
-
})
|
|
504
|
-
.catch((err) => {
|
|
505
|
-
console.error("[PieSectionPlayer] Failed to load elements:", err);
|
|
506
|
-
// Still set loaded to true to allow rendering (items will handle their own errors)
|
|
507
|
-
elementsLoaded = true;
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
// Cleanup
|
|
511
|
-
return () => {
|
|
512
|
-
// Cleanup if needed
|
|
513
|
-
};
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// Listen for TTS errors
|
|
517
|
-
$effect(() => {
|
|
518
|
-
if (!showToolbar) return;
|
|
519
|
-
void coordinator.ensureTTSReady().catch((err: unknown) => {
|
|
520
|
-
console.error("[PieSectionPlayer] Failed to lazily initialize TTS:", err);
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// Listen for TTS errors
|
|
525
|
-
$effect(() => {
|
|
526
|
-
const ttsService = services.ttsService;
|
|
527
|
-
|
|
528
|
-
const handleTTSStateChange = (state: any) => {
|
|
529
|
-
// PlaybackState.ERROR = "error"
|
|
530
|
-
if (state === "error") {
|
|
531
|
-
const errorMsg =
|
|
532
|
-
ttsService.getLastError?.() || "Text-to-speech error occurred";
|
|
533
|
-
ttsError = errorMsg;
|
|
534
|
-
console.error("[PieSectionPlayer] TTS error:", errorMsg);
|
|
535
|
-
} else if (state === "playing" || state === "loading") {
|
|
536
|
-
// Clear error when successfully starting playback
|
|
537
|
-
ttsError = null;
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
ttsService.onStateChange?.("section-player", handleTTSStateChange);
|
|
542
|
-
|
|
543
|
-
// Cleanup
|
|
544
|
-
return () => {
|
|
545
|
-
ttsService.offStateChange?.("section-player", handleTTSStateChange);
|
|
546
|
-
};
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Get instructions from controller-owned content model.
|
|
550
|
-
let instructions = $derived(sectionController?.getInstructions() || []);
|
|
551
|
-
|
|
552
|
-
// Handle session changes from items.
|
|
553
|
-
function handleItemSessionChanged(itemId: string, sessionDetail: any): void {
|
|
554
|
-
if (!sectionController) return;
|
|
555
|
-
const canonicalItemId = sectionController.getCanonicalItemId(itemId);
|
|
556
|
-
const result = sectionController.handleItemSessionChanged(
|
|
557
|
-
canonicalItemId,
|
|
558
|
-
sessionDetail,
|
|
559
|
-
);
|
|
560
|
-
if (!result) return;
|
|
561
|
-
bumpControllerState();
|
|
562
|
-
const eventDetail = result.eventDetail;
|
|
563
|
-
|
|
564
|
-
// Call handler prop if provided (for component callback usage).
|
|
565
|
-
// Keep this as raw detail to avoid double-wrapping CustomEvent.detail.
|
|
566
|
-
if (onsessionchanged) {
|
|
567
|
-
onsessionchanged(eventDetail);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Also dispatch event (for custom element usage)
|
|
571
|
-
emitSectionEvent("session-changed", eventDetail);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Establish runtime context provider at the section-player root.
|
|
575
|
-
$effect(() => {
|
|
576
|
-
if (!rootElement) return;
|
|
577
|
-
const provider = new ContextProvider(rootElement, {
|
|
578
|
-
context: assessmentToolkitRuntimeContext,
|
|
579
|
-
initialValue: runtimeContextValue,
|
|
580
|
-
});
|
|
581
|
-
provider.connect();
|
|
582
|
-
runtimeContextProvider = provider;
|
|
583
|
-
|
|
584
|
-
const root = new ContextRoot(rootElement);
|
|
585
|
-
root.attach();
|
|
586
|
-
runtimeContextRoot = root;
|
|
587
|
-
|
|
588
|
-
return () => {
|
|
589
|
-
runtimeContextRoot?.detach();
|
|
590
|
-
runtimeContextRoot = null;
|
|
591
|
-
runtimeContextProvider?.disconnect();
|
|
592
|
-
runtimeContextProvider = null;
|
|
593
|
-
};
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// Push runtime value updates into the provider.
|
|
597
|
-
$effect(() => {
|
|
598
|
-
if (!runtimeContextProvider) return;
|
|
599
|
-
runtimeContextProvider.setValue(runtimeContextValue);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Bind layout custom element properties imperatively.
|
|
603
|
-
$effect(() => {
|
|
604
|
-
const layoutElement = pageLayoutElement;
|
|
605
|
-
if (!layoutElement) return;
|
|
606
|
-
sectionControllerVersion;
|
|
607
|
-
(layoutElement as any).composition = compositionModel;
|
|
608
|
-
(layoutElement as any).env = env;
|
|
609
|
-
(layoutElement as any).toolbarPosition = toolbarPosition;
|
|
610
|
-
(layoutElement as any).showToolbar = showToolbar;
|
|
611
|
-
(layoutElement as any).onnext = navigateNext;
|
|
612
|
-
(layoutElement as any).onprevious = navigatePrevious;
|
|
613
|
-
});
|
|
614
|
-
</script>
|
|
615
|
-
|
|
616
|
-
<div
|
|
617
|
-
class={`pie-section-player ${customClassName} ${isPageMode ? "pie-section-player--page-mode" : "pie-section-player--item-mode"}`}
|
|
618
|
-
data-assessment-id={assessmentId}
|
|
619
|
-
data-section-id={sectionId}
|
|
620
|
-
bind:this={rootElement}
|
|
621
|
-
>
|
|
622
|
-
{#if error}
|
|
623
|
-
<div class="pie-section-player__error">
|
|
624
|
-
<p>Error loading section: {error}</p>
|
|
625
|
-
</div>
|
|
626
|
-
{:else if section}
|
|
627
|
-
<!-- Instructions -->
|
|
628
|
-
{#if instructions.length > 0}
|
|
629
|
-
<div class="pie-section-player__instructions">
|
|
630
|
-
{#each instructions as rb}
|
|
631
|
-
{#if rb.passage && rb.passage.config}
|
|
632
|
-
<svelte:element
|
|
633
|
-
this={resolvedPlayerTag}
|
|
634
|
-
{...({
|
|
635
|
-
config: JSON.stringify(rb.passage.config),
|
|
636
|
-
env: JSON.stringify({ mode: "view" }),
|
|
637
|
-
"skip-element-loading": true,
|
|
638
|
-
...(resolvedPlayerDefinition?.attributes || {}),
|
|
639
|
-
} as any)}
|
|
640
|
-
></svelte:element>
|
|
641
|
-
{/if}
|
|
642
|
-
{/each}
|
|
643
|
-
</div>
|
|
644
|
-
{/if}
|
|
645
|
-
|
|
646
|
-
<!-- TTS Error Banner -->
|
|
647
|
-
{#if ttsError}
|
|
648
|
-
<div class="pie-section-player__tts-error-banner" role="alert">
|
|
649
|
-
<svg
|
|
650
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
651
|
-
width="20"
|
|
652
|
-
height="20"
|
|
653
|
-
viewBox="0 0 24 24"
|
|
654
|
-
fill="none"
|
|
655
|
-
stroke="currentColor"
|
|
656
|
-
stroke-width="2"
|
|
657
|
-
stroke-linecap="round"
|
|
658
|
-
stroke-linejoin="round"
|
|
659
|
-
>
|
|
660
|
-
<circle cx="12" cy="12" r="10"></circle>
|
|
661
|
-
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
662
|
-
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
663
|
-
</svg>
|
|
664
|
-
<span>Text-to-speech unavailable: {ttsError}</span>
|
|
665
|
-
<button
|
|
666
|
-
class="pie-section-player__tts-error-dismiss"
|
|
667
|
-
onclick={() => (ttsError = null)}
|
|
668
|
-
aria-label="Dismiss error"
|
|
669
|
-
>
|
|
670
|
-
<svg
|
|
671
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
672
|
-
width="16"
|
|
673
|
-
height="16"
|
|
674
|
-
viewBox="0 0 24 24"
|
|
675
|
-
fill="none"
|
|
676
|
-
stroke="currentColor"
|
|
677
|
-
stroke-width="2"
|
|
678
|
-
stroke-linecap="round"
|
|
679
|
-
stroke-linejoin="round"
|
|
680
|
-
>
|
|
681
|
-
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
682
|
-
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
683
|
-
</svg>
|
|
684
|
-
</button>
|
|
685
|
-
</div>
|
|
686
|
-
{/if}
|
|
687
|
-
|
|
688
|
-
<!-- Main content area -->
|
|
689
|
-
<div class="pie-section-player__content">
|
|
690
|
-
{#if elementsLoaded}
|
|
691
|
-
{#key resolvedLayoutTag}
|
|
692
|
-
<svelte:element
|
|
693
|
-
this={resolvedLayoutTag}
|
|
694
|
-
class="pie-section-player__page-layout"
|
|
695
|
-
bind:this={pageLayoutElement}
|
|
696
|
-
>
|
|
697
|
-
</svelte:element>
|
|
698
|
-
{/key}
|
|
699
|
-
{:else}
|
|
700
|
-
<div class="pie-section-player__loading">
|
|
701
|
-
<p>Loading assessment elements...</p>
|
|
702
|
-
</div>
|
|
703
|
-
{/if}
|
|
704
|
-
</div>
|
|
705
|
-
{:else}
|
|
706
|
-
<div class="pie-section-player__loading">
|
|
707
|
-
<p>Loading section...</p>
|
|
708
|
-
</div>
|
|
709
|
-
{/if}
|
|
710
|
-
</div>
|
|
711
|
-
|
|
712
|
-
<style>
|
|
713
|
-
/* In no-shadow custom-element mode, enforce host-like sizing via global tag rule. */
|
|
714
|
-
:global(pie-section-player) {
|
|
715
|
-
display: block;
|
|
716
|
-
width: 100%;
|
|
717
|
-
height: 100%;
|
|
718
|
-
min-height: 0;
|
|
719
|
-
max-height: 100%;
|
|
720
|
-
overflow: hidden;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
:host {
|
|
724
|
-
display: block;
|
|
725
|
-
width: 100%;
|
|
726
|
-
height: 100%;
|
|
727
|
-
min-height: 0;
|
|
728
|
-
max-height: 100%;
|
|
729
|
-
overflow: hidden;
|
|
730
|
-
}
|
|
731
|
-
.pie-section-player {
|
|
732
|
-
display: flex;
|
|
733
|
-
width: 100%;
|
|
734
|
-
height: 100%;
|
|
735
|
-
min-height: 0;
|
|
736
|
-
max-height: 100%;
|
|
737
|
-
overflow: hidden;
|
|
738
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
|
739
|
-
Ubuntu, Cantarell, sans-serif;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/* Main content area takes remaining space */
|
|
743
|
-
.pie-section-player__content {
|
|
744
|
-
flex: 1;
|
|
745
|
-
min-height: 0;
|
|
746
|
-
min-width: 0;
|
|
747
|
-
overflow: hidden;
|
|
748
|
-
display: flex;
|
|
749
|
-
flex-direction: column;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/* Ensure dynamic page-layout custom elements are height-constrained containers. */
|
|
753
|
-
.pie-section-player__page-layout {
|
|
754
|
-
display: block;
|
|
755
|
-
flex: 1;
|
|
756
|
-
height: 100%;
|
|
757
|
-
min-height: 0;
|
|
758
|
-
min-width: 0;
|
|
759
|
-
overflow: hidden;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
.pie-section-player__error {
|
|
763
|
-
padding: 1rem;
|
|
764
|
-
background: var(--pie-incorrect-secondary, #fee);
|
|
765
|
-
border: 1px solid var(--pie-incorrect, #fcc);
|
|
766
|
-
border-radius: 4px;
|
|
767
|
-
color: var(--pie-incorrect-icon, #c00);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
.pie-section-player__loading {
|
|
771
|
-
padding: 2rem;
|
|
772
|
-
text-align: center;
|
|
773
|
-
color: var(--pie-disabled, #666);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
.pie-section-player__instructions {
|
|
777
|
-
margin-bottom: 1.5rem;
|
|
778
|
-
padding: 1rem;
|
|
779
|
-
background: var(--pie-secondary-background, #f5f5f5);
|
|
780
|
-
border-radius: 4px;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
.pie-section-player__tts-error-banner {
|
|
784
|
-
display: flex;
|
|
785
|
-
align-items: center;
|
|
786
|
-
gap: 0.75rem;
|
|
787
|
-
padding: 0.875rem 1rem;
|
|
788
|
-
margin-bottom: 1rem;
|
|
789
|
-
background: var(--pie-secondary-background, #fff3cd);
|
|
790
|
-
border: 1px solid var(--pie-missing, #ffc107);
|
|
791
|
-
border-radius: 4px;
|
|
792
|
-
color: var(--pie-text, #856404);
|
|
793
|
-
font-size: 0.875rem;
|
|
794
|
-
line-height: 1.4;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
.pie-section-player__tts-error-banner svg {
|
|
798
|
-
flex-shrink: 0;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
.pie-section-player__tts-error-banner span {
|
|
802
|
-
flex: 1;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
.pie-section-player__tts-error-dismiss {
|
|
806
|
-
display: flex;
|
|
807
|
-
align-items: center;
|
|
808
|
-
justify-content: center;
|
|
809
|
-
padding: 0.25rem;
|
|
810
|
-
background: transparent;
|
|
811
|
-
border: none;
|
|
812
|
-
border-radius: 2px;
|
|
813
|
-
color: var(--pie-text, #856404);
|
|
814
|
-
cursor: pointer;
|
|
815
|
-
transition: background-color 0.2s;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
.pie-section-player__tts-error-dismiss:hover {
|
|
819
|
-
background: rgba(0, 0, 0, 0.1);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
.pie-section-player__tts-error-dismiss:focus {
|
|
823
|
-
outline: 2px solid var(--pie-focus-checked-border, #856404);
|
|
824
|
-
outline-offset: 2px;
|
|
825
|
-
}
|
|
826
|
-
</style>
|