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,388 @@
1
+ import { eventBus } from '../core/event-bus.js';
2
+ import engagementManager from '../engagement/engagement-manager.js';
3
+ import flagManager from '../managers/flag-manager.js';
4
+ import * as NavigationState from '../navigation/NavigationState.js';
5
+
6
+ /**
7
+ * Conditional Display Utility
8
+ *
9
+ * Provides both declarative (component-based) and programmatic (helper function)
10
+ * APIs for showing/hiding content based on conditions like engagement, flags,
11
+ * and interaction completion.
12
+ *
13
+ * @example Programmatic usage
14
+ * import { conditionalDisplay } from '../framework/js/utilities/conditional-display.js';
15
+ * conditionalDisplay.showWhen(element, 'engagement.viewAllTabs');
16
+ */
17
+
18
+ /**
19
+ * Parses a condition string into a structured condition object.
20
+ *
21
+ * Supported formats:
22
+ * - 'engagement.viewAllTabs'
23
+ * - 'engagement.viewAllPanels'
24
+ * - 'engagement.viewAllFlipCards'
25
+ * - 'engagement.allInteractionsComplete'
26
+ * - 'engagement.scrollDepth'
27
+ * - 'engagement.timeOnSlide'
28
+ * - 'flag.flagName'
29
+ * - 'interaction.interactionId'
30
+ *
31
+ * @param {string} conditionString - The condition string to parse
32
+ * @returns {object} Structured condition object
33
+ */
34
+ function parseCondition(conditionString) {
35
+ if (typeof conditionString !== 'string') {
36
+ throw new Error('Condition must be a string');
37
+ }
38
+
39
+ const [type, value] = conditionString.split('.');
40
+
41
+ switch (type) {
42
+ case 'engagement':
43
+ return { type: 'engagement', requirement: value };
44
+ case 'flag':
45
+ return { type: 'flag', key: value };
46
+ case 'interaction':
47
+ return { type: 'interaction', id: value };
48
+ default:
49
+ throw new Error(`Unknown condition type: ${type}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Evaluates whether a single condition is met.
55
+ *
56
+ * @param {object} condition - The condition to evaluate
57
+ * @param {string} slideId - Current slide ID
58
+ * @returns {boolean} True if condition is met
59
+ */
60
+ function evaluateCondition(condition, slideId) {
61
+ switch (condition.type) {
62
+ case 'engagement': {
63
+ const evaluation = engagementManager.evaluateRequirements(slideId);
64
+
65
+ if (condition.requirement === 'complete') {
66
+ // Check overall completion
67
+ return evaluation.complete;
68
+ }
69
+
70
+ // Check specific requirement type
71
+ const req = evaluation.unmetRequirements.find(r => r.type === condition.requirement);
72
+ return !req; // Not in unmet list = met
73
+ }
74
+
75
+ case 'flag': {
76
+ const flagValue = flagManager.getFlag(condition.key);
77
+
78
+ if (condition.equals !== undefined) {
79
+ return flagValue === condition.equals;
80
+ }
81
+
82
+ return !!flagValue; // Default: check if truthy
83
+ }
84
+
85
+ case 'interaction': {
86
+ const engagementState = engagementManager.getSlideState(slideId);
87
+ if (!engagementState) return false;
88
+
89
+ const interaction = engagementState.tracked.interactionsCompleted[condition.id];
90
+
91
+ if (condition.requireCorrect) {
92
+ return interaction?.completed && interaction?.correct;
93
+ }
94
+
95
+ return interaction?.completed || false;
96
+ }
97
+
98
+ default:
99
+ throw new Error(`[ConditionalDisplay] Unknown condition type: ${condition.type}. Valid types: engagement, flag, interaction.`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Evaluates multiple conditions based on mode (all/any).
105
+ *
106
+ * @param {array} conditions - Array of condition objects
107
+ * @param {string} mode - 'all' (AND) or 'any' (OR)
108
+ * @param {string} slideId - Current slide ID
109
+ * @returns {boolean} True if conditions are met
110
+ */
111
+ function evaluateConditions(conditions, mode, slideId) {
112
+ if (mode === 'all') {
113
+ return conditions.every(cond => evaluateCondition(cond, slideId));
114
+ } else if (mode === 'any') {
115
+ return conditions.some(cond => evaluateCondition(cond, slideId));
116
+ }
117
+
118
+ throw new Error(`Invalid mode: ${mode}. Must be 'all' or 'any'.`);
119
+ }
120
+
121
+ /**
122
+ * Shows or hides an element with optional transition.
123
+ *
124
+ * @param {HTMLElement} element - The element to show/hide
125
+ * @param {boolean} show - True to show, false to hide
126
+ * @param {object} options - Display options
127
+ */
128
+ function setElementVisibility(element, show, options = {}) {
129
+ const {
130
+ transition = true,
131
+ display = 'block',
132
+ onShow = null,
133
+ onHide = null
134
+ } = options;
135
+
136
+ // Clean up any pending transitionend handlers to prevent stale listeners
137
+ if (element._conditionalTransitionHandler) {
138
+ element.removeEventListener('transitionend', element._conditionalTransitionHandler);
139
+ element._conditionalTransitionHandler = null;
140
+ }
141
+
142
+ if (show) {
143
+ if (transition) {
144
+ element.style.display = display;
145
+ element.classList.remove('conditional-hidden');
146
+ element.classList.add('conditional-visible');
147
+ } else {
148
+ element.style.display = display;
149
+ }
150
+
151
+ if (onShow) onShow();
152
+ } else {
153
+ if (transition) {
154
+ element.classList.remove('conditional-visible');
155
+ element.classList.add('conditional-hidden');
156
+
157
+ // Wait for transition to complete before setting display:none
158
+ const handleTransitionEnd = () => {
159
+ element.style.display = 'none';
160
+ element.removeEventListener('transitionend', handleTransitionEnd);
161
+ element._conditionalTransitionHandler = null;
162
+ };
163
+
164
+ // Store the handler on the element so we can clean it up later
165
+ element._conditionalTransitionHandler = handleTransitionEnd;
166
+ element.addEventListener('transitionend', handleTransitionEnd);
167
+ } else {
168
+ element.style.display = 'none';
169
+ }
170
+
171
+ if (onHide) onHide();
172
+ }
173
+ }
174
+
175
+ /**
176
+ * ConditionalDisplay Class
177
+ *
178
+ * Manages conditional visibility of elements based on engagement, flags, and interactions.
179
+ */
180
+ class ConditionalDisplay {
181
+ constructor() {
182
+ this.trackedElements = new Map();
183
+ }
184
+
185
+ /**
186
+ * Shows an element when condition(s) are met.
187
+ *
188
+ * @param {HTMLElement} element - The element to conditionally display
189
+ * @param {string|object|array} conditions - Condition(s) to evaluate
190
+ * @param {object} options - Configuration options
191
+ * @returns {function} Cleanup function to stop tracking
192
+ *
193
+ * @example Simple string condition
194
+ * showWhen(element, 'engagement.viewAllTabs');
195
+ *
196
+ * @example Complex condition object
197
+ * showWhen(element, {
198
+ * type: 'flag',
199
+ * key: 'step1-complete',
200
+ * equals: true
201
+ * });
202
+ *
203
+ * @example Multiple conditions
204
+ * showWhen(element, [
205
+ * { type: 'engagement', requirement: 'viewAllTabs' },
206
+ * { type: 'flag', key: 'intro-complete' }
207
+ * ], { mode: 'all' });
208
+ */
209
+ showWhen(element, conditions, options = {}) {
210
+ const {
211
+ mode = 'all',
212
+ showWhen = true, // If false, inverts logic (hide when condition met)
213
+ initialCheck = true,
214
+ transition = true,
215
+ display = 'block',
216
+ onShow = null,
217
+ onHide = null
218
+ } = options;
219
+
220
+ // Parse conditions into structured format
221
+ let parsedConditions = [];
222
+
223
+ if (typeof conditions === 'string') {
224
+ parsedConditions = [parseCondition(conditions)];
225
+ } else if (Array.isArray(conditions)) {
226
+ parsedConditions = conditions.map(c =>
227
+ typeof c === 'string' ? parseCondition(c) : c
228
+ );
229
+ } else if (typeof conditions === 'object') {
230
+ parsedConditions = [conditions];
231
+ } else {
232
+ throw new Error('Invalid conditions format');
233
+ }
234
+
235
+ // Initially hide the element
236
+ element.style.display = 'none';
237
+ element.classList.add('conditional-hidden');
238
+
239
+ // Evaluation function
240
+ const checkConditions = () => {
241
+ const slideId = NavigationState.getCurrentSlideId();
242
+ if (!slideId) return;
243
+
244
+ const conditionsMet = evaluateConditions(parsedConditions, mode, slideId);
245
+ const shouldShow = showWhen ? conditionsMet : !conditionsMet;
246
+
247
+ setElementVisibility(element, shouldShow, {
248
+ transition,
249
+ display,
250
+ onShow,
251
+ onHide
252
+ });
253
+ };
254
+
255
+ // Determine which events to listen for based on condition types
256
+ const events = new Set();
257
+ parsedConditions.forEach(cond => {
258
+ switch (cond.type) {
259
+ case 'engagement':
260
+ events.add('engagement:progress');
261
+ events.add('engagement:complete');
262
+ break;
263
+ case 'flag':
264
+ events.add('flag:updated');
265
+ events.add('flag:removed');
266
+ break;
267
+ case 'interaction':
268
+ events.add('interaction:answered');
269
+ events.add('interaction:completed');
270
+ break;
271
+ }
272
+ });
273
+
274
+ // Subscribe to relevant events
275
+ const eventHandlers = [];
276
+ events.forEach(eventName => {
277
+ const handler = () => checkConditions();
278
+ eventBus.on(eventName, handler);
279
+ eventHandlers.push({ eventName, handler });
280
+ });
281
+
282
+ // Initial check
283
+ if (initialCheck) {
284
+ checkConditions();
285
+ }
286
+
287
+ // Store tracking info
288
+ const trackingId = `${Date.now()}-${Math.random()}`;
289
+ this.trackedElements.set(trackingId, {
290
+ element,
291
+ eventHandlers,
292
+ checkConditions
293
+ });
294
+
295
+ // Return cleanup function
296
+ return () => {
297
+ const tracked = this.trackedElements.get(trackingId);
298
+ if (tracked) {
299
+ tracked.eventHandlers.forEach(({ eventName, handler }) => {
300
+ eventBus.off(eventName, handler);
301
+ });
302
+ this.trackedElements.delete(trackingId);
303
+ }
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Hides an element when condition(s) are met (inverse of showWhen).
309
+ *
310
+ * @param {HTMLElement} element - The element to conditionally hide
311
+ * @param {string|object|array} conditions - Condition(s) to evaluate
312
+ * @param {object} options - Configuration options
313
+ * @returns {function} Cleanup function
314
+ */
315
+ hideWhen(element, conditions, options = {}) {
316
+ return this.showWhen(element, conditions, {
317
+ ...options,
318
+ showWhen: false
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Immediately evaluates conditions and updates element visibility.
324
+ * Does not set up ongoing tracking.
325
+ *
326
+ * @param {HTMLElement} element - The element to update
327
+ * @param {string|object|array} conditions - Condition(s) to evaluate
328
+ * @param {object} options - Configuration options
329
+ * @returns {boolean} Whether conditions were met
330
+ */
331
+ evaluate(element, conditions, options = {}) {
332
+ const {
333
+ mode = 'all',
334
+ showWhen = true,
335
+ transition = false,
336
+ display = 'block',
337
+ onShow = null,
338
+ onHide = null
339
+ } = options;
340
+
341
+ // Parse conditions
342
+ let parsedConditions = [];
343
+
344
+ if (typeof conditions === 'string') {
345
+ parsedConditions = [parseCondition(conditions)];
346
+ } else if (Array.isArray(conditions)) {
347
+ parsedConditions = conditions.map(c =>
348
+ typeof c === 'string' ? parseCondition(c) : c
349
+ );
350
+ } else {
351
+ parsedConditions = [conditions];
352
+ }
353
+
354
+ const slideId = NavigationState.getCurrentSlideId();
355
+ if (!slideId) return false;
356
+
357
+ const conditionsMet = evaluateConditions(parsedConditions, mode, slideId);
358
+ const shouldShow = showWhen ? conditionsMet : !conditionsMet;
359
+
360
+ setElementVisibility(element, shouldShow, {
361
+ transition,
362
+ display,
363
+ onShow,
364
+ onHide
365
+ });
366
+
367
+ return conditionsMet;
368
+ }
369
+
370
+ /**
371
+ * Stops tracking all elements.
372
+ * Useful for cleanup when navigating away from a slide.
373
+ */
374
+ cleanup() {
375
+ this.trackedElements.forEach((tracked) => {
376
+ tracked.eventHandlers.forEach(({ eventName, handler }) => {
377
+ eventBus.off(eventName, handler);
378
+ });
379
+ });
380
+ this.trackedElements.clear();
381
+ }
382
+ }
383
+
384
+ // Export singleton instance
385
+ export const conditionalDisplay = new ConditionalDisplay();
386
+
387
+ // Also export helper functions for direct use
388
+ export { parseCondition, evaluateCondition, evaluateConditions };
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Course Channel - Generic pub/sub transport for course-to-course communication
3
+ *
4
+ * Content-agnostic message relay. The framework provides only the pipe:
5
+ * send, receive, reconnect. Message interpretation is left to consumer code.
6
+ *
7
+ * Configuration in course-config.js:
8
+ * environment: {
9
+ * channel: {
10
+ * endpoint: 'https://relay.example.com',
11
+ * channelId: 'my-session-123'
12
+ * }
13
+ * }
14
+ *
15
+ * Sending:
16
+ * import { sendChannelMessage } from './utilities/course-channel.js';
17
+ * sendChannelMessage({ type: 'navigate', slideId: 'slide-03' });
18
+ *
19
+ * Receiving (via EventBus):
20
+ * eventBus.on('channel:message', (data) => { ... });
21
+ */
22
+
23
+ import { eventBus } from '../core/event-bus.js';
24
+ import { logger } from './logger.js';
25
+
26
+ // Connection state
27
+ let _config = null;
28
+ let _eventSource = null;
29
+ let _reconnectTimer = null;
30
+ let _reconnectDelay = 1000;
31
+
32
+ const MAX_RECONNECT_DELAY = 30000;
33
+ const RECONNECT_BACKOFF = 2;
34
+
35
+ /**
36
+ * Build the full URL for the channel endpoint
37
+ */
38
+ function getChannelUrl() {
39
+ const base = _config.endpoint.replace(/\/+$/, '');
40
+ return `${base}/${encodeURIComponent(_config.channelId)}`;
41
+ }
42
+
43
+ /**
44
+ * Send a message to the channel
45
+ * @param {Object} data - Any JSON-serializable payload
46
+ */
47
+ export async function sendChannelMessage(data) {
48
+ if (!_config?.endpoint || !_config?.channelId) {
49
+ logger.warn('[CourseChannel] Cannot send — channel not configured');
50
+ return;
51
+ }
52
+
53
+ const url = getChannelUrl();
54
+
55
+ try {
56
+ const headers = { 'Content-Type': 'application/json' };
57
+ if (_config.apiKey) headers['Authorization'] = `Bearer ${_config.apiKey}`;
58
+
59
+ const response = await fetch(url, {
60
+ method: 'POST',
61
+ headers,
62
+ body: JSON.stringify(data)
63
+ });
64
+
65
+ if (!response.ok) {
66
+ logger.warn('[CourseChannel] Send failed:', response.status);
67
+ }
68
+ } catch (e) {
69
+ logger.warn('[CourseChannel] Network error sending:', e.message);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Check if the channel is currently connected (SSE stream open)
75
+ * @returns {boolean}
76
+ */
77
+ export function isChannelConnected() {
78
+ return _eventSource?.readyState === EventSource.OPEN;
79
+ }
80
+
81
+ /**
82
+ * Connect the SSE listener for incoming messages
83
+ */
84
+ function connect() {
85
+ if (_eventSource) {
86
+ _eventSource.close();
87
+ }
88
+
89
+ let url = getChannelUrl();
90
+ // EventSource doesn't support custom headers, so pass token as URL param
91
+ if (_config.apiKey) {
92
+ const sep = url.includes('?') ? '&' : '?';
93
+ url += `${sep}token=${encodeURIComponent(_config.apiKey)}`;
94
+ }
95
+ logger.debug('[CourseChannel] Connecting to:', url);
96
+
97
+ _eventSource = new EventSource(url);
98
+
99
+ _eventSource.onopen = () => {
100
+ _reconnectDelay = 1000; // Reset backoff on successful connect
101
+ logger.debug('[CourseChannel] Connected');
102
+ eventBus.emit('channel:connected');
103
+ };
104
+
105
+ _eventSource.onmessage = (event) => {
106
+ try {
107
+ const data = JSON.parse(event.data);
108
+ eventBus.emit('channel:message', data);
109
+ } catch (e) {
110
+ logger.warn('[CourseChannel] Failed to parse message:', e.message);
111
+ }
112
+ };
113
+
114
+ _eventSource.onerror = () => {
115
+ // EventSource auto-reconnects, but if it closes we handle it
116
+ if (_eventSource.readyState === EventSource.CLOSED) {
117
+ logger.warn('[CourseChannel] Connection closed, reconnecting...');
118
+ eventBus.emit('channel:disconnected');
119
+ scheduleReconnect();
120
+ }
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Schedule a reconnection with exponential backoff
126
+ */
127
+ function scheduleReconnect() {
128
+ if (_reconnectTimer) return;
129
+
130
+ _reconnectTimer = setTimeout(() => {
131
+ _reconnectTimer = null;
132
+ connect();
133
+ }, _reconnectDelay);
134
+
135
+ logger.debug(`[CourseChannel] Reconnecting in ${_reconnectDelay}ms`);
136
+ _reconnectDelay = Math.min(_reconnectDelay * RECONNECT_BACKOFF, MAX_RECONNECT_DELAY);
137
+ }
138
+
139
+ /**
140
+ * Emergency send via sendBeacon (for page unload)
141
+ */
142
+ function emergencySend(data) {
143
+ if (!_config?.endpoint || !_config?.channelId) return;
144
+
145
+ const url = getChannelUrl();
146
+ const body = JSON.stringify(data);
147
+
148
+ // When apiKey is configured, use fetch+keepalive to include auth header
149
+ // (sendBeacon doesn't support custom headers)
150
+ if (_config.apiKey) {
151
+ try {
152
+ fetch(url, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': `Bearer ${_config.apiKey}`
157
+ },
158
+ body,
159
+ keepalive: true
160
+ });
161
+ } catch {
162
+ // Silent fail on unload
163
+ }
164
+ } else {
165
+ const blob = new Blob([body], { type: 'application/json' });
166
+ navigator.sendBeacon(url, blob);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Clean up on page unload
172
+ */
173
+ function handleUnload() {
174
+ if (_eventSource) {
175
+ _eventSource.close();
176
+ _eventSource = null;
177
+ }
178
+ if (_reconnectTimer) {
179
+ clearTimeout(_reconnectTimer);
180
+ _reconnectTimer = null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Initialize the course channel if configured
186
+ * @param {Object} courseConfig - The course configuration object
187
+ */
188
+ export function initCourseChannel(courseConfig) {
189
+ const config = courseConfig.environment?.channel;
190
+
191
+ _config = config;
192
+
193
+ if (!config?.endpoint || !config?.channelId) {
194
+ logger.debug('[CourseChannel] Not configured, skipping initialization');
195
+ return;
196
+ }
197
+
198
+ logger.info('[CourseChannel] Initialized — channel:', config.channelId);
199
+
200
+ // Open SSE listener
201
+ connect();
202
+
203
+ // Expose emergency send for consumer code that needs unload-safe delivery
204
+ window.addEventListener('pagehide', handleUnload);
205
+
206
+ // Expose sendChannelMessage globally for course-author convenience
207
+ if (window.CourseCode) {
208
+ window.CourseCode.sendChannelMessage = sendChannelMessage;
209
+ window.CourseCode.isChannelConnected = isChannelConnected;
210
+ }
211
+ }
212
+
213
+ // Re-export emergencySend for advanced consumers
214
+ export { emergencySend };