@pie-players/pie-players-shared 0.3.43 → 0.3.44
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/config/profile.d.ts +0 -1
- package/dist/config/profile.js.map +1 -1
- package/dist/i18n/index.d.ts +0 -3
- package/dist/i18n/index.js +0 -2
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/loader.d.ts +0 -1
- package/dist/i18n/loader.js.map +1 -1
- package/dist/i18n/simple-i18n.d.ts +0 -1
- package/dist/i18n/simple-i18n.js.map +1 -1
- package/dist/i18n/types.d.ts +0 -1
- package/dist/i18n/types.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/dist/instrumentation/debug-panel-stream.d.ts +0 -1
- package/dist/instrumentation/debug-panel-stream.js.map +1 -1
- package/dist/instrumentation/index.d.ts +0 -1
- package/dist/instrumentation/index.js.map +1 -1
- package/dist/instrumentation/provider-guards.d.ts +0 -1
- package/dist/instrumentation/provider-guards.js.map +1 -1
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +0 -1
- package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -1
- package/dist/instrumentation/providers/CompositeInstrumentationProvider.d.ts +0 -1
- package/dist/instrumentation/providers/CompositeInstrumentationProvider.js.map +1 -1
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +0 -1
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -1
- package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.d.ts +0 -1
- package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.js.map +1 -1
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +0 -1
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -1
- package/dist/instrumentation/providers/index.d.ts +0 -1
- package/dist/instrumentation/providers/index.js.map +1 -1
- package/dist/instrumentation/types.d.ts +0 -1
- package/dist/instrumentation/types.js.map +1 -1
- package/dist/loader-config.d.ts +0 -1
- package/dist/loader-config.js.map +1 -1
- package/dist/loaders/ElementLoader.d.ts +0 -1
- package/dist/loaders/ElementLoader.js.map +1 -1
- package/dist/loaders/element-loader-types.d.ts +0 -1
- package/dist/loaders/element-loader-types.js.map +1 -1
- package/dist/loaders/element-loader.d.ts +0 -1
- package/dist/loaders/element-loader.js.map +1 -1
- package/dist/loaders/esm-adapter.d.ts +0 -1
- package/dist/loaders/esm-adapter.js.map +1 -1
- package/dist/loaders/iife-adapter.d.ts +0 -1
- package/dist/loaders/iife-adapter.js.map +1 -1
- package/dist/loaders/index.d.ts +0 -1
- package/dist/loaders/index.js.map +1 -1
- package/dist/object/index.d.ts +0 -1
- package/dist/object/index.js.map +1 -1
- package/dist/pie/asset-handler.d.ts +0 -1
- package/dist/pie/asset-handler.js.map +1 -1
- package/dist/pie/authoring.d.ts +0 -1
- package/dist/pie/authoring.js.map +1 -1
- package/dist/pie/component-context.d.ts +0 -1
- package/dist/pie/component-context.js.map +1 -1
- package/dist/pie/config.d.ts +0 -1
- package/dist/pie/config.js.map +1 -1
- package/dist/pie/configure-initialization.d.ts +0 -1
- package/dist/pie/configure-initialization.js.map +1 -1
- package/dist/pie/correct-response-env.d.ts +0 -1
- package/dist/pie/correct-response-env.js.map +1 -1
- package/dist/pie/custom-element-define.d.ts +0 -1
- package/dist/pie/custom-element-define.js.map +1 -1
- package/dist/pie/index.d.ts +4 -9
- package/dist/pie/index.js +4 -8
- package/dist/pie/index.js.map +1 -1
- package/dist/pie/initialization.d.ts +0 -1
- package/dist/pie/initialization.js.map +1 -1
- package/dist/pie/instrumentation-event-bridge.d.ts +0 -1
- package/dist/pie/instrumentation-event-bridge.js.map +1 -1
- package/dist/pie/instrumentation-event-map.d.ts +0 -1
- package/dist/pie/instrumentation-event-map.js.map +1 -1
- package/dist/pie/instrumentation-provider-resolution.d.ts +0 -1
- package/dist/pie/instrumentation-provider-resolution.js.map +1 -1
- package/dist/pie/item-controller-storage.d.ts +0 -1
- package/dist/pie/item-controller-storage.js.map +1 -1
- package/dist/pie/item-controller.d.ts +0 -1
- package/dist/pie/item-controller.js.map +1 -1
- package/dist/pie/item-session-contract.d.ts +0 -1
- package/dist/pie/item-session-contract.js.map +1 -1
- package/dist/pie/logger.d.ts +0 -1
- package/dist/pie/logger.js.map +1 -1
- package/dist/pie/math-rendering.d.ts +0 -1
- package/dist/pie/math-rendering.js.map +1 -1
- package/dist/pie/overrides.d.ts +0 -1
- package/dist/pie/overrides.js.map +1 -1
- package/dist/pie/player-initializer.d.ts +0 -1
- package/dist/pie/player-initializer.js.map +1 -1
- package/dist/pie/registry.d.ts +0 -1
- package/dist/pie/registry.js.map +1 -1
- package/dist/pie/resource-monitor.d.ts +0 -1
- package/dist/pie/resource-monitor.js.map +1 -1
- package/dist/pie/scoring.d.ts +0 -1
- package/dist/pie/scoring.js.map +1 -1
- package/dist/pie/stage-tracker.d.ts +0 -1
- package/dist/pie/stage-tracker.js.map +1 -1
- package/dist/pie/stages.d.ts +0 -1
- package/dist/pie/stages.js.map +1 -1
- package/dist/pie/tag-names.d.ts +0 -1
- package/dist/pie/tag-names.js.map +1 -1
- package/dist/pie/types.d.ts +0 -1
- package/dist/pie/types.js.map +1 -1
- package/dist/pie/updates.d.ts +0 -1
- package/dist/pie/updates.js.map +1 -1
- package/dist/pie/utils.d.ts +0 -1
- package/dist/pie/utils.js.map +1 -1
- package/dist/player-strategy.d.ts +0 -1
- package/dist/player-strategy.js.map +1 -1
- package/dist/security/index.d.ts +0 -1
- package/dist/security/index.js.map +1 -1
- package/dist/security/sanitize-item-markup.d.ts +0 -1
- package/dist/security/sanitize-item-markup.js.map +1 -1
- package/dist/security/sanitize-svg-icon.d.ts +0 -1
- package/dist/security/sanitize-svg-icon.js.map +1 -1
- package/dist/security/validate-style-url.d.ts +0 -1
- package/dist/security/validate-style-url.js.map +1 -1
- package/dist/security/wrap-overwide-images.d.ts +0 -1
- package/dist/security/wrap-overwide-images.js.map +1 -1
- package/dist/server/npm-registry.d.ts +0 -1
- package/dist/server/npm-registry.js.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/ui/debug-panel-persistence.d.ts +0 -1
- package/dist/ui/debug-panel-persistence.js.map +1 -1
- package/dist/ui/first-focusable.d.ts +0 -1
- package/dist/ui/first-focusable.js.map +1 -1
- package/dist/ui/focus-trap.d.ts +0 -1
- package/dist/ui/focus-trap.js.map +1 -1
- package/dist/ui/safe-storage.d.ts +0 -1
- package/dist/ui/safe-storage.js.map +1 -1
- package/package.json +6 -64
- package/dist/components/PieItemPlayer.svelte +0 -951
- package/dist/components/PiePreviewLayout.svelte +0 -154
- package/dist/components/PiePreviewToggle.svelte +0 -110
- package/dist/components/PieSpinner.svelte +0 -85
- package/dist/components/ToolSettingsButton.svelte +0 -62
- package/dist/components/ToolSettingsPanel.svelte +0 -104
- package/dist/components/index.ts +0 -6
- package/dist/config/profile.d.ts.map +0 -1
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/i18n/loader.d.ts.map +0 -1
- package/dist/i18n/simple-i18n.d.ts.map +0 -1
- package/dist/i18n/types.d.ts.map +0 -1
- package/dist/i18n/use-i18n-standalone.svelte.ts +0 -185
- package/dist/i18n/use-i18n.svelte.ts +0 -164
- package/dist/index.d.ts.map +0 -1
- package/dist/instrumentation/debug-panel-stream.d.ts.map +0 -1
- package/dist/instrumentation/index.d.ts.map +0 -1
- package/dist/instrumentation/provider-guards.d.ts.map +0 -1
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +0 -1
- package/dist/instrumentation/providers/CompositeInstrumentationProvider.d.ts.map +0 -1
- package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +0 -1
- package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.d.ts.map +0 -1
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +0 -1
- package/dist/instrumentation/providers/index.d.ts.map +0 -1
- package/dist/instrumentation/types.d.ts.map +0 -1
- package/dist/loader-config.d.ts.map +0 -1
- package/dist/loaders/ElementLoader.d.ts.map +0 -1
- package/dist/loaders/element-loader-types.d.ts.map +0 -1
- package/dist/loaders/element-loader.d.ts.map +0 -1
- package/dist/loaders/esm-adapter.d.ts.map +0 -1
- package/dist/loaders/iife-adapter.d.ts.map +0 -1
- package/dist/loaders/index.d.ts.map +0 -1
- package/dist/object/index.d.ts.map +0 -1
- package/dist/pie/asset-handler.d.ts.map +0 -1
- package/dist/pie/authoring.d.ts.map +0 -1
- package/dist/pie/component-context.d.ts.map +0 -1
- package/dist/pie/config.d.ts.map +0 -1
- package/dist/pie/configure-initialization.d.ts.map +0 -1
- package/dist/pie/correct-response-env.d.ts.map +0 -1
- package/dist/pie/custom-element-define.d.ts.map +0 -1
- package/dist/pie/index.d.ts.map +0 -1
- package/dist/pie/initialization.d.ts.map +0 -1
- package/dist/pie/instrumentation-event-bridge.d.ts.map +0 -1
- package/dist/pie/instrumentation-event-map.d.ts.map +0 -1
- package/dist/pie/instrumentation-provider-resolution.d.ts.map +0 -1
- package/dist/pie/item-controller-storage.d.ts.map +0 -1
- package/dist/pie/item-controller.d.ts.map +0 -1
- package/dist/pie/item-session-contract.d.ts.map +0 -1
- package/dist/pie/logger.d.ts.map +0 -1
- package/dist/pie/math-rendering.d.ts.map +0 -1
- package/dist/pie/overrides.d.ts.map +0 -1
- package/dist/pie/player-initializer.d.ts.map +0 -1
- package/dist/pie/registry.d.ts.map +0 -1
- package/dist/pie/resource-monitor.d.ts.map +0 -1
- package/dist/pie/scoring.d.ts.map +0 -1
- package/dist/pie/stage-tracker.d.ts.map +0 -1
- package/dist/pie/stages.d.ts.map +0 -1
- package/dist/pie/tag-names.d.ts.map +0 -1
- package/dist/pie/types.d.ts.map +0 -1
- package/dist/pie/updates.d.ts.map +0 -1
- package/dist/pie/use-resource-monitor.svelte.d.ts +0 -56
- package/dist/pie/use-resource-monitor.svelte.d.ts.map +0 -1
- package/dist/pie/use-resource-monitor.svelte.js +0 -176
- package/dist/pie/use-resource-monitor.svelte.js.map +0 -1
- package/dist/pie/utils.d.ts.map +0 -1
- package/dist/player-strategy.d.ts.map +0 -1
- package/dist/security/index.d.ts.map +0 -1
- package/dist/security/sanitize-item-markup.d.ts.map +0 -1
- package/dist/security/sanitize-svg-icon.d.ts.map +0 -1
- package/dist/security/validate-style-url.d.ts.map +0 -1
- package/dist/security/wrap-overwide-images.d.ts.map +0 -1
- package/dist/server/npm-registry.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/ui/debug-panel-persistence.d.ts.map +0 -1
- package/dist/ui/first-focusable.d.ts.map +0 -1
- package/dist/ui/focus-trap.d.ts.map +0 -1
- package/dist/ui/safe-storage.d.ts.map +0 -1
- package/dist/ui/use-promise.svelte.ts +0 -109
|
@@ -1,951 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
PieItemPlayer - Pure PIE Item Renderer
|
|
3
|
-
|
|
4
|
-
This component renders a PIE item using direct PIE elements (no pie-player wrapper).
|
|
5
|
-
It assumes PIE bundles are already loaded via window.pie.
|
|
6
|
-
|
|
7
|
-
Uses the same reactive pattern as PieItemPreview.svelte (proven to work).
|
|
8
|
-
-->
|
|
9
|
-
<script lang="ts">
|
|
10
|
-
import { onDestroy, tick, untrack } from "svelte";
|
|
11
|
-
import { isInstrumentationProvider } from "../instrumentation/provider-guards.js";
|
|
12
|
-
import type { LoaderConfig } from "../loader-config.js";
|
|
13
|
-
import { DEFAULT_LOADER_CONFIG } from "../loader-config.js";
|
|
14
|
-
import {
|
|
15
|
-
buildAuthoringAllowList,
|
|
16
|
-
createDefaultItemMarkupSanitizer,
|
|
17
|
-
type ItemMarkupSanitizer,
|
|
18
|
-
} from "../security/index.js";
|
|
19
|
-
import {
|
|
20
|
-
createDefaultImageDeleteHandler,
|
|
21
|
-
createDefaultImageInsertHandler,
|
|
22
|
-
createDefaultSoundDeleteHandler,
|
|
23
|
-
createDefaultSoundInsertHandler,
|
|
24
|
-
} from "../pie/asset-handler.js";
|
|
25
|
-
import {
|
|
26
|
-
createAuthoringAssetEventManager,
|
|
27
|
-
validateAuthoringModels,
|
|
28
|
-
type AuthoringValidationResult,
|
|
29
|
-
} from "../pie/authoring.js";
|
|
30
|
-
import { initializeConfiguresFromLoadedBundle } from "../pie/configure-initialization.js";
|
|
31
|
-
import {
|
|
32
|
-
canPopulateCorrectResponses,
|
|
33
|
-
getCorrectResponseEnv,
|
|
34
|
-
} from "../pie/correct-response-env.js";
|
|
35
|
-
import { initializePiesFromLoadedBundle } from "../pie/initialization.js";
|
|
36
|
-
import { createPieLogger, isGlobalDebugEnabled } from "../pie/logger.js";
|
|
37
|
-
import { resolveInstrumentationProvider } from "../pie/instrumentation-provider-resolution.js";
|
|
38
|
-
import { findPieController } from "../pie/scoring.js";
|
|
39
|
-
import type { AuthoringEnv } from "../pie/types.js";
|
|
40
|
-
import { BundleType } from "../pie/types.js";
|
|
41
|
-
import { updatePieElements } from "../pie/updates.js";
|
|
42
|
-
import { useResourceMonitor } from "../pie/use-resource-monitor.svelte.js";
|
|
43
|
-
import type {
|
|
44
|
-
ConfigEntity,
|
|
45
|
-
Env,
|
|
46
|
-
ImageHandler,
|
|
47
|
-
ModelUpdatedEvent,
|
|
48
|
-
SoundHandler,
|
|
49
|
-
} from "../types/index.js";
|
|
50
|
-
|
|
51
|
-
// Create logger (respects global debug flag - pass function for dynamic checking)
|
|
52
|
-
const logger = createPieLogger("pie-item-player", () =>
|
|
53
|
-
isGlobalDebugEnabled()
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// Use Svelte 5 runes for props
|
|
57
|
-
let {
|
|
58
|
-
itemConfig,
|
|
59
|
-
passageConfig = null,
|
|
60
|
-
env = { mode: "gather", role: "student" } as Env,
|
|
61
|
-
session = [] as any[],
|
|
62
|
-
addCorrectResponse = false,
|
|
63
|
-
allowedResize = false,
|
|
64
|
-
customClassName = "",
|
|
65
|
-
passageContainerClass = "",
|
|
66
|
-
containerClass = "",
|
|
67
|
-
bundleType = BundleType.player, // Default to player.js (server-processed models)
|
|
68
|
-
loaderConfig = DEFAULT_LOADER_CONFIG as LoaderConfig,
|
|
69
|
-
// Authoring mode props
|
|
70
|
-
mode = "view" as "view" | "author",
|
|
71
|
-
configuration = {} as Record<string, any>,
|
|
72
|
-
authoringBackend = "demo" as "demo" | "required",
|
|
73
|
-
// Security: sanitize markup before {@html} injection unless the host opts out.
|
|
74
|
-
trustMarkup = false,
|
|
75
|
-
sanitizeMarkup,
|
|
76
|
-
// Asset handler callbacks
|
|
77
|
-
onInsertImage,
|
|
78
|
-
onDeleteImage,
|
|
79
|
-
onInsertSound,
|
|
80
|
-
onDeleteSound,
|
|
81
|
-
// Event callbacks (Svelte 5 pattern)
|
|
82
|
-
onLoadComplete,
|
|
83
|
-
onPlayerError,
|
|
84
|
-
onSessionChanged,
|
|
85
|
-
onModelUpdated,
|
|
86
|
-
onModelLoaded,
|
|
87
|
-
baseHeadingLevel = undefined,
|
|
88
|
-
includeSrHeading = true,
|
|
89
|
-
}: {
|
|
90
|
-
itemConfig: ConfigEntity;
|
|
91
|
-
passageConfig?: ConfigEntity | null;
|
|
92
|
-
env?: Env;
|
|
93
|
-
session?: any[];
|
|
94
|
-
addCorrectResponse?: boolean;
|
|
95
|
-
allowedResize?: boolean;
|
|
96
|
-
customClassName?: string;
|
|
97
|
-
passageContainerClass?: string;
|
|
98
|
-
containerClass?: string;
|
|
99
|
-
bundleType?: BundleType;
|
|
100
|
-
loaderConfig?: LoaderConfig;
|
|
101
|
-
// Authoring mode props
|
|
102
|
-
mode?: "view" | "author";
|
|
103
|
-
configuration?: Record<string, any>;
|
|
104
|
-
authoringBackend?: "demo" | "required";
|
|
105
|
-
// Markup-trust controls
|
|
106
|
-
trustMarkup?: boolean;
|
|
107
|
-
sanitizeMarkup?: ItemMarkupSanitizer;
|
|
108
|
-
// Asset handlers
|
|
109
|
-
onInsertImage?: (handler: ImageHandler) => void;
|
|
110
|
-
onDeleteImage?: (src: string, done: (err?: Error) => void) => void;
|
|
111
|
-
onInsertSound?: (handler: SoundHandler) => void;
|
|
112
|
-
onDeleteSound?: (src: string, done: (err?: Error) => void) => void;
|
|
113
|
-
// Event callbacks
|
|
114
|
-
onLoadComplete?: (detail?: any) => void;
|
|
115
|
-
onPlayerError?: (detail?: any) => void;
|
|
116
|
-
onSessionChanged?: (detail?: any) => void;
|
|
117
|
-
onModelUpdated?: (detail?: any) => void;
|
|
118
|
-
onModelLoaded?: (detail?: any) => void;
|
|
119
|
-
/**
|
|
120
|
-
* The level of the first heading emitted inside this player.
|
|
121
|
-
*
|
|
122
|
-
* Rewrites `<p data-heading="headingN">…</p>` →
|
|
123
|
-
* `<h{clamp(baseLevel + N − 1, 1, 6)} data-heading="headingN">…</h…>`,
|
|
124
|
-
* preserving the `data-heading` attribute so host CSS keyed on
|
|
125
|
-
* `[data-heading]` continues to match.
|
|
126
|
-
*
|
|
127
|
-
* Fast path: when `baseHeadingLevel` is `undefined` **or** the markup
|
|
128
|
-
* contains no `data-heading=` substring, the input is returned unchanged
|
|
129
|
-
* (the common case for legacy content, so the transform is effectively free).
|
|
130
|
-
*
|
|
131
|
-
* PIE elements can read this value by walking up the DOM:
|
|
132
|
-
*
|
|
133
|
-
* const player = element.closest('pie-item-player');
|
|
134
|
-
* let raw = player?.baseHeadingLevel
|
|
135
|
-
* ?? player?.getAttribute('base-heading-level')
|
|
136
|
-
* ?? player?.getAttribute('baseheadinglevel');
|
|
137
|
-
*/
|
|
138
|
-
baseHeadingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
139
|
-
/**
|
|
140
|
-
* Whether to inject a visually-hidden (screen-reader-only) heading at the
|
|
141
|
-
* top of the player's rendered content.
|
|
142
|
-
*
|
|
143
|
-
* Set to `false` in contexts where an SR heading would be redundant or
|
|
144
|
-
* counter-indicated (e.g. the player is already labelled by a surrounding
|
|
145
|
-
* landmark, or the host page manages its own heading structure).
|
|
146
|
-
* Defaults to `true` so that assistive-technology users get a navigable
|
|
147
|
-
* heading out of the box.
|
|
148
|
-
*/
|
|
149
|
-
includeSrHeading?: boolean;
|
|
150
|
-
} = $props();
|
|
151
|
-
|
|
152
|
-
// Track if correct responses have been added
|
|
153
|
-
let correctResponsesAdded = $state(false);
|
|
154
|
-
|
|
155
|
-
// Asset event manager for authoring mode
|
|
156
|
-
let assetEventManager: ReturnType<typeof createAuthoringAssetEventManager> | null = $state(null);
|
|
157
|
-
let authoringBlockedError: string | null = $state(null);
|
|
158
|
-
let lastReportedAuthoringError: string | null = $state(null);
|
|
159
|
-
let runtimePlayerError: string | null = $state(null);
|
|
160
|
-
|
|
161
|
-
// Transform markup for authoring mode (append -config suffix)
|
|
162
|
-
function transformMarkupForAuthoring(
|
|
163
|
-
markup: string,
|
|
164
|
-
elements: Record<string, string>
|
|
165
|
-
): string {
|
|
166
|
-
let result = markup;
|
|
167
|
-
for (const elementTag of Object.keys(elements)) {
|
|
168
|
-
// Replace opening tags
|
|
169
|
-
const openRegex = new RegExp(`<${elementTag}(\\s|>)`, "g");
|
|
170
|
-
result = result.replace(openRegex, `<${elementTag}-config$1`);
|
|
171
|
-
// Replace closing tags
|
|
172
|
-
const closeRegex = new RegExp(`</${elementTag}>`, "g");
|
|
173
|
-
result = result.replace(closeRegex, `</${elementTag}-config>`);
|
|
174
|
-
}
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Custom-element allow-list derived from the item/passage `elements` maps.
|
|
179
|
-
// Includes both the raw tag names and their authoring-mode `-config`
|
|
180
|
-
// rewrites so `transformMarkupForAuthoring` output still passes the
|
|
181
|
-
// sanitizer.
|
|
182
|
-
const itemAllowList = $derived.by<string[]>(() => {
|
|
183
|
-
const elements = itemConfig?.elements ?? {};
|
|
184
|
-
return buildAuthoringAllowList(Object.keys(elements));
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const passageAllowList = $derived.by<string[]>(() => {
|
|
188
|
-
const elements = passageConfig?.elements ?? {};
|
|
189
|
-
return buildAuthoringAllowList(Object.keys(elements));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
function applySanitizer(markup: string, allowList: string[]): string {
|
|
193
|
-
if (!markup) return "";
|
|
194
|
-
if (trustMarkup) return markup;
|
|
195
|
-
if (sanitizeMarkup) return sanitizeMarkup(markup);
|
|
196
|
-
const sanitizer = createDefaultItemMarkupSanitizer({
|
|
197
|
-
allowedCustomElements: allowList,
|
|
198
|
-
});
|
|
199
|
-
return sanitizer(markup);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Get appropriate markup based on mode
|
|
203
|
-
const itemMarkup = $derived.by(() => {
|
|
204
|
-
if (!itemConfig?.markup) return "";
|
|
205
|
-
const raw =
|
|
206
|
-
mode === "author" && itemConfig.elements
|
|
207
|
-
? transformMarkupForAuthoring(itemConfig.markup, itemConfig.elements)
|
|
208
|
-
: itemConfig.markup;
|
|
209
|
-
return applySanitizer(raw, itemAllowList);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const passageMarkup = $derived.by(() => {
|
|
213
|
-
if (!passageConfig?.markup) return "";
|
|
214
|
-
const raw =
|
|
215
|
-
mode === "author" && passageConfig.elements
|
|
216
|
-
? transformMarkupForAuthoring(
|
|
217
|
-
passageConfig.markup,
|
|
218
|
-
passageConfig.elements
|
|
219
|
-
)
|
|
220
|
-
: passageConfig.markup;
|
|
221
|
-
return applySanitizer(raw, passageAllowList);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
function normalizePlayerErrorDetail(
|
|
225
|
-
detail: unknown,
|
|
226
|
-
fallbackCode = "ITEM_PLAYER_RUNTIME_ERROR"
|
|
227
|
-
) {
|
|
228
|
-
if (detail && typeof detail === "object") {
|
|
229
|
-
const detailObject = detail as Record<string, unknown>;
|
|
230
|
-
const message =
|
|
231
|
-
typeof detailObject.message === "string" && detailObject.message.trim().length > 0
|
|
232
|
-
? detailObject.message
|
|
233
|
-
: "Unknown PIE runtime error";
|
|
234
|
-
const code =
|
|
235
|
-
typeof detailObject.code === "string" && detailObject.code.trim().length > 0
|
|
236
|
-
? detailObject.code
|
|
237
|
-
: fallbackCode;
|
|
238
|
-
return { ...detailObject, message, code };
|
|
239
|
-
}
|
|
240
|
-
const message =
|
|
241
|
-
typeof detail === "string" && detail.trim().length > 0
|
|
242
|
-
? detail
|
|
243
|
-
: "Unknown PIE runtime error";
|
|
244
|
-
return { code: fallbackCode, message };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function trackPlayerError(detail: Record<string, unknown>) {
|
|
248
|
-
const resolvedProvider = resolveInstrumentationProvider({
|
|
249
|
-
player: { loaderConfig },
|
|
250
|
-
component: "pie-item-player",
|
|
251
|
-
debug: isGlobalDebugEnabled(),
|
|
252
|
-
});
|
|
253
|
-
if (!isInstrumentationProvider(resolvedProvider) || !resolvedProvider.isReady()) return;
|
|
254
|
-
const message =
|
|
255
|
-
typeof detail.message === "string" ? detail.message : "Unknown PIE runtime error";
|
|
256
|
-
const code =
|
|
257
|
-
typeof detail.code === "string" && detail.code.length > 0
|
|
258
|
-
? detail.code
|
|
259
|
-
: "ITEM_PLAYER_RUNTIME_ERROR";
|
|
260
|
-
resolvedProvider.trackError(new Error(message), {
|
|
261
|
-
component: "pie-item-player",
|
|
262
|
-
errorType: code,
|
|
263
|
-
...detail,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function reportPlayerError(detail: unknown, fallbackCode = "ITEM_PLAYER_RUNTIME_ERROR") {
|
|
268
|
-
const normalizedDetail = normalizePlayerErrorDetail(detail, fallbackCode);
|
|
269
|
-
runtimePlayerError = normalizedDetail.message as string;
|
|
270
|
-
logger.error("[PieItemPlayer] Runtime error:", normalizedDetail);
|
|
271
|
-
trackPlayerError(normalizedDetail);
|
|
272
|
-
dispatch("player-error", normalizedDetail);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Dispatch events (will add more as needed)
|
|
276
|
-
const dispatch = (type: string, detail?: any) => {
|
|
277
|
-
// Call callback prop if provided (Svelte 5 pattern)
|
|
278
|
-
if (type === "load-complete" && typeof onLoadComplete === "function") {
|
|
279
|
-
onLoadComplete(detail);
|
|
280
|
-
} else if (type === "player-error" && typeof onPlayerError === "function") {
|
|
281
|
-
onPlayerError(detail);
|
|
282
|
-
} else if (type === "session-changed" && typeof onSessionChanged === "function") {
|
|
283
|
-
onSessionChanged(detail);
|
|
284
|
-
} else if (type === "model-updated" && typeof onModelUpdated === "function") {
|
|
285
|
-
onModelUpdated(detail);
|
|
286
|
-
} else if (type === "model-loaded" && typeof onModelLoaded === "function") {
|
|
287
|
-
onModelLoaded(detail);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Also dispatch DOM event for backward compatibility
|
|
291
|
-
const event = new CustomEvent(type, {
|
|
292
|
-
detail,
|
|
293
|
-
bubbles: true,
|
|
294
|
-
composed: true, // Allow events to cross shadow DOM boundaries
|
|
295
|
-
});
|
|
296
|
-
dispatchEvent(event);
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const requiredHandlerNames = [
|
|
300
|
-
"onInsertImage",
|
|
301
|
-
"onDeleteImage",
|
|
302
|
-
"onInsertSound",
|
|
303
|
-
"onDeleteSound",
|
|
304
|
-
] as const;
|
|
305
|
-
|
|
306
|
-
function reportAuthoringErrorOnce(message: string) {
|
|
307
|
-
if (lastReportedAuthoringError === message) return;
|
|
308
|
-
lastReportedAuthoringError = message;
|
|
309
|
-
reportPlayerError({
|
|
310
|
-
code: "AUTHORING_BACKEND_CONFIG_ERROR",
|
|
311
|
-
message,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function buildEffectiveAuthoringHandlers() {
|
|
316
|
-
if (authoringBackend === "required") {
|
|
317
|
-
const missing: string[] = [];
|
|
318
|
-
if (!onInsertImage) missing.push("onInsertImage");
|
|
319
|
-
if (!onDeleteImage) missing.push("onDeleteImage");
|
|
320
|
-
if (!onInsertSound) missing.push("onInsertSound");
|
|
321
|
-
if (!onDeleteSound) missing.push("onDeleteSound");
|
|
322
|
-
|
|
323
|
-
if (missing.length > 0) {
|
|
324
|
-
const message = `Authoring backend is required but missing handlers: ${missing.join(", ")}. Provide all ${requiredHandlerNames.join(", ")} callbacks.`;
|
|
325
|
-
authoringBlockedError = message;
|
|
326
|
-
reportAuthoringErrorOnce(message);
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
onInsertImage: onInsertImage!,
|
|
332
|
-
onDeleteImage: onDeleteImage!,
|
|
333
|
-
onInsertSound: onInsertSound!,
|
|
334
|
-
onDeleteSound: onDeleteSound!,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
onInsertImage ||
|
|
340
|
-
onDeleteImage ||
|
|
341
|
-
onInsertSound ||
|
|
342
|
-
onDeleteSound
|
|
343
|
-
) {
|
|
344
|
-
return {
|
|
345
|
-
onInsertImage,
|
|
346
|
-
onDeleteImage,
|
|
347
|
-
onInsertSound,
|
|
348
|
-
onDeleteSound,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
logger.warn(
|
|
353
|
-
"[PieItemPlayer] Authoring backend mode is 'demo'. Using non-production media handlers.",
|
|
354
|
-
);
|
|
355
|
-
return {
|
|
356
|
-
onInsertImage: createDefaultImageInsertHandler((src) => {
|
|
357
|
-
logger.debug("[PieItemPlayer] Demo image insert completed:", src);
|
|
358
|
-
}),
|
|
359
|
-
onDeleteImage: createDefaultImageDeleteHandler(),
|
|
360
|
-
onInsertSound: createDefaultSoundInsertHandler((src) => {
|
|
361
|
-
logger.debug("[PieItemPlayer] Demo sound insert completed:", src);
|
|
362
|
-
}),
|
|
363
|
-
onDeleteSound: createDefaultSoundDeleteHandler(),
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
export async function validateModels(): Promise<AuthoringValidationResult> {
|
|
368
|
-
if (mode !== "author" || !itemConfig) {
|
|
369
|
-
return { hasErrors: false, validatedModels: [] };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const results = await Promise.all([
|
|
373
|
-
validateAuthoringModels(itemConfig, configuration, {
|
|
374
|
-
container: rootElement ?? undefined,
|
|
375
|
-
}),
|
|
376
|
-
passageConfig
|
|
377
|
-
? validateAuthoringModels(passageConfig, configuration, {
|
|
378
|
-
container: rootElement ?? undefined,
|
|
379
|
-
})
|
|
380
|
-
: Promise.resolve({ hasErrors: false, validatedModels: [] }),
|
|
381
|
-
]);
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
hasErrors: results.some((result) => result.hasErrors),
|
|
385
|
-
validatedModels: results.flatMap((result) => result.validatedModels),
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Populate session with correct responses when addCorrectResponse is true
|
|
390
|
-
async function populateCorrectResponses(force = false) {
|
|
391
|
-
// Early return checks
|
|
392
|
-
if (!addCorrectResponse || !itemConfig || (correctResponsesAdded && !force))
|
|
393
|
-
return;
|
|
394
|
-
|
|
395
|
-
// Keep evaluate mode behavior unchanged, but preserve legacy compatibility by
|
|
396
|
-
// forcing instructor role internally when generating correct responses.
|
|
397
|
-
if (!canPopulateCorrectResponses(env)) {
|
|
398
|
-
logger.debug(
|
|
399
|
-
"[PieItemPlayer] Skipping populateCorrectResponses - env not suitable (mode=%s)",
|
|
400
|
-
env.mode
|
|
401
|
-
);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
const correctResponseEnv = getCorrectResponseEnv(env);
|
|
405
|
-
const newSession: any[] = [];
|
|
406
|
-
|
|
407
|
-
for (const model of itemConfig.models) {
|
|
408
|
-
const controller = findPieController(model.element);
|
|
409
|
-
logger.debug(
|
|
410
|
-
"[PieItemPlayer] Controller lookup for %s: %s (createCorrectResponseSession=%s)",
|
|
411
|
-
model.element,
|
|
412
|
-
controller ? "FOUND" : "NOT FOUND",
|
|
413
|
-
controller ? "YES" : "NO"
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
if (controller && controller.createCorrectResponseSession) {
|
|
417
|
-
try {
|
|
418
|
-
const correctResponse =
|
|
419
|
-
(await controller.createCorrectResponseSession(
|
|
420
|
-
model,
|
|
421
|
-
correctResponseEnv
|
|
422
|
-
)) as any;
|
|
423
|
-
|
|
424
|
-
// Check if we got a valid response
|
|
425
|
-
if (!correctResponse) {
|
|
426
|
-
logger.debug(
|
|
427
|
-
"[PieItemPlayer] createCorrectResponseSession returned null for %s (env=%j)",
|
|
428
|
-
model.element,
|
|
429
|
-
correctResponseEnv
|
|
430
|
-
);
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const { id: _ignoredId, ...sessionData } = correctResponse;
|
|
435
|
-
newSession.push({
|
|
436
|
-
id: model.id,
|
|
437
|
-
element: model.element,
|
|
438
|
-
...sessionData,
|
|
439
|
-
});
|
|
440
|
-
} catch (e) {
|
|
441
|
-
logger.warn(
|
|
442
|
-
"[PieItemPlayer] Failed to create correct response for %s",
|
|
443
|
-
model.element,
|
|
444
|
-
e
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Clear existing session entries first by dispatching clear events for each existing entry
|
|
451
|
-
// This ensures the parent component clears its session state before we populate new responses
|
|
452
|
-
const existingIds = new Set(session.map((s: any) => s.id));
|
|
453
|
-
for (const id of existingIds) {
|
|
454
|
-
// Dispatch a session-changed event with null/empty to signal clearing
|
|
455
|
-
// The parent should handle this by removing the entry
|
|
456
|
-
dispatch("session-changed", { id, clear: true });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Update session with correct responses
|
|
460
|
-
session.length = 0;
|
|
461
|
-
session.push(...newSession);
|
|
462
|
-
|
|
463
|
-
// Only mark as added if we actually got responses
|
|
464
|
-
if (newSession.length > 0) {
|
|
465
|
-
correctResponsesAdded = true;
|
|
466
|
-
|
|
467
|
-
// IMPORTANT: `session` is a plain array prop that we mutate in place.
|
|
468
|
-
// Svelte reactivity won't necessarily re-run effects on in-place mutation,
|
|
469
|
-
// so we must push the updated session into the PIE elements explicitly.
|
|
470
|
-
try {
|
|
471
|
-
updatePieElements(itemConfig, session, env, rootElement ?? undefined);
|
|
472
|
-
if (passageConfig) {
|
|
473
|
-
updatePieElements(passageConfig, session, env, rootElement ?? undefined);
|
|
474
|
-
}
|
|
475
|
-
} catch (e) {
|
|
476
|
-
logger.warn(
|
|
477
|
-
"[PieItemPlayer] Failed to update PIE elements after populating correct responses",
|
|
478
|
-
e
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
logger.debug(
|
|
483
|
-
"[PieItemPlayer] Correct responses added to session:",
|
|
484
|
-
session
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
// Dispatch session-changed events for each populated response
|
|
488
|
-
// This ensures the parent component can sync its session state
|
|
489
|
-
for (const sessionEntry of newSession) {
|
|
490
|
-
dispatch("session-changed", sessionEntry);
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
logger.debug(
|
|
494
|
-
"[PieItemPlayer] No correct responses returned (likely wrong env). Will retry if env/addCorrectResponse changes."
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Build CSS classes for containers using $derived
|
|
500
|
-
const passageContainerClassFinal = $derived(
|
|
501
|
-
["pie-passage-container", customClassName, passageContainerClass]
|
|
502
|
-
.filter(Boolean)
|
|
503
|
-
.join(" ")
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
const itemContainerClassFinal = $derived(
|
|
507
|
-
["pie-item-container", customClassName, containerClass]
|
|
508
|
-
.filter(Boolean)
|
|
509
|
-
.join(" ")
|
|
510
|
-
);
|
|
511
|
-
const rootClassFinal = $derived(
|
|
512
|
-
["pie-item-player", allowedResize ? "pie-item-player--resize-allowed" : ""]
|
|
513
|
-
.filter(Boolean)
|
|
514
|
-
.join(" ")
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
// Track if we've initialized (to avoid double-initialization)
|
|
518
|
-
let initialized = $state(false);
|
|
519
|
-
|
|
520
|
-
// Set up session-changed listener after DOM is ready
|
|
521
|
-
let sessionListenerAttached = $state(false);
|
|
522
|
-
let detachSessionChangedListener: (() => void) | null = $state(null);
|
|
523
|
-
|
|
524
|
-
// Flag to prevent infinite loop when re-dispatching events
|
|
525
|
-
let isDispatching = $state(false);
|
|
526
|
-
let lastDispatchedSessionDetailSignature = $state("");
|
|
527
|
-
|
|
528
|
-
// Root element reference for resource monitor
|
|
529
|
-
let rootElement: HTMLElement | null = $state(null);
|
|
530
|
-
|
|
531
|
-
// Resource monitor (handles initialization and cleanup automatically)
|
|
532
|
-
useResourceMonitor(
|
|
533
|
-
() => rootElement,
|
|
534
|
-
() => loaderConfig,
|
|
535
|
-
() => isGlobalDebugEnabled(),
|
|
536
|
-
"pie-item-player"
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
// Initialize PIE elements AFTER markup is rendered (reactive pattern like PieItemPreview)
|
|
540
|
-
$effect(() => {
|
|
541
|
-
if (!itemConfig || initialized) return;
|
|
542
|
-
logger.debug(
|
|
543
|
-
"[PieItemPlayer] Item config received, initializing after DOM renders..."
|
|
544
|
-
);
|
|
545
|
-
logger.debug("[PieItemPlayer] Mode:", mode);
|
|
546
|
-
|
|
547
|
-
// Wait for DOM to update (markup to render)
|
|
548
|
-
tick().then(async () => {
|
|
549
|
-
try {
|
|
550
|
-
runtimePlayerError = null;
|
|
551
|
-
logger.debug("[PieItemPlayer] DOM ready, initializing PIE elements");
|
|
552
|
-
logger.debug("[PieItemPlayer] Config:", {
|
|
553
|
-
itemElements: Object.keys(itemConfig.elements || {}),
|
|
554
|
-
itemModels: (itemConfig.models || []).length,
|
|
555
|
-
passageElements: passageConfig
|
|
556
|
-
? Object.keys(passageConfig.elements || {})
|
|
557
|
-
: [],
|
|
558
|
-
passageModels: passageConfig
|
|
559
|
-
? (passageConfig.models || []).length
|
|
560
|
-
: 0,
|
|
561
|
-
sessionLength: session.length,
|
|
562
|
-
addCorrectResponse,
|
|
563
|
-
env,
|
|
564
|
-
mode,
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
if (mode === "author") {
|
|
568
|
-
// AUTHORING MODE: Initialize configure elements
|
|
569
|
-
logger.debug("[PieItemPlayer] Initializing in authoring mode");
|
|
570
|
-
authoringBlockedError = null;
|
|
571
|
-
const effectiveHandlers = buildEffectiveAuthoringHandlers();
|
|
572
|
-
if (authoringBlockedError || !effectiveHandlers) {
|
|
573
|
-
initialized = false;
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const authoringEnv: AuthoringEnv = {
|
|
578
|
-
...env,
|
|
579
|
-
mode: "author",
|
|
580
|
-
configuration,
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const initializedModels = initializeConfiguresFromLoadedBundle(itemConfig, configuration, {
|
|
584
|
-
env: authoringEnv,
|
|
585
|
-
container: rootElement ?? undefined,
|
|
586
|
-
});
|
|
587
|
-
logger.debug("[PieItemPlayer] Configure elements initialized");
|
|
588
|
-
|
|
589
|
-
if (passageConfig) {
|
|
590
|
-
initializedModels.push(
|
|
591
|
-
...initializeConfiguresFromLoadedBundle(passageConfig, configuration, {
|
|
592
|
-
env: authoringEnv,
|
|
593
|
-
container: rootElement ?? undefined,
|
|
594
|
-
})
|
|
595
|
-
);
|
|
596
|
-
logger.debug(
|
|
597
|
-
"[PieItemPlayer] Passage configure elements initialized"
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
dispatch("model-loaded", {
|
|
602
|
-
models: initializedModels,
|
|
603
|
-
configuration,
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
if (rootElement && effectiveHandlers) {
|
|
607
|
-
assetEventManager = createAuthoringAssetEventManager(
|
|
608
|
-
rootElement,
|
|
609
|
-
effectiveHandlers,
|
|
610
|
-
(context, error) => {
|
|
611
|
-
logger.error(`[PieItemPlayer] ${context} failed:`, error);
|
|
612
|
-
}
|
|
613
|
-
);
|
|
614
|
-
assetEventManager.attach();
|
|
615
|
-
logger.debug("[PieItemPlayer] Asset event manager attached");
|
|
616
|
-
}
|
|
617
|
-
} else {
|
|
618
|
-
// VIEW MODE: Initialize regular player elements
|
|
619
|
-
logger.debug("[PieItemPlayer] Initializing in view mode");
|
|
620
|
-
|
|
621
|
-
// STEP 1: Initialize bundles and register controllers (don't pass session yet)
|
|
622
|
-
// This registers controllers in the registry so we can call createCorrectResponseSession
|
|
623
|
-
initializePiesFromLoadedBundle(itemConfig, [], {
|
|
624
|
-
env,
|
|
625
|
-
bundleType,
|
|
626
|
-
container: rootElement ?? undefined,
|
|
627
|
-
});
|
|
628
|
-
logger.debug(
|
|
629
|
-
"[PieItemPlayer] Item bundle initialized (bundle type: %s)",
|
|
630
|
-
bundleType
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
if (passageConfig) {
|
|
634
|
-
initializePiesFromLoadedBundle(passageConfig, [], {
|
|
635
|
-
env,
|
|
636
|
-
bundleType,
|
|
637
|
-
container: rootElement ?? undefined,
|
|
638
|
-
});
|
|
639
|
-
logger.debug(
|
|
640
|
-
"[PieItemPlayer] Passage bundle initialized (bundle type: %s)",
|
|
641
|
-
bundleType
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// STEP 2: Don't populate correct responses during initialization
|
|
646
|
-
// Let the reactive effect handle it after initialization when env is ready
|
|
647
|
-
|
|
648
|
-
// STEP 3: Update elements with the correct session
|
|
649
|
-
logger.debug(
|
|
650
|
-
"[PieItemPlayer] Updating elements with session (length=" +
|
|
651
|
-
session.length +
|
|
652
|
-
")"
|
|
653
|
-
);
|
|
654
|
-
updatePieElements(itemConfig, session, env, rootElement ?? undefined);
|
|
655
|
-
|
|
656
|
-
if (passageConfig) {
|
|
657
|
-
updatePieElements(passageConfig, session, env, rootElement ?? undefined);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
initialized = true;
|
|
662
|
-
|
|
663
|
-
// Set up event listeners
|
|
664
|
-
if (!sessionListenerAttached) {
|
|
665
|
-
if (mode === "author") {
|
|
666
|
-
// AUTHORING MODE: Listen for model-updated events
|
|
667
|
-
const handleModelUpdated = (event: Event) => {
|
|
668
|
-
if (isDispatching) return;
|
|
669
|
-
|
|
670
|
-
const customEvent = event as ModelUpdatedEvent;
|
|
671
|
-
logger.debug(
|
|
672
|
-
"[PieItemPlayer] model-updated event received from configure element"
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
isDispatching = true;
|
|
676
|
-
try {
|
|
677
|
-
dispatch("model-updated", customEvent.detail);
|
|
678
|
-
} finally {
|
|
679
|
-
setTimeout(() => {
|
|
680
|
-
isDispatching = false;
|
|
681
|
-
}, 0);
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
if (rootElement) {
|
|
686
|
-
// Capture phase ensures we receive author updates even when the event
|
|
687
|
-
// does not bubble from nested configure editors.
|
|
688
|
-
rootElement.addEventListener(
|
|
689
|
-
"model.updated",
|
|
690
|
-
handleModelUpdated,
|
|
691
|
-
true
|
|
692
|
-
);
|
|
693
|
-
sessionListenerAttached = true;
|
|
694
|
-
detachSessionChangedListener = () => {
|
|
695
|
-
try {
|
|
696
|
-
rootElement?.removeEventListener(
|
|
697
|
-
"model.updated",
|
|
698
|
-
handleModelUpdated,
|
|
699
|
-
true
|
|
700
|
-
);
|
|
701
|
-
} catch {}
|
|
702
|
-
};
|
|
703
|
-
logger.debug(
|
|
704
|
-
"[PieItemPlayer] model-updated listener attached to root element"
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
} else {
|
|
708
|
-
// VIEW MODE: Listen for session-changed events from PIE elements
|
|
709
|
-
const handleSessionChanged = (event: Event) => {
|
|
710
|
-
// CRITICAL: Prevent infinite loop
|
|
711
|
-
// When we dispatch, it triggers this listener again
|
|
712
|
-
// Use flag to detect and break the loop
|
|
713
|
-
if (isDispatching) {
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const customEvent = event as CustomEvent;
|
|
718
|
-
logger.debug(
|
|
719
|
-
"[PieItemPlayer] session-changed event received from PIE element",
|
|
720
|
-
customEvent.detail
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
// Forward event detail with the latest in-memory session snapshot.
|
|
724
|
-
// PIE elements often emit metadata-only details, while the actual response
|
|
725
|
-
// array is mutated in-place on the `session` prop.
|
|
726
|
-
const forwardedDetail = {
|
|
727
|
-
...(customEvent.detail || {}),
|
|
728
|
-
session: { id: "", data: session },
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
// Ignore duplicate payloads that can occur during model wiring.
|
|
732
|
-
let detailSignature = "";
|
|
733
|
-
try {
|
|
734
|
-
detailSignature = JSON.stringify(forwardedDetail);
|
|
735
|
-
} catch {
|
|
736
|
-
detailSignature = String(customEvent.detail);
|
|
737
|
-
}
|
|
738
|
-
if (detailSignature === lastDispatchedSessionDetailSignature) {
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
lastDispatchedSessionDetailSignature = detailSignature;
|
|
742
|
-
|
|
743
|
-
// Set flag before dispatching
|
|
744
|
-
isDispatching = true;
|
|
745
|
-
try {
|
|
746
|
-
dispatch("session-changed", forwardedDetail);
|
|
747
|
-
} finally {
|
|
748
|
-
// Reset flag after dispatch (use setTimeout to ensure it happens after event propagation)
|
|
749
|
-
setTimeout(() => {
|
|
750
|
-
isDispatching = false;
|
|
751
|
-
}, 0);
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
// Attach to THIS component instance's root element (critical for stimulus layouts)
|
|
756
|
-
// Using document.querySelector would only attach to the first instance on the page.
|
|
757
|
-
if (rootElement) {
|
|
758
|
-
rootElement.addEventListener(
|
|
759
|
-
"session-changed",
|
|
760
|
-
handleSessionChanged
|
|
761
|
-
);
|
|
762
|
-
sessionListenerAttached = true;
|
|
763
|
-
detachSessionChangedListener = () => {
|
|
764
|
-
try {
|
|
765
|
-
rootElement?.removeEventListener(
|
|
766
|
-
"session-changed",
|
|
767
|
-
handleSessionChanged
|
|
768
|
-
);
|
|
769
|
-
} catch {}
|
|
770
|
-
};
|
|
771
|
-
logger.debug(
|
|
772
|
-
"[PieItemPlayer] session-changed listener attached to root element"
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Note: Resource monitor starts automatically via useResourceMonitor when rootElement is set
|
|
779
|
-
|
|
780
|
-
logger.debug(
|
|
781
|
-
"[PieItemPlayer] Initialization complete, dispatching load-complete event"
|
|
782
|
-
);
|
|
783
|
-
dispatch("load-complete");
|
|
784
|
-
} catch (e: any) {
|
|
785
|
-
reportPlayerError(
|
|
786
|
-
{
|
|
787
|
-
code: "ITEM_PLAYER_INITIALIZATION_ERROR",
|
|
788
|
-
message: e instanceof Error ? e.message : String(e),
|
|
789
|
-
cause: e instanceof Error ? e.stack || e.message : String(e),
|
|
790
|
-
},
|
|
791
|
-
"ITEM_PLAYER_INITIALIZATION_ERROR"
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
onDestroy(() => {
|
|
798
|
-
try {
|
|
799
|
-
detachSessionChangedListener?.();
|
|
800
|
-
assetEventManager?.detach();
|
|
801
|
-
} catch {}
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
// React to addCorrectResponse and env changes to populate/clear correct responses
|
|
805
|
-
$effect(() => {
|
|
806
|
-
if (!initialized) return;
|
|
807
|
-
|
|
808
|
-
// Read env to ensure effect tracks it (so it re-runs when env changes)
|
|
809
|
-
const currentEnv = env;
|
|
810
|
-
|
|
811
|
-
if (addCorrectResponse && !correctResponsesAdded) {
|
|
812
|
-
// Need to populate correct responses
|
|
813
|
-
// populateCorrectResponses preserves legacy compatibility by forcing
|
|
814
|
-
// instructor role internally for createCorrectResponseSession.
|
|
815
|
-
untrack(async () => {
|
|
816
|
-
await populateCorrectResponses();
|
|
817
|
-
// Elements will be updated by the env/session effect below
|
|
818
|
-
});
|
|
819
|
-
} else if (!addCorrectResponse && correctResponsesAdded) {
|
|
820
|
-
// Switching FROM browse mode - clear correct responses
|
|
821
|
-
untrack(() => {
|
|
822
|
-
session.length = 0;
|
|
823
|
-
correctResponsesAdded = false;
|
|
824
|
-
// Elements will be updated by the env/session effect below
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
// Update PIE elements when env or session changes (after initialization) - using $effect
|
|
830
|
-
let isUpdating = false;
|
|
831
|
-
$effect(() => {
|
|
832
|
-
if (!initialized || !env || !itemConfig || !session) return;
|
|
833
|
-
if (isUpdating) return; // Prevent re-entry
|
|
834
|
-
|
|
835
|
-
// Log changes
|
|
836
|
-
logger.debug("[PieItemPlayer] Dependencies changed, updating elements");
|
|
837
|
-
logger.debug("[PieItemPlayer] Env:", env);
|
|
838
|
-
logger.debug(
|
|
839
|
-
"[PieItemPlayer] Session (length=" + session.length + "):",
|
|
840
|
-
session
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
isUpdating = true;
|
|
844
|
-
untrack(() => {
|
|
845
|
-
try {
|
|
846
|
-
updatePieElements(itemConfig, session, env, rootElement ?? undefined);
|
|
847
|
-
|
|
848
|
-
if (passageConfig) {
|
|
849
|
-
updatePieElements(passageConfig, session, env, rootElement ?? undefined);
|
|
850
|
-
}
|
|
851
|
-
} catch (e: any) {
|
|
852
|
-
reportPlayerError(
|
|
853
|
-
{
|
|
854
|
-
code: "ITEM_PLAYER_UPDATE_ERROR",
|
|
855
|
-
message: e instanceof Error ? e.message : String(e),
|
|
856
|
-
cause: e instanceof Error ? e.stack || e.message : String(e),
|
|
857
|
-
},
|
|
858
|
-
"ITEM_PLAYER_UPDATE_ERROR"
|
|
859
|
-
);
|
|
860
|
-
} finally {
|
|
861
|
-
isUpdating = false;
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
$effect(() => {
|
|
867
|
-
if (!rootElement) return;
|
|
868
|
-
const handleControllerError = (event: Event) => {
|
|
869
|
-
const customEvent = event as CustomEvent;
|
|
870
|
-
reportPlayerError(customEvent.detail, "PIE_CONTROLLER_RUNTIME_ERROR");
|
|
871
|
-
};
|
|
872
|
-
rootElement.addEventListener(
|
|
873
|
-
"pie-controller-error",
|
|
874
|
-
handleControllerError as EventListener
|
|
875
|
-
);
|
|
876
|
-
return () => {
|
|
877
|
-
rootElement?.removeEventListener(
|
|
878
|
-
"pie-controller-error",
|
|
879
|
-
handleControllerError as EventListener
|
|
880
|
-
);
|
|
881
|
-
};
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
// Note: Resource monitor cleanup is handled automatically by useResourceMonitor's onDestroy
|
|
885
|
-
</script>
|
|
886
|
-
|
|
887
|
-
<div class={rootClassFinal} bind:this={rootElement}>
|
|
888
|
-
{#if runtimePlayerError}
|
|
889
|
-
<div
|
|
890
|
-
class="pie-player-error"
|
|
891
|
-
style="
|
|
892
|
-
padding: 20px;
|
|
893
|
-
margin: 20px 0;
|
|
894
|
-
border: 2px solid #d32f2f;
|
|
895
|
-
border-radius: 4px;
|
|
896
|
-
background-color: #ffebee;
|
|
897
|
-
color: #c62828;
|
|
898
|
-
font-family: sans-serif;
|
|
899
|
-
"
|
|
900
|
-
>
|
|
901
|
-
<h3 style="margin: 0 0 10px 0">Player Error</h3>
|
|
902
|
-
<p style="margin: 0">{runtimePlayerError}</p>
|
|
903
|
-
</div>
|
|
904
|
-
{/if}
|
|
905
|
-
{#if authoringBlockedError}
|
|
906
|
-
<div
|
|
907
|
-
class="pie-player-error"
|
|
908
|
-
style="
|
|
909
|
-
padding: 20px;
|
|
910
|
-
margin: 20px 0;
|
|
911
|
-
border: 2px solid #d32f2f;
|
|
912
|
-
border-radius: 4px;
|
|
913
|
-
background-color: #ffebee;
|
|
914
|
-
color: #c62828;
|
|
915
|
-
font-family: sans-serif;
|
|
916
|
-
"
|
|
917
|
-
>
|
|
918
|
-
<h3 style="margin: 0 0 10px 0">Authoring Backend Configuration Error</h3>
|
|
919
|
-
<p style="margin: 0">{authoringBlockedError}</p>
|
|
920
|
-
</div>
|
|
921
|
-
{:else if passageMarkup}
|
|
922
|
-
<div class={passageContainerClassFinal}>
|
|
923
|
-
{@html passageMarkup}
|
|
924
|
-
</div>
|
|
925
|
-
{/if}
|
|
926
|
-
|
|
927
|
-
{#if !authoringBlockedError && itemMarkup}
|
|
928
|
-
<div class={itemContainerClassFinal}>
|
|
929
|
-
{@html itemMarkup}
|
|
930
|
-
</div>
|
|
931
|
-
{/if}
|
|
932
|
-
</div>
|
|
933
|
-
|
|
934
|
-
<style>
|
|
935
|
-
.pie-item-player {
|
|
936
|
-
display: block;
|
|
937
|
-
width: 100%;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
.pie-passage-container,
|
|
941
|
-
.pie-item-container {
|
|
942
|
-
display: block;
|
|
943
|
-
width: 100%;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
.pie-item-player--resize-allowed .pie-passage-container {
|
|
947
|
-
max-width: 100%;
|
|
948
|
-
overflow: auto;
|
|
949
|
-
resize: horizontal;
|
|
950
|
-
}
|
|
951
|
-
</style>
|