@pie-players/pie-players-shared 0.3.69 → 0.3.70
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loaders/iife-adapter.js +7 -3
- package/dist/pie/element-observer.d.ts +59 -0
- package/dist/pie/element-observer.js +131 -0
- package/dist/pie/index.d.ts +4 -2
- package/dist/pie/index.js +7 -2
- package/dist/pie/initialization.d.ts +13 -13
- package/dist/pie/initialization.js +97 -147
- package/dist/pie/initialize-element.d.ts +23 -0
- package/dist/pie/initialize-element.js +78 -0
- package/dist/pie/instrumentation-event-map.d.ts +1 -0
- package/dist/pie/instrumentation-event-map.js +18 -0
- package/dist/pie/math-rendering.js +6 -2
- package/dist/pie/types.d.ts +10 -0
- package/dist/pie/utils.d.ts +27 -0
- package/dist/pie/utils.js +56 -1
- package/dist/security/index.d.ts +3 -2
- package/dist/security/index.js +3 -2
- package/dist/security/sanitize-forbidden-lists.js +9 -0
- package/dist/security/sanitize-item-markup.js +4 -0
- package/dist/security/sanitize-style-attribute.d.ts +48 -0
- package/dist/security/sanitize-style-attribute.js +129 -0
- package/dist/security/sanitize-svg-icon.js +2 -0
- package/dist/security/validate-style-url.d.ts +13 -0
- package/dist/security/validate-style-url.js +36 -2
- package/dist/security/wrap-overwide-images.d.ts +7 -0
- package/dist/security/wrap-overwide-images.js +10 -1
- package/dist/security/wrap-overwide-tables.d.ts +7 -0
- package/dist/security/wrap-overwide-tables.js +10 -1
- package/dist/security/wrap-overwide.d.ts +16 -0
- package/dist/security/wrap-overwide.js +52 -0
- package/dist/ui/overlay-containment.d.ts +48 -0
- package/dist/ui/overlay-containment.js +65 -0
- package/package.json +6 -6
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
|
-
|
|
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}${
|
|
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
|
+
};
|
package/dist/pie/index.d.ts
CHANGED
|
@@ -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
|
|
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[];
|
|
@@ -5,84 +5,33 @@
|
|
|
5
5
|
* This is the core of the PIE player system.
|
|
6
6
|
*/
|
|
7
7
|
import { BUILDER_BUNDLE_URL } from "../config/profile.js";
|
|
8
|
+
import { DEFAULT_IIFE_BUNDLE_RETRY_CONFIG } from "../loader-config.js";
|
|
8
9
|
import { mergeObjectsIgnoringNullUndefined } from "../object/index.js";
|
|
9
|
-
import { wrapModelRichContent } from "../security/wrap-model-rich-content.js";
|
|
10
10
|
import { editorPostFix } from "../types/index.js";
|
|
11
|
+
import { initializePieElement } from "./initialize-element.js";
|
|
11
12
|
import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
12
13
|
import { initializeMathRendering } from "./math-rendering.js";
|
|
13
14
|
import { pieRegistry } from "./registry.js";
|
|
14
|
-
import { findPieController } from "./scoring.js";
|
|
15
15
|
import { defineCustomElementSafely } from "./custom-element-define.js";
|
|
16
16
|
import { validateCustomElementTag } from "./tag-names.js";
|
|
17
17
|
import { BundleType, isCustomElementConstructor, isPieAvailable, Status, } from "./types.js";
|
|
18
18
|
import { updatePieElement } from "./updates.js";
|
|
19
|
-
import {
|
|
19
|
+
import { getPackageWithoutVersion, getPieElementBundlesUrl, } from "./utils.js";
|
|
20
20
|
// Create module-level logger (respects global debug flag - pass function for dynamic checking)
|
|
21
21
|
const logger = createPieLogger("pie-initialization", () => isGlobalDebugEnabled());
|
|
22
|
+
/**
|
|
23
|
+
* Deadline for `loadPieModule`'s bundle `<script>` load when the caller
|
|
24
|
+
* sets no `loadTimeoutMs`. Shared with the `ElementLoader` primitive's
|
|
25
|
+
* `DEFAULT_LOAD_TIMEOUT_MS` so a bundle gets the same budget whichever
|
|
26
|
+
* path a host loads it through.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_LOAD_TIMEOUT_MS = DEFAULT_IIFE_BUNDLE_RETRY_CONFIG.timeoutMs;
|
|
22
29
|
// Default options for loading PIE elements
|
|
23
30
|
const defaultOptions = {
|
|
24
31
|
buildServiceBase: BUILDER_BUNDLE_URL,
|
|
25
32
|
bundleType: BundleType.player, // Default to player.js (no controllers, server-processed models)
|
|
26
33
|
env: { mode: "gather", role: "student" },
|
|
27
34
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Helper function to initialize a PIE element
|
|
30
|
-
*/
|
|
31
|
-
const initializePieElement = (element, options) => {
|
|
32
|
-
const { config, session, env, eventListeners } = options;
|
|
33
|
-
if (element.__pieInitialized) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const tagName = element.tagName.toLowerCase();
|
|
37
|
-
logger.debug(`[initializePieElement] Initializing ${tagName}#${element.id}`);
|
|
38
|
-
// Find model for this element
|
|
39
|
-
let model = config?.models?.find((m) => m.id === element.id);
|
|
40
|
-
if (!model) {
|
|
41
|
-
// Only warn if this element is from a client-player.js bundle (where models are expected)
|
|
42
|
-
// player.js bundles use server-processed models, so missing models are expected there
|
|
43
|
-
const registry = pieRegistry();
|
|
44
|
-
const registryEntry = registry[tagName];
|
|
45
|
-
if (registryEntry && registryEntry.bundleType === BundleType.clientPlayer) {
|
|
46
|
-
logger.warn(`[initializePieElement] Model not found for PIE element ${tagName}#${element.id} (client-player.js bundle)`);
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Set session (with element property for updateSession callback)
|
|
51
|
-
const elementSession = findOrAddSession(session, model.id, model.element);
|
|
52
|
-
element.session = elementSession;
|
|
53
|
-
element.__pieInitialized = true;
|
|
54
|
-
logger.debug(`[initializePieElement] Session set for ${tagName}#${element.id}:`, elementSession);
|
|
55
|
-
// Set model - use controller if available (client-player.js), or use server-processed model (player.js)
|
|
56
|
-
const controller = findPieController(tagName);
|
|
57
|
-
if (!env) {
|
|
58
|
-
logger.error(`[initializePieElement] ❌ FATAL: No env provided for ${tagName}`);
|
|
59
|
-
throw new Error(`No env provided for ${tagName}. PIE elements require an env object with mode and role.`);
|
|
60
|
-
}
|
|
61
|
-
if (!controller) {
|
|
62
|
-
// No controller available - using server-processed model (player.js bundle)
|
|
63
|
-
logger.debug(`[initializePieElement] ℹ️ No controller for ${tagName}, using server-processed model`);
|
|
64
|
-
logger.debug(`[initializePieElement] Model already processed by server:`, {
|
|
65
|
-
id: model.id,
|
|
66
|
-
element: model.element,
|
|
67
|
-
hasCorrectResponse: "correctResponse" in model,
|
|
68
|
-
mode: env.mode,
|
|
69
|
-
role: env.role,
|
|
70
|
-
});
|
|
71
|
-
// Set model directly - server already processed it
|
|
72
|
-
element.model = wrapModelRichContent(model);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// Controller available - run client-side processing (client-player.js bundle)
|
|
76
|
-
// Note: updatePieElementWithRef handles controller invocation
|
|
77
|
-
logger.debug(`[initializePieElement] Controller found for ${tagName}, will invoke model() function`);
|
|
78
|
-
}
|
|
79
|
-
// Add event listeners
|
|
80
|
-
if (eventListeners) {
|
|
81
|
-
Object.entries(eventListeners).forEach(([evt, fn]) => {
|
|
82
|
-
element.addEventListener(evt, fn);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
35
|
const getEditorElementTagName = (elementTagName, pkg) => validateCustomElementTag(elementTagName + editorPostFix, `editor element tag for ${pkg}`);
|
|
87
36
|
const updateRegisteredElement = (elementTagName, config, session, options, omitEnv = false) => {
|
|
88
37
|
updatePieElement(elementTagName, {
|
|
@@ -98,7 +47,10 @@ const updateRegisteredElement = (elementTagName, config, session, options, omitE
|
|
|
98
47
|
/**
|
|
99
48
|
* Shared element registration logic
|
|
100
49
|
* Extracted from initializePiesFromLoadedBundle and loadPieModule to eliminate ~200 lines of duplication
|
|
101
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* Binds the elements already present in `options.container`. Elements that
|
|
52
|
+
* arrive later are bound by the container owner's observer — see
|
|
53
|
+
* `element-observer.ts`.
|
|
102
54
|
*
|
|
103
55
|
* `elementModule` may be `null`. In that case we cannot register *new*
|
|
104
56
|
* tags (no element constructor source), but we can still update tags
|
|
@@ -111,11 +63,6 @@ const updateRegisteredElement = (elementTagName, config, session, options, omitE
|
|
|
111
63
|
*/
|
|
112
64
|
const registerPieElementsFromBundle = (elementModule, config, session, registry, options) => {
|
|
113
65
|
const promises = [];
|
|
114
|
-
const isNodeWithinContainer = (node, container) => {
|
|
115
|
-
if (!container || container === document)
|
|
116
|
-
return true;
|
|
117
|
-
return node instanceof Node && container.contains(node);
|
|
118
|
-
};
|
|
119
66
|
if (elementModule) {
|
|
120
67
|
logger.debug("[registerPieElementsFromBundle] Available packages in bundle:", Object.keys(elementModule));
|
|
121
68
|
}
|
|
@@ -123,15 +70,6 @@ const registerPieElementsFromBundle = (elementModule, config, session, registry,
|
|
|
123
70
|
logger.debug("[registerPieElementsFromBundle] No bundle module supplied; will only update tags already registered with customElements.");
|
|
124
71
|
}
|
|
125
72
|
logger.debug("[registerPieElementsFromBundle] config.elements:", config.elements);
|
|
126
|
-
// Store latest config/session in window so MutationObserver can access current values
|
|
127
|
-
if (typeof window !== "undefined") {
|
|
128
|
-
window._pieCurrentContext = {
|
|
129
|
-
config,
|
|
130
|
-
session,
|
|
131
|
-
env: options.env,
|
|
132
|
-
container: options.container,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
73
|
Object.entries(config.elements).forEach(([elName, pkg]) => {
|
|
136
74
|
const elementTagName = validateCustomElementTag(elName, `element tag in config.elements for ${String(pkg)}`);
|
|
137
75
|
logger.debug(`[registerPieElementsFromBundle] Processing element: ${elementTagName} -> ${pkg}`);
|
|
@@ -227,58 +165,6 @@ const registerPieElementsFromBundle = (elementModule, config, session, registry,
|
|
|
227
165
|
promises.push(customElements.whenDefined(elementTagName).then(() => {
|
|
228
166
|
logger.debug("[registerPieElementsFromBundle] defined custom PIE element: %s", elementTagName);
|
|
229
167
|
}));
|
|
230
|
-
// Setup MutationObserver that uses current context (only once)
|
|
231
|
-
if (!window._pieElementObserver) {
|
|
232
|
-
window._pieElementObserver = new MutationObserver((mutations) => {
|
|
233
|
-
// Use current context from window instead of stale closure
|
|
234
|
-
const context = window._pieCurrentContext;
|
|
235
|
-
if (!context) {
|
|
236
|
-
logger.warn("[MutationObserver] No current context available");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
mutations.forEach((mutation) => {
|
|
240
|
-
if (mutation.type === "childList") {
|
|
241
|
-
mutation.addedNodes.forEach((node) => {
|
|
242
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
243
|
-
if (!isNodeWithinContainer(node, context.container)) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const tagName = node.tagName.toLowerCase();
|
|
247
|
-
if (registry[tagName]) {
|
|
248
|
-
initializePieElement(node, {
|
|
249
|
-
config: context.config,
|
|
250
|
-
session: context.session,
|
|
251
|
-
env: context.env,
|
|
252
|
-
eventListeners: options.eventListeners?.[tagName],
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
// Check children of added nodes
|
|
256
|
-
node
|
|
257
|
-
.querySelectorAll("*")
|
|
258
|
-
.forEach((childNode) => {
|
|
259
|
-
if (!isNodeWithinContainer(childNode, context.container)) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const childTagName = childNode.tagName.toLowerCase();
|
|
263
|
-
if (registry[childTagName]) {
|
|
264
|
-
initializePieElement(childNode, {
|
|
265
|
-
config: context.config,
|
|
266
|
-
session: context.session,
|
|
267
|
-
env: context.env,
|
|
268
|
-
eventListeners: options.eventListeners?.[childTagName],
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
window._pieElementObserver.observe(document.body, {
|
|
278
|
-
childList: true,
|
|
279
|
-
subtree: true,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
168
|
// Handle editor elements if needed
|
|
283
169
|
if (options.bundleType === BundleType.editor) {
|
|
284
170
|
if (isCustomElementConstructor(elementData.Configure)) {
|
|
@@ -327,6 +213,10 @@ const registerPieElementsFromBundle = (elementModule, config, session, registry,
|
|
|
327
213
|
* (bundle not loaded by *anyone*) still surface — every unregistered tag
|
|
328
214
|
* gets its own warning, and `updatePieElements` later reports any tag
|
|
329
215
|
* that never resolves.
|
|
216
|
+
*
|
|
217
|
+
* Binds the elements present in `opts.container` now. Elements that arrive
|
|
218
|
+
* later are the container owner's concern: a player with a lifecycle calls
|
|
219
|
+
* `observePieElements` and releases it on teardown.
|
|
330
220
|
*/
|
|
331
221
|
export const initializePiesFromLoadedBundle = (config, session, opts = {}) => {
|
|
332
222
|
const registry = pieRegistry();
|
|
@@ -341,7 +231,14 @@ export const initializePiesFromLoadedBundle = (config, session, opts = {}) => {
|
|
|
341
231
|
registerPieElementsFromBundle(null, config, session, registry, options);
|
|
342
232
|
};
|
|
343
233
|
/**
|
|
344
|
-
* Load a PIE bundle from a URL and initialize elements
|
|
234
|
+
* Load a PIE bundle from a URL and initialize elements.
|
|
235
|
+
*
|
|
236
|
+
* Rejects — rather than hanging or throwing on the window — for every way
|
|
237
|
+
* the load can fail: the `error` event (404, blocked request, CSP refusal),
|
|
238
|
+
* the `loadTimeoutMs` deadline (a stalled request that never fires either
|
|
239
|
+
* event), a bundle whose script ran without populating `window.pie`, and a
|
|
240
|
+
* throw out of registration. Every rejection names the bundle URL and drops
|
|
241
|
+
* the injected `<script>`.
|
|
345
242
|
*/
|
|
346
243
|
export const loadPieModule = async (config, session, opts = {}) => {
|
|
347
244
|
if (!session) {
|
|
@@ -352,31 +249,84 @@ export const loadPieModule = async (config, session, opts = {}) => {
|
|
|
352
249
|
const registry = pieRegistry();
|
|
353
250
|
const options = mergeObjectsIgnoringNullUndefined(defaultOptions, opts);
|
|
354
251
|
const url = opts.bundleUrl || getPieElementBundlesUrl(config, options);
|
|
252
|
+
const loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS;
|
|
355
253
|
const script = document.createElement("script");
|
|
356
254
|
script.src = url;
|
|
357
255
|
script.defer = true;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
256
|
+
let timer;
|
|
257
|
+
// Removing a `<script>` does not abort a request already in flight, so a
|
|
258
|
+
// late `load` can still fire after the deadline rejected. Each handler
|
|
259
|
+
// checks this so registration never runs for a load the caller has
|
|
260
|
+
// already been told failed.
|
|
261
|
+
let settled = false;
|
|
262
|
+
try {
|
|
263
|
+
await new Promise((resolve, reject) => {
|
|
264
|
+
const succeed = () => {
|
|
265
|
+
settled = true;
|
|
266
|
+
resolve();
|
|
267
|
+
};
|
|
268
|
+
const fail = (error) => {
|
|
269
|
+
settled = true;
|
|
270
|
+
reject(error);
|
|
271
|
+
};
|
|
272
|
+
script.addEventListener("load", () => {
|
|
273
|
+
if (settled)
|
|
274
|
+
return;
|
|
275
|
+
logger.debug("[loadPieModule] Script loaded from:", url);
|
|
276
|
+
if (!isPieAvailable(window)) {
|
|
277
|
+
// Deliberately a rejection, not a resolve. The script executed
|
|
278
|
+
// and registered nothing, so the URL did not serve a PIE IIFE
|
|
279
|
+
// bundle; resolving would report a successful load to a caller
|
|
280
|
+
// that then waits on elements which never arrive.
|
|
281
|
+
// `initializePiesFromLoadedBundle` tolerates the same missing
|
|
282
|
+
// global because there the host's own loader owns registration.
|
|
283
|
+
// Here this function owns it, so there is no other party to
|
|
284
|
+
// wait for. Matches the IIFE `ElementLoader` adapter, which
|
|
285
|
+
// fails with `cause: "window.pie.default missing after bundle
|
|
286
|
+
// load"`.
|
|
287
|
+
fail(new Error(`PIE bundle loaded but window.pie is absent; is ${url} a proper PIE IIFE module?`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
365
290
|
logger.debug("[loadPieModule] window.pie available");
|
|
366
291
|
const elementModule = window.pie.default;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
292
|
+
try {
|
|
293
|
+
// Use shared registration logic (returns array of promises)
|
|
294
|
+
const registrationPromises = registerPieElementsFromBundle(elementModule, config, session, registry, options);
|
|
295
|
+
// Wait for all element definitions to complete
|
|
296
|
+
Promise.all(registrationPromises).then(succeed, fail);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// `registerPieElementsFromBundle` throws synchronously for a
|
|
300
|
+
// package missing from the bundle and for a client-player
|
|
301
|
+
// bundle with no controller. Inside a DOM event handler that
|
|
302
|
+
// throw reaches the window instead of the caller.
|
|
303
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
script.addEventListener("error", () => {
|
|
307
|
+
if (settled)
|
|
308
|
+
return;
|
|
309
|
+
fail(new Error(`failed to load PIE bundle script: ${url}`));
|
|
310
|
+
});
|
|
311
|
+
if (loadTimeoutMs > 0) {
|
|
312
|
+
timer = setTimeout(() => {
|
|
313
|
+
if (settled)
|
|
314
|
+
return;
|
|
315
|
+
fail(new Error(`PIE bundle script load timed out after ${loadTimeoutMs}ms: ${url}`));
|
|
316
|
+
}, loadTimeoutMs);
|
|
375
317
|
}
|
|
318
|
+
document.head.appendChild(script);
|
|
376
319
|
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// Drop the injected node so a retry starts from a clean head.
|
|
323
|
+
script.remove();
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
if (timer)
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
}
|
|
380
330
|
return { session };
|
|
381
331
|
};
|
|
382
332
|
/**
|