@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.
Files changed (210) hide show
  1. package/dist/config/profile.d.ts +0 -1
  2. package/dist/config/profile.js.map +1 -1
  3. package/dist/i18n/index.d.ts +0 -3
  4. package/dist/i18n/index.js +0 -2
  5. package/dist/i18n/index.js.map +1 -1
  6. package/dist/i18n/loader.d.ts +0 -1
  7. package/dist/i18n/loader.js.map +1 -1
  8. package/dist/i18n/simple-i18n.d.ts +0 -1
  9. package/dist/i18n/simple-i18n.js.map +1 -1
  10. package/dist/i18n/types.d.ts +0 -1
  11. package/dist/i18n/types.js.map +1 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.js +0 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/instrumentation/debug-panel-stream.d.ts +0 -1
  16. package/dist/instrumentation/debug-panel-stream.js.map +1 -1
  17. package/dist/instrumentation/index.d.ts +0 -1
  18. package/dist/instrumentation/index.js.map +1 -1
  19. package/dist/instrumentation/provider-guards.d.ts +0 -1
  20. package/dist/instrumentation/provider-guards.js.map +1 -1
  21. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +0 -1
  22. package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -1
  23. package/dist/instrumentation/providers/CompositeInstrumentationProvider.d.ts +0 -1
  24. package/dist/instrumentation/providers/CompositeInstrumentationProvider.js.map +1 -1
  25. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +0 -1
  26. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -1
  27. package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.d.ts +0 -1
  28. package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.js.map +1 -1
  29. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +0 -1
  30. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -1
  31. package/dist/instrumentation/providers/index.d.ts +0 -1
  32. package/dist/instrumentation/providers/index.js.map +1 -1
  33. package/dist/instrumentation/types.d.ts +0 -1
  34. package/dist/instrumentation/types.js.map +1 -1
  35. package/dist/loader-config.d.ts +0 -1
  36. package/dist/loader-config.js.map +1 -1
  37. package/dist/loaders/ElementLoader.d.ts +0 -1
  38. package/dist/loaders/ElementLoader.js.map +1 -1
  39. package/dist/loaders/element-loader-types.d.ts +0 -1
  40. package/dist/loaders/element-loader-types.js.map +1 -1
  41. package/dist/loaders/element-loader.d.ts +0 -1
  42. package/dist/loaders/element-loader.js.map +1 -1
  43. package/dist/loaders/esm-adapter.d.ts +0 -1
  44. package/dist/loaders/esm-adapter.js.map +1 -1
  45. package/dist/loaders/iife-adapter.d.ts +0 -1
  46. package/dist/loaders/iife-adapter.js.map +1 -1
  47. package/dist/loaders/index.d.ts +0 -1
  48. package/dist/loaders/index.js.map +1 -1
  49. package/dist/object/index.d.ts +0 -1
  50. package/dist/object/index.js.map +1 -1
  51. package/dist/pie/asset-handler.d.ts +0 -1
  52. package/dist/pie/asset-handler.js.map +1 -1
  53. package/dist/pie/authoring.d.ts +0 -1
  54. package/dist/pie/authoring.js.map +1 -1
  55. package/dist/pie/component-context.d.ts +0 -1
  56. package/dist/pie/component-context.js.map +1 -1
  57. package/dist/pie/config.d.ts +0 -1
  58. package/dist/pie/config.js.map +1 -1
  59. package/dist/pie/configure-initialization.d.ts +0 -1
  60. package/dist/pie/configure-initialization.js.map +1 -1
  61. package/dist/pie/correct-response-env.d.ts +0 -1
  62. package/dist/pie/correct-response-env.js.map +1 -1
  63. package/dist/pie/custom-element-define.d.ts +0 -1
  64. package/dist/pie/custom-element-define.js.map +1 -1
  65. package/dist/pie/index.d.ts +4 -9
  66. package/dist/pie/index.js +4 -8
  67. package/dist/pie/index.js.map +1 -1
  68. package/dist/pie/initialization.d.ts +0 -1
  69. package/dist/pie/initialization.js.map +1 -1
  70. package/dist/pie/instrumentation-event-bridge.d.ts +0 -1
  71. package/dist/pie/instrumentation-event-bridge.js.map +1 -1
  72. package/dist/pie/instrumentation-event-map.d.ts +0 -1
  73. package/dist/pie/instrumentation-event-map.js.map +1 -1
  74. package/dist/pie/instrumentation-provider-resolution.d.ts +0 -1
  75. package/dist/pie/instrumentation-provider-resolution.js.map +1 -1
  76. package/dist/pie/item-controller-storage.d.ts +0 -1
  77. package/dist/pie/item-controller-storage.js.map +1 -1
  78. package/dist/pie/item-controller.d.ts +0 -1
  79. package/dist/pie/item-controller.js.map +1 -1
  80. package/dist/pie/item-session-contract.d.ts +0 -1
  81. package/dist/pie/item-session-contract.js.map +1 -1
  82. package/dist/pie/logger.d.ts +0 -1
  83. package/dist/pie/logger.js.map +1 -1
  84. package/dist/pie/math-rendering.d.ts +0 -1
  85. package/dist/pie/math-rendering.js.map +1 -1
  86. package/dist/pie/overrides.d.ts +0 -1
  87. package/dist/pie/overrides.js.map +1 -1
  88. package/dist/pie/player-initializer.d.ts +0 -1
  89. package/dist/pie/player-initializer.js.map +1 -1
  90. package/dist/pie/registry.d.ts +0 -1
  91. package/dist/pie/registry.js.map +1 -1
  92. package/dist/pie/resource-monitor.d.ts +0 -1
  93. package/dist/pie/resource-monitor.js.map +1 -1
  94. package/dist/pie/scoring.d.ts +0 -1
  95. package/dist/pie/scoring.js.map +1 -1
  96. package/dist/pie/stage-tracker.d.ts +0 -1
  97. package/dist/pie/stage-tracker.js.map +1 -1
  98. package/dist/pie/stages.d.ts +0 -1
  99. package/dist/pie/stages.js.map +1 -1
  100. package/dist/pie/tag-names.d.ts +0 -1
  101. package/dist/pie/tag-names.js.map +1 -1
  102. package/dist/pie/types.d.ts +0 -1
  103. package/dist/pie/types.js.map +1 -1
  104. package/dist/pie/updates.d.ts +0 -1
  105. package/dist/pie/updates.js.map +1 -1
  106. package/dist/pie/utils.d.ts +0 -1
  107. package/dist/pie/utils.js.map +1 -1
  108. package/dist/player-strategy.d.ts +0 -1
  109. package/dist/player-strategy.js.map +1 -1
  110. package/dist/security/index.d.ts +0 -1
  111. package/dist/security/index.js.map +1 -1
  112. package/dist/security/sanitize-item-markup.d.ts +0 -1
  113. package/dist/security/sanitize-item-markup.js.map +1 -1
  114. package/dist/security/sanitize-svg-icon.d.ts +0 -1
  115. package/dist/security/sanitize-svg-icon.js.map +1 -1
  116. package/dist/security/validate-style-url.d.ts +0 -1
  117. package/dist/security/validate-style-url.js.map +1 -1
  118. package/dist/security/wrap-overwide-images.d.ts +0 -1
  119. package/dist/security/wrap-overwide-images.js.map +1 -1
  120. package/dist/server/npm-registry.d.ts +0 -1
  121. package/dist/server/npm-registry.js.map +1 -1
  122. package/dist/types/index.d.ts +0 -1
  123. package/dist/types/index.js.map +1 -1
  124. package/dist/ui/debug-panel-persistence.d.ts +0 -1
  125. package/dist/ui/debug-panel-persistence.js.map +1 -1
  126. package/dist/ui/first-focusable.d.ts +0 -1
  127. package/dist/ui/first-focusable.js.map +1 -1
  128. package/dist/ui/focus-trap.d.ts +0 -1
  129. package/dist/ui/focus-trap.js.map +1 -1
  130. package/dist/ui/safe-storage.d.ts +0 -1
  131. package/dist/ui/safe-storage.js.map +1 -1
  132. package/package.json +6 -64
  133. package/dist/components/PieItemPlayer.svelte +0 -951
  134. package/dist/components/PiePreviewLayout.svelte +0 -154
  135. package/dist/components/PiePreviewToggle.svelte +0 -110
  136. package/dist/components/PieSpinner.svelte +0 -85
  137. package/dist/components/ToolSettingsButton.svelte +0 -62
  138. package/dist/components/ToolSettingsPanel.svelte +0 -104
  139. package/dist/components/index.ts +0 -6
  140. package/dist/config/profile.d.ts.map +0 -1
  141. package/dist/i18n/index.d.ts.map +0 -1
  142. package/dist/i18n/loader.d.ts.map +0 -1
  143. package/dist/i18n/simple-i18n.d.ts.map +0 -1
  144. package/dist/i18n/types.d.ts.map +0 -1
  145. package/dist/i18n/use-i18n-standalone.svelte.ts +0 -185
  146. package/dist/i18n/use-i18n.svelte.ts +0 -164
  147. package/dist/index.d.ts.map +0 -1
  148. package/dist/instrumentation/debug-panel-stream.d.ts.map +0 -1
  149. package/dist/instrumentation/index.d.ts.map +0 -1
  150. package/dist/instrumentation/provider-guards.d.ts.map +0 -1
  151. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +0 -1
  152. package/dist/instrumentation/providers/CompositeInstrumentationProvider.d.ts.map +0 -1
  153. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +0 -1
  154. package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.d.ts.map +0 -1
  155. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +0 -1
  156. package/dist/instrumentation/providers/index.d.ts.map +0 -1
  157. package/dist/instrumentation/types.d.ts.map +0 -1
  158. package/dist/loader-config.d.ts.map +0 -1
  159. package/dist/loaders/ElementLoader.d.ts.map +0 -1
  160. package/dist/loaders/element-loader-types.d.ts.map +0 -1
  161. package/dist/loaders/element-loader.d.ts.map +0 -1
  162. package/dist/loaders/esm-adapter.d.ts.map +0 -1
  163. package/dist/loaders/iife-adapter.d.ts.map +0 -1
  164. package/dist/loaders/index.d.ts.map +0 -1
  165. package/dist/object/index.d.ts.map +0 -1
  166. package/dist/pie/asset-handler.d.ts.map +0 -1
  167. package/dist/pie/authoring.d.ts.map +0 -1
  168. package/dist/pie/component-context.d.ts.map +0 -1
  169. package/dist/pie/config.d.ts.map +0 -1
  170. package/dist/pie/configure-initialization.d.ts.map +0 -1
  171. package/dist/pie/correct-response-env.d.ts.map +0 -1
  172. package/dist/pie/custom-element-define.d.ts.map +0 -1
  173. package/dist/pie/index.d.ts.map +0 -1
  174. package/dist/pie/initialization.d.ts.map +0 -1
  175. package/dist/pie/instrumentation-event-bridge.d.ts.map +0 -1
  176. package/dist/pie/instrumentation-event-map.d.ts.map +0 -1
  177. package/dist/pie/instrumentation-provider-resolution.d.ts.map +0 -1
  178. package/dist/pie/item-controller-storage.d.ts.map +0 -1
  179. package/dist/pie/item-controller.d.ts.map +0 -1
  180. package/dist/pie/item-session-contract.d.ts.map +0 -1
  181. package/dist/pie/logger.d.ts.map +0 -1
  182. package/dist/pie/math-rendering.d.ts.map +0 -1
  183. package/dist/pie/overrides.d.ts.map +0 -1
  184. package/dist/pie/player-initializer.d.ts.map +0 -1
  185. package/dist/pie/registry.d.ts.map +0 -1
  186. package/dist/pie/resource-monitor.d.ts.map +0 -1
  187. package/dist/pie/scoring.d.ts.map +0 -1
  188. package/dist/pie/stage-tracker.d.ts.map +0 -1
  189. package/dist/pie/stages.d.ts.map +0 -1
  190. package/dist/pie/tag-names.d.ts.map +0 -1
  191. package/dist/pie/types.d.ts.map +0 -1
  192. package/dist/pie/updates.d.ts.map +0 -1
  193. package/dist/pie/use-resource-monitor.svelte.d.ts +0 -56
  194. package/dist/pie/use-resource-monitor.svelte.d.ts.map +0 -1
  195. package/dist/pie/use-resource-monitor.svelte.js +0 -176
  196. package/dist/pie/use-resource-monitor.svelte.js.map +0 -1
  197. package/dist/pie/utils.d.ts.map +0 -1
  198. package/dist/player-strategy.d.ts.map +0 -1
  199. package/dist/security/index.d.ts.map +0 -1
  200. package/dist/security/sanitize-item-markup.d.ts.map +0 -1
  201. package/dist/security/sanitize-svg-icon.d.ts.map +0 -1
  202. package/dist/security/validate-style-url.d.ts.map +0 -1
  203. package/dist/security/wrap-overwide-images.d.ts.map +0 -1
  204. package/dist/server/npm-registry.d.ts.map +0 -1
  205. package/dist/types/index.d.ts.map +0 -1
  206. package/dist/ui/debug-panel-persistence.d.ts.map +0 -1
  207. package/dist/ui/first-focusable.d.ts.map +0 -1
  208. package/dist/ui/focus-trap.d.ts.map +0 -1
  209. package/dist/ui/safe-storage.d.ts.map +0 -1
  210. 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>