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,112 @@
1
+ /**
2
+ * @file scorm-driver-base.js
3
+ * @description Base class for pipwerks-based SCORM drivers (2004, 1.2).
4
+ * Consolidates shared pipwerks initialization, connection state, and utilities.
5
+ *
6
+ * Subclasses must implement:
7
+ * - getFormat()
8
+ * - getCapabilities()
9
+ * - initialize() (calls _initPipwerks)
10
+ * - terminate()
11
+ * - commit()
12
+ * - ping()
13
+ * - _populateCache()
14
+ * - All semantic reads/writes
15
+ * - getSuspendData() / setSuspendData()
16
+ */
17
+
18
+ import { logger } from '../utilities/logger.js';
19
+
20
+ export class ScormDriverBase {
21
+ constructor() {
22
+ this._isConnected = false;
23
+ this._isTerminated = false;
24
+ this._scorm = null; // Initialized lazily via _initPipwerks()
25
+ }
26
+
27
+ // =========================================================================
28
+ // Interface: Connection State
29
+ // =========================================================================
30
+
31
+ isConnected() {
32
+ return this._isConnected;
33
+ }
34
+
35
+ isTerminated() {
36
+ return this._isTerminated;
37
+ }
38
+
39
+ // =========================================================================
40
+ // Shared Initialization
41
+ // =========================================================================
42
+
43
+ /**
44
+ * Dynamically imports pipwerks and configures it for the specified SCORM version.
45
+ * Call this from the subclass initialize() method.
46
+ * @param {'2004'|'1.2'} version - SCORM version to force
47
+ * @returns {Promise<void>}
48
+ */
49
+ async _initPipwerks(version) {
50
+ const pipwerksModule = await import('../vendor/pipwerks.js');
51
+ const pipwerks = pipwerksModule.default;
52
+
53
+ this._scorm = pipwerks.SCORM;
54
+ this._scorm.version = version;
55
+
56
+ // Disable pipwerks auto-handling — we manage status ourselves
57
+ this._scorm.handleCompletionStatus = false;
58
+ this._scorm.handleExitMode = false;
59
+ }
60
+
61
+ // =========================================================================
62
+ // Shared Utilities
63
+ // =========================================================================
64
+
65
+ _ensureInitialized() {
66
+ if (!this._isConnected) {
67
+ throw new Error(`${this.getFormat()} not initialized. Call initialize() first.`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Terminates the SCORM connection via pipwerks quit().
73
+ * Used directly by SCORM 1.2; overridden by SCORM 2004 for error details.
74
+ */
75
+ async terminate() {
76
+ if (!this._isConnected || this._isTerminated) {
77
+ return true;
78
+ }
79
+
80
+ const success = this._scorm.quit();
81
+ this._isTerminated = success;
82
+
83
+ if (!success) {
84
+ throw new Error(`[${this.constructor.name}] SCORM termination failed`);
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Commits buffered writes via pipwerks save().
92
+ * SCORM 2004 overrides this for recovery mode support.
93
+ */
94
+ async commit() {
95
+ this._ensureInitialized();
96
+
97
+ if (this._isTerminated) {
98
+ if (import.meta.env.DEV) {
99
+ logger.warn(`[${this.constructor.name}] Ignoring commit() - SCORM session already terminated`);
100
+ }
101
+ return false;
102
+ }
103
+
104
+ const success = this._scorm.save();
105
+
106
+ if (!success) {
107
+ throw new Error(`[${this.constructor.name}] SCORM commit failed`);
108
+ }
109
+
110
+ return true;
111
+ }
112
+ }
@@ -0,0 +1,404 @@
1
+ /**
2
+ * @file engagement-manager.js
3
+ * @description Tracks user engagement with slide content to gate navigation.
4
+ *
5
+ * Stateless manager — all state is stored in StateManager's 'engagement' domain.
6
+ * Pure progress/formatting functions are in engagement-progress.js.
7
+ * Requirement evaluation strategies are in requirement-strategies.js.
8
+ * Component registration and tracking methods are in engagement-trackers.js.
9
+ *
10
+ * @version 2.2.0
11
+ */
12
+
13
+ import { eventBus } from '../core/event-bus.js';
14
+ import { logger } from '../utilities/logger.js';
15
+ import stateManager from '../state/index.js';
16
+ import * as NavigationState from '../navigation/NavigationState.js';
17
+ import strategies, { validTypes, getTrackedFieldDefaults } from './requirement-strategies.js';
18
+ import {
19
+ calculateProgress,
20
+ formatTimeHuman,
21
+ formatInteractionId,
22
+ mergeWithDefaults,
23
+ stripDefaultValues
24
+ } from './engagement-progress.js';
25
+ import * as trackers from './engagement-trackers.js';
26
+
27
+ class EngagementManager {
28
+ constructor() {
29
+ this.domain = 'engagement';
30
+ this.isInitialized = false;
31
+ this.courseConfig = null;
32
+ this._timeTrackingIntervals = new Map();
33
+ }
34
+
35
+ /**
36
+ * Subscribes to events needed for dynamic tracking.
37
+ * @param {object} courseConfig - The course configuration object
38
+ */
39
+ initialize(courseConfig) {
40
+ if (this.isInitialized) return;
41
+
42
+ if (!courseConfig || !courseConfig.structure) {
43
+ throw new Error('[EngagementManager] courseConfig with structure is REQUIRED');
44
+ }
45
+
46
+ this.courseConfig = courseConfig;
47
+
48
+ eventBus.on('flag:updated', this._handleFlagUpdate.bind(this));
49
+ eventBus.on('flag:removed', this._handleFlagUpdate.bind(this));
50
+
51
+ eventBus.on('interaction:recorded', (interaction) => {
52
+ const currentSlideId = NavigationState.getCurrentSlideId();
53
+ if (currentSlideId) {
54
+ const isCorrect = interaction.result === 'correct';
55
+ this.trackInteraction(currentSlideId, interaction.id, true, isCorrect);
56
+ }
57
+ });
58
+
59
+ this.isInitialized = true;
60
+ logger.debug('[EngagementManager] Initialized (refactored v2.2)');
61
+ }
62
+
63
+ // =========================================================================
64
+ // Slide Lifecycle
65
+ // =========================================================================
66
+
67
+ /**
68
+ * Gets engagement requirements for a slide from course config.
69
+ * @private
70
+ */
71
+ _getRequirementsFromConfig(slideId) {
72
+ if (!this.courseConfig || !this.courseConfig.structure) {
73
+ throw new Error(`[EngagementManager] CRITICAL: courseConfig not available when looking up slide: ${slideId}`);
74
+ }
75
+
76
+ const findSlide = (items) => {
77
+ for (const item of items) {
78
+ if (item.id === slideId) return item;
79
+ if (item.children) {
80
+ const found = findSlide(item.children);
81
+ if (found) return found;
82
+ }
83
+ }
84
+ return null;
85
+ };
86
+
87
+ const slide = findSlide(this.courseConfig.structure);
88
+ if (!slide) {
89
+ throw new Error(`[EngagementManager] CRITICAL: Slide "${slideId}" not found in courseConfig.structure`);
90
+ }
91
+
92
+ if (!slide.engagement) {
93
+ throw new Error(`[EngagementManager] CRITICAL: Slide "${slideId}" has no engagement config in courseConfig`);
94
+ }
95
+
96
+ return slide.engagement;
97
+ }
98
+
99
+ /**
100
+ * Initializes engagement tracking for a slide.
101
+ * Preserves existing completion state if slide was already completed.
102
+ */
103
+ initSlide(slideId, requirements) {
104
+ if (!slideId || typeof slideId !== 'string') {
105
+ throw new Error('[EngagementManager] slideId must be a non-empty string');
106
+ }
107
+
108
+ if (!requirements || typeof requirements !== 'object') {
109
+ throw new Error(`[EngagementManager] Slide "${slideId}" has invalid engagement requirements`);
110
+ }
111
+
112
+ const isRequired = requirements.required || false;
113
+
114
+ if (!isRequired) {
115
+ logger.debug(`[EngagementManager] Slide "${slideId}" does not require tracking, skipping state initialization`);
116
+ eventBus.emit('engagement:initialized', { slideId, requirements });
117
+ return;
118
+ }
119
+
120
+ const state = this._getState();
121
+ const existingState = state[slideId];
122
+ const wasCompleted = existingState?.complete === true;
123
+
124
+ if (wasCompleted) {
125
+ state[slideId].required = true;
126
+ logger.debug(`[EngagementManager] Slide "${slideId}" was already completed`);
127
+ } else if (existingState) {
128
+ state[slideId].required = true;
129
+ logger.debug(`[EngagementManager] Slide "${slideId}" has existing progress, preserving tracked data`);
130
+ } else {
131
+ state[slideId] = {
132
+ required: true,
133
+ tracked: getTrackedFieldDefaults(),
134
+ complete: false
135
+ };
136
+ logger.debug(`[EngagementManager] Initialized tracking for: ${slideId}`);
137
+ }
138
+
139
+ this._setState(state);
140
+ this._startTimeTrackingIfNeeded(slideId, requirements);
141
+ eventBus.emit('engagement:initialized', { slideId, requirements });
142
+ }
143
+
144
+ /**
145
+ * Cleans up engagement tracking when leaving a slide.
146
+ */
147
+ cleanupSlide(slideId) {
148
+ if (!slideId) return;
149
+
150
+ this._stopTimeTracking(slideId);
151
+
152
+ const evaluation = this.evaluateRequirements(slideId);
153
+
154
+ if (!evaluation.complete) {
155
+ eventBus.emit('engagement:incomplete', {
156
+ slideId,
157
+ unmetRequirements: evaluation.unmetRequirements
158
+ });
159
+ }
160
+
161
+ logger.debug(`[EngagementManager] Cleaned up: ${slideId}`, evaluation);
162
+ }
163
+
164
+ // =========================================================================
165
+ // Time Tracking
166
+ // =========================================================================
167
+
168
+ /** @private */
169
+ _startTimeTrackingIfNeeded(slideId, requirements) {
170
+ this._stopTimeTracking(slideId);
171
+
172
+ if (!requirements.required) return;
173
+
174
+ const hasTimeRequirement = requirements.requirements?.some(
175
+ req => req.type === 'timeOnSlide'
176
+ );
177
+
178
+ if (!hasTimeRequirement) return;
179
+
180
+ const state = this._getState();
181
+ if (state[slideId]?.complete) {
182
+ logger.debug(`[EngagementManager] Slide "${slideId}" already complete, skipping time tracking`);
183
+ return;
184
+ }
185
+
186
+ logger.debug(`[EngagementManager] Starting time tracking for: ${slideId}`);
187
+
188
+ const intervalId = setInterval(() => {
189
+ const evaluation = this.evaluateRequirements(slideId);
190
+ if (!evaluation.complete) {
191
+ eventBus.emit('engagement:progress', {
192
+ slideId,
193
+ complete: false,
194
+ progress: evaluation.progress
195
+ });
196
+ } else {
197
+ this._stopTimeTracking(slideId);
198
+ }
199
+ }, 100);
200
+
201
+ this._timeTrackingIntervals.set(slideId, intervalId);
202
+ }
203
+
204
+ /** @private */
205
+ _stopTimeTracking(slideId) {
206
+ const intervalId = this._timeTrackingIntervals.get(slideId);
207
+ if (intervalId) {
208
+ clearInterval(intervalId);
209
+ this._timeTrackingIntervals.delete(slideId);
210
+ logger.debug(`[EngagementManager] Stopped time tracking for: ${slideId}`);
211
+ }
212
+ }
213
+
214
+ // =========================================================================
215
+ // Queries
216
+ // =========================================================================
217
+
218
+ isSlideComplete(slideId) {
219
+ return this.evaluateRequirements(slideId).complete;
220
+ }
221
+
222
+ getSlideState(slideId) {
223
+ const state = this._getState();
224
+ return state[slideId] || null;
225
+ }
226
+
227
+ getProgress(slideId) {
228
+ const slideState = this.getSlideState(slideId);
229
+ if (!slideState) return null;
230
+ const requirementsConfig = this._getRequirementsFromConfig(slideId);
231
+ const requirements = requirementsConfig?.requirements || [];
232
+ return calculateProgress(slideId, slideState.tracked, requirements, strategies, this._buildContext(slideId));
233
+ }
234
+
235
+ // =========================================================================
236
+ // Evaluation
237
+ // =========================================================================
238
+
239
+ evaluateRequirements(slideId) {
240
+ const state = this._getState();
241
+ const slideState = state[slideId];
242
+
243
+ if (!slideState) {
244
+ try {
245
+ const requirementsConfig = this._getRequirementsFromConfig(slideId);
246
+ const isRequired = requirementsConfig?.required || false;
247
+ return { complete: !isRequired, progress: {}, unmetRequirements: [] };
248
+ } catch (_error) {
249
+ return { complete: true, progress: {}, unmetRequirements: [] };
250
+ }
251
+ }
252
+
253
+ const isRequired = slideState.required || false;
254
+ if (!isRequired) {
255
+ return { complete: true, progress: {}, unmetRequirements: [] };
256
+ }
257
+
258
+ const requirementsConfig = this._getRequirementsFromConfig(slideId);
259
+ const requirements = requirementsConfig.requirements || [];
260
+ const mode = requirementsConfig.mode || 'all';
261
+
262
+ const unmetRequirements = [];
263
+
264
+ for (const req of requirements) {
265
+ try {
266
+ const result = this._evaluateRequirement(slideId, req, slideState.tracked);
267
+ if (!result.met) {
268
+ unmetRequirements.push(result);
269
+ }
270
+ } catch (error) {
271
+ logger.error(`[EngagementManager] Error evaluating requirement for slide "${slideId}": ${error.message}`, { requirement: req });
272
+ // Fail safe: treat as unmet requirement to prevent skipping
273
+ unmetRequirements.push({
274
+ met: false,
275
+ requirement: req,
276
+ progress: 0,
277
+ reason: `Evaluation Error: ${error.message}`
278
+ });
279
+ }
280
+ }
281
+
282
+ const complete = mode === 'all'
283
+ ? unmetRequirements.length === 0
284
+ : unmetRequirements.length < requirements.length;
285
+
286
+ if (complete && !slideState.complete) {
287
+ slideState.complete = true;
288
+ this._setState(state);
289
+ eventBus.emit('engagement:complete', { slideId });
290
+ }
291
+
292
+ let progress = {};
293
+ try {
294
+ progress = calculateProgress(slideId, slideState.tracked, requirements, strategies, this._buildContext(slideId));
295
+ } catch (error) {
296
+ logger.error(`[EngagementManager] Error calculating progress for slide "${slideId}": ${error.message}`);
297
+ }
298
+
299
+ return {
300
+ complete,
301
+ progress,
302
+ unmetRequirements
303
+ };
304
+ }
305
+
306
+ /** @private */
307
+ _evaluateRequirement(slideId, requirement, tracked) {
308
+ const strategy = strategies[requirement.type];
309
+ if (!strategy) {
310
+ throw new Error(`[EngagementManager] Unknown requirement type: ${requirement.type}. Valid types: ${validTypes.join(', ')}.`);
311
+ }
312
+ return strategy.evaluate(requirement, tracked, this._buildContext(slideId));
313
+ }
314
+
315
+ /** @private */
316
+ _buildContext(slideId) {
317
+ return {
318
+ slideId,
319
+ stateManager,
320
+ interactionRegistry: window.CourseCode?.interactionRegistry,
321
+ formatTime: formatTimeHuman,
322
+ formatInteractionId: formatInteractionId
323
+ };
324
+ }
325
+
326
+ // =========================================================================
327
+ // Reset
328
+ // =========================================================================
329
+
330
+ resetSlide(slideId) {
331
+ const state = this._getState();
332
+ if (!state[slideId]) return;
333
+ const requirements = this._getRequirementsFromConfig(slideId);
334
+ if (!requirements) {
335
+ throw new Error(`[EngagementManager] No requirements found for slide: ${slideId}. Ensure slide has engagement config in course-config.js.`);
336
+ }
337
+ delete state[slideId];
338
+ this._setState(state);
339
+ this.initSlide(slideId, requirements);
340
+ logger.debug(`[EngagementManager] Reset: ${slideId}`);
341
+ }
342
+
343
+ resetAllSlides() {
344
+ this._setState({});
345
+ logger.debug('[EngagementManager] Reset all engagement');
346
+ eventBus.emit('engagement:reset-all');
347
+ }
348
+
349
+ // =========================================================================
350
+ // Internal
351
+ // =========================================================================
352
+
353
+ /** @private */
354
+ _handleFlagUpdate({ key, value: _value }) {
355
+ const state = this._getState();
356
+
357
+ Object.entries(state).forEach(([slideId, slideState]) => {
358
+ const requirementsConfig = this._getRequirementsFromConfig(slideId);
359
+ if (!requirementsConfig || !requirementsConfig.required) return;
360
+ if (slideState.complete) return;
361
+
362
+ const hasFlagRequirements = requirementsConfig.requirements?.some(
363
+ req => req.type === 'flag' || req.type === 'allFlags'
364
+ );
365
+
366
+ if (hasFlagRequirements) {
367
+ logger.debug(`[EngagementManager] Flag "${key}" updated, re-evaluating: ${slideId}`);
368
+ this._checkAndEmitProgress(slideId);
369
+ }
370
+ });
371
+ }
372
+
373
+ /** @private */
374
+ _checkAndEmitProgress(slideId) {
375
+ const evaluation = this.evaluateRequirements(slideId);
376
+ eventBus.emit('engagement:progress', {
377
+ slideId,
378
+ complete: evaluation.complete,
379
+ progress: evaluation.progress
380
+ });
381
+ }
382
+
383
+ /** @private */
384
+ _getState() {
385
+ const rawState = stateManager.getDomainState(this.domain) || {};
386
+ return mergeWithDefaults(rawState);
387
+ }
388
+
389
+ /** @private */
390
+ _setState(state) {
391
+ try {
392
+ const optimizedState = stripDefaultValues(state);
393
+ stateManager.setDomainState(this.domain, optimizedState);
394
+ } catch (error) {
395
+ logger.error('[EngagementManager] Failed to save state:', { domain: 'engagement', operation: '_setState', stack: error.stack, slideIds: Object.keys(state) });
396
+ throw error;
397
+ }
398
+ }
399
+ }
400
+
401
+ // Mix in tracker methods from engagement-trackers.js
402
+ Object.assign(EngagementManager.prototype, trackers);
403
+
404
+ export default new EngagementManager();
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @file engagement-progress.js
3
+ * @description Pure functions for calculating engagement progress,
4
+ * building tooltips, and serializing/deserializing engagement state.
5
+ * No side effects or state manager dependencies.
6
+ *
7
+ * Tracked field defaults are derived from strategy declarations in
8
+ * requirement-strategies.js — no hardcoded field lists here.
9
+ */
10
+
11
+ import { getTrackedFieldDefaults } from './requirement-strategies.js';
12
+
13
+ /**
14
+ * Calculates progress with partial completion support.
15
+ * @param {string} slideId - The slide identifier
16
+ * @param {object} tracked - The tracked data
17
+ * @param {array} requirements - The requirements array
18
+ * @param {object} strategies - The requirement strategies map
19
+ * @param {object} ctx - The strategy context
20
+ * @returns {object} Progress summary with percentage, items, and tooltip
21
+ */
22
+ export function calculateProgress(slideId, tracked, requirements, strategies, ctx) {
23
+ const items = [];
24
+ let totalProgress = 0;
25
+
26
+ for (const req of requirements) {
27
+ const strategy = strategies[req.type];
28
+ const result = strategy.evaluate(req, tracked, ctx);
29
+ const requirementProgress = result.met ? 1 : strategy.progress(req, tracked, result, ctx);
30
+ const dynamicLabel = req.message || strategy.label(req, tracked, result, ctx);
31
+
32
+ items.push({
33
+ type: req.type,
34
+ label: dynamicLabel,
35
+ complete: result.met
36
+ });
37
+
38
+ totalProgress += requirementProgress;
39
+ }
40
+
41
+ const percentage = requirements.length > 0
42
+ ? Math.round((totalProgress / requirements.length) * 100)
43
+ : 100;
44
+
45
+ const tooltip = buildTooltip(items, percentage);
46
+
47
+ return { percentage, items, tooltip };
48
+ }
49
+
50
+ /**
51
+ * Builds a tooltip string from progress items.
52
+ * @param {array} items - Progress items with labels and completion status
53
+ * @param {number} percentage - Completion percentage
54
+ * @returns {string} Tooltip text
55
+ */
56
+ export function buildTooltip(items, percentage) {
57
+ if (!items || items.length === 0) {
58
+ return `Slide Progress: ${percentage}%`;
59
+ }
60
+
61
+ if (percentage === 100) {
62
+ return 'All requirements complete';
63
+ }
64
+
65
+ const incomplete = items.filter(item => !item.complete);
66
+ if (incomplete.length === 0) {
67
+ return 'All requirements complete';
68
+ }
69
+
70
+ const labels = incomplete.map(item => item.label);
71
+
72
+ if (labels.length === 1) {
73
+ return labels[0];
74
+ } else if (labels.length === 2) {
75
+ return `${labels[0]} and ${labels[1]}`;
76
+ } else {
77
+ return `${labels.slice(0, -1).join(', ')}, and ${labels.at(-1)}`;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Formats seconds into a human-friendly string.
83
+ * @param {number} seconds - Time in seconds
84
+ * @returns {string} Human-readable time string (e.g., "2 minutes", "45 seconds")
85
+ */
86
+ export function formatTimeHuman(seconds) {
87
+ if (seconds < 60) {
88
+ return `${seconds} second${seconds === 1 ? '' : 's'}`;
89
+ }
90
+ const minutes = Math.floor(seconds / 60);
91
+ const remainingSeconds = seconds % 60;
92
+ if (remainingSeconds === 0) {
93
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
94
+ }
95
+ return `${minutes}m ${remainingSeconds}s`;
96
+ }
97
+
98
+ /**
99
+ * Formats an interaction ID into a human-readable label.
100
+ * Converts kebab-case IDs like "system-architecture-dd" to "System Architecture Dd".
101
+ * @param {string} interactionId - The interaction identifier
102
+ * @returns {string} Human-readable label
103
+ */
104
+ export function formatInteractionId(interactionId) {
105
+ if (!interactionId) return 'interaction';
106
+
107
+ return interactionId
108
+ .split(/[-_]/)
109
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
110
+ .join(' ');
111
+ }
112
+
113
+ /**
114
+ * Merges stored engagement state with default values.
115
+ * Defaults are derived from strategy field declarations — no hardcoded list.
116
+ * @param {object} state - The raw state from storage
117
+ * @returns {object} State with defaults filled in
118
+ */
119
+ export function mergeWithDefaults(state) {
120
+ const fieldDefaults = getTrackedFieldDefaults();
121
+ const merged = {};
122
+
123
+ for (const [slideId, slideState] of Object.entries(state)) {
124
+ const tracked = {};
125
+ for (const [key, defaultVal] of Object.entries(fieldDefaults)) {
126
+ const stored = slideState.tracked?.[key];
127
+ if (stored !== undefined && stored !== null) {
128
+ tracked[key] = stored;
129
+ } else {
130
+ // Deep-clone defaults so slides don't share array/object references
131
+ tracked[key] = Array.isArray(defaultVal) ? [] :
132
+ (typeof defaultVal === 'object' && defaultVal !== null) ? { ...defaultVal } :
133
+ defaultVal;
134
+ }
135
+ }
136
+ merged[slideId] = {
137
+ required: slideState.required || false,
138
+ tracked,
139
+ complete: slideState.complete || false
140
+ };
141
+ }
142
+
143
+ return merged;
144
+ }
145
+
146
+ /**
147
+ * Strips default values from engagement state to reduce storage size.
148
+ * Uses strategy field declarations to determine what constitutes a "default".
149
+ * @param {object} state - The full engagement state
150
+ * @returns {object} Optimized state with defaults removed
151
+ */
152
+ export function stripDefaultValues(state) {
153
+ const fieldDefaults = getTrackedFieldDefaults();
154
+ const optimized = {};
155
+
156
+ for (const [slideId, slideState] of Object.entries(state)) {
157
+ const optimizedSlide = {
158
+ required: slideState.required
159
+ };
160
+
161
+ if (slideState.tracked) {
162
+ const tracked = {};
163
+
164
+ for (const [key, defaultVal] of Object.entries(fieldDefaults)) {
165
+ const val = slideState.tracked[key];
166
+ if (val === undefined || val === null) continue;
167
+
168
+ // Only include if value differs from default
169
+ if (Array.isArray(defaultVal)) {
170
+ if (val.length > 0) tracked[key] = val;
171
+ } else if (typeof defaultVal === 'object' && defaultVal !== null) {
172
+ if (Object.keys(val).length > 0) tracked[key] = val;
173
+ } else if (typeof defaultVal === 'boolean') {
174
+ if (val) tracked[key] = val;
175
+ } else if (typeof defaultVal === 'number') {
176
+ if (val > 0) tracked[key] = val;
177
+ }
178
+ }
179
+
180
+ if (Object.keys(tracked).length > 0) {
181
+ optimizedSlide.tracked = tracked;
182
+ }
183
+ }
184
+
185
+ optimizedSlide.complete = slideState.complete || false;
186
+
187
+ optimized[slideId] = optimizedSlide;
188
+ }
189
+
190
+ return optimized;
191
+ }