@pie-element/mc-populated-blank 0.3.0-next.9 → 0.3.0
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 +51 -21
- package/author.js +2 -0
- package/dist/author/defaults.d.ts +73 -0
- package/dist/author/index.d.ts +2 -10
- package/dist/author/index.js +139 -165
- package/dist/author/index.js.map +1 -1
- package/dist/browser/McPopulatedBlank-LqV-pdXv.js +2541 -0
- package/dist/browser/McPopulatedBlank-LqV-pdXv.js.map +1 -0
- package/dist/browser/author/index.js +11 -32
- package/dist/browser/author/index.js.map +1 -1
- package/dist/browser/{client-BmZWrsT3.js → client-Dfdyltec.js} +655 -643
- package/dist/browser/client-Dfdyltec.js.map +1 -0
- package/dist/browser/controller/index.js +2 -220
- package/dist/browser/controller-DSkEI2th.js +222 -0
- package/dist/browser/controller-DSkEI2th.js.map +1 -0
- package/dist/browser/delivery/index.js +72 -1024
- package/dist/browser/delivery/index.js.map +1 -1
- package/dist/browser/{layoutLimits-Bp17YP2V.js → layoutLimits-C2D60jW0.js} +1 -2
- package/dist/browser/layoutLimits-C2D60jW0.js.map +1 -0
- package/dist/browser/print/index.js +94 -103
- package/dist/browser/print/index.js.map +1 -1
- package/dist/controller/defaults.d.ts +8 -30
- package/dist/controller/index.d.ts +80 -65
- package/dist/controller/index.js +142 -115
- package/dist/controller/index.js.map +1 -1
- package/dist/delivery/assets/audioButtonSkinAssets.d.ts +4 -0
- package/dist/delivery/computeDisplayState.d.ts +12 -7
- package/dist/delivery/computeLayoutProfile.d.ts +8 -0
- package/dist/delivery/computeLayoutStyle.d.ts +1 -1
- package/dist/delivery/i18n.d.ts +8 -0
- package/dist/delivery/index.d.ts +8 -23
- package/dist/delivery/index.js +3233 -752
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/variant-css-map.d.ts +15 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.iife.d.ts +1 -154
- package/dist/index.iife.js +414 -163
- package/dist/index.js +2 -6847
- package/dist/print/index.d.ts +3 -7
- package/dist/print/index.js +4667 -349
- package/dist/print/index.js.map +1 -1
- package/dist/print/preparePrint.d.ts +15 -0
- package/dist/runtime-support.js +5 -5
- package/dist/runtime-support.js.map +1 -1
- package/dist/shared/layoutLimits.d.ts +0 -1
- package/dist/shared/types.d.ts +14 -20
- package/module/print.js +3781 -918
- package/module/print.js.map +1 -1
- package/package.json +14 -4
- package/dist/browser/client-BmZWrsT3.js.map +0 -1
- package/dist/browser/controller/index.js.map +0 -1
- package/dist/browser/layoutLimits-Bp17YP2V.js.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PieUpdateSession } from '@pie-element/shared-types';
|
|
2
|
+
import type { McpbChoice, McpbQuestion, McpbSession, McpbEnv, McpbCorrectness } from '../shared/types.js';
|
|
2
3
|
export declare function countBlankTokens(template: string): number;
|
|
3
4
|
export declare const getCorrectness: (question: McpbQuestion, session: McpbSession) => McpbCorrectness;
|
|
4
|
-
|
|
5
|
+
/** One blank, one key: an answered session scores 1 only when it picked the key. */
|
|
6
|
+
export declare const getPartialScore: (question: McpbQuestion, session: McpbSession) => 1 | 0;
|
|
5
7
|
export declare const outcome: (question: McpbQuestion, session: McpbSession, env: McpbEnv) => Promise<unknown>;
|
|
8
|
+
/** The authoring starting point: a question that passes `validate()` as it stands. */
|
|
6
9
|
export declare const createDefaultModel: (model?: McpbQuestion) => {
|
|
7
10
|
layoutLimits: {
|
|
8
11
|
blankStandaloneWidthRem: number;
|
|
@@ -26,7 +29,6 @@ export declare const createDefaultModel: (model?: McpbQuestion) => {
|
|
|
26
29
|
audioBlankTemplateMarginTopRem: number;
|
|
27
30
|
audioBlankTemplateMarginBottomRem: number;
|
|
28
31
|
audioInstructionsMaxWidthPx: number;
|
|
29
|
-
narrowHorizontalChoiceMaxWidthPx: number;
|
|
30
32
|
stimulusGridColumnGapRem: number;
|
|
31
33
|
stimulusGridRowGapRem: number;
|
|
32
34
|
stimulusSentenceMarginTopRem: number;
|
|
@@ -41,39 +43,44 @@ export declare const createDefaultModel: (model?: McpbQuestion) => {
|
|
|
41
43
|
inlineTemplateMarginTopRem: number;
|
|
42
44
|
inlineChoicesMarginTopRem: number;
|
|
43
45
|
};
|
|
44
|
-
id
|
|
45
|
-
element
|
|
46
|
-
prompt
|
|
47
|
-
promptEnabled
|
|
48
|
-
interactionMode
|
|
49
|
-
layoutProfile
|
|
50
|
-
choiceLayout
|
|
51
|
-
layoutProfilePresets
|
|
52
|
-
audioButtonSkin
|
|
53
|
-
audioButtonSkinsByLocale
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
completeAudioEnabled: boolean;
|
|
66
|
-
audioUrl: string;
|
|
67
|
-
audioTranscript: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
element?: string;
|
|
48
|
+
prompt?: string;
|
|
49
|
+
promptEnabled?: boolean;
|
|
50
|
+
interactionMode?: import("../shared/types.js").McpbInteractionMode;
|
|
51
|
+
layoutProfile?: string;
|
|
52
|
+
choiceLayout?: string;
|
|
53
|
+
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits.js").LayoutLimits>>;
|
|
54
|
+
audioButtonSkin?: import("../shared/types.js").McpbAudioButtonSkin | null;
|
|
55
|
+
audioButtonSkinsByLocale?: Record<string, import("../shared/types.js").McpbAudioButtonSkin>;
|
|
56
|
+
sentenceHtml?: string;
|
|
57
|
+
template?: string;
|
|
58
|
+
choiceMode?: import("../shared/types.js").McpbChoiceMode;
|
|
59
|
+
choices?: McpbChoice[];
|
|
60
|
+
choiceGroupLabel?: string;
|
|
61
|
+
correctChoiceId?: string;
|
|
62
|
+
hasAudio?: boolean;
|
|
63
|
+
autoplayAudioEnabled?: boolean;
|
|
64
|
+
completeAudioEnabled?: boolean;
|
|
65
|
+
audioUrl?: string;
|
|
66
|
+
audioTranscript?: string;
|
|
68
67
|
useFeatureButtonAudio?: boolean;
|
|
69
|
-
locale
|
|
70
|
-
|
|
68
|
+
locale?: string;
|
|
69
|
+
language?: string;
|
|
71
70
|
lockChoiceOrder?: boolean;
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
shuffle?: boolean;
|
|
72
|
+
teacherInstructions?: string;
|
|
73
|
+
teacherInstructionsEnabled?: boolean;
|
|
74
|
+
printAnswerKey?: boolean;
|
|
74
75
|
customType?: string;
|
|
75
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* What `model()` and `outcome()` read: the item's own fields, with neutral
|
|
79
|
+
* values for the rest. Starter content here would reach a learner as the
|
|
80
|
+
* item's prompt or choices.
|
|
81
|
+
*/
|
|
76
82
|
export declare const normalize: (question?: McpbQuestion) => {
|
|
83
|
+
lockChoiceOrder: boolean;
|
|
77
84
|
layoutLimits: {
|
|
78
85
|
blankStandaloneWidthRem: number;
|
|
79
86
|
blankWideWidthRem: number;
|
|
@@ -96,7 +103,6 @@ export declare const normalize: (question?: McpbQuestion) => {
|
|
|
96
103
|
audioBlankTemplateMarginTopRem: number;
|
|
97
104
|
audioBlankTemplateMarginBottomRem: number;
|
|
98
105
|
audioInstructionsMaxWidthPx: number;
|
|
99
|
-
narrowHorizontalChoiceMaxWidthPx: number;
|
|
100
106
|
stimulusGridColumnGapRem: number;
|
|
101
107
|
stimulusGridRowGapRem: number;
|
|
102
108
|
stimulusSentenceMarginTopRem: number;
|
|
@@ -111,43 +117,52 @@ export declare const normalize: (question?: McpbQuestion) => {
|
|
|
111
117
|
inlineTemplateMarginTopRem: number;
|
|
112
118
|
inlineChoicesMarginTopRem: number;
|
|
113
119
|
};
|
|
114
|
-
id
|
|
115
|
-
element
|
|
116
|
-
prompt
|
|
117
|
-
promptEnabled
|
|
118
|
-
interactionMode
|
|
119
|
-
layoutProfile
|
|
120
|
-
choiceLayout
|
|
121
|
-
layoutProfilePresets
|
|
122
|
-
audioButtonSkin
|
|
123
|
-
audioButtonSkinsByLocale
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
completeAudioEnabled: boolean;
|
|
136
|
-
audioUrl: string;
|
|
137
|
-
audioTranscript: string;
|
|
120
|
+
id?: string;
|
|
121
|
+
element?: string;
|
|
122
|
+
prompt?: string;
|
|
123
|
+
promptEnabled?: boolean;
|
|
124
|
+
interactionMode?: import("../shared/types.js").McpbInteractionMode;
|
|
125
|
+
layoutProfile?: string;
|
|
126
|
+
choiceLayout?: string;
|
|
127
|
+
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits.js").LayoutLimits>>;
|
|
128
|
+
audioButtonSkin?: import("../shared/types.js").McpbAudioButtonSkin | null;
|
|
129
|
+
audioButtonSkinsByLocale?: Record<string, import("../shared/types.js").McpbAudioButtonSkin>;
|
|
130
|
+
sentenceHtml?: string;
|
|
131
|
+
template?: string;
|
|
132
|
+
choiceMode?: import("../shared/types.js").McpbChoiceMode;
|
|
133
|
+
choices?: McpbChoice[];
|
|
134
|
+
choiceGroupLabel?: string;
|
|
135
|
+
correctChoiceId?: string;
|
|
136
|
+
hasAudio?: boolean;
|
|
137
|
+
autoplayAudioEnabled?: boolean;
|
|
138
|
+
completeAudioEnabled?: boolean;
|
|
139
|
+
audioUrl?: string;
|
|
140
|
+
audioTranscript?: string;
|
|
138
141
|
useFeatureButtonAudio?: boolean;
|
|
139
|
-
locale
|
|
142
|
+
locale?: string;
|
|
143
|
+
language?: string;
|
|
140
144
|
shuffle?: boolean;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
teacherInstructions?: string;
|
|
146
|
+
teacherInstructionsEnabled?: boolean;
|
|
147
|
+
printAnswerKey?: boolean;
|
|
144
148
|
customType?: string;
|
|
145
149
|
};
|
|
146
150
|
export declare const normalizeSession: (s: McpbSession) => McpbSession;
|
|
147
|
-
|
|
148
|
-
shuffledValues: string[];
|
|
149
|
-
}) => Promise<void>;
|
|
150
|
-
export declare const model: (question: McpbQuestion, session: McpbSession | null, env: McpbEnv | null, updateSession?: UpdateSessionFn) => Promise<Record<string, unknown>>;
|
|
151
|
+
export declare const model: (question: McpbQuestion, session: McpbSession | null, env: McpbEnv | null, updateSession?: PieUpdateSession) => Promise<Record<string, unknown>>;
|
|
151
152
|
export declare const createCorrectResponseSession: (question: McpbQuestion, env: McpbEnv) => Promise<unknown>;
|
|
152
|
-
|
|
153
|
+
type ValidateConfig = {
|
|
154
|
+
minAnswerChoices?: number;
|
|
155
|
+
maxAnswerChoices?: number;
|
|
156
|
+
prompt?: {
|
|
157
|
+
required?: boolean;
|
|
158
|
+
};
|
|
159
|
+
teacherInstructions?: {
|
|
160
|
+
required?: boolean;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Authoring errors in multiple-choice's shape: a message per field, and under
|
|
165
|
+
* `choices` a message per choice keyed by its id.
|
|
166
|
+
*/
|
|
167
|
+
export declare const validate: (question?: McpbQuestion, config?: ValidateConfig) => Record<string, string | Record<string, string>>;
|
|
153
168
|
export {};
|
package/dist/controller/index.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
//#region ../../shared/controller-utils/dist/index.js
|
|
2
|
+
function n(e) {
|
|
3
|
+
return Array.isArray(e) ? e.filter((e) => e != null) : [];
|
|
4
|
+
}
|
|
5
|
+
function r(e) {
|
|
6
|
+
let t = [...e];
|
|
7
|
+
for (let e = t.length - 1; e > 0; e--) {
|
|
8
|
+
let n = Math.floor(Math.random() * (e + 1));
|
|
9
|
+
[t[e], t[n]] = [t[n], t[e]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
}
|
|
13
|
+
function i(e) {
|
|
14
|
+
return e == null ? !0 : Array.isArray(e) ? e.length === 0 : typeof e == "object" && Object.keys(e).length === 0;
|
|
15
|
+
}
|
|
16
|
+
async function a(e, t, a, o = "value") {
|
|
17
|
+
let s = n(t?.data?.shuffledValues ?? t?.shuffledValues ?? []);
|
|
18
|
+
if (!t) {
|
|
19
|
+
console.warn("Unable to save shuffled choices because there's no session.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!i(s)) return n(s.map((t) => e.find((e) => e[o] === t)));
|
|
23
|
+
let c = r(e);
|
|
24
|
+
if (a && typeof a == "function") try {
|
|
25
|
+
let e = n(c.map((e) => e[o]));
|
|
26
|
+
i(e) ? console.error(`shuffledValues is an empty array - refusing to call updateSession. shuffledChoices: ${JSON.stringify(c)}, key: ${o}`) : await a(t.id, t.element, { shuffledValues: e });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.warn("Unable to save shuffled order for choices"), console.error(e);
|
|
29
|
+
}
|
|
30
|
+
else console.warn("Unable to save shuffled choices, shuffle will happen every time.");
|
|
31
|
+
return c;
|
|
32
|
+
}
|
|
33
|
+
function o(e, t, n) {
|
|
34
|
+
return !!(e.lockChoiceOrder || n?.["@pie-element"]?.lockChoiceOrder);
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
1
37
|
//#region src/shared/layoutLimits.ts
|
|
2
38
|
/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */
|
|
3
39
|
var DEFAULT_LAYOUT_LIMITS = {
|
|
@@ -22,7 +58,6 @@ var DEFAULT_LAYOUT_LIMITS = {
|
|
|
22
58
|
audioBlankTemplateMarginTopRem: .8,
|
|
23
59
|
audioBlankTemplateMarginBottomRem: 1.8,
|
|
24
60
|
audioInstructionsMaxWidthPx: 875,
|
|
25
|
-
narrowHorizontalChoiceMaxWidthPx: 230,
|
|
26
61
|
stimulusGridColumnGapRem: 2,
|
|
27
62
|
stimulusGridRowGapRem: .7,
|
|
28
63
|
stimulusSentenceMarginTopRem: .2,
|
|
@@ -41,38 +76,44 @@ var DEFAULT_LAYOUT_LIMITS = {
|
|
|
41
76
|
//#region src/controller/defaults.ts
|
|
42
77
|
/** Token authors must include exactly once in `template` (HTML). */
|
|
43
78
|
var BLANK_TOKEN = "{{blank}}";
|
|
79
|
+
/**
|
|
80
|
+
* The values `normalize` fills in for fields an item leaves out. They carry no
|
|
81
|
+
* content and no `id` or `element`: those come from the item config, and a
|
|
82
|
+
* default here would stand in for a field the author never set.
|
|
83
|
+
*/
|
|
84
|
+
var defaults_default$1 = { model: {
|
|
85
|
+
prompt: "",
|
|
86
|
+
promptEnabled: true,
|
|
87
|
+
interactionMode: "populate_blank",
|
|
88
|
+
layoutProfile: "",
|
|
89
|
+
choiceLayout: "",
|
|
90
|
+
layoutProfilePresets: {},
|
|
91
|
+
layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },
|
|
92
|
+
audioButtonSkin: null,
|
|
93
|
+
audioButtonSkinsByLocale: {},
|
|
94
|
+
sentenceHtml: "",
|
|
95
|
+
template: "",
|
|
96
|
+
choiceMode: "text",
|
|
97
|
+
choices: [],
|
|
98
|
+
choiceGroupLabel: "",
|
|
99
|
+
correctChoiceId: "",
|
|
100
|
+
hasAudio: false,
|
|
101
|
+
autoplayAudioEnabled: false,
|
|
102
|
+
completeAudioEnabled: false,
|
|
103
|
+
audioUrl: "",
|
|
104
|
+
audioTranscript: "",
|
|
105
|
+
locale: "",
|
|
106
|
+
teacherInstructions: "",
|
|
107
|
+
teacherInstructionsEnabled: true
|
|
108
|
+
} };
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/author/defaults.ts
|
|
111
|
+
/** The model a new item starts from: a valid question an author edits in place. */
|
|
44
112
|
var defaults_default = {
|
|
45
113
|
model: {
|
|
46
|
-
|
|
47
|
-
element: "mc-populated-blank",
|
|
114
|
+
...defaults_default$1.model,
|
|
48
115
|
prompt: "<p><strong>Demo prompt</strong></p>",
|
|
49
|
-
promptEnabled: true,
|
|
50
|
-
interactionMode: "populate_blank",
|
|
51
|
-
layoutProfile: "",
|
|
52
|
-
choiceLayout: "",
|
|
53
|
-
layoutProfilePresets: {},
|
|
54
|
-
layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },
|
|
55
|
-
audioButtonSkin: null,
|
|
56
|
-
audioButtonSkinsByLocale: {},
|
|
57
|
-
uiText: {
|
|
58
|
-
answerChoices: "Answer choices",
|
|
59
|
-
selectedAnswerInSentence: "blank",
|
|
60
|
-
blankPreSelectionHint: "The answer you choose will appear on the blank line above.",
|
|
61
|
-
showCorrectAnswer: "Show correct answer",
|
|
62
|
-
hideCorrectAnswer: "Hide correct answer",
|
|
63
|
-
clickToEnableAutoplay: "Click to enable audio autoplay",
|
|
64
|
-
audioResourceUnavailable: "Audio is enabled but no playable audio URL is configured.",
|
|
65
|
-
transcriptLabel: "Transcript",
|
|
66
|
-
listenLabelEn: "Listen",
|
|
67
|
-
listenLabelEs: "Escuchar",
|
|
68
|
-
listenSilentAlt: "Repeat instructions",
|
|
69
|
-
listenPlayingAlt: "Instructions are playing",
|
|
70
|
-
listenSilentAltEs: "Escuchar. Repetir las instrucciones.",
|
|
71
|
-
listenPlayingAltEs: "Escuchar. Estas son las instrucciones."
|
|
72
|
-
},
|
|
73
|
-
sentenceHtml: "",
|
|
74
116
|
template: `<p>The answer is ${BLANK_TOKEN}.</p>`,
|
|
75
|
-
choiceMode: "text",
|
|
76
117
|
choices: [
|
|
77
118
|
{
|
|
78
119
|
id: "c1",
|
|
@@ -87,15 +128,7 @@ var defaults_default = {
|
|
|
87
128
|
labelHtml: "<p>Option C</p>"
|
|
88
129
|
}
|
|
89
130
|
],
|
|
90
|
-
correctChoiceId: "c2"
|
|
91
|
-
hasAudio: false,
|
|
92
|
-
autoplayAudioEnabled: false,
|
|
93
|
-
completeAudioEnabled: false,
|
|
94
|
-
audioUrl: "",
|
|
95
|
-
audioTranscript: "",
|
|
96
|
-
locale: "",
|
|
97
|
-
teacherInstructions: "",
|
|
98
|
-
teacherInstructionsEnabled: false
|
|
131
|
+
correctChoiceId: "c2"
|
|
99
132
|
},
|
|
100
133
|
configuration: { prompt: {
|
|
101
134
|
label: "Prompt",
|
|
@@ -115,10 +148,8 @@ var getCorrectness = (question, session) => {
|
|
|
115
148
|
if (session.choiceId === correct) return "correct";
|
|
116
149
|
return "incorrect";
|
|
117
150
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return 1;
|
|
121
|
-
};
|
|
151
|
+
/** One blank, one key: an answered session scores 1 only when it picked the key. */
|
|
152
|
+
var getPartialScore = (question, session) => getCorrectness(question, session) === "correct" ? 1 : 0;
|
|
122
153
|
var outcome = (question, session, env) => new Promise((resolve) => {
|
|
123
154
|
if (!session || isEmptyObject(session)) {
|
|
124
155
|
resolve({
|
|
@@ -129,8 +160,8 @@ var outcome = (question, session, env) => new Promise((resolve) => {
|
|
|
129
160
|
return;
|
|
130
161
|
}
|
|
131
162
|
session = normalizeSession(session);
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
163
|
+
const normalizedQuestion = normalize(question);
|
|
164
|
+
if (getCorrectness(normalizedQuestion, session) === "unanswered") {
|
|
134
165
|
resolve({
|
|
135
166
|
score: 0,
|
|
136
167
|
empty: true,
|
|
@@ -138,68 +169,58 @@ var outcome = (question, session, env) => new Promise((resolve) => {
|
|
|
138
169
|
});
|
|
139
170
|
return;
|
|
140
171
|
}
|
|
141
|
-
const score =
|
|
172
|
+
const score = getPartialScore(normalizedQuestion, session);
|
|
142
173
|
resolve({
|
|
143
174
|
score,
|
|
144
175
|
empty: false,
|
|
145
176
|
traceLog: [
|
|
146
177
|
`Mode: ${env?.mode || "unknown"}.`,
|
|
147
178
|
`Student selected choice: ${session.choiceId}.`,
|
|
148
|
-
`Correct choice: ${
|
|
179
|
+
`Correct choice: ${normalizedQuestion.correctChoiceId || "none"}.`,
|
|
149
180
|
`Final score: ${score}.`
|
|
150
181
|
]
|
|
151
182
|
});
|
|
152
183
|
});
|
|
153
|
-
var
|
|
154
|
-
...
|
|
184
|
+
var withDefaults = (base, model = {}) => ({
|
|
185
|
+
...base,
|
|
155
186
|
...model,
|
|
156
187
|
layoutLimits: {
|
|
157
188
|
...DEFAULT_LAYOUT_LIMITS,
|
|
158
189
|
...model?.layoutLimits || {}
|
|
159
190
|
}
|
|
160
191
|
});
|
|
161
|
-
|
|
162
|
-
var
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
|
-
var getStoredShuffle = (session) => Array.isArray(session?.data?.shuffledValues) ? session.data.shuffledValues : Array.isArray(session?.shuffledValues) ? session.shuffledValues : [];
|
|
178
|
-
var applyShuffledValues = (choices, shuffledValues, choiceKey) => {
|
|
179
|
-
const orderedChoices = shuffledValues.map((value) => choices.find((choice) => choice?.[choiceKey] === value)).filter((c) => !!c);
|
|
180
|
-
if (orderedChoices.length === choices.length) return orderedChoices;
|
|
181
|
-
const orderedValues = new Set(orderedChoices.map((choice) => choice[choiceKey]));
|
|
182
|
-
const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));
|
|
183
|
-
return [...orderedChoices, ...leftovers];
|
|
184
|
-
};
|
|
185
|
-
var getOrderedChoices = async (question, session, env, updateSession) => {
|
|
186
|
-
const choices = Array.isArray(question?.choices) ? [...question.choices] : [];
|
|
187
|
-
if (!choices.length || !shouldShuffleChoices(question)) return choices;
|
|
188
|
-
if (shouldLockChoices(question, env || {})) return choices;
|
|
189
|
-
const shuffledValues = getStoredShuffle(session);
|
|
190
|
-
if (shuffledValues.length) return applyShuffledValues(choices, shuffledValues, "id");
|
|
191
|
-
const shuffledChoices = shuffleArray(choices);
|
|
192
|
-
if (updateSession && typeof updateSession === "function" && session?.id && session?.element) {
|
|
193
|
-
const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);
|
|
194
|
-
if (shuffledIds.length) await updateSession(session.id, session.element, { shuffledValues: shuffledIds });
|
|
195
|
-
}
|
|
196
|
-
return shuffledChoices;
|
|
192
|
+
/** The authoring starting point: a question that passes `validate()` as it stands. */
|
|
193
|
+
var createDefaultModel = (model = {}) => withDefaults(defaults_default.model, model);
|
|
194
|
+
/**
|
|
195
|
+
* What `model()` and `outcome()` read: the item's own fields, with neutral
|
|
196
|
+
* values for the rest. Starter content here would reach a learner as the
|
|
197
|
+
* item's prompt or choices.
|
|
198
|
+
*/
|
|
199
|
+
var normalize = (question = {}) => {
|
|
200
|
+
const { shuffle, ...rest } = question || {};
|
|
201
|
+
return {
|
|
202
|
+
...withDefaults(defaults_default$1.model, rest),
|
|
203
|
+
lockChoiceOrder: typeof rest.lockChoiceOrder === "boolean" ? rest.lockChoiceOrder : shuffle !== true
|
|
204
|
+
};
|
|
197
205
|
};
|
|
206
|
+
var normalizeSession = (s) => ({ ...s });
|
|
207
|
+
/**
|
|
208
|
+
* The fields delivery renders. An imported choice can carry more, such as a
|
|
209
|
+
* correctness flag or feedback, which must not reach a learner's browser.
|
|
210
|
+
*/
|
|
211
|
+
var toDeliveryChoice = ({ id, labelHtml, imageUrl, imageAlt }) => ({
|
|
212
|
+
id,
|
|
213
|
+
...labelHtml !== void 0 ? { labelHtml } : {},
|
|
214
|
+
...imageUrl !== void 0 ? { imageUrl } : {},
|
|
215
|
+
...imageAlt !== void 0 ? { imageAlt } : {}
|
|
216
|
+
});
|
|
198
217
|
var model = async (question, session, env, updateSession) => {
|
|
199
218
|
const safeSession = session || {};
|
|
200
219
|
const safeEnv = env || {};
|
|
201
220
|
const normalizedQuestion = normalize(question);
|
|
202
|
-
|
|
221
|
+
let orderedChoices = Array.isArray(normalizedQuestion.choices) ? [...normalizedQuestion.choices] : [];
|
|
222
|
+
if (orderedChoices.length && !o(normalizedQuestion, safeSession, safeEnv)) orderedChoices = await a(orderedChoices, safeSession, updateSession, "id") ?? orderedChoices;
|
|
223
|
+
const choices = orderedChoices.filter((c) => !!c && typeof c === "object").map(toDeliveryChoice);
|
|
203
224
|
const out = {
|
|
204
225
|
prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,
|
|
205
226
|
interactionMode: normalizedQuestion.interactionMode || "populate_blank",
|
|
@@ -209,11 +230,11 @@ var model = async (question, session, env, updateSession) => {
|
|
|
209
230
|
layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },
|
|
210
231
|
audioButtonSkin: normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === "object" ? normalizedQuestion.audioButtonSkin : null,
|
|
211
232
|
audioButtonSkinsByLocale: normalizedQuestion.audioButtonSkinsByLocale && typeof normalizedQuestion.audioButtonSkinsByLocale === "object" ? normalizedQuestion.audioButtonSkinsByLocale : {},
|
|
212
|
-
uiText: normalizedQuestion.uiText && typeof normalizedQuestion.uiText === "object" ? normalizedQuestion.uiText : {},
|
|
213
233
|
sentenceHtml: normalizedQuestion.sentenceHtml || null,
|
|
214
234
|
template: normalizedQuestion.template,
|
|
215
235
|
choiceMode: normalizedQuestion.choiceMode,
|
|
216
236
|
choices,
|
|
237
|
+
choiceGroupLabel: normalizedQuestion.choiceGroupLabel || "",
|
|
217
238
|
hasAudio: normalizedQuestion.hasAudio,
|
|
218
239
|
autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,
|
|
219
240
|
completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,
|
|
@@ -222,6 +243,7 @@ var model = async (question, session, env, updateSession) => {
|
|
|
222
243
|
customType: normalizedQuestion.customType || "",
|
|
223
244
|
useFeatureButtonAudio: typeof normalizedQuestion.useFeatureButtonAudio === "boolean" ? normalizedQuestion.useFeatureButtonAudio : void 0,
|
|
224
245
|
locale: normalizedQuestion.locale || "",
|
|
246
|
+
language: normalizedQuestion.language || normalizedQuestion.locale || void 0,
|
|
225
247
|
disabled: safeEnv.mode !== "gather",
|
|
226
248
|
mode: safeEnv.mode
|
|
227
249
|
};
|
|
@@ -239,18 +261,22 @@ var createCorrectResponseSession = (question, env) => {
|
|
|
239
261
|
return new Promise((resolve) => {
|
|
240
262
|
if (env.mode !== "evaluate" && env.role === "instructor") resolve({
|
|
241
263
|
id: "1",
|
|
242
|
-
element: "mc-populated-blank",
|
|
243
264
|
choiceId: question?.correctChoiceId || ""
|
|
244
265
|
});
|
|
245
266
|
else resolve(null);
|
|
246
267
|
});
|
|
247
268
|
};
|
|
248
|
-
|
|
269
|
+
/** Markup with its tags removed, keeping media an author can answer from. */
|
|
270
|
+
var getContent = (html) => (html || "").replace(/(<(?!img|iframe|source)([^>]+)>)/gi, "").trim();
|
|
271
|
+
/**
|
|
272
|
+
* Authoring errors in multiple-choice's shape: a message per field, and under
|
|
273
|
+
* `choices` a message per choice keyed by its id.
|
|
274
|
+
*/
|
|
275
|
+
var validate = (question = {}, config = {}) => {
|
|
249
276
|
const errors = {};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
277
|
+
const { minAnswerChoices = 2, maxAnswerChoices } = config;
|
|
278
|
+
if ((question.promptEnabled || config.prompt?.required) && !getContent(question.prompt)) errors.prompt = "This field is required.";
|
|
279
|
+
if (config.teacherInstructions?.required && !getContent(question.teacherInstructions)) errors.teacherInstructions = "This field is required.";
|
|
254
280
|
const interactionMode = question.interactionMode || "populate_blank";
|
|
255
281
|
const n = countBlankTokens(question.template || "");
|
|
256
282
|
if (interactionMode === "audio_mc_only") {
|
|
@@ -260,33 +286,35 @@ var validate = (question = {}, _config = {}) => {
|
|
|
260
286
|
else if (n > 1) errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;
|
|
261
287
|
} else errors.interactionMode = "Unknown interaction mode";
|
|
262
288
|
const choices = Array.isArray(question.choices) ? question.choices : [];
|
|
263
|
-
if (choices.length <
|
|
289
|
+
if (choices.length < minAnswerChoices) errors.answerChoices = `There should be at least ${minAnswerChoices} choices defined.`;
|
|
290
|
+
else if (maxAnswerChoices != null && choices.length > maxAnswerChoices) errors.answerChoices = `No more than ${maxAnswerChoices} choices should be defined.`;
|
|
264
291
|
const mode = question.choiceMode || "text";
|
|
265
|
-
|
|
266
|
-
|
|
292
|
+
const choicesErrors = {};
|
|
293
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
294
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
295
|
+
choices.forEach((c, i) => {
|
|
296
|
+
const key = c?.id || String(i);
|
|
267
297
|
if (!c?.id) {
|
|
268
|
-
|
|
269
|
-
|
|
298
|
+
choicesErrors[key] = "Choice needs an id.";
|
|
299
|
+
return;
|
|
270
300
|
}
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
errors.choices = `Choice ${i + 1} needs label text`;
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
} else {
|
|
278
|
-
if (!c.imageUrl?.trim()) {
|
|
279
|
-
errors.choices = `Choice ${i + 1} needs an image URL`;
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
if (!c.imageAlt?.trim()) {
|
|
283
|
-
errors.choices = `Choice ${i + 1} needs image alt text`;
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
301
|
+
if (seenIds.has(c.id)) {
|
|
302
|
+
choicesErrors[key] = "Choice id should be unique.";
|
|
303
|
+
return;
|
|
286
304
|
}
|
|
287
|
-
|
|
305
|
+
seenIds.add(c.id);
|
|
306
|
+
if (mode === "text") {
|
|
307
|
+
const label = getContent(c.labelHtml);
|
|
308
|
+
if (!label) choicesErrors[key] = "Content should not be empty.";
|
|
309
|
+
else if (seenLabels.has(label)) choicesErrors[key] = "Content should be unique.";
|
|
310
|
+
seenLabels.add(label);
|
|
311
|
+
} else if (!c.imageUrl?.trim()) choicesErrors[key] = "An image is required.";
|
|
312
|
+
else if (!c.imageAlt?.trim()) choicesErrors[key] = "Image alt text is required.";
|
|
313
|
+
});
|
|
314
|
+
if (Object.keys(choicesErrors).length) errors.choices = choicesErrors;
|
|
288
315
|
const correct = question.correctChoiceId;
|
|
289
|
-
if (!correct
|
|
316
|
+
if (!correct) errors.correctResponse = "No correct response defined.";
|
|
317
|
+
else if (!choices.some((c) => c?.id === correct)) errors.correctResponse = "The correct response must be one of the choices.";
|
|
290
318
|
if (question.hasAudio) {
|
|
291
319
|
if (!!!question.audioUrl?.trim()) errors.audioUrl = "A playable audio URL is required when audio is enabled";
|
|
292
320
|
}
|
|
@@ -313,7 +341,6 @@ var validate = (question = {}, _config = {}) => {
|
|
|
313
341
|
"audioBlankTemplateMarginTopRem",
|
|
314
342
|
"audioBlankTemplateMarginBottomRem",
|
|
315
343
|
"audioInstructionsMaxWidthPx",
|
|
316
|
-
"narrowHorizontalChoiceMaxWidthPx",
|
|
317
344
|
"stimulusGridColumnGapRem",
|
|
318
345
|
"stimulusGridRowGapRem",
|
|
319
346
|
"stimulusSentenceMarginTopRem",
|