@pie-element/mc-populated-blank 0.3.0-next.19 → 0.3.0-next.21
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/author/index.js +135 -133
- package/dist/author/index.js.map +1 -1
- package/dist/browser/{McPopulatedBlank-DJOmkW2m.js → McPopulatedBlank-B0ep0vS4.js} +2 -2
- package/dist/browser/{McPopulatedBlank-DJOmkW2m.js.map → McPopulatedBlank-B0ep0vS4.js.map} +1 -1
- package/dist/browser/author/index.js +1 -1
- package/dist/browser/author/index.js.map +1 -1
- package/dist/browser/{client-CWh3mUUV.js → client-Dfdyltec.js} +632 -617
- package/dist/browser/client-Dfdyltec.js.map +1 -0
- package/dist/browser/controller-DEUOAZHa.js.map +1 -1
- package/dist/browser/delivery/index.js +1 -1
- package/dist/browser/print/index.js +2 -2
- package/dist/browser/print/index.js.map +1 -1
- package/dist/controller/defaults.d.ts +1 -1
- package/dist/controller/index.d.ts +11 -11
- package/dist/controller/index.js.map +1 -1
- package/dist/delivery/computeLayoutStyle.d.ts +1 -1
- package/dist/delivery/index.js +146 -144
- package/dist/delivery/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.iife.d.ts +1 -1
- package/dist/index.iife.js +7 -7
- package/dist/print/index.js +146 -144
- package/dist/print/index.js.map +1 -1
- package/dist/print/preparePrint.d.ts +1 -1
- package/dist/shared/types.d.ts +1 -1
- package/module/print.js +1082 -1067
- package/module/print.js.map +1 -1
- package/package.json +4 -1
- package/dist/browser/client-CWh3mUUV.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller-DEUOAZHa.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/author/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits';\n\nexport { DEFAULT_LAYOUT_LIMITS };\n\n/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\n/**\n * The values `normalize` fills in for fields an item leaves out. They carry no\n * content and no `id` or `element`: those come from the item config, and a\n * default here would stand in for a field the author never set.\n */\nexport default {\n model: {\n prompt: '',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n sentenceHtml: '',\n template: '',\n choiceMode: 'text' as const,\n choices: [],\n choiceGroupLabel: '',\n correctChoiceId: '',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: true,\n },\n};\n","import controllerDefaults, { BLANK_TOKEN } from '../controller/defaults';\n\n/** The model a new item starts from: a valid question an author edits in place. */\nexport default {\n model: {\n ...controllerDefaults.model,\n prompt: '<p><strong>Demo prompt</strong></p>',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import authorDefaults from '../author/defaults';\nimport defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';\nimport type {\n McpbChoice,\n McpbQuestion,\n McpbSession,\n McpbEnv,\n McpbCorrectness,\n} from '../shared/types';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: McpbQuestion, session: McpbSession): McpbCorrectness => {\n if (!session?.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\n/** One blank, one key: an answered session scores 1 only when it picked the key. */\nexport const getPartialScore = (question: McpbQuestion, session: McpbSession) =>\n getCorrectness(question, session) === 'correct' ? 1 : 0;\n\nexport const outcome = (question: McpbQuestion, session: McpbSession, env: McpbEnv) =>\n new Promise((resolve) => {\n if (!session || isEmptyObject(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const normalizedQuestion = normalize(question);\n const correctness = getCorrectness(normalizedQuestion, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = getPartialScore(normalizedQuestion, session);\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${normalizedQuestion.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nconst withDefaults = (base: McpbQuestion, model: McpbQuestion = {}) => ({\n ...base,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\n/** The authoring starting point: a question that passes `validate()` as it stands. */\nexport const createDefaultModel = (model: McpbQuestion = {}) =>\n withDefaults(authorDefaults.model, model);\n\n/**\n * What `model()` and `outcome()` read: the item's own fields, with neutral\n * values for the rest. Starter content here would reach a learner as the\n * item's prompt or choices.\n */\nexport const normalize = (question: McpbQuestion = {}) => {\n const { shuffle, ...rest } = question || {};\n return {\n ...withDefaults(defaults.model, rest),\n // `lockChoiceOrder` alone decides the order, as in multiple-choice. Older items\n // spell an unlocked order `shuffle: true`, which counts only when it is unset.\n lockChoiceOrder:\n typeof rest.lockChoiceOrder === 'boolean' ? rest.lockChoiceOrder : shuffle !== true,\n };\n};\n\nexport const normalizeSession = (s: McpbSession): McpbSession => ({ ...s });\n\n/** The role plays no part (PIE-714): an instructor sees the order the student saw. */\nconst shouldLockChoices = (question: McpbQuestion, env: McpbEnv) =>\n !!question?.lockChoiceOrder || !!env?.['@pie-element']?.lockChoiceOrder;\n\nfunction shuffleArray<T>(items: T[]): T[] {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n}\n\n/**\n * The fields delivery renders. An imported choice can carry more, such as a\n * correctness flag or feedback, which must not reach a learner's browser.\n */\nconst toDeliveryChoice = ({ id, labelHtml, imageUrl, imageAlt }: McpbChoice): McpbChoice => ({\n id,\n ...(labelHtml !== undefined ? { labelHtml } : {}),\n ...(imageUrl !== undefined ? { imageUrl } : {}),\n ...(imageAlt !== undefined ? { imageAlt } : {}),\n});\n\nconst getStoredShuffle = (session: McpbSession): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\n/** The stored order, as `getShuffledChoices` applies it: a choice it does not list is left out. */\nconst applyShuffledValues = (\n choices: McpbChoice[],\n shuffledValues: string[],\n choiceKey: keyof McpbChoice\n) =>\n shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter((c): c is McpbChoice => !!c);\n\ntype UpdateSessionFn = (\n id: string,\n element: string,\n data: { shuffledValues: string[] }\n) => Promise<void>;\n\nconst getOrderedChoices = async (\n question: McpbQuestion,\n session: McpbSession,\n env: McpbEnv,\n updateSession?: UpdateSessionFn\n) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (\n question: McpbQuestion,\n session: McpbSession | null,\n env: McpbEnv | null,\n updateSession?: UpdateSessionFn\n) => {\n const safeSession: McpbSession = session || {};\n const safeEnv: McpbEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = (await getOrderedChoices(normalizedQuestion, safeSession, safeEnv, updateSession))\n .filter((c): c is McpbChoice => !!c && typeof c === 'object')\n .map(toDeliveryChoice);\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n choiceGroupLabel: normalizedQuestion.choiceGroupLabel || '',\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n customType: normalizedQuestion.customType || '',\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n language: normalizedQuestion.language || normalizedQuestion.locale || undefined,\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, safeSession);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: McpbQuestion, env: McpbEnv) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n // The player sets `id` and `element` on the entry from the item config.\n resolve({\n id: '1',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n/** Markup with its tags removed, keeping media an author can answer from. */\nconst getContent = (html?: string) =>\n (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '').trim();\n\ntype ValidateConfig = {\n minAnswerChoices?: number;\n maxAnswerChoices?: number;\n prompt?: { required?: boolean };\n teacherInstructions?: { required?: boolean };\n};\n\n/**\n * Authoring errors in multiple-choice's shape: a message per field, and under\n * `choices` a message per choice keyed by its id.\n */\nexport const validate = (question: McpbQuestion = {}, config: ValidateConfig = {}) => {\n const errors: Record<string, string | Record<string, string>> = {};\n const { minAnswerChoices = 2, maxAnswerChoices } = config;\n\n if ((question.promptEnabled || config.prompt?.required) && !getContent(question.prompt)) {\n errors.prompt = 'This field is required.';\n }\n\n if (config.teacherInstructions?.required && !getContent(question.teacherInstructions)) {\n errors.teacherInstructions = 'This field is required.';\n }\n\n const interactionMode = question.interactionMode || 'populate_blank';\n const template = question.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(question.choices) ? question.choices : [];\n if (choices.length < minAnswerChoices) {\n errors.answerChoices = `There should be at least ${minAnswerChoices} choices defined.`;\n } else if (maxAnswerChoices != null && choices.length > maxAnswerChoices) {\n errors.answerChoices = `No more than ${maxAnswerChoices} choices should be defined.`;\n }\n\n const mode = question.choiceMode || 'text';\n const choicesErrors: Record<string, string> = {};\n const seenIds = new Set<string>();\n const seenLabels = new Set<string>();\n choices.forEach((c, i) => {\n const key = c?.id || String(i);\n if (!c?.id) {\n choicesErrors[key] = 'Choice needs an id.';\n return;\n }\n // The session stores the picked id, so two choices sharing one cannot be told apart.\n if (seenIds.has(c.id)) {\n choicesErrors[key] = 'Choice id should be unique.';\n return;\n }\n seenIds.add(c.id);\n if (mode === 'text') {\n const label = getContent(c.labelHtml);\n if (!label) {\n choicesErrors[key] = 'Content should not be empty.';\n } else if (seenLabels.has(label)) {\n choicesErrors[key] = 'Content should be unique.';\n }\n seenLabels.add(label);\n } else if (!c.imageUrl?.trim()) {\n choicesErrors[key] = 'An image is required.';\n } else if (!c.imageAlt?.trim()) {\n choicesErrors[key] = 'Image alt text is required.';\n }\n });\n if (Object.keys(choicesErrors).length) {\n errors.choices = choicesErrors;\n }\n\n const correct = question.correctChoiceId;\n if (!correct) {\n errors.correctResponse = 'No correct response defined.';\n } else if (!choices.some((c) => c?.id === correct)) {\n errors.correctResponse = 'The correct response must be one of the choices.';\n }\n\n if (question.hasAudio) {\n const hasAudioUrl = !!question.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = question?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'choiceImageMaxHeightRem',\n 'choiceImageMaxWidthRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'audioInstructionsMaxWidthPx',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ] as const;\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null) continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"controller-DEUOAZHa.js","names":[],"sources":["../../src/controller/defaults.ts","../../src/author/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits.js';\n\nexport { DEFAULT_LAYOUT_LIMITS };\n\n/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\n/**\n * The values `normalize` fills in for fields an item leaves out. They carry no\n * content and no `id` or `element`: those come from the item config, and a\n * default here would stand in for a field the author never set.\n */\nexport default {\n model: {\n prompt: '',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n sentenceHtml: '',\n template: '',\n choiceMode: 'text' as const,\n choices: [],\n choiceGroupLabel: '',\n correctChoiceId: '',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: true,\n },\n};\n","import controllerDefaults, { BLANK_TOKEN } from '../controller/defaults.js';\n\n/** The model a new item starts from: a valid question an author edits in place. */\nexport default {\n model: {\n ...controllerDefaults.model,\n prompt: '<p><strong>Demo prompt</strong></p>',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import authorDefaults from '../author/defaults.js';\nimport defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults.js';\nimport type {\n McpbChoice,\n McpbQuestion,\n McpbSession,\n McpbEnv,\n McpbCorrectness,\n} from '../shared/types.js';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: McpbQuestion, session: McpbSession): McpbCorrectness => {\n if (!session?.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\n/** One blank, one key: an answered session scores 1 only when it picked the key. */\nexport const getPartialScore = (question: McpbQuestion, session: McpbSession) =>\n getCorrectness(question, session) === 'correct' ? 1 : 0;\n\nexport const outcome = (question: McpbQuestion, session: McpbSession, env: McpbEnv) =>\n new Promise((resolve) => {\n if (!session || isEmptyObject(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const normalizedQuestion = normalize(question);\n const correctness = getCorrectness(normalizedQuestion, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = getPartialScore(normalizedQuestion, session);\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${normalizedQuestion.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nconst withDefaults = (base: McpbQuestion, model: McpbQuestion = {}) => ({\n ...base,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\n/** The authoring starting point: a question that passes `validate()` as it stands. */\nexport const createDefaultModel = (model: McpbQuestion = {}) =>\n withDefaults(authorDefaults.model, model);\n\n/**\n * What `model()` and `outcome()` read: the item's own fields, with neutral\n * values for the rest. Starter content here would reach a learner as the\n * item's prompt or choices.\n */\nexport const normalize = (question: McpbQuestion = {}) => {\n const { shuffle, ...rest } = question || {};\n return {\n ...withDefaults(defaults.model, rest),\n // `lockChoiceOrder` alone decides the order, as in multiple-choice. Older items\n // spell an unlocked order `shuffle: true`, which counts only when it is unset.\n lockChoiceOrder:\n typeof rest.lockChoiceOrder === 'boolean' ? rest.lockChoiceOrder : shuffle !== true,\n };\n};\n\nexport const normalizeSession = (s: McpbSession): McpbSession => ({ ...s });\n\n/** The role plays no part (PIE-714): an instructor sees the order the student saw. */\nconst shouldLockChoices = (question: McpbQuestion, env: McpbEnv) =>\n !!question?.lockChoiceOrder || !!env?.['@pie-element']?.lockChoiceOrder;\n\nfunction shuffleArray<T>(items: T[]): T[] {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n}\n\n/**\n * The fields delivery renders. An imported choice can carry more, such as a\n * correctness flag or feedback, which must not reach a learner's browser.\n */\nconst toDeliveryChoice = ({ id, labelHtml, imageUrl, imageAlt }: McpbChoice): McpbChoice => ({\n id,\n ...(labelHtml !== undefined ? { labelHtml } : {}),\n ...(imageUrl !== undefined ? { imageUrl } : {}),\n ...(imageAlt !== undefined ? { imageAlt } : {}),\n});\n\nconst getStoredShuffle = (session: McpbSession): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\n/** The stored order, as `getShuffledChoices` applies it: a choice it does not list is left out. */\nconst applyShuffledValues = (\n choices: McpbChoice[],\n shuffledValues: string[],\n choiceKey: keyof McpbChoice\n) =>\n shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter((c): c is McpbChoice => !!c);\n\ntype UpdateSessionFn = (\n id: string,\n element: string,\n data: { shuffledValues: string[] }\n) => Promise<void>;\n\nconst getOrderedChoices = async (\n question: McpbQuestion,\n session: McpbSession,\n env: McpbEnv,\n updateSession?: UpdateSessionFn\n) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (\n question: McpbQuestion,\n session: McpbSession | null,\n env: McpbEnv | null,\n updateSession?: UpdateSessionFn\n) => {\n const safeSession: McpbSession = session || {};\n const safeEnv: McpbEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = (await getOrderedChoices(normalizedQuestion, safeSession, safeEnv, updateSession))\n .filter((c): c is McpbChoice => !!c && typeof c === 'object')\n .map(toDeliveryChoice);\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n choiceGroupLabel: normalizedQuestion.choiceGroupLabel || '',\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n customType: normalizedQuestion.customType || '',\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n language: normalizedQuestion.language || normalizedQuestion.locale || undefined,\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, safeSession);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: McpbQuestion, env: McpbEnv) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n // The player sets `id` and `element` on the entry from the item config.\n resolve({\n id: '1',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n/** Markup with its tags removed, keeping media an author can answer from. */\nconst getContent = (html?: string) =>\n (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '').trim();\n\ntype ValidateConfig = {\n minAnswerChoices?: number;\n maxAnswerChoices?: number;\n prompt?: { required?: boolean };\n teacherInstructions?: { required?: boolean };\n};\n\n/**\n * Authoring errors in multiple-choice's shape: a message per field, and under\n * `choices` a message per choice keyed by its id.\n */\nexport const validate = (question: McpbQuestion = {}, config: ValidateConfig = {}) => {\n const errors: Record<string, string | Record<string, string>> = {};\n const { minAnswerChoices = 2, maxAnswerChoices } = config;\n\n if ((question.promptEnabled || config.prompt?.required) && !getContent(question.prompt)) {\n errors.prompt = 'This field is required.';\n }\n\n if (config.teacherInstructions?.required && !getContent(question.teacherInstructions)) {\n errors.teacherInstructions = 'This field is required.';\n }\n\n const interactionMode = question.interactionMode || 'populate_blank';\n const template = question.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(question.choices) ? question.choices : [];\n if (choices.length < minAnswerChoices) {\n errors.answerChoices = `There should be at least ${minAnswerChoices} choices defined.`;\n } else if (maxAnswerChoices != null && choices.length > maxAnswerChoices) {\n errors.answerChoices = `No more than ${maxAnswerChoices} choices should be defined.`;\n }\n\n const mode = question.choiceMode || 'text';\n const choicesErrors: Record<string, string> = {};\n const seenIds = new Set<string>();\n const seenLabels = new Set<string>();\n choices.forEach((c, i) => {\n const key = c?.id || String(i);\n if (!c?.id) {\n choicesErrors[key] = 'Choice needs an id.';\n return;\n }\n // The session stores the picked id, so two choices sharing one cannot be told apart.\n if (seenIds.has(c.id)) {\n choicesErrors[key] = 'Choice id should be unique.';\n return;\n }\n seenIds.add(c.id);\n if (mode === 'text') {\n const label = getContent(c.labelHtml);\n if (!label) {\n choicesErrors[key] = 'Content should not be empty.';\n } else if (seenLabels.has(label)) {\n choicesErrors[key] = 'Content should be unique.';\n }\n seenLabels.add(label);\n } else if (!c.imageUrl?.trim()) {\n choicesErrors[key] = 'An image is required.';\n } else if (!c.imageAlt?.trim()) {\n choicesErrors[key] = 'Image alt text is required.';\n }\n });\n if (Object.keys(choicesErrors).length) {\n errors.choices = choicesErrors;\n }\n\n const correct = question.correctChoiceId;\n if (!correct) {\n errors.correctResponse = 'No correct response defined.';\n } else if (!choices.some((c) => c?.id === correct)) {\n errors.correctResponse = 'The correct response must be one of the choices.';\n }\n\n if (question.hasAudio) {\n const hasAudioUrl = !!question.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = question?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'choiceImageMaxHeightRem',\n 'choiceImageMaxWidthRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'audioInstructionsMaxWidthPx',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ] as const;\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null) continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as e, C as t, D as n, E as r, F as i, N as a, P as o, S as s, T as c, _ as l, d as u, g as d, h as f, j as p, k as m, m as h, n as g, p as _, t as v, u as y, w as b, x } from "../client-
|
|
2
|
-
import { n as S, t as C } from "../McPopulatedBlank-
|
|
1
|
+
import { A as e, C as t, D as n, E as r, F as i, N as a, P as o, S as s, T as c, _ as l, d as u, g as d, h as f, j as p, k as m, m as h, n as g, p as _, t as v, u as y, w as b, x } from "../client-Dfdyltec.js";
|
|
2
|
+
import { n as S, t as C } from "../McPopulatedBlank-B0ep0vS4.js";
|
|
3
3
|
import { n as w, o as T } from "../controller-DEUOAZHa.js";
|
|
4
4
|
//#region src/print/preparePrint.ts
|
|
5
5
|
async function E(e, t) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/print/preparePrint.ts","../../../src/print/Print.svelte","../../../src/print/index.ts"],"sourcesContent":["import { createCorrectResponseSession, model as toViewModel } from '../controller';\nimport type { McpbQuestion, McpbRole, McpbSession } from '../shared/types';\n\nexport interface PrintView {\n model: Record<string, unknown>;\n session: McpbSession | null;\n teacherInstructions: string;\n audioUrl: string;\n audioTranscript: string;\n}\n\n/**\n * `pie-print` hands over the item's own model and `options.role`, so print asks\n * the controller for what a player shows that role in `view` mode, in the\n * authored choice order. An instructor's key is the correct-response session,\n * the same path a player shows it through; `printAnswerKey: false` withholds it.\n */\nexport async function preparePrint(\n question: McpbQuestion,\n role: McpbRole | undefined\n): Promise<PrintView> {\n const env = { mode: 'view' as const, role };\n const viewModel = await toViewModel({ ...question, lockChoiceOrder: true }, null, env);\n const key =\n question.printAnswerKey === false\n ? null\n : ((await createCorrectResponseSession(question, env)) as McpbSession | null);\n const { teacherInstructions, audioUrl, audioTranscript, ...model } = viewModel;\n return {\n // Delivery renders these as a collapsed toggle and a player; print writes them out.\n model: { ...model, teacherInstructions: null, hasAudio: false },\n session: key,\n teacherInstructions: String(teacherInstructions || ''),\n audioUrl: String(audioUrl || ''),\n audioTranscript: String(audioTranscript || ''),\n };\n}\n","<svelte:options\n customElement={{\n shadow: 'none',\n props: {\n model: { type: 'Object' },\n options: { type: 'Object' },\n },\n }}\n/>\n\n<script lang=\"ts\">\nimport McPopulatedBlank from '../delivery/McPopulatedBlank.svelte';\nimport { t as translate } from '../delivery/i18n';\nimport { preparePrint, type PrintView } from './preparePrint';\n\nlet { model = null, options = null }: { model?: any; options?: any } = $props();\n\nlet view = $state<PrintView | null>(null);\n\n$effect(() => {\n const question = model;\n const role = options?.role;\n if (!question) {\n view = null;\n return;\n }\n let current = true;\n preparePrint(question, role).then((next) => {\n if (current) view = next;\n });\n return () => {\n current = false;\n };\n});\n\nconst t = (key: string) => translate(key, (view?.model.language as string) || undefined);\n</script>\n\n{#if view}\n <div class=\"mpb-print\">\n {#if view.teacherInstructions}\n <div class=\"mpb-print-block mpb-print-teacher-instructions\">{@html view.teacherInstructions}</div>\n {/if}\n\n {#if view.audioUrl || view.audioTranscript}\n <div class=\"mpb-print-block mpb-print-audio\">\n {#if view.audioUrl}\n <div><strong>{t('printAudio')}</strong> {view.audioUrl}</div>\n {/if}\n {#if view.audioTranscript}\n <div><strong>{t('printTranscript')}</strong> {view.audioTranscript}</div>\n {/if}\n </div>\n {/if}\n\n <McPopulatedBlank model={view.model} session={view.session} />\n </div>\n{/if}\n\n<style>\n /* Inset to line up with the delivery root's padding. */\n .mpb-print-block {\n padding: 0 1rem;\n margin: 1rem 0;\n }\n\n .mpb-print-teacher-instructions :global(p) {\n margin: 0.5em 0;\n }\n\n .mpb-print-audio {\n font-size: 0.875rem;\n line-height: 1.25rem;\n }\n</style>\n","import PrintComponent from './Print.svelte';\n\n/** `pie-print` sets `options` before the model; `role` decides whether the key prints. */\nexport default (PrintComponent as any).element;\n"],"mappings":";;;;AAiBA,eAAsB,EACpB,GACA,GACoB;CACpB,IAAM,IAAM;EAAE,MAAM;EAAiB;CAAK,GACpC,IAAY,MAAM,EAAY;EAAE,GAAG;EAAU,iBAAiB;CAAK,GAAG,MAAM,CAAG,GAC/E,IACJ,EAAS,mBAAmB,KACxB,OACE,MAAM,EAA6B,GAAU,CAAG,GAClD,EAAE,wBAAqB,aAAU,oBAAiB,GAAG,MAAU;CACrE,OAAO;EAEL,OAAO;GAAE,GAAG;GAAO,qBAAqB;GAAM,UAAU;EAAM;EAC9D,SAAS;EACT,qBAAqB,OAAO,KAAuB,EAAE;EACrD,UAAU,OAAO,KAAY,EAAE;EAC/B,iBAAiB,OAAO,KAAmB,EAAE;CAC/C;AACF;;;;;;;iBC1BA;;CAKA,
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/print/preparePrint.ts","../../../src/print/Print.svelte","../../../src/print/index.ts"],"sourcesContent":["import { createCorrectResponseSession, model as toViewModel } from '../controller/index.js';\nimport type { McpbQuestion, McpbRole, McpbSession } from '../shared/types.js';\n\nexport interface PrintView {\n model: Record<string, unknown>;\n session: McpbSession | null;\n teacherInstructions: string;\n audioUrl: string;\n audioTranscript: string;\n}\n\n/**\n * `pie-print` hands over the item's own model and `options.role`, so print asks\n * the controller for what a player shows that role in `view` mode, in the\n * authored choice order. An instructor's key is the correct-response session,\n * the same path a player shows it through; `printAnswerKey: false` withholds it.\n */\nexport async function preparePrint(\n question: McpbQuestion,\n role: McpbRole | undefined\n): Promise<PrintView> {\n const env = { mode: 'view' as const, role };\n const viewModel = await toViewModel({ ...question, lockChoiceOrder: true }, null, env);\n const key =\n question.printAnswerKey === false\n ? null\n : ((await createCorrectResponseSession(question, env)) as McpbSession | null);\n const { teacherInstructions, audioUrl, audioTranscript, ...model } = viewModel;\n return {\n // Delivery renders these as a collapsed toggle and a player; print writes them out.\n model: { ...model, teacherInstructions: null, hasAudio: false },\n session: key,\n teacherInstructions: String(teacherInstructions || ''),\n audioUrl: String(audioUrl || ''),\n audioTranscript: String(audioTranscript || ''),\n };\n}\n","<svelte:options\n customElement={{\n shadow: 'none',\n props: {\n model: { type: 'Object' },\n options: { type: 'Object' },\n },\n }}\n/>\n\n<script lang=\"ts\">\nimport McPopulatedBlank from '../delivery/McPopulatedBlank.svelte';\nimport { t as translate } from '../delivery/i18n';\nimport { preparePrint, type PrintView } from './preparePrint';\n\nlet { model = null, options = null }: { model?: any; options?: any } = $props();\n\nlet view = $state<PrintView | null>(null);\n\n$effect(() => {\n const question = model;\n const role = options?.role;\n if (!question) {\n view = null;\n return;\n }\n let current = true;\n preparePrint(question, role).then((next) => {\n if (current) view = next;\n });\n return () => {\n current = false;\n };\n});\n\nconst t = (key: string) => translate(key, (view?.model.language as string) || undefined);\n</script>\n\n{#if view}\n <div class=\"mpb-print\">\n {#if view.teacherInstructions}\n <div class=\"mpb-print-block mpb-print-teacher-instructions\">{@html view.teacherInstructions}</div>\n {/if}\n\n {#if view.audioUrl || view.audioTranscript}\n <div class=\"mpb-print-block mpb-print-audio\">\n {#if view.audioUrl}\n <div><strong>{t('printAudio')}</strong> {view.audioUrl}</div>\n {/if}\n {#if view.audioTranscript}\n <div><strong>{t('printTranscript')}</strong> {view.audioTranscript}</div>\n {/if}\n </div>\n {/if}\n\n <McPopulatedBlank model={view.model} session={view.session} />\n </div>\n{/if}\n\n<style>\n /* Inset to line up with the delivery root's padding. */\n .mpb-print-block {\n padding: 0 1rem;\n margin: 1rem 0;\n }\n\n .mpb-print-teacher-instructions :global(p) {\n margin: 0.5em 0;\n }\n\n .mpb-print-audio {\n font-size: 0.875rem;\n line-height: 1.25rem;\n }\n</style>\n","import PrintComponent from './Print.svelte';\n\n/** `pie-print` sets `options` before the model; `role` decides whether the key prints. */\nexport default (PrintComponent as any).element;\n"],"mappings":";;;;AAiBA,eAAsB,EACpB,GACA,GACoB;CACpB,IAAM,IAAM;EAAE,MAAM;EAAiB;CAAK,GACpC,IAAY,MAAM,EAAY;EAAE,GAAG;EAAU,iBAAiB;CAAK,GAAG,MAAM,CAAG,GAC/E,IACJ,EAAS,mBAAmB,KACxB,OACE,MAAM,EAA6B,GAAU,CAAG,GAClD,EAAE,wBAAqB,aAAU,oBAAiB,GAAG,MAAU;CACrE,OAAO;EAEL,OAAO;GAAE,GAAG;GAAO,qBAAqB;GAAM,UAAU;EAAM;EAC9D,SAAS;EACT,qBAAqB,OAAO,KAAuB,EAAE;EACrD,UAAU,OAAO,KAAY,EAAE;EAC/B,iBAAiB,OAAO,KAAmB,EAAE;CAC/C;AACF;;;;;;;iBC1BA;;CAKA,IAAM,IAAK,EAAA,GAAA,SAAA,GAAG,IAAI,GAAE,IAAO,EAAA,GAAA,WAAA,GAAG,IAAI,GAE9B,IAAO,EAAyB,IAAI;CAExC,QAAc;EACZ,IAAM,IAAW,EAAK,GAChB,IAAO,EAAO,CAAA,EAAE;EACtB,IAAI,CAAC,GAAU;GACb,EAAA,GAAO,IAAI;GACX;EACF;EACA,IAAI,IAAU;EAId,OAHA,EAAa,GAAU,CAAI,CAAC,CAAC,MAAM,MAAS;GAC1C,AAAI,KAAS,EAAA,GAAO,GAAI,EAAA;EAC1B,CAAC,SACY;GACX,IAAU;EACZ;CACF,CAAC;CAED,IAAM,KAAK,MAAgB,EAAU,GAAK,EAAC,CAAI,CAAA,EAAE,MAAM,YAAuB,KAAA,CAAS;;;;;gBApBzE,MAAI;;;;;;kBAAY,MAAI;;;;MAwB/B,IAAG,EAAA,SAAH,CAAG;OAEC,IAAG,EAAA;KAAH,SAAkE,EAAA,CAAI,CAAA,CAAC,qBAAmB,EAAA,KAA1F,CAAI,QAAJ,CAAI;;;GADF,EAAA,CAAI,CAAA,CAAC,uBAAmB,EAAA,CAAA;;;OAK1B,IAAG,EAAA,SAAH,CAAI;QAEA,IAAG,EAAA,GAAE,IAAM,EAAX,CAAK,SAAA,GAAM,EAAA,SAAN,CAAM;MAAX,CAAK;uBAAmC,EAAA,CAAI,CAAA,CAAC,YAAQ,IAAA;cAAxC,EAAE,YAAY,CAAC,CAAA,QAA5B,CAAK;;;IADH,EAAA,CAAI,CAAA,CAAC,YAAQ,EAAA,CAAA;;;QAIf,IAAG,EAAA,GAAE,IAAM,EAAX,CAAK,SAAA,GAAM,EAAA,SAAN,CAAQ;MAAb,CAAK;uBAAwC,EAAA,CAAI,CAAA,CAAC,mBAAe,IAAA;cAApD,EAAE,iBAAiB,CAAC,CAAA,QAAjC,CAAK;;;IADH,EAAA,CAAI,CAAA,CAAC,mBAAe,EAAA,CAAA;SAJ1B,CAAI,QAAJ,CAAI;;;IADF,EAAA,CAAI,CAAA,CAAC,YAAY,EAAA,CAAI,CAAA,CAAC,oBAAe,EAAA,CAAA;MAWzC,EAAA,EAAA,GAAA,CAAgB,GAAA;;WAAQ,EAAA,CAAI,CAAA,CAAC;;;WAAgB,EAAA,CAAI,CAAA,CAAC;;QAhBpD,CAAG,QAAH,CAAG;;;IADD,CAAI,KAAA,EAAA,CAAA;;AAFD;;;;;;;ACjCR,IAAA,IAAgB,EAAuB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits';
|
|
1
|
+
import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits.js';
|
|
2
2
|
export { DEFAULT_LAYOUT_LIMITS };
|
|
3
3
|
/** Token authors must include exactly once in `template` (HTML). */
|
|
4
4
|
export declare const BLANK_TOKEN = "{{blank}}";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpbChoice, McpbQuestion, McpbSession, McpbEnv, McpbCorrectness } from '../shared/types';
|
|
1
|
+
import type { McpbChoice, McpbQuestion, McpbSession, McpbEnv, McpbCorrectness } from '../shared/types.js';
|
|
2
2
|
export declare function countBlankTokens(template: string): number;
|
|
3
3
|
export declare const getCorrectness: (question: McpbQuestion, session: McpbSession) => McpbCorrectness;
|
|
4
4
|
/** One blank, one key: an answered session scores 1 only when it picked the key. */
|
|
@@ -46,15 +46,15 @@ export declare const createDefaultModel: (model?: McpbQuestion) => {
|
|
|
46
46
|
element?: string;
|
|
47
47
|
prompt?: string;
|
|
48
48
|
promptEnabled?: boolean;
|
|
49
|
-
interactionMode?: import("../shared/types").McpbInteractionMode;
|
|
49
|
+
interactionMode?: import("../shared/types.js").McpbInteractionMode;
|
|
50
50
|
layoutProfile?: string;
|
|
51
51
|
choiceLayout?: string;
|
|
52
|
-
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits").LayoutLimits>>;
|
|
53
|
-
audioButtonSkin?: import("../shared/types").McpbAudioButtonSkin | null;
|
|
54
|
-
audioButtonSkinsByLocale?: Record<string, import("../shared/types").McpbAudioButtonSkin>;
|
|
52
|
+
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits.js").LayoutLimits>>;
|
|
53
|
+
audioButtonSkin?: import("../shared/types.js").McpbAudioButtonSkin | null;
|
|
54
|
+
audioButtonSkinsByLocale?: Record<string, import("../shared/types.js").McpbAudioButtonSkin>;
|
|
55
55
|
sentenceHtml?: string;
|
|
56
56
|
template?: string;
|
|
57
|
-
choiceMode?: import("../shared/types").McpbChoiceMode;
|
|
57
|
+
choiceMode?: import("../shared/types.js").McpbChoiceMode;
|
|
58
58
|
choices?: McpbChoice[];
|
|
59
59
|
choiceGroupLabel?: string;
|
|
60
60
|
correctChoiceId?: string;
|
|
@@ -120,15 +120,15 @@ export declare const normalize: (question?: McpbQuestion) => {
|
|
|
120
120
|
element?: string;
|
|
121
121
|
prompt?: string;
|
|
122
122
|
promptEnabled?: boolean;
|
|
123
|
-
interactionMode?: import("../shared/types").McpbInteractionMode;
|
|
123
|
+
interactionMode?: import("../shared/types.js").McpbInteractionMode;
|
|
124
124
|
layoutProfile?: string;
|
|
125
125
|
choiceLayout?: string;
|
|
126
|
-
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits").LayoutLimits>>;
|
|
127
|
-
audioButtonSkin?: import("../shared/types").McpbAudioButtonSkin | null;
|
|
128
|
-
audioButtonSkinsByLocale?: Record<string, import("../shared/types").McpbAudioButtonSkin>;
|
|
126
|
+
layoutProfilePresets?: Record<string, Partial<import("../shared/layoutLimits.js").LayoutLimits>>;
|
|
127
|
+
audioButtonSkin?: import("../shared/types.js").McpbAudioButtonSkin | null;
|
|
128
|
+
audioButtonSkinsByLocale?: Record<string, import("../shared/types.js").McpbAudioButtonSkin>;
|
|
129
129
|
sentenceHtml?: string;
|
|
130
130
|
template?: string;
|
|
131
|
-
choiceMode?: import("../shared/types").McpbChoiceMode;
|
|
131
|
+
choiceMode?: import("../shared/types.js").McpbChoiceMode;
|
|
132
132
|
choices?: McpbChoice[];
|
|
133
133
|
choiceGroupLabel?: string;
|
|
134
134
|
correctChoiceId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/layoutLimits.ts","../../src/controller/defaults.ts","../../src/author/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["export type LayoutLimits = {\n blankStandaloneWidthRem: number;\n blankWideWidthRem: number;\n blankUnderlineWidthPx: number;\n blankUnderlineWideWidthPx: number;\n horizontalChoiceWidthPx: number;\n horizontalChoiceWidthVw: number;\n horizontalChoiceTileMinHeightRem: number;\n horizontalChoiceContentMinHeightRem: number;\n choiceImageMaxHeightRem: number;\n choiceImageMaxWidthRem: number;\n listenButtonSizePx: number;\n stimulusMinColumnPx: number;\n textMinColumnPx: number;\n legendMaxChars: number;\n choiceGroupGapRem: number;\n choiceRowGapRem: number;\n toggleButtonGapRem: number;\n horizontalChoiceRadioTopMarginRem: number;\n audioBlankTemplateMarginTopRem: number;\n audioBlankTemplateMarginBottomRem: number;\n audioInstructionsMaxWidthPx: number;\n stimulusGridColumnGapRem: number;\n stimulusGridRowGapRem: number;\n stimulusSentenceMarginTopRem: number;\n stimulusChoicesMarginTopRem: number;\n tokenGridColumnGapRem: number;\n tokenGridRowGapRem: number;\n tokenTemplateMarginTopRem: number;\n tokenInlineTokenGapRem: number;\n tokenChoicesMarginTopRem: number;\n inlineGridColumnGapRem: number;\n inlineGridRowGapRem: number;\n inlineTemplateMarginTopRem: number;\n inlineChoicesMarginTopRem: number;\n};\n\n/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */\nexport const DEFAULT_LAYOUT_LIMITS: LayoutLimits = {\n blankStandaloneWidthRem: 7,\n blankWideWidthRem: 10,\n blankUnderlineWidthPx: 2,\n blankUnderlineWideWidthPx: 4,\n horizontalChoiceWidthPx: 170,\n horizontalChoiceWidthVw: 30,\n horizontalChoiceTileMinHeightRem: 11,\n horizontalChoiceContentMinHeightRem: 7.5,\n // 9.375rem = 150px at the standard 16px root font size — Learnosity graphic-choice\n // SVGs are authored at 150x150; images up to that size must render at full size.\n // Shared by the choice tile image and the selected-answer image inside the cloze\n // blank, so the same choice image always renders at the same size in both places.\n choiceImageMaxHeightRem: 9.375,\n choiceImageMaxWidthRem: 9.375,\n listenButtonSizePx: 128,\n stimulusMinColumnPx: 210,\n textMinColumnPx: 260,\n legendMaxChars: 120,\n choiceGroupGapRem: 0.5,\n choiceRowGapRem: 0.5,\n toggleButtonGapRem: 0.5,\n horizontalChoiceRadioTopMarginRem: 0.5,\n audioBlankTemplateMarginTopRem: 0.8,\n audioBlankTemplateMarginBottomRem: 1.8,\n audioInstructionsMaxWidthPx: 875,\n stimulusGridColumnGapRem: 2,\n stimulusGridRowGapRem: 0.7,\n stimulusSentenceMarginTopRem: 0.2,\n stimulusChoicesMarginTopRem: 0.9,\n tokenGridColumnGapRem: 1.5,\n tokenGridRowGapRem: 0.8,\n tokenTemplateMarginTopRem: 0.6,\n tokenInlineTokenGapRem: 0.35,\n tokenChoicesMarginTopRem: 0.2,\n inlineGridColumnGapRem: 1.5,\n inlineGridRowGapRem: 0.65,\n inlineTemplateMarginTopRem: 0.45,\n inlineChoicesMarginTopRem: 0.25,\n};\n","import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits';\n\nexport { DEFAULT_LAYOUT_LIMITS };\n\n/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\n/**\n * The values `normalize` fills in for fields an item leaves out. They carry no\n * content and no `id` or `element`: those come from the item config, and a\n * default here would stand in for a field the author never set.\n */\nexport default {\n model: {\n prompt: '',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n sentenceHtml: '',\n template: '',\n choiceMode: 'text' as const,\n choices: [],\n choiceGroupLabel: '',\n correctChoiceId: '',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: true,\n },\n};\n","import controllerDefaults, { BLANK_TOKEN } from '../controller/defaults';\n\n/** The model a new item starts from: a valid question an author edits in place. */\nexport default {\n model: {\n ...controllerDefaults.model,\n prompt: '<p><strong>Demo prompt</strong></p>',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import authorDefaults from '../author/defaults';\nimport defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults';\nimport type {\n McpbChoice,\n McpbQuestion,\n McpbSession,\n McpbEnv,\n McpbCorrectness,\n} from '../shared/types';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: McpbQuestion, session: McpbSession): McpbCorrectness => {\n if (!session?.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\n/** One blank, one key: an answered session scores 1 only when it picked the key. */\nexport const getPartialScore = (question: McpbQuestion, session: McpbSession) =>\n getCorrectness(question, session) === 'correct' ? 1 : 0;\n\nexport const outcome = (question: McpbQuestion, session: McpbSession, env: McpbEnv) =>\n new Promise((resolve) => {\n if (!session || isEmptyObject(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const normalizedQuestion = normalize(question);\n const correctness = getCorrectness(normalizedQuestion, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = getPartialScore(normalizedQuestion, session);\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${normalizedQuestion.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nconst withDefaults = (base: McpbQuestion, model: McpbQuestion = {}) => ({\n ...base,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\n/** The authoring starting point: a question that passes `validate()` as it stands. */\nexport const createDefaultModel = (model: McpbQuestion = {}) =>\n withDefaults(authorDefaults.model, model);\n\n/**\n * What `model()` and `outcome()` read: the item's own fields, with neutral\n * values for the rest. Starter content here would reach a learner as the\n * item's prompt or choices.\n */\nexport const normalize = (question: McpbQuestion = {}) => {\n const { shuffle, ...rest } = question || {};\n return {\n ...withDefaults(defaults.model, rest),\n // `lockChoiceOrder` alone decides the order, as in multiple-choice. Older items\n // spell an unlocked order `shuffle: true`, which counts only when it is unset.\n lockChoiceOrder:\n typeof rest.lockChoiceOrder === 'boolean' ? rest.lockChoiceOrder : shuffle !== true,\n };\n};\n\nexport const normalizeSession = (s: McpbSession): McpbSession => ({ ...s });\n\n/** The role plays no part (PIE-714): an instructor sees the order the student saw. */\nconst shouldLockChoices = (question: McpbQuestion, env: McpbEnv) =>\n !!question?.lockChoiceOrder || !!env?.['@pie-element']?.lockChoiceOrder;\n\nfunction shuffleArray<T>(items: T[]): T[] {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n}\n\n/**\n * The fields delivery renders. An imported choice can carry more, such as a\n * correctness flag or feedback, which must not reach a learner's browser.\n */\nconst toDeliveryChoice = ({ id, labelHtml, imageUrl, imageAlt }: McpbChoice): McpbChoice => ({\n id,\n ...(labelHtml !== undefined ? { labelHtml } : {}),\n ...(imageUrl !== undefined ? { imageUrl } : {}),\n ...(imageAlt !== undefined ? { imageAlt } : {}),\n});\n\nconst getStoredShuffle = (session: McpbSession): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\n/** The stored order, as `getShuffledChoices` applies it: a choice it does not list is left out. */\nconst applyShuffledValues = (\n choices: McpbChoice[],\n shuffledValues: string[],\n choiceKey: keyof McpbChoice\n) =>\n shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter((c): c is McpbChoice => !!c);\n\ntype UpdateSessionFn = (\n id: string,\n element: string,\n data: { shuffledValues: string[] }\n) => Promise<void>;\n\nconst getOrderedChoices = async (\n question: McpbQuestion,\n session: McpbSession,\n env: McpbEnv,\n updateSession?: UpdateSessionFn\n) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (\n question: McpbQuestion,\n session: McpbSession | null,\n env: McpbEnv | null,\n updateSession?: UpdateSessionFn\n) => {\n const safeSession: McpbSession = session || {};\n const safeEnv: McpbEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = (await getOrderedChoices(normalizedQuestion, safeSession, safeEnv, updateSession))\n .filter((c): c is McpbChoice => !!c && typeof c === 'object')\n .map(toDeliveryChoice);\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n choiceGroupLabel: normalizedQuestion.choiceGroupLabel || '',\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n customType: normalizedQuestion.customType || '',\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n language: normalizedQuestion.language || normalizedQuestion.locale || undefined,\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, safeSession);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: McpbQuestion, env: McpbEnv) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n // The player sets `id` and `element` on the entry from the item config.\n resolve({\n id: '1',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n/** Markup with its tags removed, keeping media an author can answer from. */\nconst getContent = (html?: string) =>\n (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '').trim();\n\ntype ValidateConfig = {\n minAnswerChoices?: number;\n maxAnswerChoices?: number;\n prompt?: { required?: boolean };\n teacherInstructions?: { required?: boolean };\n};\n\n/**\n * Authoring errors in multiple-choice's shape: a message per field, and under\n * `choices` a message per choice keyed by its id.\n */\nexport const validate = (question: McpbQuestion = {}, config: ValidateConfig = {}) => {\n const errors: Record<string, string | Record<string, string>> = {};\n const { minAnswerChoices = 2, maxAnswerChoices } = config;\n\n if ((question.promptEnabled || config.prompt?.required) && !getContent(question.prompt)) {\n errors.prompt = 'This field is required.';\n }\n\n if (config.teacherInstructions?.required && !getContent(question.teacherInstructions)) {\n errors.teacherInstructions = 'This field is required.';\n }\n\n const interactionMode = question.interactionMode || 'populate_blank';\n const template = question.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(question.choices) ? question.choices : [];\n if (choices.length < minAnswerChoices) {\n errors.answerChoices = `There should be at least ${minAnswerChoices} choices defined.`;\n } else if (maxAnswerChoices != null && choices.length > maxAnswerChoices) {\n errors.answerChoices = `No more than ${maxAnswerChoices} choices should be defined.`;\n }\n\n const mode = question.choiceMode || 'text';\n const choicesErrors: Record<string, string> = {};\n const seenIds = new Set<string>();\n const seenLabels = new Set<string>();\n choices.forEach((c, i) => {\n const key = c?.id || String(i);\n if (!c?.id) {\n choicesErrors[key] = 'Choice needs an id.';\n return;\n }\n // The session stores the picked id, so two choices sharing one cannot be told apart.\n if (seenIds.has(c.id)) {\n choicesErrors[key] = 'Choice id should be unique.';\n return;\n }\n seenIds.add(c.id);\n if (mode === 'text') {\n const label = getContent(c.labelHtml);\n if (!label) {\n choicesErrors[key] = 'Content should not be empty.';\n } else if (seenLabels.has(label)) {\n choicesErrors[key] = 'Content should be unique.';\n }\n seenLabels.add(label);\n } else if (!c.imageUrl?.trim()) {\n choicesErrors[key] = 'An image is required.';\n } else if (!c.imageAlt?.trim()) {\n choicesErrors[key] = 'Image alt text is required.';\n }\n });\n if (Object.keys(choicesErrors).length) {\n errors.choices = choicesErrors;\n }\n\n const correct = question.correctChoiceId;\n if (!correct) {\n errors.correctResponse = 'No correct response defined.';\n } else if (!choices.some((c) => c?.id === correct)) {\n errors.correctResponse = 'The correct response must be one of the choices.';\n }\n\n if (question.hasAudio) {\n const hasAudioUrl = !!question.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = question?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'choiceImageMaxHeightRem',\n 'choiceImageMaxWidthRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'audioInstructionsMaxWidthPx',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ] as const;\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null) continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/layoutLimits.ts","../../src/controller/defaults.ts","../../src/author/defaults.ts","../../src/controller/index.ts"],"sourcesContent":["export type LayoutLimits = {\n blankStandaloneWidthRem: number;\n blankWideWidthRem: number;\n blankUnderlineWidthPx: number;\n blankUnderlineWideWidthPx: number;\n horizontalChoiceWidthPx: number;\n horizontalChoiceWidthVw: number;\n horizontalChoiceTileMinHeightRem: number;\n horizontalChoiceContentMinHeightRem: number;\n choiceImageMaxHeightRem: number;\n choiceImageMaxWidthRem: number;\n listenButtonSizePx: number;\n stimulusMinColumnPx: number;\n textMinColumnPx: number;\n legendMaxChars: number;\n choiceGroupGapRem: number;\n choiceRowGapRem: number;\n toggleButtonGapRem: number;\n horizontalChoiceRadioTopMarginRem: number;\n audioBlankTemplateMarginTopRem: number;\n audioBlankTemplateMarginBottomRem: number;\n audioInstructionsMaxWidthPx: number;\n stimulusGridColumnGapRem: number;\n stimulusGridRowGapRem: number;\n stimulusSentenceMarginTopRem: number;\n stimulusChoicesMarginTopRem: number;\n tokenGridColumnGapRem: number;\n tokenGridRowGapRem: number;\n tokenTemplateMarginTopRem: number;\n tokenInlineTokenGapRem: number;\n tokenChoicesMarginTopRem: number;\n inlineGridColumnGapRem: number;\n inlineGridRowGapRem: number;\n inlineTemplateMarginTopRem: number;\n inlineChoicesMarginTopRem: number;\n};\n\n/** Baseline values tuned to match historical CQT visuals; override via model.layoutLimits. */\nexport const DEFAULT_LAYOUT_LIMITS: LayoutLimits = {\n blankStandaloneWidthRem: 7,\n blankWideWidthRem: 10,\n blankUnderlineWidthPx: 2,\n blankUnderlineWideWidthPx: 4,\n horizontalChoiceWidthPx: 170,\n horizontalChoiceWidthVw: 30,\n horizontalChoiceTileMinHeightRem: 11,\n horizontalChoiceContentMinHeightRem: 7.5,\n // 9.375rem = 150px at the standard 16px root font size — Learnosity graphic-choice\n // SVGs are authored at 150x150; images up to that size must render at full size.\n // Shared by the choice tile image and the selected-answer image inside the cloze\n // blank, so the same choice image always renders at the same size in both places.\n choiceImageMaxHeightRem: 9.375,\n choiceImageMaxWidthRem: 9.375,\n listenButtonSizePx: 128,\n stimulusMinColumnPx: 210,\n textMinColumnPx: 260,\n legendMaxChars: 120,\n choiceGroupGapRem: 0.5,\n choiceRowGapRem: 0.5,\n toggleButtonGapRem: 0.5,\n horizontalChoiceRadioTopMarginRem: 0.5,\n audioBlankTemplateMarginTopRem: 0.8,\n audioBlankTemplateMarginBottomRem: 1.8,\n audioInstructionsMaxWidthPx: 875,\n stimulusGridColumnGapRem: 2,\n stimulusGridRowGapRem: 0.7,\n stimulusSentenceMarginTopRem: 0.2,\n stimulusChoicesMarginTopRem: 0.9,\n tokenGridColumnGapRem: 1.5,\n tokenGridRowGapRem: 0.8,\n tokenTemplateMarginTopRem: 0.6,\n tokenInlineTokenGapRem: 0.35,\n tokenChoicesMarginTopRem: 0.2,\n inlineGridColumnGapRem: 1.5,\n inlineGridRowGapRem: 0.65,\n inlineTemplateMarginTopRem: 0.45,\n inlineChoicesMarginTopRem: 0.25,\n};\n","import { DEFAULT_LAYOUT_LIMITS } from '../shared/layoutLimits.js';\n\nexport { DEFAULT_LAYOUT_LIMITS };\n\n/** Token authors must include exactly once in `template` (HTML). */\nexport const BLANK_TOKEN = '{{blank}}';\n\n/**\n * The values `normalize` fills in for fields an item leaves out. They carry no\n * content and no `id` or `element`: those come from the item config, and a\n * default here would stand in for a field the author never set.\n */\nexport default {\n model: {\n prompt: '',\n promptEnabled: true,\n interactionMode: 'populate_blank' as const,\n layoutProfile: '',\n choiceLayout: '',\n layoutProfilePresets: {},\n layoutLimits: { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin: null,\n audioButtonSkinsByLocale: {},\n sentenceHtml: '',\n template: '',\n choiceMode: 'text' as const,\n choices: [],\n choiceGroupLabel: '',\n correctChoiceId: '',\n hasAudio: false,\n autoplayAudioEnabled: false,\n completeAudioEnabled: false,\n audioUrl: '',\n audioTranscript: '',\n locale: '',\n teacherInstructions: '',\n teacherInstructionsEnabled: true,\n },\n};\n","import controllerDefaults, { BLANK_TOKEN } from '../controller/defaults.js';\n\n/** The model a new item starts from: a valid question an author edits in place. */\nexport default {\n model: {\n ...controllerDefaults.model,\n prompt: '<p><strong>Demo prompt</strong></p>',\n template: `<p>The answer is ${BLANK_TOKEN}.</p>`,\n choices: [\n { id: 'c1', labelHtml: '<p>Option A</p>' },\n { id: 'c2', labelHtml: '<p>Option B</p>' },\n { id: 'c3', labelHtml: '<p>Option C</p>' },\n ],\n correctChoiceId: 'c2',\n },\n configuration: {\n prompt: {\n label: 'Prompt',\n settings: true,\n },\n },\n};\n","import authorDefaults from '../author/defaults.js';\nimport defaults, { BLANK_TOKEN, DEFAULT_LAYOUT_LIMITS } from './defaults.js';\nimport type {\n McpbChoice,\n McpbQuestion,\n McpbSession,\n McpbEnv,\n McpbCorrectness,\n} from '../shared/types.js';\n\nconst isEmptyObject = (value: unknown): boolean =>\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0;\n\nexport function countBlankTokens(template: string): number {\n if (!template) return 0;\n const re = /\\{\\{blank\\}\\}/g;\n return (template.match(re) || []).length;\n}\n\nexport const getCorrectness = (question: McpbQuestion, session: McpbSession): McpbCorrectness => {\n if (!session?.choiceId) {\n return 'unanswered';\n }\n const correct = question?.correctChoiceId || '';\n if (session.choiceId === correct) {\n return 'correct';\n }\n return 'incorrect';\n};\n\n/** One blank, one key: an answered session scores 1 only when it picked the key. */\nexport const getPartialScore = (question: McpbQuestion, session: McpbSession) =>\n getCorrectness(question, session) === 'correct' ? 1 : 0;\n\nexport const outcome = (question: McpbQuestion, session: McpbSession, env: McpbEnv) =>\n new Promise((resolve) => {\n if (!session || isEmptyObject(session)) {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n session = normalizeSession(session);\n const normalizedQuestion = normalize(question);\n const correctness = getCorrectness(normalizedQuestion, session);\n\n if (correctness === 'unanswered') {\n resolve({\n score: 0,\n empty: true,\n traceLog: ['Student did not select any answers. Score is 0.'],\n });\n return;\n }\n\n const score = getPartialScore(normalizedQuestion, session);\n const traceLog = [\n `Mode: ${env?.mode || 'unknown'}.`,\n `Student selected choice: ${session.choiceId}.`,\n `Correct choice: ${normalizedQuestion.correctChoiceId || 'none'}.`,\n `Final score: ${score}.`,\n ];\n resolve({ score, empty: false, traceLog });\n });\n\nconst withDefaults = (base: McpbQuestion, model: McpbQuestion = {}) => ({\n ...base,\n ...model,\n layoutLimits: {\n ...DEFAULT_LAYOUT_LIMITS,\n ...(model?.layoutLimits || {}),\n },\n});\n\n/** The authoring starting point: a question that passes `validate()` as it stands. */\nexport const createDefaultModel = (model: McpbQuestion = {}) =>\n withDefaults(authorDefaults.model, model);\n\n/**\n * What `model()` and `outcome()` read: the item's own fields, with neutral\n * values for the rest. Starter content here would reach a learner as the\n * item's prompt or choices.\n */\nexport const normalize = (question: McpbQuestion = {}) => {\n const { shuffle, ...rest } = question || {};\n return {\n ...withDefaults(defaults.model, rest),\n // `lockChoiceOrder` alone decides the order, as in multiple-choice. Older items\n // spell an unlocked order `shuffle: true`, which counts only when it is unset.\n lockChoiceOrder:\n typeof rest.lockChoiceOrder === 'boolean' ? rest.lockChoiceOrder : shuffle !== true,\n };\n};\n\nexport const normalizeSession = (s: McpbSession): McpbSession => ({ ...s });\n\n/** The role plays no part (PIE-714): an instructor sees the order the student saw. */\nconst shouldLockChoices = (question: McpbQuestion, env: McpbEnv) =>\n !!question?.lockChoiceOrder || !!env?.['@pie-element']?.lockChoiceOrder;\n\nfunction shuffleArray<T>(items: T[]): T[] {\n const out = [...items];\n for (let i = out.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [out[i], out[j]] = [out[j], out[i]];\n }\n return out;\n}\n\n/**\n * The fields delivery renders. An imported choice can carry more, such as a\n * correctness flag or feedback, which must not reach a learner's browser.\n */\nconst toDeliveryChoice = ({ id, labelHtml, imageUrl, imageAlt }: McpbChoice): McpbChoice => ({\n id,\n ...(labelHtml !== undefined ? { labelHtml } : {}),\n ...(imageUrl !== undefined ? { imageUrl } : {}),\n ...(imageAlt !== undefined ? { imageAlt } : {}),\n});\n\nconst getStoredShuffle = (session: McpbSession): string[] =>\n Array.isArray(session?.data?.shuffledValues)\n ? session.data.shuffledValues\n : Array.isArray(session?.shuffledValues)\n ? session.shuffledValues\n : [];\n\n/** The stored order, as `getShuffledChoices` applies it: a choice it does not list is left out. */\nconst applyShuffledValues = (\n choices: McpbChoice[],\n shuffledValues: string[],\n choiceKey: keyof McpbChoice\n) =>\n shuffledValues\n .map((value) => choices.find((choice) => choice?.[choiceKey] === value))\n .filter((c): c is McpbChoice => !!c);\n\ntype UpdateSessionFn = (\n id: string,\n element: string,\n data: { shuffledValues: string[] }\n) => Promise<void>;\n\nconst getOrderedChoices = async (\n question: McpbQuestion,\n session: McpbSession,\n env: McpbEnv,\n updateSession?: UpdateSessionFn\n) => {\n const choices = Array.isArray(question?.choices) ? [...question.choices] : [];\n if (!choices.length || shouldLockChoices(question, env || {})) {\n return choices;\n }\n\n const shuffledValues = getStoredShuffle(session);\n if (shuffledValues.length) {\n return applyShuffledValues(choices, shuffledValues, 'id');\n }\n\n const shuffledChoices = shuffleArray(choices);\n\n if (updateSession && typeof updateSession === 'function' && session?.id && session?.element) {\n const shuffledIds = shuffledChoices.map((choice) => choice?.id).filter(Boolean);\n if (shuffledIds.length) {\n await updateSession(session.id, session.element, { shuffledValues: shuffledIds });\n }\n }\n\n return shuffledChoices;\n};\n\nexport const model = async (\n question: McpbQuestion,\n session: McpbSession | null,\n env: McpbEnv | null,\n updateSession?: UpdateSessionFn\n) => {\n const safeSession: McpbSession = session || {};\n const safeEnv: McpbEnv = env || {};\n const normalizedQuestion = normalize(question);\n const choices = (await getOrderedChoices(normalizedQuestion, safeSession, safeEnv, updateSession))\n .filter((c): c is McpbChoice => !!c && typeof c === 'object')\n .map(toDeliveryChoice);\n\n const out: Record<string, unknown> = {\n prompt: normalizedQuestion.promptEnabled ? normalizedQuestion.prompt : null,\n interactionMode: normalizedQuestion.interactionMode || 'populate_blank',\n layoutProfile: normalizedQuestion.layoutProfile || '',\n choiceLayout: normalizedQuestion.choiceLayout || '',\n layoutProfilePresets:\n normalizedQuestion.layoutProfilePresets &&\n typeof normalizedQuestion.layoutProfilePresets === 'object'\n ? normalizedQuestion.layoutProfilePresets\n : {},\n layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },\n audioButtonSkin:\n normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'\n ? normalizedQuestion.audioButtonSkin\n : null,\n audioButtonSkinsByLocale:\n normalizedQuestion.audioButtonSkinsByLocale &&\n typeof normalizedQuestion.audioButtonSkinsByLocale === 'object'\n ? normalizedQuestion.audioButtonSkinsByLocale\n : {},\n sentenceHtml: normalizedQuestion.sentenceHtml || null,\n template: normalizedQuestion.template,\n choiceMode: normalizedQuestion.choiceMode,\n choices,\n choiceGroupLabel: normalizedQuestion.choiceGroupLabel || '',\n hasAudio: normalizedQuestion.hasAudio,\n autoplayAudioEnabled: !!normalizedQuestion.autoplayAudioEnabled,\n completeAudioEnabled: !!normalizedQuestion.completeAudioEnabled,\n audioUrl: normalizedQuestion.hasAudio ? normalizedQuestion.audioUrl : null,\n audioTranscript: normalizedQuestion.hasAudio ? normalizedQuestion.audioTranscript : null,\n customType: normalizedQuestion.customType || '',\n useFeatureButtonAudio:\n typeof normalizedQuestion.useFeatureButtonAudio === 'boolean'\n ? normalizedQuestion.useFeatureButtonAudio\n : undefined,\n locale: normalizedQuestion.locale || '',\n language: normalizedQuestion.language || normalizedQuestion.locale || undefined,\n disabled: safeEnv.mode !== 'gather',\n mode: safeEnv.mode,\n };\n\n if (safeEnv.mode === 'evaluate') {\n const correctness = getCorrectness(normalizedQuestion, safeSession);\n out.correctness = correctness;\n out.responseCorrect = correctness === 'correct';\n out.correctChoiceId = normalizedQuestion.correctChoiceId;\n }\n\n if (safeEnv.role === 'instructor' && (safeEnv.mode === 'view' || safeEnv.mode === 'evaluate')) {\n out.teacherInstructions = normalizedQuestion.teacherInstructionsEnabled\n ? normalizedQuestion.teacherInstructions\n : null;\n } else {\n out.teacherInstructions = null;\n }\n\n return out;\n};\n\nexport const createCorrectResponseSession = (question: McpbQuestion, env: McpbEnv) => {\n return new Promise((resolve) => {\n if (env.mode !== 'evaluate' && env.role === 'instructor') {\n // The player sets `id` and `element` on the entry from the item config.\n resolve({\n id: '1',\n choiceId: question?.correctChoiceId || '',\n });\n } else {\n resolve(null);\n }\n });\n};\n\n/** Markup with its tags removed, keeping media an author can answer from. */\nconst getContent = (html?: string) =>\n (html || '').replace(/(<(?!img|iframe|source)([^>]+)>)/gi, '').trim();\n\ntype ValidateConfig = {\n minAnswerChoices?: number;\n maxAnswerChoices?: number;\n prompt?: { required?: boolean };\n teacherInstructions?: { required?: boolean };\n};\n\n/**\n * Authoring errors in multiple-choice's shape: a message per field, and under\n * `choices` a message per choice keyed by its id.\n */\nexport const validate = (question: McpbQuestion = {}, config: ValidateConfig = {}) => {\n const errors: Record<string, string | Record<string, string>> = {};\n const { minAnswerChoices = 2, maxAnswerChoices } = config;\n\n if ((question.promptEnabled || config.prompt?.required) && !getContent(question.prompt)) {\n errors.prompt = 'This field is required.';\n }\n\n if (config.teacherInstructions?.required && !getContent(question.teacherInstructions)) {\n errors.teacherInstructions = 'This field is required.';\n }\n\n const interactionMode = question.interactionMode || 'populate_blank';\n const template = question.template || '';\n const n = countBlankTokens(template);\n if (interactionMode === 'audio_mc_only') {\n if (n > 0) {\n errors.template = `Template cannot contain ${BLANK_TOKEN} in audio-only mode`;\n }\n } else if (interactionMode === 'populate_blank') {\n if (n === 0) {\n errors.template = `Template must contain exactly one ${BLANK_TOKEN} placeholder`;\n } else if (n > 1) {\n errors.template = `Template must contain only one ${BLANK_TOKEN} placeholder`;\n }\n } else {\n errors.interactionMode = 'Unknown interaction mode';\n }\n\n const choices = Array.isArray(question.choices) ? question.choices : [];\n if (choices.length < minAnswerChoices) {\n errors.answerChoices = `There should be at least ${minAnswerChoices} choices defined.`;\n } else if (maxAnswerChoices != null && choices.length > maxAnswerChoices) {\n errors.answerChoices = `No more than ${maxAnswerChoices} choices should be defined.`;\n }\n\n const mode = question.choiceMode || 'text';\n const choicesErrors: Record<string, string> = {};\n const seenIds = new Set<string>();\n const seenLabels = new Set<string>();\n choices.forEach((c, i) => {\n const key = c?.id || String(i);\n if (!c?.id) {\n choicesErrors[key] = 'Choice needs an id.';\n return;\n }\n // The session stores the picked id, so two choices sharing one cannot be told apart.\n if (seenIds.has(c.id)) {\n choicesErrors[key] = 'Choice id should be unique.';\n return;\n }\n seenIds.add(c.id);\n if (mode === 'text') {\n const label = getContent(c.labelHtml);\n if (!label) {\n choicesErrors[key] = 'Content should not be empty.';\n } else if (seenLabels.has(label)) {\n choicesErrors[key] = 'Content should be unique.';\n }\n seenLabels.add(label);\n } else if (!c.imageUrl?.trim()) {\n choicesErrors[key] = 'An image is required.';\n } else if (!c.imageAlt?.trim()) {\n choicesErrors[key] = 'Image alt text is required.';\n }\n });\n if (Object.keys(choicesErrors).length) {\n errors.choices = choicesErrors;\n }\n\n const correct = question.correctChoiceId;\n if (!correct) {\n errors.correctResponse = 'No correct response defined.';\n } else if (!choices.some((c) => c?.id === correct)) {\n errors.correctResponse = 'The correct response must be one of the choices.';\n }\n\n if (question.hasAudio) {\n const hasAudioUrl = !!question.audioUrl?.trim();\n if (!hasAudioUrl) {\n errors.audioUrl = 'A playable audio URL is required when audio is enabled';\n }\n }\n\n const limits = question?.layoutLimits;\n if (limits && typeof limits === 'object') {\n const numericLimitKeys = [\n 'blankStandaloneWidthRem',\n 'blankWideWidthRem',\n 'blankUnderlineWidthPx',\n 'blankUnderlineWideWidthPx',\n 'horizontalChoiceWidthPx',\n 'horizontalChoiceWidthVw',\n 'horizontalChoiceTileMinHeightRem',\n 'horizontalChoiceContentMinHeightRem',\n 'choiceImageMaxHeightRem',\n 'choiceImageMaxWidthRem',\n 'listenButtonSizePx',\n 'stimulusMinColumnPx',\n 'textMinColumnPx',\n 'legendMaxChars',\n 'choiceGroupGapRem',\n 'choiceRowGapRem',\n 'toggleButtonGapRem',\n 'horizontalChoiceRadioTopMarginRem',\n 'audioBlankTemplateMarginTopRem',\n 'audioBlankTemplateMarginBottomRem',\n 'audioInstructionsMaxWidthPx',\n 'stimulusGridColumnGapRem',\n 'stimulusGridRowGapRem',\n 'stimulusSentenceMarginTopRem',\n 'stimulusChoicesMarginTopRem',\n 'tokenGridColumnGapRem',\n 'tokenGridRowGapRem',\n 'tokenTemplateMarginTopRem',\n 'tokenInlineTokenGapRem',\n 'tokenChoicesMarginTopRem',\n 'inlineGridColumnGapRem',\n 'inlineGridRowGapRem',\n 'inlineTemplateMarginTopRem',\n 'inlineChoicesMarginTopRem',\n ] as const;\n for (const key of numericLimitKeys) {\n if (limits[key] === undefined || limits[key] === null) continue;\n const value = Number(limits[key]);\n if (!Number.isFinite(value) || value <= 0) {\n errors.layoutLimits = `${key} must be a positive number when provided`;\n break;\n }\n }\n }\n\n return errors;\n};\n"],"mappings":"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"}
|