@pie-players/pie-players-shared 0.3.69 → 0.3.71

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 (41) hide show
  1. package/dist/i18n/messages/en-US.d.ts +2 -0
  2. package/dist/i18n/messages/en-US.js +2 -0
  3. package/dist/i18n/messages/nl-NL.d.ts +2 -0
  4. package/dist/i18n/messages/nl-NL.js +2 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/loaders/iife-adapter.js +7 -3
  8. package/dist/pie/element-observer.d.ts +59 -0
  9. package/dist/pie/element-observer.js +131 -0
  10. package/dist/pie/index.d.ts +4 -2
  11. package/dist/pie/index.js +7 -2
  12. package/dist/pie/initialization.d.ts +13 -13
  13. package/dist/pie/initialization.js +97 -147
  14. package/dist/pie/initialize-element.d.ts +23 -0
  15. package/dist/pie/initialize-element.js +78 -0
  16. package/dist/pie/instrumentation-event-map.d.ts +1 -0
  17. package/dist/pie/instrumentation-event-map.js +18 -0
  18. package/dist/pie/math-rendering.js +6 -2
  19. package/dist/pie/types.d.ts +10 -0
  20. package/dist/pie/utils.d.ts +27 -0
  21. package/dist/pie/utils.js +56 -1
  22. package/dist/security/index.d.ts +3 -2
  23. package/dist/security/index.js +3 -2
  24. package/dist/security/sanitize-forbidden-lists.js +9 -0
  25. package/dist/security/sanitize-item-markup.js +4 -0
  26. package/dist/security/sanitize-style-attribute.d.ts +48 -0
  27. package/dist/security/sanitize-style-attribute.js +129 -0
  28. package/dist/security/sanitize-svg-icon.js +2 -0
  29. package/dist/security/validate-style-url.d.ts +13 -0
  30. package/dist/security/validate-style-url.js +36 -2
  31. package/dist/security/wrap-overwide-images.d.ts +7 -0
  32. package/dist/security/wrap-overwide-images.js +10 -1
  33. package/dist/security/wrap-overwide-tables.d.ts +7 -0
  34. package/dist/security/wrap-overwide-tables.js +10 -1
  35. package/dist/security/wrap-overwide.d.ts +16 -0
  36. package/dist/security/wrap-overwide.js +52 -0
  37. package/dist/ui/overlay-containment.d.ts +48 -0
  38. package/dist/ui/overlay-containment.js +65 -0
  39. package/package.json +6 -10
  40. package/dist/ui/zoom-compensation.d.ts +0 -44
  41. package/dist/ui/zoom-compensation.js +0 -43
