coursecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
@@ -0,0 +1,377 @@
1
+ import AccessibilityManager from '../../managers/accessibility-manager.js';
2
+ import {
3
+ validateAgainstSchema,
4
+ createInteractionEventHandler,
5
+ renderInteractionControls,
6
+ renderFeedbackContainer,
7
+ displayFeedback,
8
+ clearFeedback,
9
+ normalizeInitialResponse,
10
+ validateContainer,
11
+ registerCoreInteraction
12
+ } from './interaction-base.js';
13
+
14
+ // Metadata for multiple-choice interaction type
15
+ export const metadata = {
16
+ creator: 'createMultipleChoiceQuestion',
17
+ scormType: 'choice',
18
+ showCheckAnswer: true,
19
+ isAnswered: (response) => {
20
+ return Array.isArray(response) && response.length > 0;
21
+ },
22
+ getCorrectAnswer: (config) => {
23
+ if (config.multiple) {
24
+ return JSON.stringify(config.choices.filter(c => c.correct).map(c => c.value));
25
+ }
26
+ return config.correctAnswer || '';
27
+ },
28
+ formatCorrectAnswer: (question, correctAnswer) => {
29
+ let html = '<ul class="list-disc pl-4 m-0">';
30
+ const correctAnswers = Array.isArray(correctAnswer) ? correctAnswer : [correctAnswer];
31
+ const choices = question.choices || [];
32
+
33
+ correctAnswers.forEach(answer => {
34
+ const choice = choices.find(c => c.value === answer || c.correct);
35
+ if (choice) {
36
+ html += `<li class="correct-item">${choice.text}</li>`;
37
+ } else {
38
+ html += `<li class="correct-item">${answer}</li>`;
39
+ }
40
+ });
41
+ html += '</ul>';
42
+ return html;
43
+ },
44
+ formatUserResponse: (question, response) => {
45
+ let html = '<ul class="list-disc pl-4 m-0">';
46
+ const userAnswers = Array.isArray(response) ? response : [response];
47
+ const choices = question.choices || [];
48
+
49
+ userAnswers.forEach(answer => {
50
+ const choice = choices.find(c => c.value === answer);
51
+ if (choice) {
52
+ html += `<li class="response-item">${choice.text}</li>`;
53
+ } else {
54
+ html += `<li class="response-item">${answer}</li>`;
55
+ }
56
+ });
57
+ html += '</ul>';
58
+ return html;
59
+ }
60
+ };
61
+
62
+ // Schema for validation, linting, and AI-assisted authoring
63
+ export const schema = {
64
+ type: 'multiple-choice',
65
+ description: 'Single or multi-select question with radio buttons or checkboxes',
66
+ scormType: 'choice',
67
+ example: `<div class="interaction multiple-choice" data-interaction-id="demo-mc">
68
+ <div class="question-prompt"><h3>Which planet is closest to the Sun?</h3></div>
69
+ <div class="radio-group">
70
+ <label class="radio-option"><input type="radio" name="demo-mc" value="a"> <span class="radio-custom"></span><div class="radio-label"><span>Venus</span></div></label>
71
+ <label class="radio-option"><input type="radio" name="demo-mc" value="b"> <span class="radio-custom"></span><div class="radio-label"><span>Mercury</span></div></label>
72
+ <label class="radio-option"><input type="radio" name="demo-mc" value="c"> <span class="radio-custom"></span><div class="radio-label"><span>Mars</span></div></label>
73
+ </div>
74
+ <div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
75
+ </div>`,
76
+ properties: {
77
+ choices: {
78
+ type: 'array',
79
+ required: true,
80
+ minItems: 2,
81
+ description: 'Array of choice options',
82
+ itemSchema: {
83
+ value: { type: 'string', required: true },
84
+ text: { type: 'string', required: true },
85
+ correct: { type: 'boolean', description: 'For multi-select mode' },
86
+ description: { type: 'string' }
87
+ }
88
+ },
89
+ correctAnswer: {
90
+ type: 'string',
91
+ requiredUnless: 'multiple',
92
+ description: 'Value of correct choice (single-select mode)'
93
+ },
94
+ multiple: {
95
+ type: 'boolean',
96
+ default: false,
97
+ description: 'Enable multi-select mode'
98
+ }
99
+ }
100
+ };
101
+
102
+ export function createMultipleChoiceQuestion(config) {
103
+ validateAgainstSchema(config, schema);
104
+
105
+ const { id, prompt, choices, correctAnswer, multiple = false, controlled = false } = config;
106
+
107
+ // Validate choices array
108
+ if (!Array.isArray(choices) || choices.length === 0) {
109
+ throw new Error(`Multiple choice question "${id}" must have at least one choice`);
110
+ }
111
+
112
+ // Validate correctAnswer for single-select mode
113
+ if (!multiple && !correctAnswer) {
114
+ throw new Error(`Single-select multiple choice question "${id}" must have correctAnswer defined`);
115
+ }
116
+
117
+ let _container = null;
118
+
119
+ const questionObj = {
120
+ id,
121
+ type: multiple ? 'multiple-choice-multiple' : 'multiple-choice-single',
122
+
123
+ render: (container, initialResponse = null) => {
124
+ validateContainer(container, id);
125
+ _container = container;
126
+
127
+ // Normalize initial response to array format
128
+ const initialValue = normalizeInitialResponse(initialResponse);
129
+ const initialValues = Array.isArray(initialValue) ? initialValue :
130
+ (initialValue ? [initialValue] : []);
131
+
132
+ let html = `
133
+ <div class="interaction multiple-choice" data-interaction-id="${id}">
134
+ <div class="question-prompt">
135
+ <h3>${prompt}</h3>
136
+ </div>
137
+ <div class="${multiple ? 'checkbox-group' : 'radio-group'}">
138
+ `;
139
+
140
+ choices.forEach((choice, index) => {
141
+ const inputType = multiple ? 'checkbox' : 'radio';
142
+ const choiceId = `${id}_choice_${index}`;
143
+ const optionClass = multiple ? 'checkbox-option' : 'radio-option';
144
+ const customClass = multiple ? 'checkbox-custom' : 'radio-custom';
145
+ const labelClass = multiple ? 'checkbox-label' : 'radio-label';
146
+ const isChecked = initialValues.includes(choice.value);
147
+
148
+ html += `
149
+ <label class="${optionClass}" role="option" aria-selected="${isChecked}">
150
+ <input
151
+ type="${inputType}"
152
+ id="${choiceId}"
153
+ name="${id}_choice"
154
+ value="${choice.value}"
155
+ ${choice.correct ? 'data-correct="true"' : ''}
156
+ ${isChecked ? 'checked' : ''}
157
+ aria-describedby="${id}_feedback ${choiceId}_description"
158
+ aria-label="${choice.text}"
159
+ tabindex="0"
160
+ data-testid="${id}-choice-${index}"
161
+ />
162
+ <span class="${customClass}"></span>
163
+ <div class="${labelClass}">
164
+ <span data-editable-choice data-edit-for-interaction="${id}" data-choice-index="${index}">${choice.text}</span>
165
+ ${choice.description ? `<div id="${choiceId}_description" class="radio-description">${choice.description}</div>` : ''}
166
+ </div>
167
+ </label>
168
+ `;
169
+ });
170
+
171
+ html += `
172
+ </div>
173
+ ${renderInteractionControls(id, controlled)}
174
+ ${renderFeedbackContainer(id)}
175
+ </div>
176
+ `;
177
+
178
+ container.innerHTML = html;
179
+
180
+ // Setup ARIA state management and keyboard navigation
181
+ setupAccessibility(container, questionObj);
182
+
183
+ // Attach event handler only in uncontrolled mode
184
+ if (!controlled) {
185
+ const correctPattern = multiple
186
+ ? JSON.stringify(choices.filter(c => c.correct).map(c => c.value))
187
+ : correctAnswer;
188
+
189
+ container.addEventListener('click', createInteractionEventHandler(questionObj, {
190
+ ...config,
191
+ scormType: 'choice',
192
+ correctPattern
193
+ }));
194
+ }
195
+ },
196
+
197
+ evaluate: (selectedValues) => {
198
+ if (!selectedValues || !Array.isArray(selectedValues) || selectedValues.length === 0) {
199
+ const _correctValues = multiple ? choices.filter(c => c.correct).map(c => c.value) : [correctAnswer];
200
+ return {
201
+ score: 0,
202
+ correct: false,
203
+ response: multiple ? JSON.stringify([]) : '',
204
+ error: 'No answer selected'
205
+ };
206
+ }
207
+
208
+ if (multiple) {
209
+ const correctValues = choices.filter(c => c.correct).map(c => c.value);
210
+ const allCorrect = correctValues.every(v => selectedValues.includes(v)) &&
211
+ selectedValues.every(v => correctValues.includes(v));
212
+ const score = allCorrect ? 1 : (selectedValues.filter(v => correctValues.includes(v)).length / correctValues.length);
213
+
214
+ return {
215
+ score,
216
+ correct: allCorrect,
217
+ response: JSON.stringify(selectedValues)
218
+ };
219
+ } else {
220
+ const isCorrect = selectedValues.length === 1 && selectedValues[0] === correctAnswer;
221
+ return {
222
+ score: isCorrect ? 1 : 0,
223
+ correct: isCorrect,
224
+ response: selectedValues[0] || ''
225
+ };
226
+ }
227
+ },
228
+
229
+ checkAnswer: () => {
230
+ validateContainer(_container, id);
231
+
232
+ const inputs = _container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
233
+ const selectedValues = Array.from(inputs)
234
+ .filter(input => input.checked)
235
+ .map(input => input.value);
236
+
237
+ if (selectedValues.length === 0) {
238
+ const errorMsg = 'Please select an answer before checking.';
239
+ displayFeedback(_container, id, errorMsg, 'error');
240
+ AccessibilityManager.announce(errorMsg, 'assertive');
241
+ return null;
242
+ }
243
+
244
+ const evaluation = questionObj.evaluate(selectedValues);
245
+
246
+ if (evaluation.correct) {
247
+ const successMsg = 'Correct! Well done.';
248
+ displayFeedback(_container, id, successMsg, 'correct');
249
+ AccessibilityManager.announce(successMsg);
250
+ } else {
251
+ const selectedText = selectedValues.map(value =>
252
+ choices.find(c => c.value === value)?.text || value
253
+ ).join(', ');
254
+
255
+ const expectedText = multiple
256
+ ? choices.filter(c => c.correct).map(c => c.text).join(', ')
257
+ : (choices.find(c => c.value === correctAnswer)?.text || correctAnswer);
258
+
259
+ const errorMsg = `Incorrect. You selected: ${selectedText}. Expected: ${expectedText}`;
260
+ displayFeedback(_container, id, errorMsg, 'incorrect');
261
+ AccessibilityManager.announce(errorMsg);
262
+ }
263
+
264
+ return evaluation;
265
+ },
266
+
267
+ reset: () => {
268
+ validateContainer(_container, id);
269
+
270
+ const inputs = _container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
271
+ const choiceItems = _container.querySelectorAll('.radio-option, .checkbox-option');
272
+
273
+ inputs.forEach(input => {
274
+ input.checked = false;
275
+ input.setAttribute('aria-checked', 'false');
276
+ });
277
+
278
+ choiceItems.forEach(item => {
279
+ item.setAttribute('aria-selected', 'false');
280
+ item.classList.remove('selected');
281
+ });
282
+
283
+ clearFeedback(_container, id);
284
+ AccessibilityManager.announce('Question reset. All selections cleared.');
285
+ },
286
+
287
+ setResponse: (response) => {
288
+ validateContainer(_container, id);
289
+
290
+ // Normalize response to array
291
+ const responseArray = Array.isArray(response) ? response : (response ? [response] : []);
292
+
293
+ const inputs = _container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
294
+ if (inputs.length === 0) {
295
+ throw new Error(`No input elements found for multiple choice question "${id}"`);
296
+ }
297
+
298
+ inputs.forEach(input => {
299
+ input.checked = responseArray.includes(input.value);
300
+ });
301
+
302
+ updateAriaStates(_container);
303
+ },
304
+
305
+ getResponse: () => {
306
+ validateContainer(_container, id);
307
+
308
+ const selectedInputs = _container.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked');
309
+ return Array.from(selectedInputs).map(input => input.value);
310
+ },
311
+
312
+ getCorrectAnswer: () => {
313
+ if (multiple) {
314
+ return choices.filter(c => c.correct).map(c => c.value);
315
+ }
316
+ return correctAnswer;
317
+ }
318
+ };
319
+
320
+ // For uncontrolled interactions, register with the central registry for lifecycle mgmt
321
+ if (!controlled) {
322
+ registerCoreInteraction(config, questionObj);
323
+ }
324
+
325
+ return questionObj;
326
+ }
327
+
328
+ /**
329
+ * Updates ARIA states for all choice items based on input checked state
330
+ */
331
+ function updateAriaStates(container) {
332
+ const choiceItems = container.querySelectorAll('.radio-option, .checkbox-option');
333
+
334
+ choiceItems.forEach(item => {
335
+ const input = item.querySelector('input');
336
+ if (!input) return;
337
+
338
+ const isSelected = input.checked;
339
+ item.setAttribute('aria-selected', isSelected.toString());
340
+
341
+ if (isSelected) {
342
+ item.classList.add('selected');
343
+ } else {
344
+ item.classList.remove('selected');
345
+ }
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Sets up accessibility features including ARIA state management and keyboard navigation
351
+ */
352
+ function setupAccessibility(container, questionObj) {
353
+ const inputs = container.querySelectorAll('input[type="radio"], input[type="checkbox"]');
354
+
355
+ inputs.forEach(input => {
356
+ // Update ARIA states on change
357
+ input.addEventListener('change', () => updateAriaStates(container));
358
+
359
+ // Enhanced keyboard support
360
+ input.addEventListener('keydown', (e) => {
361
+ if (e.key === 'Enter' || e.key === ' ') {
362
+ e.preventDefault();
363
+ if (questionObj.type === 'multiple-choice-multiple') {
364
+ input.checked = !input.checked;
365
+ } else {
366
+ input.checked = true;
367
+ }
368
+ updateAriaStates(container);
369
+ input.dispatchEvent(new Event('change'));
370
+ }
371
+ });
372
+ });
373
+
374
+ // Initialize ARIA states
375
+ updateAriaStates(container);
376
+ }
377
+
@@ -0,0 +1,271 @@
1
+ import {
2
+ validateAgainstSchema,
3
+ createInteractionEventHandler,
4
+ renderInteractionControls,
5
+ displayFeedback,
6
+ clearFeedback,
7
+ normalizeInitialResponse,
8
+ validateContainer,
9
+ registerCoreInteraction
10
+ } from './interaction-base.js';
11
+
12
+ // Metadata for numeric interaction type
13
+ export const metadata = {
14
+ creator: 'createNumericQuestion',
15
+ scormType: 'numeric',
16
+ showCheckAnswer: true,
17
+ isAnswered: (response) => {
18
+ return response !== null && response !== undefined && String(response).trim().length > 0;
19
+ },
20
+ getCorrectAnswer: (config) => JSON.stringify(config.correctRange || {}),
21
+ formatCorrectAnswer: (question, correctAnswer) => {
22
+ let html = '';
23
+ if (question.correctRange) {
24
+ if (question.correctRange.exact !== undefined) {
25
+ html += `<p class="correct-item">${question.correctRange.exact}</p>`;
26
+ } else if (question.correctRange.min !== undefined && question.correctRange.max !== undefined) {
27
+ html += `<p class="correct-item">${question.correctRange.min} - ${question.correctRange.max}</p>`;
28
+ }
29
+ } else {
30
+ html += `<p class="correct-item">${correctAnswer}</p>`;
31
+ }
32
+ return html;
33
+ },
34
+ formatUserResponse: (question, response) => `<p class="response-item">${response}</p>`
35
+ };
36
+
37
+ // Schema for validation, linting, and AI-assisted authoring
38
+ export const schema = {
39
+ type: 'numeric',
40
+ description: 'Numeric input with exact or range-based validation',
41
+ scormType: 'numeric',
42
+ example: `<div class="interaction numeric fill-in-stacked" data-interaction-id="demo-num">
43
+ <div class="question-prompt"><h3>What year was the xAPI specification released?</h3></div>
44
+ <div class="fill-in-container">
45
+ <div class="fill-in-item">
46
+ <div class="numeric-input-wrapper">
47
+ <input type="number" class="fill-in-stacked-input" placeholder="Enter a number..." step="any">
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
52
+ </div>`,
53
+ properties: {
54
+ correctRange: {
55
+ type: 'object',
56
+ required: true,
57
+ description: 'Correct answer spec (exact value OR min/max range)',
58
+ valueSchema: {
59
+ exact: { type: 'number' },
60
+ min: { type: 'number' },
61
+ max: { type: 'number' }
62
+ }
63
+ },
64
+ tolerance: {
65
+ type: 'number',
66
+ default: 0,
67
+ description: 'Acceptable margin for exact values'
68
+ },
69
+ units: {
70
+ type: 'string',
71
+ description: 'Display units (e.g., "km", "%")'
72
+ }
73
+ },
74
+ notes: 'correctRange requires either "exact" or both "min" and "max"'
75
+ };
76
+
77
+ export function createNumericQuestion(config) {
78
+ validateAgainstSchema(config, schema);
79
+
80
+ const { id, prompt, correctRange, units = '', tolerance = 0, placeholder = '', controlled = false } = config;
81
+
82
+ let _container = null;
83
+
84
+ const questionObj = {
85
+ id,
86
+ type: 'numeric',
87
+
88
+ render: (container, initialResponse = null) => {
89
+ validateContainer(container, id);
90
+ _container = container;
91
+
92
+ const initialValue = normalizeInitialResponse(initialResponse) ?? '';
93
+
94
+ const html = `
95
+ <div class="interaction numeric fill-in-stacked" data-interaction-id="${id}">
96
+ <div class="question-prompt">
97
+ <h3>${prompt}</h3>
98
+ </div>
99
+ <div class="fill-in-container">
100
+ <div class="fill-in-item">
101
+ <div class="numeric-input-wrapper">
102
+ <input
103
+ type="number"
104
+ class="fill-in-stacked-input"
105
+ id="${id}_input"
106
+ name="${id}_input"
107
+ step="any"
108
+ value="${initialValue}"
109
+ placeholder="${placeholder}"
110
+ aria-describedby="${id}_feedback"
111
+ aria-label="${prompt}"
112
+ ${correctRange.min !== undefined ? `min="${correctRange.min}"` : ''}
113
+ ${correctRange.max !== undefined ? `max="${correctRange.max}"` : ''}
114
+ data-testid="${id}-input"
115
+ />
116
+ ${units ? `<span class="units">${units}</span>` : ''}
117
+ </div>
118
+ <div id="${id}_feedback" class="feedback" aria-live="polite"></div>
119
+ </div>
120
+ </div>
121
+ ${renderInteractionControls(id, controlled)}
122
+ <div class="overall-feedback" id="${id}_overall_feedback" aria-live="polite"></div>
123
+ </div>
124
+ `;
125
+
126
+ container.innerHTML = html;
127
+
128
+ // Setup Enter key handling
129
+ const input = container.querySelector('input[type="number"]');
130
+ input.addEventListener('keydown', (e) => {
131
+ if (e.key === 'Enter') {
132
+ e.preventDefault();
133
+ questionObj.checkAnswer();
134
+ }
135
+ });
136
+
137
+ // Attach event handler only in uncontrolled mode
138
+ if (!controlled) {
139
+ container.addEventListener('click', createInteractionEventHandler(questionObj, {
140
+ ...config,
141
+ scormType: 'numeric',
142
+ correctPattern: JSON.stringify(correctRange)
143
+ }));
144
+ }
145
+ },
146
+
147
+ evaluate: (response) => {
148
+ if (response === null || response === undefined || response === '') {
149
+ return {
150
+ score: 0,
151
+ correct: false,
152
+ response: '',
153
+ error: 'No answer provided'
154
+ };
155
+ }
156
+
157
+ const value = parseFloat(response);
158
+ if (isNaN(value)) {
159
+ return {
160
+ score: 0,
161
+ correct: false,
162
+ response,
163
+ error: 'Invalid numeric value'
164
+ };
165
+ }
166
+
167
+ let correct = false;
168
+
169
+ if (correctRange.exact !== undefined) {
170
+ correct = Math.abs(value - correctRange.exact) <= tolerance;
171
+ } else if (correctRange.min !== undefined && correctRange.max !== undefined) {
172
+ correct = value >= correctRange.min && value <= correctRange.max;
173
+ } else if (correctRange.min !== undefined) {
174
+ correct = value >= correctRange.min;
175
+ } else if (correctRange.max !== undefined) {
176
+ correct = value <= correctRange.max;
177
+ }
178
+
179
+ return {
180
+ score: correct ? 1 : 0,
181
+ correct,
182
+ response,
183
+ value
184
+ };
185
+ },
186
+
187
+ checkAnswer: () => {
188
+ validateContainer(_container, id);
189
+
190
+ const input = _container.querySelector('input[type="number"]');
191
+ if (!input) {
192
+ throw new Error(`Input element not found for numeric question "${id}"`);
193
+ }
194
+
195
+ const response = input.value.trim();
196
+
197
+ if (response === '') {
198
+ displayFeedback(_container, id, 'Please enter a number.', 'error');
199
+ return null;
200
+ }
201
+
202
+ const evaluation = questionObj.evaluate(response);
203
+
204
+ if (evaluation.correct) {
205
+ displayFeedback(_container, id, `Correct! ${evaluation.value}${units}`, 'correct');
206
+ } else {
207
+ let expectedText = '';
208
+ if (correctRange.exact !== undefined) {
209
+ expectedText = `Expected: ${correctRange.exact}${units}`;
210
+ } else if (correctRange.min !== undefined && correctRange.max !== undefined) {
211
+ expectedText = `Expected: between ${correctRange.min} and ${correctRange.max}${units}`;
212
+ } else if (correctRange.min !== undefined) {
213
+ expectedText = `Expected: at least ${correctRange.min}${units}`;
214
+ } else if (correctRange.max !== undefined) {
215
+ expectedText = `Expected: at most ${correctRange.max}${units}`;
216
+ }
217
+ displayFeedback(_container, id, `Incorrect. ${expectedText}`, 'incorrect');
218
+ }
219
+
220
+ return evaluation;
221
+ },
222
+
223
+ reset: () => {
224
+ validateContainer(_container, id);
225
+
226
+ const input = _container.querySelector('input[type="number"]');
227
+ if (input) {
228
+ input.value = '';
229
+ }
230
+
231
+ clearFeedback(_container, id);
232
+ },
233
+
234
+ setResponse: (response) => {
235
+ validateContainer(_container, id);
236
+
237
+ if (response === null || response === undefined) {
238
+ return;
239
+ }
240
+
241
+ const input = _container.querySelector('input[type="number"]');
242
+ if (!input) {
243
+ throw new Error(`Input element not found for numeric question "${id}"`);
244
+ }
245
+
246
+ input.value = response;
247
+ },
248
+
249
+ getResponse: () => {
250
+ validateContainer(_container, id);
251
+
252
+ const input = _container.querySelector('input[type="number"]');
253
+ if (!input) {
254
+ throw new Error(`Input element not found for numeric question "${id}"`);
255
+ }
256
+
257
+ return input.value || null;
258
+ },
259
+
260
+ getCorrectAnswer: () => {
261
+ return correctRange;
262
+ }
263
+ };
264
+
265
+ // For uncontrolled interactions, register with the central registry for lifecycle mgmt
266
+ if (!controlled) {
267
+ registerCoreInteraction(config, questionObj);
268
+ }
269
+
270
+ return questionObj;
271
+ }