@pie-element/mc-populated-blank 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +0 -2
  2. package/dist/author/index.js +186 -152
  3. package/dist/author/index.js.map +1 -1
  4. package/dist/controller/defaults.d.ts +41 -73
  5. package/dist/controller/defaults.d.ts.map +1 -1
  6. package/dist/controller/index.d.ts +154 -10
  7. package/dist/controller/index.d.ts.map +1 -1
  8. package/dist/controller/index.js +29 -30
  9. package/dist/controller/index.js.map +1 -1
  10. package/dist/delivery/computeAudioMode.d.ts +7 -0
  11. package/dist/delivery/computeAudioMode.d.ts.map +1 -0
  12. package/dist/delivery/computeChoiceCorrectness.d.ts +18 -0
  13. package/dist/delivery/computeChoiceCorrectness.d.ts.map +1 -0
  14. package/dist/delivery/computeDisplayState.d.ts +47 -0
  15. package/dist/delivery/computeDisplayState.d.ts.map +1 -0
  16. package/dist/delivery/computeLayoutProfile.d.ts +18 -0
  17. package/dist/delivery/computeLayoutProfile.d.ts.map +1 -0
  18. package/dist/delivery/computeLayoutStyle.d.ts +27 -0
  19. package/dist/delivery/computeLayoutStyle.d.ts.map +1 -0
  20. package/dist/delivery/index.d.ts.map +1 -1
  21. package/dist/delivery/index.js +1591 -927
  22. package/dist/delivery/index.js.map +1 -1
  23. package/dist/delivery/variant-css-map.d.ts.map +1 -1
  24. package/dist/index.iife.d.ts +151 -10
  25. package/dist/index.iife.d.ts.map +1 -1
  26. package/dist/index.iife.js +233 -114
  27. package/dist/index.js +1591 -927
  28. package/dist/index.js.map +1 -1
  29. package/dist/print/index.js +194 -166
  30. package/dist/print/index.js.map +1 -1
  31. package/dist/shared/layoutLimits.d.ts +40 -0
  32. package/dist/shared/layoutLimits.d.ts.map +1 -0
  33. package/dist/shared/types.d.ts +82 -0
  34. package/dist/shared/types.d.ts.map +1 -0
  35. package/package.json +5 -14
  36. package/src/author/Author.svelte +0 -41
  37. package/src/author/index.ts +0 -28
  38. package/src/controller/defaults.ts +0 -91
  39. package/src/controller/index.ts +0 -359
  40. package/src/delivery/McPopulatedBlank.svelte +0 -1246
  41. package/src/delivery/cqt-css/sel-r1-g-stem.css +0 -29
  42. package/src/delivery/cqt-css/sel-r1-gg-plus.css +0 -29
  43. package/src/delivery/cqt-css/sel-r1-ggplus.css +0 -34
  44. package/src/delivery/cqt-css/sel-r1-gplusggg.css +0 -20
  45. package/src/delivery/cqt-css/sel-r1-plusggg.css +0 -20
  46. package/src/delivery/cqt-css/sel-r1-s3.css +0 -91
  47. package/src/delivery/cqt-css/sel-vic.css +0 -38
  48. package/src/delivery/cqt-css/sr-vic.css +0 -14
  49. package/src/delivery/index.ts +0 -103
  50. package/src/delivery/variant-css-map.ts +0 -110
  51. package/src/index.iife.ts +0 -13
  52. package/src/index.ts +0 -1
  53. package/src/print/Print.svelte +0 -95
  54. package/src/print/index.ts +0 -18
  55. package/src/types/css-raw.d.ts +0 -4
