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,511 @@
1
+ /**
2
+ * @file interaction-base.js
3
+ * @description Shared utilities and patterns for all interaction components.
4
+ * Eliminates duplication and ensures consistent error handling across interactions.
5
+ */
6
+
7
+ import InteractionManager from '../../managers/interaction-manager.js';
8
+ import interactionRegistry from '../../managers/interaction-registry.js';
9
+ import stateManager from '../../state/index.js';
10
+
11
+ import engagementManager from '../../engagement/engagement-manager.js';
12
+ import * as NavigationState from '../../navigation/NavigationState.js';
13
+ import { iconManager } from '../../utilities/icons.js';
14
+ import { logger } from '../../utilities/logger.js';
15
+ import { escapeHTML } from '../../utilities/utilities.js';
16
+ import { formatLearnerResponseForScorm } from '../../validation/scorm-validators.js';
17
+
18
+ /**
19
+ * Validate a configuration object against an interaction schema.
20
+ * Each interaction passes its own schema directly to avoid circular imports.
21
+ * @param {object} config - The interaction configuration to validate
22
+ * @param {object} interactionSchema - The schema object (from the interaction's export)
23
+ * @param {object} [baseProps] - Optional base schema properties to merge (defaults to baseSchema)
24
+ * @throws {Error} If validation fails
25
+ * @returns {true} If validation passes
26
+ */
27
+ export function validateAgainstSchema(config, interactionSchema, baseProps = null) {
28
+ if (!config || typeof config !== 'object') {
29
+ throw new Error(`Invalid interaction config: expected object, got ${typeof config}`);
30
+ }
31
+
32
+ if (!interactionSchema || !interactionSchema.properties) {
33
+ throw new Error('Invalid schema: must have properties object');
34
+ }
35
+
36
+ // Merge base properties with interaction-specific properties
37
+ const schema = {
38
+ ...interactionSchema,
39
+ properties: {
40
+ ...baseProps,
41
+ ...interactionSchema.properties
42
+ }
43
+ };
44
+
45
+ const errors = [];
46
+
47
+ // Validate each property defined in the schema
48
+ for (const [propName, propDef] of Object.entries(schema.properties)) {
49
+ const value = config[propName];
50
+ const isPresent = value !== undefined && value !== null && value !== '';
51
+
52
+ // Check required
53
+ if (propDef.required && !isPresent) {
54
+ // Handle requiredUnless condition
55
+ if (propDef.requiredUnless && config[propDef.requiredUnless]) {
56
+ continue; // Skip - alternative condition satisfied
57
+ }
58
+ errors.push(`Missing required property: "${propName}"`);
59
+ continue;
60
+ }
61
+
62
+ // Skip validation if not present and not required
63
+ if (!isPresent) continue;
64
+
65
+ // Type checking
66
+ const expectedTypes = Array.isArray(propDef.type) ? propDef.type : [propDef.type];
67
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
68
+
69
+ if (!expectedTypes.includes(actualType)) {
70
+ errors.push(`Property "${propName}" expected ${expectedTypes.join(' or ')}, got ${actualType}`);
71
+ continue;
72
+ }
73
+
74
+ // Array-specific validation
75
+ if (actualType === 'array') {
76
+ if (propDef.minItems && value.length < propDef.minItems) {
77
+ errors.push(`Property "${propName}" must have at least ${propDef.minItems} items`);
78
+ }
79
+ }
80
+
81
+ // Enum validation
82
+ if (propDef.enum && !propDef.enum.includes(value)) {
83
+ errors.push(`Property "${propName}" must be one of: ${propDef.enum.join(', ')}`);
84
+ }
85
+ }
86
+
87
+ if (errors.length > 0) {
88
+ throw new Error(`Invalid ${type} configuration:\n - ${errors.join('\n - ')}`);
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ // Domain key for storing live interaction responses in suspend_data
95
+ const RESPONSES_DOMAIN = 'interactionResponses';
96
+
97
+ /**
98
+ * Saves the current response state for an interaction.
99
+ * Called on every response change (selection, input, etc.) for live state tracking.
100
+ * Silently skips if stateManager is not initialized (e.g., during static validation).
101
+ * @param {string} id - The interaction ID
102
+ * @param {*} response - The current response value
103
+ * @param {boolean} submitted - Whether the interaction has been submitted (Check Answer clicked)
104
+ */
105
+ export function saveInteractionState(id, response, submitted = false) {
106
+ // Silently skip if stateManager not initialized (static validation, early creation)
107
+ if (!stateManager.isInitialized) {
108
+ return;
109
+ }
110
+ try {
111
+ const state = stateManager.getDomainState(RESPONSES_DOMAIN) || {};
112
+ state[id] = { response, submitted };
113
+ stateManager.setDomainState(RESPONSES_DOMAIN, state, { source: 'interaction-base' });
114
+ } catch (error) {
115
+ logger.warn(`[interaction-base] Failed to save interaction state for "${id}":`, error.message);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Retrieves the saved response state for an interaction.
121
+ * Used during render to restore previous selections.
122
+ * Returns null if stateManager is not initialized (e.g., during static validation).
123
+ * @param {string} id - The interaction ID
124
+ * @returns {{ response: *, submitted: boolean } | null} The saved state or null if none exists
125
+ */
126
+ export function getInteractionState(id) {
127
+ // Silently return null if stateManager not initialized (static validation, early creation)
128
+ if (!stateManager.isInitialized) {
129
+ return null;
130
+ }
131
+ try {
132
+ const state = stateManager.getDomainState(RESPONSES_DOMAIN) || {};
133
+ return state[id] || null;
134
+ } catch (error) {
135
+ logger.warn(`[interaction-base] Failed to get interaction state for "${id}":`, error.message);
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Validates that required config properties exist.
142
+ * @throws {Error} If required properties are missing
143
+ */
144
+ export function validateInteractionConfig(config, requiredProps) {
145
+ if (!config || typeof config !== 'object') {
146
+ throw new Error('Interaction config must be an object');
147
+ }
148
+
149
+ if (!config.id || typeof config.id !== 'string') {
150
+ throw new Error('Interaction must have a valid string id');
151
+ }
152
+
153
+ if (!config.prompt || typeof config.prompt !== 'string') {
154
+ throw new Error(`Interaction "${config.id}" must have a valid prompt`);
155
+ }
156
+
157
+ for (const prop of requiredProps) {
158
+ if (config[prop] === undefined) {
159
+ throw new Error(`Interaction "${config.id}" is missing required property: ${prop}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Creates a standardized interaction event handler that uses event delegation.
166
+ * Handles check-answer, reset, and custom actions.
167
+ */
168
+ export function createInteractionEventHandler(questionObj, config, customHandlers = {}) {
169
+ return function handleInteractionEvent(event) {
170
+ const actionTarget = event.target.closest('[data-action]');
171
+ if (!actionTarget) return;
172
+
173
+ const action = actionTarget.dataset.action;
174
+
175
+ // Only handle actions for this interaction
176
+ if (actionTarget.dataset.interaction !== config.id) return;
177
+
178
+ switch (action) {
179
+ case 'check-answer':
180
+ const evaluation = questionObj.checkAnswer();
181
+ if (!evaluation) return; // checkAnswer handles error display
182
+
183
+ // NEW: Track for engagement
184
+ const currentSlideId = NavigationState.getCurrentSlideId();
185
+ if (currentSlideId) {
186
+ engagementManager.trackInteraction(
187
+ currentSlideId,
188
+ config.id,
189
+ true, // completed
190
+ evaluation.correct
191
+ );
192
+ }
193
+
194
+ // Only record to InteractionManager if NOT in controlled mode
195
+ if (!config.controlled) {
196
+ recordInteractionResult(config, evaluation);
197
+ }
198
+ break;
199
+
200
+ case 'reset':
201
+ questionObj.reset();
202
+ break;
203
+
204
+ case 'show-hint':
205
+ if (questionObj.showHint) {
206
+ questionObj.showHint();
207
+ }
208
+ break;
209
+
210
+ default:
211
+ // Check for custom handlers
212
+ if (customHandlers[action]) {
213
+ customHandlers[action](event, actionTarget);
214
+ }
215
+ break;
216
+ }
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Records interaction result to InteractionManager with proper SCORM type mapping.
222
+ * Also marks the interaction as submitted in the response state.
223
+ *
224
+ * Formats the learner_response according to SCORM 2004 4th Edition requirements.
225
+ * Each interaction type has specific format requirements:
226
+ * - true-false: "true" or "false"
227
+ * - matching: "source[.]target[,]source[.]target"
228
+ * - sequencing: "item[,]item[,]item"
229
+ * - choice: "a[,]b[,]c"
230
+ * - fill-in: plain text
231
+ */
232
+ export function recordInteractionResult(config, evaluation) {
233
+ // Mark as submitted in state (for restoration purposes)
234
+ saveInteractionState(config.id, evaluation.response, true);
235
+
236
+ // Format the response according to SCORM 2004 requirements
237
+ const scormType = config.scormType || 'other';
238
+ const formattedResponse = formatLearnerResponseForScorm(scormType, evaluation.response);
239
+
240
+ // Format correct_responses using the same SCORM format as learner_response
241
+ // This is critical for types like 'matching' which require source[.]target format
242
+ let formattedCorrectResponses;
243
+ if (evaluation.correctResponses) {
244
+ formattedCorrectResponses = evaluation.correctResponses;
245
+ } else if (config.correctPattern) {
246
+ // Format the correctPattern using the same formatter as learner_response
247
+ formattedCorrectResponses = [formatLearnerResponseForScorm(scormType, config.correctPattern)];
248
+ } else {
249
+ formattedCorrectResponses = [''];
250
+ }
251
+
252
+ const interactionData = {
253
+ id: config.id,
254
+ type: scormType,
255
+ learner_response: formattedResponse,
256
+ result: evaluation.correct ? 'correct' : 'incorrect',
257
+ correct_responses: formattedCorrectResponses,
258
+ description: config.prompt
259
+ };
260
+
261
+ try {
262
+ InteractionManager.record(interactionData);
263
+ } catch (error) {
264
+ logger.error(`Failed to record interaction "${config.id}": ${error.message}`, { domain: 'interaction', operation: 'record', stack: error.stack, interactionId: config.id });
265
+ throw error; // Re-throw to prevent silent failures
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Creates standard interaction controls HTML.
271
+ * Uses utility classes for layout: .flex .flex-wrap .justify-center .gap-3
272
+ * Note: No margin needed - parent .interaction uses gap for spacing
273
+ */
274
+ export function renderInteractionControls(id, controlled = false, customButtons = []) {
275
+ if (controlled) return '';
276
+
277
+ const buttons = [
278
+ `<button type="button" class="btn btn-success" data-action="check-answer" data-interaction="${id}" data-testid="${id}-check-answer">Check Answer</button>`,
279
+ `<button type="button" class="btn btn-reset" data-action="reset" data-interaction="${id}" data-testid="${id}-reset">Reset</button>`,
280
+ ...customButtons
281
+ ];
282
+
283
+ return `<div class="flex flex-wrap justify-center gap-3" data-testid="${id}-controls">${buttons.join('')}</div>`;
284
+ }
285
+
286
+ /**
287
+ * Creates a feedback container with proper ARIA attributes.
288
+ */
289
+ export function renderFeedbackContainer(id) {
290
+ return `<div id="${id}_feedback" class="feedback" aria-live="polite" data-testid="${id}-feedback"></div>`;
291
+ }
292
+
293
+ /**
294
+ * Displays feedback in the interaction's feedback element.
295
+ */
296
+ export function displayFeedback(container, id, message, type = 'info') {
297
+ if (!container) {
298
+ throw new Error(`Cannot display feedback: container is null for interaction "${id}"`);
299
+ }
300
+
301
+ const feedbackEl = container.querySelector(`#${id}_feedback, .feedback, .overall-feedback`);
302
+ if (!feedbackEl) {
303
+ throw new Error(`Feedback element not found for interaction "${id}"`);
304
+ }
305
+
306
+ const icon = type === 'correct' ? iconManager.getIcon('check') : type === 'incorrect' ? iconManager.getIcon('x') : '';
307
+ feedbackEl.innerHTML = `<div class="feedback ${type}">${icon} ${escapeHTML(message)}</div>`;
308
+ }
309
+
310
+ /**
311
+ * Clears feedback from an interaction.
312
+ */
313
+ export function clearFeedback(container, id) {
314
+ if (!container) return;
315
+
316
+ const feedbackEl = container.querySelector(`#${id}_feedback, .feedback, .overall-feedback`);
317
+ if (feedbackEl) {
318
+ feedbackEl.innerHTML = '';
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Validates and normalizes initial response for rendering.
324
+ * Returns null if invalid/empty, or the normalized response.
325
+ */
326
+ export function normalizeInitialResponse(response) {
327
+ if (response === null || response === undefined || response === '') {
328
+ return null;
329
+ }
330
+ return response;
331
+ }
332
+
333
+ /**
334
+ * Safely escapes a string for use in a CSS selector.
335
+ * @throws {Error} If CSS.escape is not available and value contains special characters
336
+ */
337
+ export function escapeCssSelector(value) {
338
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
339
+ return CSS.escape(value);
340
+ }
341
+
342
+ // No fallback - if CSS.escape doesn't exist, throw error
343
+ throw new Error('CSS.escape is not available and is required for safe selector escaping');
344
+ }
345
+
346
+ /**
347
+ * Ensures container is valid before performing operations.
348
+ * @throws {Error} If container is null or not an Element
349
+ */
350
+ export function validateContainer(container, interactionId) {
351
+ if (!container) {
352
+ throw new Error(`Container is null for interaction "${interactionId}". Ensure render() was called first.`);
353
+ }
354
+
355
+ if (!(container instanceof Element)) {
356
+ throw new Error(`Container must be a DOM Element for interaction "${interactionId}"`);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Parses a response that could be a string, array, or object.
362
+ * Returns normalized data structure or throws error.
363
+ */
364
+ export function parseResponse(response, expectedType = 'any') {
365
+ if (response === null || response === undefined) {
366
+ return null;
367
+ }
368
+
369
+ // If it's already the expected type, return it
370
+ if (expectedType === 'array' && Array.isArray(response)) {
371
+ return response;
372
+ }
373
+
374
+ if (expectedType === 'object' && typeof response === 'object' && !Array.isArray(response)) {
375
+ return response;
376
+ }
377
+
378
+ // Try parsing string as JSON
379
+ if (typeof response === 'string') {
380
+ const trimmed = response.trim();
381
+ if (!trimmed) return null;
382
+
383
+ // Try JSON parse
384
+ try {
385
+ const parsed = JSON.parse(trimmed);
386
+ if (expectedType === 'array' && !Array.isArray(parsed)) {
387
+ throw new Error(`Expected array, got ${typeof parsed}`);
388
+ }
389
+ if (expectedType === 'object' && (typeof parsed !== 'object' || Array.isArray(parsed))) {
390
+ throw new Error(`Expected object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
391
+ }
392
+ return parsed;
393
+ } catch (parseError) {
394
+ // If JSON parse fails and we need array/object, throw error
395
+ if (expectedType === 'array' || expectedType === 'object') {
396
+ throw new Error(`Failed to parse response as JSON: ${parseError.message}`);
397
+ }
398
+ // For 'any' or 'string', return the string value
399
+ return trimmed;
400
+ }
401
+ }
402
+
403
+ return response;
404
+ }
405
+
406
+ /**
407
+ * Creates evaluation error result for invalid responses.
408
+ */
409
+ export function createInvalidEvaluation(_interactionType) {
410
+ return {
411
+ score: 0,
412
+ correct: false,
413
+ response: '',
414
+ error: 'Invalid or missing response'
415
+ };
416
+ }
417
+
418
+ // Debounce delay for auto-saving response state (ms)
419
+ const RESPONSE_SAVE_DEBOUNCE_MS = 300;
420
+
421
+ // Track debounce timers per interaction ID
422
+ const _responseDebounceTimers = new Map();
423
+
424
+ /**
425
+ * Registers an uncontrolled interaction with the InteractionRegistry.
426
+ * This is the single registration point for all standalone interactions.
427
+ * The registry tracks currently rendered interactions for engagement and automation.
428
+ * Also restores any previously saved response state and sets up auto-save on changes.
429
+ * @param {object} config - The interaction configuration.
430
+ * @param {object} questionObj - The live interaction instance.
431
+ */
432
+ export function registerCoreInteraction(config, questionObj) {
433
+ // Delegate to the InteractionRegistry (separate from persistence manager)
434
+ interactionRegistry.register(config, questionObj);
435
+
436
+ // Defer state restoration and auto-save setup to next frame
437
+ // This ensures the DOM container exists after render() completes
438
+ requestAnimationFrame(() => {
439
+ // Restore previously saved response state if it exists
440
+ const savedState = getInteractionState(config.id);
441
+ if (savedState && savedState.response !== null && savedState.response !== undefined) {
442
+ try {
443
+ if (typeof questionObj.setResponse === 'function') {
444
+ questionObj.setResponse(savedState.response);
445
+ logger.debug(`[interaction-base] Restored state for interaction "${config.id}"`);
446
+
447
+ // If it was previously submitted, also restore the feedback state
448
+ if (savedState.submitted && typeof questionObj.checkAnswer === 'function') {
449
+ questionObj.checkAnswer();
450
+ }
451
+ }
452
+ } catch (error) {
453
+ // Silently ignore - container may not exist for controlled interactions
454
+ logger.debug(`[interaction-base] Could not restore state for "${config.id}" (may be controlled):`, error.message);
455
+ }
456
+ }
457
+
458
+ // Set up auto-save on response changes (debounced)
459
+ // This listens for change/input events on the interaction container
460
+ // and saves the current response to state for restoration on re-render
461
+ _setupResponseAutoSave(config.id, questionObj);
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Sets up debounced auto-save of response state when the user interacts.
467
+ * Listens for change and input events on the interaction container.
468
+ * @private
469
+ * @param {string} id - The interaction ID
470
+ * @param {object} questionObj - The live interaction instance
471
+ */
472
+ function _setupResponseAutoSave(id, questionObj) {
473
+ const container = document.querySelector(`[data-interaction-id="${id}"]`);
474
+ if (!container) {
475
+ // Container not found - this is expected for controlled interactions (assessments)
476
+ // which manage their own state, or during static validation
477
+ return;
478
+ }
479
+
480
+ // Debounced save function
481
+ const debouncedSave = () => {
482
+ // Clear any existing timer
483
+ if (_responseDebounceTimers.has(id)) {
484
+ clearTimeout(_responseDebounceTimers.get(id));
485
+ }
486
+
487
+ // Set new timer
488
+ const timer = setTimeout(() => {
489
+ try {
490
+ if (typeof questionObj.getResponse === 'function') {
491
+ const response = questionObj.getResponse();
492
+ if (response !== null && response !== undefined) {
493
+ // Get current state to preserve submitted flag
494
+ const currentState = getInteractionState(id);
495
+ const submitted = currentState?.submitted || false;
496
+ saveInteractionState(id, response, submitted);
497
+ }
498
+ }
499
+ } catch (error) {
500
+ logger.warn(`[interaction-base] Auto-save failed for "${id}":`, error.message);
501
+ }
502
+ _responseDebounceTimers.delete(id);
503
+ }, RESPONSE_SAVE_DEBOUNCE_MS);
504
+
505
+ _responseDebounceTimers.set(id, timer);
506
+ };
507
+
508
+ // Listen for both change (radios, checkboxes, selects) and input (text fields)
509
+ container.addEventListener('change', debouncedSave);
510
+ container.addEventListener('input', debouncedSave);
511
+ }