@pie-players/pie-players-shared 0.3.73 → 0.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/components/vendor/nds/README.md +7 -5
- package/dist/config/profile.d.ts +0 -2
- package/dist/config/profile.js +2 -8
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +2 -3
- package/dist/i18n/messages/en-US.d.ts +10 -0
- package/dist/i18n/messages/en-US.js +12 -2
- package/dist/i18n/messages/nl-NL.d.ts +6 -0
- package/dist/i18n/messages/nl-NL.js +8 -2
- package/dist/i18n/provider.d.ts +4 -5
- package/dist/i18n/provider.js +4 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/instrumentation/index.d.ts +5 -4
- package/dist/instrumentation/index.js +5 -4
- package/dist/instrumentation/new-relic-agent.d.ts +20 -0
- package/dist/instrumentation/new-relic-agent.js +24 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +5 -4
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +7 -7
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +21 -32
- package/dist/instrumentation/types.d.ts +7 -3
- package/dist/loader-config.d.ts +16 -6
- package/dist/loaders/controller-loading.d.ts +14 -0
- package/dist/loaders/controller-loading.js +12 -0
- package/dist/loaders/defaults.d.ts +10 -0
- package/dist/loaders/defaults.js +10 -0
- package/dist/loaders/element-loader.d.ts +20 -8
- package/dist/loaders/element-loader.js +102 -36
- package/dist/loaders/element-package-policy.d.ts +2 -0
- package/dist/loaders/element-package-policy.js +2 -1
- package/dist/loaders/esm-adapter.d.ts +18 -0
- package/dist/loaders/esm-adapter.js +617 -102
- package/dist/loaders/iife-adapter.d.ts +0 -5
- package/dist/loaders/iife-adapter.js +3 -9
- package/dist/loaders/index.d.ts +10 -3
- package/dist/loaders/index.js +8 -3
- package/dist/loaders/mathjax-version-conflict.d.ts +17 -0
- package/dist/loaders/mathjax-version-conflict.js +51 -0
- package/dist/loaders/module-shim.d.ts +12 -0
- package/dist/loaders/module-shim.js +12 -0
- package/dist/loaders/preloaded-alignment.d.ts +24 -0
- package/dist/loaders/preloaded-alignment.js +57 -0
- package/dist/loaders/preloaded-authored-tags.d.ts +26 -0
- package/dist/loaders/preloaded-authored-tags.js +69 -0
- package/dist/loaders/preloaded-registration.d.ts +56 -0
- package/dist/loaders/preloaded-registration.js +129 -0
- package/dist/media/index.d.ts +69 -0
- package/dist/media/index.js +194 -0
- package/dist/pie/authoring.d.ts +1 -1
- package/dist/pie/authoring.js +25 -18
- package/dist/pie/element-announcements.d.ts +39 -0
- package/dist/pie/element-announcements.js +67 -0
- package/dist/pie/element-observer.d.ts +3 -1
- package/dist/pie/element-observer.js +1 -0
- package/dist/pie/index.d.ts +4 -4
- package/dist/pie/index.js +5 -3
- package/dist/pie/initialization.js +13 -22
- package/dist/pie/initialize-element.d.ts +3 -0
- package/dist/pie/initialize-element.js +3 -4
- package/dist/pie/instrumentation-event-map.js +2 -9
- package/dist/pie/instrumentation-provider-resolution.d.ts +2 -1
- package/dist/pie/instrumentation-provider-resolution.js +0 -1
- package/dist/pie/item-session-contract.d.ts +8 -0
- package/dist/pie/item-session-contract.js +30 -9
- package/dist/pie/markup-math.d.ts +20 -0
- package/dist/pie/markup-math.js +118 -0
- package/dist/pie/math-rendering.d.ts +10 -21
- package/dist/pie/math-rendering.js +50 -35
- package/dist/pie/registry.d.ts +13 -1
- package/dist/pie/registry.js +39 -1
- package/dist/pie/resource-monitor.d.ts +6 -33
- package/dist/pie/resource-monitor.js +14 -213
- package/dist/pie/scoring.d.ts +11 -2
- package/dist/pie/scoring.js +18 -16
- package/dist/pie/session-commit.d.ts +16 -0
- package/dist/pie/session-commit.js +33 -5
- package/dist/pie/session-snapshot.d.ts +80 -0
- package/dist/pie/session-snapshot.js +166 -0
- package/dist/pie/tag-names.d.ts +1 -0
- package/dist/pie/tag-names.js +1 -0
- package/dist/pie/types.d.ts +4 -5
- package/dist/pie/types.js +1 -0
- package/dist/pie/updates.d.ts +2 -2
- package/dist/pie/updates.js +33 -15
- package/dist/pie/versioned-tag.d.ts +4 -0
- package/dist/pie/versioned-tag.js +4 -0
- package/dist/security/index.d.ts +2 -3
- package/dist/security/index.js +2 -3
- package/dist/security/sanitize-item-markup.d.ts +0 -2
- package/dist/security/sanitize-item-markup.js +0 -4
- package/dist/security/sanitize-svg-icon.d.ts +0 -2
- package/dist/security/sanitize-svg-icon.js +0 -5
- package/dist/security/wrap-overwide.d.ts +5 -0
- package/dist/security/wrap-overwide.js +16 -0
- package/dist/timed-media/policy.js +5 -16
- package/dist/types/index.d.ts +30 -311
- package/dist/types/index.js +0 -68
- package/dist/ui/attribute-coercion.d.ts +12 -0
- package/dist/ui/attribute-coercion.js +33 -0
- package/dist/ui/content-styles.d.ts +18 -16
- package/dist/ui/content-styles.js +67 -36
- package/dist/ui/overlay-containment.d.ts +31 -0
- package/dist/ui/overlay-containment.js +63 -0
- package/dist/ui/overlay-placement.d.ts +110 -0
- package/dist/ui/overlay-placement.js +222 -0
- package/dist/ui/pointer-gesture.d.ts +32 -0
- package/dist/ui/pointer-gesture.js +56 -0
- package/dist/ui/pointer-rotate.d.ts +29 -0
- package/dist/ui/pointer-rotate.js +39 -0
- package/package.json +21 -17
- package/dist/pie/component-context.d.ts +0 -21
- package/dist/pie/component-context.js +0 -29
- package/dist/pie/player-initializer.d.ts +0 -54
- package/dist/pie/player-initializer.js +0 -122
- package/dist/security/wrap-model-rich-content.d.ts +0 -1
- package/dist/security/wrap-model-rich-content.js +0 -40
- package/dist/server/npm-auth-env.d.ts +0 -13
- package/dist/server/npm-auth-env.js +0 -64
- package/dist/server/npm-registry.d.ts +0 -7
- package/dist/server/npm-registry.js +0 -59
- package/dist/ui/debug-panel-persistence.d.ts +0 -17
- package/dist/ui/debug-panel-persistence.js +0 -37
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-local snapshot of a committed session.
|
|
3
|
+
*
|
|
4
|
+
* A crash or an OS kill fires no lifecycle event, so the only state that
|
|
5
|
+
* survives it is state already written. The snapshot writes each committed
|
|
6
|
+
* session to device storage and offers it back on the next load.
|
|
7
|
+
*
|
|
8
|
+
* It is offered, never applied. School devices are shared, and a player cannot
|
|
9
|
+
* tell a legitimate recovery from a previous student's draft, so the host
|
|
10
|
+
* decides.
|
|
11
|
+
*
|
|
12
|
+
* `sessionStorage` is the default, matching the annotation toolbar's existing
|
|
13
|
+
* use of it for learner annotations: it is tab-scoped and cleared when the tab
|
|
14
|
+
* closes, so a response does not outlive the sitting, and browser session
|
|
15
|
+
* restore brings it back after a crash. A host that wants recovery across a
|
|
16
|
+
* full browser restart injects a `localStorage`-backed store and owns the
|
|
17
|
+
* retention consequences.
|
|
18
|
+
*/
|
|
19
|
+
export interface SessionSnapshotStore {
|
|
20
|
+
read(key: string): string | null;
|
|
21
|
+
write(key: string, value: string): void;
|
|
22
|
+
clear(key: string): void;
|
|
23
|
+
}
|
|
24
|
+
export type SessionSnapshotConfig = boolean | {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Defaults to `sessionStorage`. */
|
|
27
|
+
store?: SessionSnapshotStore;
|
|
28
|
+
/** Defaults to a key derived from the delivery identity. */
|
|
29
|
+
key?: string;
|
|
30
|
+
};
|
|
31
|
+
export interface SessionSnapshotRecord {
|
|
32
|
+
key: string;
|
|
33
|
+
session: unknown;
|
|
34
|
+
/** `Date.now()` at the time of the write. */
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SessionSnapshot {
|
|
38
|
+
readonly key: string;
|
|
39
|
+
write(session: unknown): void;
|
|
40
|
+
read(): SessionSnapshotRecord | null;
|
|
41
|
+
clear(): void;
|
|
42
|
+
}
|
|
43
|
+
export type SessionSnapshotIdentity = {
|
|
44
|
+
itemId?: string;
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
assignmentId?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Whether an identity names one sitting.
|
|
50
|
+
*
|
|
51
|
+
* `sessionId` is the only field that distinguishes one learner's attempt from
|
|
52
|
+
* another's. Keyed on the item alone, a shared device offers the previous
|
|
53
|
+
* student's draft to the next one - and the offer is itself a disclosure, before
|
|
54
|
+
* the host ever decides whether to apply it. So without a `sessionId` there is
|
|
55
|
+
* no snapshot, and a host driving the player by props alone opts in by passing
|
|
56
|
+
* an explicit `key`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isSnapshotIdentitySpecific(identity: SessionSnapshotIdentity): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Key built from the delivery identity, so a snapshot is only offered back to
|
|
61
|
+
* the sitting that wrote it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function sessionSnapshotKey(identity: SessionSnapshotIdentity): string;
|
|
64
|
+
/**
|
|
65
|
+
* `sessionStorage`-backed store. Every access is guarded: private mode and
|
|
66
|
+
* blocked site data make the accessor throw, and a player must keep working
|
|
67
|
+
* without a snapshot rather than fail.
|
|
68
|
+
*/
|
|
69
|
+
export declare function sessionStorageSnapshotStore(): SessionSnapshotStore;
|
|
70
|
+
export declare function isSessionSnapshotEnabled(config: SessionSnapshotConfig | null | undefined): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Returns `null` when the host has not enabled a snapshot, and when the
|
|
73
|
+
* identity does not name one sitting. PIE writes learner responses to the
|
|
74
|
+
* device only on an explicit opt-in, and only under a key no other learner
|
|
75
|
+
* shares.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createSessionSnapshot(args: {
|
|
78
|
+
config: SessionSnapshotConfig | null | undefined;
|
|
79
|
+
identity: SessionSnapshotIdentity;
|
|
80
|
+
}): SessionSnapshot | null;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-local snapshot of a committed session.
|
|
3
|
+
*
|
|
4
|
+
* A crash or an OS kill fires no lifecycle event, so the only state that
|
|
5
|
+
* survives it is state already written. The snapshot writes each committed
|
|
6
|
+
* session to device storage and offers it back on the next load.
|
|
7
|
+
*
|
|
8
|
+
* It is offered, never applied. School devices are shared, and a player cannot
|
|
9
|
+
* tell a legitimate recovery from a previous student's draft, so the host
|
|
10
|
+
* decides.
|
|
11
|
+
*
|
|
12
|
+
* `sessionStorage` is the default, matching the annotation toolbar's existing
|
|
13
|
+
* use of it for learner annotations: it is tab-scoped and cleared when the tab
|
|
14
|
+
* closes, so a response does not outlive the sitting, and browser session
|
|
15
|
+
* restore brings it back after a crash. A host that wants recovery across a
|
|
16
|
+
* full browser restart injects a `localStorage`-backed store and owns the
|
|
17
|
+
* retention consequences.
|
|
18
|
+
*/
|
|
19
|
+
const KEY_PREFIX = "pie-session-snapshot";
|
|
20
|
+
/**
|
|
21
|
+
* Whether an identity names one sitting.
|
|
22
|
+
*
|
|
23
|
+
* `sessionId` is the only field that distinguishes one learner's attempt from
|
|
24
|
+
* another's. Keyed on the item alone, a shared device offers the previous
|
|
25
|
+
* student's draft to the next one - and the offer is itself a disclosure, before
|
|
26
|
+
* the host ever decides whether to apply it. So without a `sessionId` there is
|
|
27
|
+
* no snapshot, and a host driving the player by props alone opts in by passing
|
|
28
|
+
* an explicit `key`.
|
|
29
|
+
*/
|
|
30
|
+
export function isSnapshotIdentitySpecific(identity) {
|
|
31
|
+
return Boolean(identity.sessionId);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Key built from the delivery identity, so a snapshot is only offered back to
|
|
35
|
+
* the sitting that wrote it.
|
|
36
|
+
*/
|
|
37
|
+
export function sessionSnapshotKey(identity) {
|
|
38
|
+
const parts = [
|
|
39
|
+
identity.assignmentId || "",
|
|
40
|
+
identity.sessionId || "",
|
|
41
|
+
identity.itemId || "",
|
|
42
|
+
];
|
|
43
|
+
return `${KEY_PREFIX}:${parts.join("|")}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `sessionStorage`-backed store. Every access is guarded: private mode and
|
|
47
|
+
* blocked site data make the accessor throw, and a player must keep working
|
|
48
|
+
* without a snapshot rather than fail.
|
|
49
|
+
*/
|
|
50
|
+
export function sessionStorageSnapshotStore() {
|
|
51
|
+
return {
|
|
52
|
+
read(key) {
|
|
53
|
+
try {
|
|
54
|
+
if (typeof sessionStorage === "undefined")
|
|
55
|
+
return null;
|
|
56
|
+
return sessionStorage.getItem(key);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
write(key, value) {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof sessionStorage === "undefined")
|
|
65
|
+
return;
|
|
66
|
+
sessionStorage.setItem(key, value);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Quota, private mode, blocked site data: no snapshot this sitting.
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
clear(key) {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof sessionStorage === "undefined")
|
|
75
|
+
return;
|
|
76
|
+
sessionStorage.removeItem(key);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function isSessionSnapshotEnabled(config) {
|
|
85
|
+
if (typeof config === "boolean")
|
|
86
|
+
return config;
|
|
87
|
+
if (!config)
|
|
88
|
+
return false;
|
|
89
|
+
return config.enabled !== false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns `null` when the host has not enabled a snapshot, and when the
|
|
93
|
+
* identity does not name one sitting. PIE writes learner responses to the
|
|
94
|
+
* device only on an explicit opt-in, and only under a key no other learner
|
|
95
|
+
* shares.
|
|
96
|
+
*/
|
|
97
|
+
export function createSessionSnapshot(args) {
|
|
98
|
+
if (!isSessionSnapshotEnabled(args.config))
|
|
99
|
+
return null;
|
|
100
|
+
const settings = typeof args.config === "object" && args.config ? args.config : {};
|
|
101
|
+
if (!settings.key && !isSnapshotIdentitySpecific(args.identity))
|
|
102
|
+
return null;
|
|
103
|
+
const key = settings.key || sessionSnapshotKey(args.identity);
|
|
104
|
+
const store = settings.store || sessionStorageSnapshotStore();
|
|
105
|
+
// A host-injected store is guarded the same way the default one is: a player
|
|
106
|
+
// keeps working without a snapshot rather than failing on storage.
|
|
107
|
+
const safeRead = () => {
|
|
108
|
+
try {
|
|
109
|
+
return store.read(key);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const safeWrite = (value) => {
|
|
116
|
+
try {
|
|
117
|
+
store.write(key, value);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// no snapshot this sitting
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const safeClear = () => {
|
|
124
|
+
try {
|
|
125
|
+
store.clear(key);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// no snapshot this sitting
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
key,
|
|
133
|
+
write(session) {
|
|
134
|
+
let payload;
|
|
135
|
+
try {
|
|
136
|
+
payload = JSON.stringify({ session, timestamp: Date.now() });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
safeWrite(payload);
|
|
142
|
+
},
|
|
143
|
+
read() {
|
|
144
|
+
const raw = safeRead();
|
|
145
|
+
if (!raw)
|
|
146
|
+
return null;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(raw);
|
|
149
|
+
if (!parsed || typeof parsed !== "object" || !("session" in parsed)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
key,
|
|
154
|
+
session: parsed.session,
|
|
155
|
+
timestamp: typeof parsed.timestamp === "number" ? parsed.timestamp : 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
clear() {
|
|
163
|
+
safeClear();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
package/dist/pie/tag-names.d.ts
CHANGED
|
@@ -15,3 +15,4 @@ export declare const toViewTag: (baseTag: string, view: PieViewMode | string, cu
|
|
|
15
15
|
* Build a safe print tag by appending a deterministic, positive hash suffix.
|
|
16
16
|
*/
|
|
17
17
|
export declare const toPrintHashedTag: (baseTag: string, hashSource: string, hashFn: (value: string) => number) => string;
|
|
18
|
+
export { parseVersionedTagName } from "./versioned-tag.js";
|
package/dist/pie/tag-names.js
CHANGED
package/dist/pie/types.d.ts
CHANGED
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
* This module has NO runtime dependencies on other PIE modules.
|
|
6
6
|
*/
|
|
7
7
|
import type { ConfigEntity, Env, PieController, PieModel } from "../types/index.js";
|
|
8
|
-
/**
|
|
9
|
-
* Player modes
|
|
10
|
-
*/
|
|
11
|
-
export type PlayerMode = "gather" | "view" | "evaluate" | "author";
|
|
12
8
|
/**
|
|
13
9
|
* PIE custom element interface
|
|
14
10
|
*/
|
|
@@ -52,7 +48,8 @@ export interface AssetHandler {
|
|
|
52
48
|
export declare enum BundleType {
|
|
53
49
|
player = "player.js",// Elements only (no controllers)
|
|
54
50
|
clientPlayer = "client-player.js",// Elements + controllers
|
|
55
|
-
editor = "editor.js"
|
|
51
|
+
editor = "editor.js",// Editor UI
|
|
52
|
+
esm = "esm"
|
|
56
53
|
}
|
|
57
54
|
/**
|
|
58
55
|
* Internal PIE registry entry status
|
|
@@ -93,6 +90,8 @@ export interface UpdatePieElementOptions {
|
|
|
93
90
|
session: any[];
|
|
94
91
|
env?: Env;
|
|
95
92
|
invokeControllerForModel?: boolean;
|
|
93
|
+
/** The delivering player's bundle type, see `findPieController`. */
|
|
94
|
+
bundleType?: BundleType;
|
|
96
95
|
eventListeners?: EventListeners;
|
|
97
96
|
container?: Element | Document;
|
|
98
97
|
/**
|
package/dist/pie/types.js
CHANGED
|
@@ -12,6 +12,7 @@ export var BundleType;
|
|
|
12
12
|
BundleType["player"] = "player.js";
|
|
13
13
|
BundleType["clientPlayer"] = "client-player.js";
|
|
14
14
|
BundleType["editor"] = "editor.js";
|
|
15
|
+
BundleType["esm"] = "esm";
|
|
15
16
|
})(BundleType || (BundleType = {}));
|
|
16
17
|
/**
|
|
17
18
|
* Internal PIE registry entry status
|
package/dist/pie/updates.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Functions for updating PIE elements with new models, sessions, and env.
|
|
5
5
|
*/
|
|
6
6
|
import type { ConfigEntity, Env } from "../types/index.js";
|
|
7
|
-
import type { UpdatePieElementOptions } from "./types.js";
|
|
7
|
+
import type { BundleType, UpdatePieElementOptions } from "./types.js";
|
|
8
8
|
/**
|
|
9
9
|
* Update a PIE element by ref (direct Element reference)
|
|
10
10
|
*/
|
|
@@ -16,4 +16,4 @@ export declare const updatePieElement: (elName: string, opts: UpdatePieElementOp
|
|
|
16
16
|
/**
|
|
17
17
|
* Update all PIE elements in a config
|
|
18
18
|
*/
|
|
19
|
-
export declare const updatePieElements: (config: ConfigEntity, session: any[], env: Env, container?: Element | Document, onElementSessionUpdate?: UpdatePieElementOptions["onElementSessionUpdate"]) => Promise<void>;
|
|
19
|
+
export declare const updatePieElements: (config: ConfigEntity, session: any[], env: Env, container?: Element | Document, onElementSessionUpdate?: UpdatePieElementOptions["onElementSessionUpdate"], bundleType?: BundleType) => Promise<void>;
|
package/dist/pie/updates.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Functions for updating PIE elements with new models, sessions, and env.
|
|
5
5
|
*/
|
|
6
6
|
import { mergeObjectsIgnoringNullUndefined } from "../object/index.js";
|
|
7
|
-
import { wrapModelRichContent } from "../security/wrap-model-rich-content.js";
|
|
8
7
|
import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
9
8
|
import { findPieController } from "./scoring.js";
|
|
10
9
|
import { defaultPieElementOptions } from "./types.js";
|
|
@@ -55,11 +54,23 @@ const emitControllerError = (pieElement, detail) => {
|
|
|
55
54
|
composed: true,
|
|
56
55
|
}));
|
|
57
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Reports whether an element has left the document since its update started.
|
|
59
|
+
* A legacy element unmounts its React root when it is disconnected, and a
|
|
60
|
+
* write after that throws React error #409 in releases before PIE-703, or
|
|
61
|
+
* renders a tree nothing unmounts in later ones. The controller call is when a
|
|
62
|
+
* host can remove the player. An element that was never inserted still takes
|
|
63
|
+
* the update, so a caller can set one up before inserting it.
|
|
64
|
+
*/
|
|
65
|
+
const trackRemoval = (element) => {
|
|
66
|
+
const wasConnected = element.isConnected;
|
|
67
|
+
return () => wasConnected && !element.isConnected;
|
|
68
|
+
};
|
|
58
69
|
/**
|
|
59
70
|
* Helper function to apply controller to element
|
|
60
71
|
* Extracted to eliminate duplication and ensure consistent controller invocation
|
|
61
72
|
*/
|
|
62
|
-
const applyControllerToElement = async (element, model, elementSession, controller, env, logPrefix, onElementSessionUpdate) => {
|
|
73
|
+
const applyControllerToElement = async (element, model, elementSession, controller, env, logPrefix, wasRemoved, onElementSessionUpdate) => {
|
|
63
74
|
logger.debug(`${logPrefix} Using controller, env:`, env);
|
|
64
75
|
logger.debug(`${logPrefix} Model before filter:`, {
|
|
65
76
|
id: model.id,
|
|
@@ -93,15 +104,16 @@ const applyControllerToElement = async (element, model, elementSession, controll
|
|
|
93
104
|
element: model.element,
|
|
94
105
|
...controllerResultObject,
|
|
95
106
|
};
|
|
96
|
-
const wrappedModel = wrapModelRichContent(filteredModel);
|
|
97
107
|
logger.debug(`${logPrefix} ✅ Controller filtered model:`, {
|
|
98
|
-
id:
|
|
99
|
-
element:
|
|
100
|
-
hasCorrectResponse: "correctResponse" in
|
|
108
|
+
id: filteredModel.id,
|
|
109
|
+
element: filteredModel.element,
|
|
110
|
+
hasCorrectResponse: "correctResponse" in filteredModel,
|
|
101
111
|
mode: env.mode,
|
|
102
112
|
role: env.role,
|
|
103
113
|
});
|
|
104
|
-
|
|
114
|
+
if (wasRemoved())
|
|
115
|
+
return;
|
|
116
|
+
element.model = filteredModel;
|
|
105
117
|
element.session = elementSession;
|
|
106
118
|
}
|
|
107
119
|
catch (err) {
|
|
@@ -110,7 +122,7 @@ const applyControllerToElement = async (element, model, elementSession, controll
|
|
|
110
122
|
}
|
|
111
123
|
};
|
|
112
124
|
const resolveAndValidateUpdateOptions = (opts) => {
|
|
113
|
-
const { config, session, env, eventListeners, invokeControllerForModel, onElementSessionUpdate, } = mergeObjectsIgnoringNullUndefined(defaultPieElementOptions, opts);
|
|
125
|
+
const { config, session, env, eventListeners, invokeControllerForModel, bundleType, onElementSessionUpdate, } = mergeObjectsIgnoringNullUndefined(defaultPieElementOptions, opts);
|
|
114
126
|
if (!env) {
|
|
115
127
|
throw new Error("env is required");
|
|
116
128
|
}
|
|
@@ -126,11 +138,12 @@ const resolveAndValidateUpdateOptions = (opts) => {
|
|
|
126
138
|
env,
|
|
127
139
|
eventListeners,
|
|
128
140
|
invokeControllerForModel,
|
|
141
|
+
bundleType,
|
|
129
142
|
onElementSessionUpdate,
|
|
130
143
|
};
|
|
131
144
|
};
|
|
132
145
|
const updateSinglePieElement = async (pieElement, controllerLookupTag, options, logContext) => {
|
|
133
|
-
const { config, session, env, eventListeners, invokeControllerForModel, onElementSessionUpdate, } = options;
|
|
146
|
+
const { config, session, env, eventListeners, invokeControllerForModel, bundleType, onElementSessionUpdate, } = options;
|
|
134
147
|
const model = config.models?.find((m) => m.id === pieElement.id);
|
|
135
148
|
if (!model) {
|
|
136
149
|
logger.error(`${logContext} Model not found for`, controllerLookupTag);
|
|
@@ -144,10 +157,10 @@ const updateSinglePieElement = async (pieElement, controllerLookupTag, options,
|
|
|
144
157
|
});
|
|
145
158
|
}
|
|
146
159
|
if (env && invokeControllerForModel) {
|
|
147
|
-
const controller = findPieController(controllerLookupTag);
|
|
160
|
+
const controller = findPieController(controllerLookupTag, bundleType);
|
|
148
161
|
if (!controller) {
|
|
149
162
|
logger.debug(`${logContext} ℹ️ No controller for ${controllerLookupTag}, using server-processed model`);
|
|
150
|
-
pieElement.model =
|
|
163
|
+
pieElement.model = model;
|
|
151
164
|
pieElement.session = elementSession;
|
|
152
165
|
return;
|
|
153
166
|
}
|
|
@@ -160,8 +173,9 @@ const updateSinglePieElement = async (pieElement, controllerLookupTag, options,
|
|
|
160
173
|
// caller computes/emits the session signature; otherwise a late, async
|
|
161
174
|
// shuffle write lands after the cycle that read the session and the order
|
|
162
175
|
// never round-trips (PIE-631).
|
|
176
|
+
const wasRemoved = trackRemoval(pieElement);
|
|
163
177
|
try {
|
|
164
|
-
await applyControllerToElement(pieElement, model, elementSession, controller, env, `${logContext}(${controllerLookupTag}#${pieElement.id})`, onElementSessionUpdate);
|
|
178
|
+
await applyControllerToElement(pieElement, model, elementSession, controller, env, `${logContext}(${controllerLookupTag}#${pieElement.id})`, wasRemoved, onElementSessionUpdate);
|
|
165
179
|
}
|
|
166
180
|
catch (err) {
|
|
167
181
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -172,19 +186,22 @@ const updateSinglePieElement = async (pieElement, controllerLookupTag, options,
|
|
|
172
186
|
? "PIE_CONTROLLER_CONTRACT_ERROR"
|
|
173
187
|
: "PIE_CONTROLLER_RUNTIME_ERROR",
|
|
174
188
|
message: `${controllerLookupTag} controller failed while applying model for ${pieElement.id}. ${errorMessage}`,
|
|
189
|
+
recoverable: true,
|
|
175
190
|
elementName: controllerLookupTag,
|
|
176
191
|
elementId: pieElement.id,
|
|
177
192
|
controllerShape: describeControllerShape(controller),
|
|
178
193
|
cause: errorMessage,
|
|
179
194
|
});
|
|
180
195
|
// Fall back to raw model on controller error
|
|
181
|
-
|
|
196
|
+
if (wasRemoved())
|
|
197
|
+
return;
|
|
198
|
+
pieElement.model = model;
|
|
182
199
|
pieElement.session = elementSession;
|
|
183
200
|
}
|
|
184
201
|
}
|
|
185
202
|
else {
|
|
186
203
|
logger.debug(`${logContext} Direct model assignment for ${controllerLookupTag}#${pieElement.id} (no controller invocation requested)`);
|
|
187
|
-
pieElement.model =
|
|
204
|
+
pieElement.model = model;
|
|
188
205
|
pieElement.session = elementSession;
|
|
189
206
|
}
|
|
190
207
|
};
|
|
@@ -215,7 +232,7 @@ export const updatePieElement = (elName, opts) => {
|
|
|
215
232
|
/**
|
|
216
233
|
* Update all PIE elements in a config
|
|
217
234
|
*/
|
|
218
|
-
export const updatePieElements = (config, session, env, container, onElementSessionUpdate) => {
|
|
235
|
+
export const updatePieElements = (config, session, env, container, onElementSessionUpdate, bundleType) => {
|
|
219
236
|
logger.debug("[updatePieElements] Updating all elements with env:", env);
|
|
220
237
|
return Promise.all(Object.entries(config.elements).map(([elName, _pkg]) => updatePieElement(elName, {
|
|
221
238
|
config,
|
|
@@ -223,5 +240,6 @@ export const updatePieElements = (config, session, env, container, onElementSess
|
|
|
223
240
|
env,
|
|
224
241
|
container,
|
|
225
242
|
onElementSessionUpdate,
|
|
243
|
+
bundleType,
|
|
226
244
|
}))).then(() => undefined);
|
|
227
245
|
};
|
|
@@ -6,6 +6,10 @@ const encodeVersionForTag = (version) => version
|
|
|
6
6
|
.replace(/[.+]/g, "-")
|
|
7
7
|
.replace(/[^0-9A-Za-z-]/g, "-")
|
|
8
8
|
.replace(/-{2,}/g, "-");
|
|
9
|
+
/**
|
|
10
|
+
* Split a runtime tag into its base name and the encoded version that follows
|
|
11
|
+
* `--version-`, when it has one.
|
|
12
|
+
*/
|
|
9
13
|
export function parseVersionedTagName(tagName) {
|
|
10
14
|
const versionMatch = tagName.match(new RegExp(`${VERSION_DELIMITER}(${TAG_VERSION_PATTERN})$`));
|
|
11
15
|
if (!versionMatch || versionMatch.index === undefined) {
|
package/dist/security/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { buildAuthoringAllowList, createDefaultItemMarkupSanitizer,
|
|
1
|
+
export { buildAuthoringAllowList, createDefaultItemMarkupSanitizer, sanitizeItemMarkup, type ItemMarkupSanitizer, type SanitizeItemMarkupOptions, } from "./sanitize-item-markup.js";
|
|
2
2
|
export { parseAllowedStyleOrigins, validateExternalStyleUrl, type StyleUrlValidationError, type StyleUrlValidationOk, type StyleUrlValidationOptions, type StyleUrlValidationResult, } from "./validate-style-url.js";
|
|
3
|
-
export {
|
|
3
|
+
export { sanitizeSvgIcon } from "./sanitize-svg-icon.js";
|
|
4
4
|
export { sanitizeStyleAttribute } from "./sanitize-style-attribute.js";
|
|
5
5
|
export { isOverwideImageWrapMutation, wrapOverwideImages, wrapOverwideImagesInElement, } from "./wrap-overwide-images.js";
|
|
6
|
-
export { wrapModelRichContent } from "./wrap-model-rich-content.js";
|
|
7
6
|
export { isOverwideTableWrapMutation, wrapOverwideTables, wrapOverwideTablesInElement, } from "./wrap-overwide-tables.js";
|
package/dist/security/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { buildAuthoringAllowList, createDefaultItemMarkupSanitizer,
|
|
1
|
+
export { buildAuthoringAllowList, createDefaultItemMarkupSanitizer, sanitizeItemMarkup, } from "./sanitize-item-markup.js";
|
|
2
2
|
export { parseAllowedStyleOrigins, validateExternalStyleUrl, } from "./validate-style-url.js";
|
|
3
|
-
export {
|
|
3
|
+
export { sanitizeSvgIcon } from "./sanitize-svg-icon.js";
|
|
4
4
|
export { sanitizeStyleAttribute } from "./sanitize-style-attribute.js";
|
|
5
5
|
export { isOverwideImageWrapMutation, wrapOverwideImages, wrapOverwideImagesInElement, } from "./wrap-overwide-images.js";
|
|
6
|
-
export { wrapModelRichContent } from "./wrap-model-rich-content.js";
|
|
7
6
|
export { isOverwideTableWrapMutation, wrapOverwideTables, wrapOverwideTablesInElement, } from "./wrap-overwide-tables.js";
|
|
@@ -52,5 +52,3 @@ export declare function createDefaultItemMarkupSanitizer(options?: SanitizeItemM
|
|
|
52
52
|
* keeps the rewritten `-config` tags instead of stripping them.
|
|
53
53
|
*/
|
|
54
54
|
export declare function buildAuthoringAllowList(elementTagNames: Iterable<string>): string[];
|
|
55
|
-
/** Reset the memoised DOMPurify instance. Only intended for tests. */
|
|
56
|
-
export declare function resetPurifierForTesting(): void;
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* the real divergence — an image has `alt`, a table has `<caption>`,
|
|
15
15
|
* `aria-label` and `aria-labelledby` — which is why it is a function rather than
|
|
16
16
|
* a template.
|
|
17
|
+
*
|
|
18
|
+
* Content inside an editing host is never wrapped, on either pass. The editor
|
|
19
|
+
* owns that DOM: ProseMirror reads the wrapper as a foreign mutation and redraws
|
|
20
|
+
* the node without it, so an observer-driven wrap and the editor undo each other
|
|
21
|
+
* for as long as the editor holds an image.
|
|
17
22
|
*/
|
|
18
23
|
/** What one content kind needs in order to be wrapped. */
|
|
19
24
|
export interface OverwideWrapSpec {
|
|
@@ -14,8 +14,22 @@
|
|
|
14
14
|
* the real divergence — an image has `alt`, a table has `<caption>`,
|
|
15
15
|
* `aria-label` and `aria-labelledby` — which is why it is a function rather than
|
|
16
16
|
* a template.
|
|
17
|
+
*
|
|
18
|
+
* Content inside an editing host is never wrapped, on either pass. The editor
|
|
19
|
+
* owns that DOM: ProseMirror reads the wrapper as a foreign mutation and redraws
|
|
20
|
+
* the node without it, so an observer-driven wrap and the editor undo each other
|
|
21
|
+
* for as long as the editor holds an image.
|
|
17
22
|
*/
|
|
18
23
|
const PIE_CUSTOM_ELEMENT_TAG_REGEX = /^pie-/i;
|
|
24
|
+
/**
|
|
25
|
+
* An editing host: `contenteditable` in any state but `false`. A
|
|
26
|
+
* `contenteditable="false"` island inside a host still belongs to the host's
|
|
27
|
+
* editor — ProseMirror marks every leaf node that way, images included — so the
|
|
28
|
+
* test looks past such islands to the nearest host. A read-only editor root
|
|
29
|
+
* (`contenteditable="false"`) is no host; ProseMirror registers no DOM mutations
|
|
30
|
+
* while its view is not editable.
|
|
31
|
+
*/
|
|
32
|
+
const EDITING_HOST_SELECTOR = '[contenteditable]:not([contenteditable="false" i])';
|
|
19
33
|
function isInsidePieCustomElement(node, root) {
|
|
20
34
|
let ancestor = node.parentElement;
|
|
21
35
|
while (ancestor && ancestor !== root) {
|
|
@@ -49,6 +63,8 @@ export function wrapOverwideInElement(root, spec, options = {}) {
|
|
|
49
63
|
// Authored-markup pass: leave PIE custom-element internals alone.
|
|
50
64
|
if (skipPieDescendants && isInsidePieCustomElement(node, root))
|
|
51
65
|
continue;
|
|
66
|
+
if (node.closest(EDITING_HOST_SELECTOR))
|
|
67
|
+
continue;
|
|
52
68
|
const wrapper = ownerDocument.createElement(spec.wrapperTag);
|
|
53
69
|
wrapper.className = spec.wrapperClass;
|
|
54
70
|
wrapper.setAttribute("tabindex", "0");
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* reports them as a framework error and delivers as an ordinary section, which is
|
|
7
7
|
* loud without costing the learner the content.
|
|
8
8
|
*/
|
|
9
|
+
import { normalizeMediaFragment } from "../media/index.js";
|
|
9
10
|
/**
|
|
10
11
|
* Applied when `playbackPolicy` is absent entirely. The restrictive values are
|
|
11
12
|
* the defaults because a timed-media section is authored to sequence: a section
|
|
@@ -43,21 +44,6 @@ function isCorrectnessCondition(condition) {
|
|
|
43
44
|
function trimmed(value) {
|
|
44
45
|
return typeof value === "string" ? value.trim() : "";
|
|
45
46
|
}
|
|
46
|
-
function normalizeRange(value) {
|
|
47
|
-
if (!value || typeof value !== "object")
|
|
48
|
-
return null;
|
|
49
|
-
const raw = value;
|
|
50
|
-
const start = Number(raw.startSeconds);
|
|
51
|
-
if (!Number.isFinite(start) || start < 0)
|
|
52
|
-
return null;
|
|
53
|
-
const end = Number(raw.endSeconds);
|
|
54
|
-
// An end at or before the start is a window that can never contain a
|
|
55
|
-
// position, so it is treated as no end — the same rule
|
|
56
|
-
// `normalizeMediaFragment` applies in the toolkit, kept identical on purpose.
|
|
57
|
-
if (!Number.isFinite(end) || end <= start)
|
|
58
|
-
return { startSeconds: start };
|
|
59
|
-
return { startSeconds: start, endSeconds: end };
|
|
60
|
-
}
|
|
61
47
|
function normalizePlaybackPolicy(value) {
|
|
62
48
|
if (value === undefined || value === null) {
|
|
63
49
|
return { policy: { ...TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS }, error: null };
|
|
@@ -179,7 +165,10 @@ export function normalizeTimedMediaSectionData(args) {
|
|
|
179
165
|
continue;
|
|
180
166
|
}
|
|
181
167
|
seenIdentifiers.add(identifier);
|
|
182
|
-
|
|
168
|
+
// The same rule as every other authored media range: an end at or before
|
|
169
|
+
// the start is a window that can never contain a position, so it is
|
|
170
|
+
// treated as no end.
|
|
171
|
+
const range = normalizeMediaFragment(cue.range);
|
|
183
172
|
if (!range) {
|
|
184
173
|
errors.push({
|
|
185
174
|
code: "invalid-cue-range",
|