@@ -1,359 +0,0 @@
1
- import defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';
2
-
3
- const isEmptyObject = (value: unknown): boolean =>
4
- !!value &&
5
- typeof value === 'object' &&
6
- !Array.isArray(value) &&
7
- Object.keys(value as Record<string, unknown>).length === 0;
8
-
9
- export function countBlankTokens(template: string): number {
10
- if (!template) return 0;
11
- const re = /\{\{blank\}\}/g;
12
- return (template.match(re) || []).length;
13
- }
14
-
15
- export const getCorrectness = (question: any, session: any) => {
16
- if (!session || !session.choiceId) {
17
- return 'unanswered';
18
- }
19
- const correct = question?.correctChoiceId || '';
20
- if (session.choiceId === correct) {
21
- return 'correct';
22
- }
23
- return 'incorrect';
24
- };
25
-
26
- export const getPartialScore = (_question: any, session: any) => {
27
- if (!session || isEmptyObject(session) || !session.choiceId) {
28
- return 0;
29
- }
30
- return 1;
31
- };
32
-
33
- export const isComplete = (question: any, session: any, audioComplete = false) => {
34
- if (!session || !session.choiceId) {
35
- return false;
36
- }
37
- const autoplayAudioEnabled = !!question?.autoplayAudioEnabled;
38
- const completeAudioEnabled = !!question?.completeAudioEnabled;
39
- const requiresAudioCompletion =
40
- autoplayAudioEnabled && completeAudioEnabled && !!question?.hasAudio;
41
- if (requiresAudioCompletion && !audioComplete) {
42
- return false;
43
- }
44
- return true;
45
- };
46
-
47
- export const outcome = (question: any, session: any, env: any) =>
48
- new Promise((resolve) => {
49
- if (!session || isEmptyObject(session)) {
50
- resolve({
51
- score: 0,
52
- empty: true,
53
- traceLog: ['Student did not select any answers. Score is 0.'],
54
- });
55
- return;
56
- }
57
-
58
- session = normalizeSession(session);
59
- const correctness = getCorrectness(question, session);
60
-
61
- if (correctness === 'unanswered') {
62
- resolve({
63
- score: 0,
64
- empty: true,
65
- traceLog: ['Student did not select any answers. Score is 0.'],
66
- });
67
- return;
68
- }
69
-
70
- const score = correctness === 'correct' ? 1 : 0;
71
- const traceLog = [
72
- `Mode: ${env?.mode || 'unknown'}.`,
73
- `Student selected choice: ${session.choiceId}.`,
74
- `Correct choice: ${question?.correctChoiceId || 'none'}.`,
75
- `Final score: ${score}.`,
76
- ];
77
- resolve({ score, empty: false, traceLog });
78
- });
79
-
80
- export const createDefaultModel = (model: any = {}) => ({
81
- ...defaults.model,
82
- ...model,
83
- layoutLimits: {
84
- ...DEFAULT_LAYOUT_LIMITS,
85
- ...(model?.layoutLimits || {}),
86
- },
87
- });
88
-
89
- export const normalize = (question: any = {}) => createDefaultModel(question);
90
-
91
- export const normalizeSession = (s: any) => ({ ...s });
92
-
93
- const shouldShuffleChoices = (question: any) => !!question?.shuffle;
94
-
95
- const shouldLockChoices = (question: any, env: any) => {
96
- if (question?.lockChoiceOrder) return true;
97
- if (env?.['@pie-element']?.lockChoiceOrder) return true;
98
- return env?.role === 'instructor';
99
- };
100
-
101
- function shuffleArray<T>(items: T[]): T[] {
102
- const out = [...items];
103
- for (let i = out.length - 1; i > 0; i--) {
104
- const j = Math.floor(Math.random() * (i + 1));
105
- [out[i], out[j]] = [out[j], out[i]];
106
- }
107
- return out;
108
- }
109
-
110
- const getStoredShuffle = (session: any): string[] =>
111
- Array.isArray(session?.data?.shuffledValues)
112
- ? session.data.shuffledValues
113
- : Array.isArray(session?.shuffledValues)
114
- ? session.shuffledValues
115
- : [];
116
-
117
- const applyShuffledValues = (choices: any[], shuffledValues: string[], choiceKey: string) => {
118
- const orderedChoices = shuffledValues
119
- .map((value) => choices.find((choice) => choice?.[choiceKey] === value))
120
- .filter(Boolean);
121
-
122
- if (orderedChoices.length === choices.length) {
123
- return orderedChoices;
124
- }
125
-
126
- const orderedValues = new Set(orderedChoices.map((choice: any) => choice[choiceKey]));
127
- const leftovers = choices.filter((choice) => !orderedValues.has(choice?.[choiceKey]));
128
- return [...orderedChoices, ...leftovers];
129
- };
130
-
131
- const getOrderedChoices = async (question: any, session: any, env: any, updateSession?: any) => {
132
- const choices = Array.isArray(question?.choices) ? [...question.choices] : [];
133
- if (!choices.length || !shouldShuffleChoices(question)) {
134
- return choices;
135
- }
136
-
137
- if (shouldLockChoices(question, env || {})) {
138
- return choices;
139
- }
140
-
141
- const shuffledValues = getStoredShuffle(session);
142
- if (shuffledValues.length) {
143
- return applyShuffledValues(choices, shuffledValues, 'id');
144
- }
145
-
146
- const shuffledChoices = shuffleArray(choices);
147
-
148
- if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {
149
- const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);
150
- if (shuffledIds.length) {
151
- await updateSession(session.id, session.element, { shuffledValues: shuffledIds });
152
- }
153
- }
154
-
155
- return shuffledChoices;
156
- };
157
-
158
- export const model = async (question: any, session: any, env: any, updateSession?: any) => {
159
- session = session || {};
160
- const safeEnv = env || {};
161
- const normalizedQuestion = normalize(question);
162
- const choices = await getOrderedChoices(normalizedQuestion, session, safeEnv, updateSession);
163
-
164
- const out: any = {
165
- prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,
166
- interactionMode: normalizedQuestion.interactionMode || 'populate_blank',
167
- layoutProfile: normalizedQuestion.layoutProfile || '',
168
- choiceLayout: normalizedQuestion.choiceLayout || '',
169
- layoutProfilePresets:
170
- normalizedQuestion.layoutProfilePresets &&
171
- typeof normalizedQuestion.layoutProfilePresets === 'object'
172
- ? normalizedQuestion.layoutProfilePresets
173
- : {},
174
- layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },
175
- audioButtonSkin:
176
- normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'
177
- ? normalizedQuestion.audioButtonSkin
178
- : null,
179
- audioButtonSkinsByLocale:
180
- normalizedQuestion.audioButtonSkinsByLocale &&
181
- typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'
182
- ? normalizedQuestion.audioButtonSkinsByLocale
183
- : {},
184
- uiText:
185
- normalizedQuestion.uiText && typeof normalizedQuestion.uiText === 'object'
186
- ? normalizedQuestion.uiText
187
- : {},
188
- sentenceHtml: normalizedQuestion.sentenceHtml || null,
189
- template: normalizedQuestion.template,
190
- choiceMode: normalizedQuestion.choiceMode,
191
- choices,
192
- hasAudio: normalizedQuestion.hasAudio,
193
- autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,
194
- completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,
195
- audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,
196
- audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,
197
- showVisibleTranscript: !!normalizedQuestion.showVisibleTranscript,
198
- customType: normalizedQuestion.customType || '',
199
- useFeatureButtonAudio:
200
- typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'
201
- ? normalizedQuestion.useFeatureButtonAudio
202
- : undefined,
203
- locale: normalizedQuestion.locale || '',
204
- disabled: safeEnv.mode !== 'gather',
205
- mode: safeEnv.mode,
206
- };
207
-
208
- if (safeEnv.mode === 'evaluate') {
209
- const correctness = getCorrectness(normalizedQuestion, session);
210
- out.correctness = correctness;
211
- out.responseCorrect = correctness === 'correct';
212
- out.correctChoiceId = normalizedQuestion.correctChoiceId;
213
- }
214
-
215
- if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {
216
- out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled
217
- ? normalizedQuestion.teacherInstructions
218
- : null;
219
- } else {
220
- out.teacherInstructions = null;
221
- }
222
-
223
- return out;
224
- };
225
-
226
- export const createCorrectResponseSession = (question: any, env: any) => {
227
- return new Promise((resolve) => {
228
- if (env.mode !== 'evaluate' && env.role === 'instructor') {
229
- resolve({
230
- id: '1',
231
- element: 'mc-populated-blank',
232
- choiceId: question?.correctChoiceId || '',
233
- });
234
- } else {
235
- resolve(null);
236
- }
237
- });
238
- };
239
-
240
- export const validate = (model: any = {}, _config: any = {}) => {
241
- const errors: any = {};
242
-
243
- if (model.promptEnabled) {
244
- const p = model.prompt?.trim() || '';
245
- if (!p || p === '<p></p>') {
246
- errors.prompt = 'Prompt is required when prompt is enabled';
247
- }
248
- }
249
-
250
- const interactionMode = model.interactionMode || 'populate_blank';
251
- const template = model.template || '';
252
- const n = countBlankTokens(template);
253
- if (interactionMode === 'audio_mc_only') {
254
- if (n > 0) {
255
- errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;
256
- }
257
- } else if (interactionMode === 'populate_blank') {
258
- if (n === 0) {
259
- errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;
260
- } else if (n > 1) {
261
- errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;
262
- }
263
- } else {
264
- errors.interactionMode = 'Unknown interaction mode';
265
- }
266
-
267
- const choices = Array.isArray(model.choices) ? model.choices : [];
268
- if (choices.length < 2) {
269
- errors.choices = 'At least two choices are required';
270
- }
271
-
272
- const mode = model.choiceMode || 'text';
273
- for (let i = 0; i < choices.length; i++) {
274
- const c = choices[i];
275
- if (!c?.id) {
276
- errors.choices = `Choice ${i + 1} is missing an id`;
277
- break;
278
- }
279
- if (mode === 'text') {
280
- const lbl = (c.labelHtml || '').trim();
281
- if (!lbl || lbl === '<p></p>') {
282
- errors.choices = `Choice ${i + 1} needs label text`;
283
- break;
284
- }
285
- } else {
286
- if (!c.imageUrl?.trim()) {
287
- errors.choices = `Choice ${i + 1} needs an image URL`;
288
- break;
289
- }
290
- if (!c.imageAlt?.trim()) {
291
- errors.choices = `Choice ${i + 1} needs image alt text`;
292
- break;
293
- }
294
- }
295
- }
296
-
297
- const correct = model.correctChoiceId;
298
- if (!correct || !choices.some((c: any) => c.id === correct)) {
299
- errors.correctChoiceId = 'Correct choice must match one of the choice ids';
300
- }
301
-
302
- if (model.hasAudio) {
303
- const hasAudioUrl = !!model.audioUrl?.trim();
304
- if (!hasAudioUrl) {
305
- errors.audioUrl = 'A playable audio URL is required when audio is enabled';
306
- }
307
- }
308
-
309
- const limits = model?.layoutLimits;
310
- if (limits && typeof limits === 'object') {
311
- const numericLimitKeys = [
312
- 'blankStandaloneWidthRem',
313
- 'blankWideWidthRem',
314
- 'blankUnderlineWidthPx',
315
- 'blankUnderlineWideWidthPx',
316
- 'horizontalChoiceWidthPx',
317
- 'horizontalChoiceWidthVw',
318
- 'horizontalChoiceTileMinHeightRem',
319
- 'horizontalChoiceContentMinHeightRem',
320
- 'selectedImageMaxHeightRem',
321
- 'choiceImageMaxHeightRem',
322
- 'listenButtonSizePx',
323
- 'stimulusMinColumnPx',
324
- 'textMinColumnPx',
325
- 'legendMaxChars',
326
- 'choiceGroupGapRem',
327
- 'choiceRowGapRem',
328
- 'toggleButtonGapRem',
329
- 'horizontalChoiceRadioTopMarginRem',
330
- 'audioBlankTemplateMarginTopRem',
331
- 'audioBlankTemplateMarginBottomRem',
332
- 'audioInstructionsMaxWidthPx',
333
- 'narrowHorizontalChoiceMaxWidthPx',
334
- 'stimulusGridColumnGapRem',
335
- 'stimulusGridRowGapRem',
336
- 'stimulusSentenceMarginTopRem',
337
- 'stimulusChoicesMarginTopRem',
338
- 'tokenGridColumnGapRem',
339
- 'tokenGridRowGapRem',
340
- 'tokenTemplateMarginTopRem',
341
- 'tokenInlineTokenGapRem',
342
- 'tokenChoicesMarginTopRem',
343
- 'inlineGridColumnGapRem',
344
- 'inlineGridRowGapRem',
345
- 'inlineTemplateMarginTopRem',
346
- 'inlineChoicesMarginTopRem',
347
- ];
348
- for (const key of numericLimitKeys) {
349
- if (limits[key] === undefined || limits[key] === null || limits[key] === '') continue;
350
- const value = Number(limits[key]);
351
- if (!Number.isFinite(value) || value <= 0) {
352
- errors.layoutLimits = `${key} must be a positive number when provided`;
353
- break;
354
- }
355
- }
356
- }
357
-
358
- return errors;
359
- };