@@ -95,6 +95,8 @@ declare const enUS: {
95
95
  assessment: {
96
96
  sectionPosition: string;
97
97
  noSections: string;
98
+ loadFailed: string;
99
+ restoreFailed: string;
98
100
  };
99
101
  /** Formative delivery: check-answer control and its outcome announcements. */
100
102
  formative: {
@@ -95,6 +95,8 @@ const enUS = {
95
95
  assessment: {
96
96
  sectionPosition: "Section {position} of {total}",
97
97
  noSections: "No sections",
98
+ loadFailed: "The assessment could not be loaded. Try again.",
99
+ restoreFailed: "Your saved answers for this section could not be restored. Try again.",
98
100
  },
99
101
  /** Formative delivery: check-answer control and its outcome announcements. */
100
102
  formative: {
@@ -76,6 +76,8 @@ declare const nlNL: {
76
76
  assessment: {
77
77
  sectionPosition: string;
78
78
  noSections: string;
79
+ loadFailed: string;
80
+ restoreFailed: string;
79
81
  };
80
82
  formative: {
81
83
  checkAnswer: string;
@@ -76,6 +76,8 @@ const nlNL = {
76
76
  assessment: {
77
77
  sectionPosition: "Onderdeel {position} van {total}",
78
78
  noSections: "Geen onderdelen",
79
+ loadFailed: "De toets kon niet worden geladen. Probeer het opnieuw.",
80
+ restoreFailed: "Je opgeslagen antwoorden voor dit onderdeel konden niet worden hersteld. Probeer het opnieuw.",
79
81
  },
80
82
  formative: {
81
83
  checkAnswer: "Antwoord controleren",
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { editorPostFix } from "./types/index.js";
11
11
  export * from "./ui/attribute-coercion.js";
12
12
  export * from "./ui/content-styles.js";
13
13
  export * from "./ui/pointer-drag.js";
14
+ export * from "./ui/overlay-containment.js";
14
15
  export * from "./ui/focus-trap.js";
15
16
  export * from "./ui/first-focusable.js";
16
17
  export * from "./ui/debug-panel-persistence.js";
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { editorPostFix } from "./types/index.js";
11
11
  export * from "./ui/attribute-coercion.js";
12
12
  export * from "./ui/content-styles.js";
13
13
  export * from "./ui/pointer-drag.js";
14
+ export * from "./ui/overlay-containment.js";
14
15
  export * from "./ui/focus-trap.js";
15
16
  export * from "./ui/first-focusable.js";
16
17
  export * from "./ui/debug-panel-persistence.js";
@@ -24,7 +24,7 @@ import { defineCustomElementSafely } from "../pie/custom-element-define.js";
24
24
  import { pieRegistry } from "../pie/registry.js";
25
25
  import { validateCustomElementTag } from "../pie/tag-names.js";
26
26
  import { BundleType, isCustomElementConstructor, Status, } from "../pie/types.js";
27
- import { getPackageWithoutVersion, parsePackageName } from "../pie/utils.js";
27
+ import { encodeElementPackageSpecs, getPackageWithoutVersion, parsePackageName, } from "../pie/utils.js";
28
28
  import { AdapterFailure, } from "./element-loader-types.js";
29
29
  /**
30
30
  * Default PIE bundle service base URL. Exported so widgets can use it as
@@ -352,9 +352,13 @@ function buildBundleUrl(elements, bundleType, config) {
352
352
  ? `${config.bundleInfo.url}${separator}elements=${encodeURIComponent(elementTags)}`
353
353
  : config.bundleInfo.url;
354
354
  }
355
- const packageVersions = Object.values(elements).join("+");
355
+ // The two encoders here are deliberately different: the path takes
356
+ // per-spec encoding because a scoped spec's `/` and `@` must stay literal
357
+ // for the route to match, while the `elements=` value is an ordinary query
358
+ // parameter. See `encodeElementPackageSpec`.
359
+ const packageVersions = encodeElementPackageSpecs(Object.values(elements));
356
360
  const host = normalizeBundleHost(config.bundleHost);
357
- const base = `${host}${encodeURI(packageVersions)}/${bundleType}`;
361
+ const base = `${host}${packageVersions}/${bundleType}`;
358
362
  return elementTags
359
363
  ? `${base}?elements=${encodeURIComponent(elementTags)}`
360
364
  : base;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Binding PIE elements that arrive after their bundle was registered.
3
+ *
4
+ * A bundle registration binds models and sessions to the PIE elements present
5
+ * in its container at registration time. Elements that arrive afterwards — ones
6
+ * a host appends to authored markup after the player mounted, or ones a tag's
7
+ * own render pass paints into its subtree — are bound by a `MutationObserver`.
8
+ *
9
+ * The observer is scoped to the registration's container, so a mutation
10
+ * elsewhere on the host page never reaches the callback and no `contains()`
11
+ * walk is needed to reject one. One observer serves every registration made
12
+ * against the same container — an item player registers its item config and its
13
+ * passage config separately — and it disconnects when the last of those
14
+ * registrations is released.
15
+ *
16
+ * This replaces a pair of `window` globals: one observer on `document.body` for
17
+ * the lifetime of the page, and one context slot that every registration
18
+ * overwrote. With a single slot the second registration displaced the first, so
19
+ * a late passage element, or any element in a player that was not the most
20
+ * recently registered, silently never bound.
21
+ */
22
+ import type { ConfigEntity, Env } from "../types/index.js";
23
+ import type { EventListenersMap } from "./types.js";
24
+ /**
25
+ * What one registration binds a PIE element with.
26
+ */
27
+ export interface PieElementContext {
28
+ config: ConfigEntity;
29
+ session: any[];
30
+ env?: Env;
31
+ eventListeners?: EventListenersMap;
32
+ }
33
+ /**
34
+ * Reads the context to bind with.
35
+ *
36
+ * Called when an element arrives, not when the registration is made: a player
37
+ * recomputes its session and env on render, so a value captured at
38
+ * registration time is stale by the time a late element needs it.
39
+ */
40
+ export type PieElementContextSource = () => PieElementContext;
41
+ /**
42
+ * Watch `container` for late-arriving PIE elements and bind them with whatever
43
+ * `getContext` returns at that moment. `container` defaults to `document.body`,
44
+ * matching an unscoped `LoadPieElementsOptions.container`.
45
+ *
46
+ * Returns the release for this registration. Calling it more than once is a
47
+ * no-op; the observer disconnects once every registration against the container
48
+ * has been released, so the caller that acquired it owns it.
49
+ */
50
+ export declare const observePieElements: (container: Element | Document | undefined, getContext: PieElementContextSource) => (() => void);
51
+ /**
52
+ * The contexts registered for `root` or for any container inside it, resolved
53
+ * now, in registration order.
54
+ *
55
+ * A host holds the custom element it mounted while the player registers its own
56
+ * inner root as the container, so the lookup accepts an ancestor of the
57
+ * container.
58
+ */
59
+ export declare const pieElementContextsWithin: (root: Element | Document) => PieElementContext[];
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Binding PIE elements that arrive after their bundle was registered.
3
+ *
4
+ * A bundle registration binds models and sessions to the PIE elements present
5
+ * in its container at registration time. Elements that arrive afterwards — ones
6
+ * a host appends to authored markup after the player mounted, or ones a tag's
7
+ * own render pass paints into its subtree — are bound by a `MutationObserver`.
8
+ *
9
+ * The observer is scoped to the registration's container, so a mutation
10
+ * elsewhere on the host page never reaches the callback and no `contains()`
11
+ * walk is needed to reject one. One observer serves every registration made
12
+ * against the same container — an item player registers its item config and its
13
+ * passage config separately — and it disconnects when the last of those
14
+ * registrations is released.
15
+ *
16
+ * This replaces a pair of `window` globals: one observer on `document.body` for
17
+ * the lifetime of the page, and one context slot that every registration
18
+ * overwrote. With a single slot the second registration displaced the first, so
19
+ * a late passage element, or any element in a player that was not the most
20
+ * recently registered, silently never bound.
21
+ */
22
+ import { initializePieElement } from "./initialize-element.js";
23
+ import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
24
+ import { pieRegistry } from "./registry.js";
25
+ const logger = createPieLogger("pie-element-observer", () => isGlobalDebugEnabled());
26
+ const observed = new Map();
27
+ const resolveContexts = (sources) => {
28
+ const contexts = [];
29
+ for (const source of sources) {
30
+ try {
31
+ contexts.push(source());
32
+ }
33
+ catch (error) {
34
+ logger.error("[pieElementObserver] A context source threw; skipping it.", error);
35
+ }
36
+ }
37
+ return contexts;
38
+ };
39
+ const bindElement = (element, contexts) => {
40
+ const tagName = element.tagName.toLowerCase();
41
+ if (!pieRegistry()[tagName])
42
+ return;
43
+ for (const context of contexts) {
44
+ const bound = initializePieElement(element, {
45
+ config: context.config,
46
+ session: context.session,
47
+ env: context.env,
48
+ eventListeners: context.eventListeners?.[tagName],
49
+ });
50
+ if (bound)
51
+ return;
52
+ }
53
+ };
54
+ const handleMutations = (sources, mutations) => {
55
+ // Resolved once per delivery, not per element.
56
+ const contexts = resolveContexts(sources);
57
+ if (contexts.length === 0)
58
+ return;
59
+ for (const mutation of mutations) {
60
+ if (mutation.type !== "childList")
61
+ continue;
62
+ for (const node of mutation.addedNodes) {
63
+ if (node.nodeType !== Node.ELEMENT_NODE)
64
+ continue;
65
+ const element = node;
66
+ bindElement(element, contexts);
67
+ for (const descendant of Array.from(element.querySelectorAll("*"))) {
68
+ bindElement(descendant, contexts);
69
+ }
70
+ }
71
+ }
72
+ };
73
+ /**
74
+ * Watch `container` for late-arriving PIE elements and bind them with whatever
75
+ * `getContext` returns at that moment. `container` defaults to `document.body`,
76
+ * matching an unscoped `LoadPieElementsOptions.container`.
77
+ *
78
+ * Returns the release for this registration. Calling it more than once is a
79
+ * no-op; the observer disconnects once every registration against the container
80
+ * has been released, so the caller that acquired it owns it.
81
+ */
82
+ export const observePieElements = (container, getContext) => {
83
+ if (typeof document === "undefined" ||
84
+ typeof MutationObserver === "undefined") {
85
+ return () => { };
86
+ }
87
+ const target = container ?? document.body;
88
+ if (!target)
89
+ return () => { };
90
+ let entry = observed.get(target);
91
+ if (!entry) {
92
+ const sources = new Set();
93
+ const observer = new MutationObserver((mutations) => handleMutations(sources, mutations));
94
+ observer.observe(target, { childList: true, subtree: true });
95
+ entry = { sources, observer };
96
+ observed.set(target, entry);
97
+ logger.debug("[observePieElements] Observing a new container");
98
+ }
99
+ const { sources, observer } = entry;
100
+ sources.add(getContext);
101
+ logger.debug(`[observePieElements] Container now has ${sources.size} registration(s)`);
102
+ let released = false;
103
+ return () => {
104
+ if (released)
105
+ return;
106
+ released = true;
107
+ sources.delete(getContext);
108
+ if (sources.size > 0)
109
+ return;
110
+ observer.disconnect();
111
+ observed.delete(target);
112
+ logger.debug("[observePieElements] Released the last registration, disconnected");
113
+ };
114
+ };
115
+ /**
116
+ * The contexts registered for `root` or for any container inside it, resolved
117
+ * now, in registration order.
118
+ *
119
+ * A host holds the custom element it mounted while the player registers its own
120
+ * inner root as the container, so the lookup accepts an ancestor of the
121
+ * container.
122
+ */
123
+ export const pieElementContextsWithin = (root) => {
124
+ const sources = [];
125
+ for (const [container, entry] of observed) {
126
+ if (container !== root && !root.contains(container))
127
+ continue;
128
+ sources.push(...entry.sources);
129
+ }
130
+ return resolveContexts(sources);
131
+ };
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export { assertPieConfigContract, addMarkupForPackage, addRubricIfNeeded, elementForPackage, makeUniqueTags, modelsForPackage, validatePieConfigContract, } from "./config.js";
12
12
  export { initializePiesFromLoadedBundle, loadBundleFromString, loadPieModule, loadPieModuleFromString, } from "./initialization.js";
13
+ export { observePieElements, pieElementContextsWithin, } from "./element-observer.js";
14
+ export type { PieElementContext, PieElementContextSource, } from "./element-observer.js";
13
15
  export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolveConfigureConfiguration, validateAuthoringModels, } from "./authoring.js";
14
16
  export type { AuthoringMediaHandlers, AuthoringValidationResult, InitializedConfigureModel, } from "./authoring.js";
15
17
  export { initializeMathRendering, renderMath, setMathRenderer, } from "./math-rendering.js";
@@ -20,7 +22,7 @@ export { STAGES, applicableStages, stageOrdinal, } from "./stages.js";
20
22
  export type { LoadingCompleteDetail, Stage, StageChangeDetail, StageSourceCe, StageStatus, } from "./stages.js";
21
23
  export { createStageTracker } from "./stage-tracker.js";
22
24
  export type { CreateStageTrackerOptions, StageTracker, } from "./stage-tracker.js";
23
- export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
25
+ export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, ITEM_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
24
26
  export type { InstrumentationEventMapping } from "./instrumentation-event-map.js";
25
27
  export type { ItemControllerOptions } from "./item-controller.js";
26
28
  export { ItemController, normalizeItemSessionContainer, } from "./item-controller.js";
@@ -42,4 +44,4 @@ export type { PieViewMode } from "./tag-names.js";
42
44
  export { updatePieElement, updatePieElements, updatePieElementWithRef, } from "./updates.js";
43
45
  export type { ElementOverrides } from "./overrides.js";
44
46
  export { addOrUpdateOverrideInUrl, applyElementOverrides, applyElementVersionOverridesPreserveTags, extractPackageInfo, formatElementOverrideParam, parseElementOverridesFromCurrentUrl, parseElementOverridesFromUrl, } from "./overrides.js";
45
- export { findOrAddSession, getPackageWithoutVersion, getPieElementBundlesUrl, parsePackageName, } from "./utils.js";
47
+ export { encodeElementPackageSpecs, findOrAddSession, getPackageWithoutVersion, getPieElementBundlesUrl, parsePackageName, } from "./utils.js";
package/dist/pie/index.js CHANGED
@@ -14,6 +14,11 @@ export { assertPieConfigContract, addMarkupForPackage, addRubricIfNeeded, elemen
14
14
  // sync `assertRegistered`) lives under `pie-players-shared/loaders`.
15
15
  // Initialization
16
16
  export { initializePiesFromLoadedBundle, loadBundleFromString, loadPieModule, loadPieModuleFromString, } from "./initialization.js";
17
+ // Late-arrival element binding. The owner of a container observes it and
18
+ // releases the observer on teardown; a host reads `pieElementContextsWithin` to
19
+ // recover the config and session a mounted player registered for a container it
20
+ // owns.
21
+ export { observePieElements, pieElementContextsWithin, } from "./element-observer.js";
17
22
  export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolveConfigureConfiguration, validateAuthoringModels, } from "./authoring.js";
18
23
  export { initializeMathRendering, renderMath, setMathRenderer, } from "./math-rendering.js";
19
24
  export { attachInstrumentationEventBridge } from "./instrumentation-event-bridge.js";
@@ -23,7 +28,7 @@ export { resolveInstrumentationProvider } from "./instrumentation-provider-resol
23
28
  // `pie-stage-change` event family stays coherent across CE shapes.
24
29
  export { STAGES, applicableStages, stageOrdinal, } from "./stages.js";
25
30
  export { createStageTracker } from "./stage-tracker.js";
26
- export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
31
+ export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, ITEM_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
27
32
  export { ItemController, normalizeItemSessionContainer, } from "./item-controller.js";
28
33
  export { hasResponseValue, normalizeItemSessionChange, } from "./item-session-contract.js";
29
34
  export { MemoryItemSessionStorage, SessionStorageItemSessionStorage, } from "./item-controller-storage.js";
@@ -43,4 +48,4 @@ export { toPrintHashedTag, toViewTag, validateCustomElementTag, VIEW_TAG_SUFFIX,
43
48
  export { updatePieElement, updatePieElements, updatePieElementWithRef, } from "./updates.js";
44
49
  export { addOrUpdateOverrideInUrl, applyElementOverrides, applyElementVersionOverridesPreserveTags, extractPackageInfo, formatElementOverrideParam, parseElementOverridesFromCurrentUrl, parseElementOverridesFromUrl, } from "./overrides.js";
45
50
  // Utils
46
- export { findOrAddSession, getPackageWithoutVersion, getPieElementBundlesUrl, parsePackageName, } from "./utils.js";
51
+ export { encodeElementPackageSpecs, findOrAddSession, getPackageWithoutVersion, getPieElementBundlesUrl, parsePackageName, } from "./utils.js";
@@ -4,19 +4,8 @@
4
4
  * Bundle loading and element initialization logic.
5
5
  * This is the core of the PIE player system.
6
6
  */
7
- import type { ConfigEntity, Env } from "../types/index.js";
7
+ import type { ConfigEntity } from "../types/index.js";
8
8
  import type { LoadPieElementsOptions } from "./types.js";
9
- declare global {
10
- interface Window {
11
- _pieElementObserver?: MutationObserver;
12
- _pieCurrentContext?: {
13
- config: ConfigEntity;
14
- session: any[];
15
- env?: Env;
16
- container?: Element | Document;
17
- };
18
- }
19
- }
20
9
  /**
21
10
  * Initialize PIE elements from a bundle that may already be loaded.
22
11
  *
@@ -44,10 +33,21 @@ declare global {
44
33
  * (bundle not loaded by *anyone*) still surface — every unregistered tag
45
34
  * gets its own warning, and `updatePieElements` later reports any tag
46
35
  * that never resolves.
36
+ *
37
+ * Binds the elements present in `opts.container` now. Elements that arrive
38
+ * later are the container owner's concern: a player with a lifecycle calls
39
+ * `observePieElements` and releases it on teardown.
47
40
  */
48
41
  export declare const initializePiesFromLoadedBundle: (config: ConfigEntity, session: any[], opts?: LoadPieElementsOptions) => void;
49
42
  /**
50
- * Load a PIE bundle from a URL and initialize elements
43
+ * Load a PIE bundle from a URL and initialize elements.
44
+ *
45
+ * Rejects — rather than hanging or throwing on the window — for every way
46
+ * the load can fail: the `error` event (404, blocked request, CSP refusal),
47
+ * the `loadTimeoutMs` deadline (a stalled request that never fires either
48
+ * event), a bundle whose script ran without populating `window.pie`, and a
49
+ * throw out of registration. Every rejection names the bundle URL and drops
50
+ * the injected `<script>`.
51
51
  */
52
52
  export declare const loadPieModule: (config: ConfigEntity, session: any[], opts?: LoadPieElementsOptions) => Promise<{
53
53
  session: any[];