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,146 @@
1
+ /**
2
+ * @file ui-initializer.js
3
+ * @description Automatically initializes UI components based on data-attributes.
4
+ * Uses the component catalog for dynamic discovery — no hardcoded component list.
5
+ */
6
+
7
+ import { logger } from './logger.js';
8
+ import { getComponentInit, getComponentStyles, isComponentRegistered, getRegisteredComponentTypes } from '../core/component-catalog.js';
9
+ import { init as initNotificationTriggers } from '../components/ui-components/notifications.js';
10
+
11
+
12
+ import engagementManager from '../engagement/engagement-manager.js';
13
+ import * as NavigationState from '../navigation/NavigationState.js';
14
+
15
+ // Track which custom component styles have been injected
16
+ const injectedStyles = new Set();
17
+
18
+ /**
19
+ * Inject custom component styles into the document head
20
+ * @param {string} type - Component type
21
+ * @param {string} styles - CSS string
22
+ */
23
+ function injectStyles(type, styles) {
24
+ if (injectedStyles.has(type) || !styles) return;
25
+
26
+ const styleEl = document.createElement('style');
27
+ styleEl.setAttribute('data-component-styles', type);
28
+ styleEl.textContent = styles;
29
+ document.head.appendChild(styleEl);
30
+ injectedStyles.add(type);
31
+ }
32
+
33
+ /**
34
+ * Scans a container element for declarative UI components and initializes them.
35
+ * @param {HTMLElement} container - The container element to scan.
36
+ */
37
+ export function initializeDeclarativeComponents(container) {
38
+ if (!container || typeof container.querySelectorAll !== 'function') {
39
+ return;
40
+ }
41
+
42
+ const components = container.querySelectorAll('[data-component]');
43
+
44
+ components.forEach(element => {
45
+ const componentName = element.dataset.component;
46
+
47
+ if (!isComponentRegistered(componentName)) {
48
+ logger.warn(`[UI-Initializer] Unknown component: '${componentName}'. Registered: ${getRegisteredComponentTypes().join(', ')}`);
49
+ return;
50
+ }
51
+
52
+ // Inject styles for custom components (CSS-in-JS)
53
+ const styles = getComponentStyles(componentName);
54
+ if (styles) {
55
+ injectStyles(componentName, styles);
56
+ }
57
+
58
+ // Get and call init function from catalog
59
+ const initializer = getComponentInit(componentName);
60
+ if (initializer && typeof initializer === 'function') {
61
+ try {
62
+ initializer(element);
63
+ } catch (error) {
64
+ logger.error(`[UI-Initializer] Failed to initialize '${componentName}' component: ${error.message}`, { domain: 'ui', operation: 'initializeComponent', stack: error.stack, component: componentName });
65
+ }
66
+ }
67
+ // CSS-only components (no init or no-op init) handled purely by CSS
68
+ });
69
+
70
+ // Tooltips auto-initialize via event delegation - no call needed
71
+
72
+ // Register all flip cards with engagement manager (batch registration like tabs)
73
+ // This must happen AFTER all flip cards are initialized
74
+ registerFlipCardsForEngagement(container);
75
+
76
+ // Register all modals with engagement manager (batch registration)
77
+ // This must happen AFTER all modal triggers are initialized
78
+ registerModalsForEngagement(container);
79
+
80
+ // Initialize declarative notification triggers (event delegation pattern)
81
+ initNotificationTriggers(container);
82
+
83
+
84
+ // Register lightbox triggers with engagement manager (batch registration)
85
+ // This must happen AFTER all lightbox triggers are initialized by the catalog
86
+ registerLightboxesForEngagement(container);
87
+ }
88
+
89
+ /**
90
+ * Registers all flip cards in the container with the engagement manager.
91
+ * This batch registration ensures flipCardsTotal is set correctly after all cards are found.
92
+ * @param {HTMLElement} container - The container to scan for flip cards
93
+ */
94
+ function registerFlipCardsForEngagement(container) {
95
+ const flipCards = container.querySelectorAll('[data-component="flip-card"][data-flip-card-id]');
96
+ if (!flipCards.length) return;
97
+
98
+ const currentSlideId = NavigationState.getCurrentSlideId();
99
+ if (!currentSlideId) return;
100
+
101
+ const cardIds = Array.from(flipCards).map(card => card.dataset.flipCardId).filter(Boolean);
102
+ if (cardIds.length > 0) {
103
+ engagementManager.registerFlipCards(currentSlideId, cardIds);
104
+ logger.debug(`[UI-Initializer] Registered ${cardIds.length} flip cards for engagement tracking`);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Registers all modal triggers in the container with the engagement manager.
110
+ * This batch registration ensures modalsTotal is set correctly after all triggers are found.
111
+ * @param {HTMLElement} container - The container to scan for modal triggers
112
+ */
113
+ function registerModalsForEngagement(container) {
114
+ const modalTriggers = container.querySelectorAll('[data-component="modal-trigger"][data-modal-id]');
115
+ if (!modalTriggers.length) return;
116
+
117
+ const currentSlideId = NavigationState.getCurrentSlideId();
118
+ if (!currentSlideId) return;
119
+
120
+ const modalIds = Array.from(modalTriggers).map(trigger => trigger.dataset.modalId).filter(Boolean);
121
+ if (modalIds.length > 0) {
122
+ engagementManager.registerModals(currentSlideId, modalIds);
123
+ logger.debug(`[UI-Initializer] Registered ${modalIds.length} modals for engagement tracking`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Registers all lightbox triggers in the container with the engagement manager.
129
+ * This batch registration ensures lightboxesTotal is set correctly after all triggers are found.
130
+ * @param {HTMLElement} container - The container to scan for lightbox triggers
131
+ */
132
+ function registerLightboxesForEngagement(container) {
133
+ const lightboxTriggers = container.querySelectorAll('[data-component="lightbox"]');
134
+ if (!lightboxTriggers.length) return;
135
+
136
+ const currentSlideId = NavigationState.getCurrentSlideId();
137
+ if (!currentSlideId) return;
138
+
139
+ const lightboxIds = Array.from(lightboxTriggers)
140
+ .map(trigger => trigger.id || trigger.dataset.lightboxId)
141
+ .filter(Boolean);
142
+ if (lightboxIds.length > 0) {
143
+ engagementManager.registerLightbox(currentSlideId, lightboxIds);
144
+ logger.debug(`[UI-Initializer] Registered ${lightboxIds.length} lightboxes for engagement tracking`);
145
+ }
146
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @file utilities.js
3
+ * @description Core utility functions for the SCORM framework.
4
+ * These are pure, stateless helper functions.
5
+ */
6
+
7
+ /**
8
+ * Format duration in milliseconds to a human-readable string.
9
+ * @param {number} ms - Duration in milliseconds
10
+ * @returns {string} Formatted duration (e.g., "2m 30s")
11
+ */
12
+ export function formatDuration(ms) {
13
+ if (!ms || ms < 0) {
14
+ return '0s';
15
+ }
16
+
17
+ const seconds = Math.floor(ms / 1000);
18
+ const minutes = Math.floor(seconds / 60);
19
+ const hours = Math.floor(minutes / 60);
20
+
21
+ if (hours > 0) {
22
+ return `${hours}h ${minutes % 60}m`;
23
+ } else if (minutes > 0) {
24
+ return `${minutes}m ${seconds % 60}s`;
25
+ } else {
26
+ return `${seconds}s`;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Deep clone an object (handles primitives, arrays, objects, and Dates).
32
+ * @param {*} obj - Value to clone
33
+ * @returns {*} Cloned value
34
+ */
35
+ export function deepClone(obj) {
36
+ if (obj === null || typeof obj !== 'object') {
37
+ return obj;
38
+ }
39
+ if (obj instanceof Date) {
40
+ return new Date(obj.getTime());
41
+ }
42
+ if (Array.isArray(obj)) {
43
+ return obj.map(item => deepClone(item));
44
+ }
45
+
46
+ const clonedObj = {};
47
+ for (const key in obj) {
48
+ if (obj.hasOwnProperty(key)) {
49
+ clonedObj[key] = deepClone(obj[key]);
50
+ }
51
+ }
52
+ return clonedObj;
53
+ }
54
+
55
+ /**
56
+ * Generate a unique ID with optional prefix.
57
+ * @param {string} prefix - Optional prefix for the ID
58
+ * @returns {string} Unique ID (format: prefix-timestamp-random)
59
+ */
60
+ export function generateId(prefix = 'id') {
61
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
62
+ }
63
+
64
+ /**
65
+ * Escape HTML entities in text.
66
+ * @param {string} text - Text to escape
67
+ * @returns {string} Text with HTML entities escaped
68
+ */
69
+ export function escapeHTML(text) {
70
+ if (typeof text !== 'string') {
71
+ return '';
72
+ }
73
+ const map = {
74
+ '&': '&amp;',
75
+ '<': '&lt;',
76
+ '>': '&gt;',
77
+ '"': '&quot;',
78
+ "'": '&#039;'
79
+ };
80
+ return text.replace(/[&<>"']/g, m => map[m]);
81
+ }
82
+
83
+ /**
84
+ * Check if an element is fully visible in the viewport.
85
+ * @param {HTMLElement} element - Element to check
86
+ * @returns {boolean} True if element is fully visible within viewport
87
+ */
88
+ export function isElementVisible(element) {
89
+ if (!element || !(element instanceof HTMLElement)) {
90
+ return false;
91
+ }
92
+
93
+ const rect = element.getBoundingClientRect();
94
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
95
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
96
+
97
+ return (
98
+ rect.top >= 0 &&
99
+ rect.left >= 0 &&
100
+ rect.bottom <= viewportHeight &&
101
+ rect.right <= viewportWidth
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Scroll element into view with smooth behavior.
107
+ * @param {HTMLElement} element - Element to scroll to
108
+ * @param {Object} options - ScrollIntoView options
109
+ */
110
+ export function scrollToElement(element, options = {}) {
111
+ if (!element || !(element instanceof HTMLElement)) {
112
+ return;
113
+ }
114
+
115
+ const defaultOptions = {
116
+ behavior: 'smooth',
117
+ block: 'start',
118
+ inline: 'nearest'
119
+ };
120
+
121
+ element.scrollIntoView({ ...defaultOptions, ...options });
122
+ }
123
+
124
+ /**
125
+ * Get all focusable elements within a container.
126
+ * @param {HTMLElement} container - Container element
127
+ * @returns {Array<HTMLElement>} Array of focusable elements
128
+ */
129
+ export function getFocusableElements(container) {
130
+ if (!container || !(container instanceof HTMLElement)) {
131
+ return [];
132
+ }
133
+
134
+ const selector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
135
+ return Array.from(container.querySelectorAll(selector));
136
+ }
137
+
138
+ /**
139
+ * Trap focus within a container (for modals, dialogs).
140
+ * Returns a cleanup function that must be called to remove the trap.
141
+ * @param {HTMLElement} container - Container to trap focus in
142
+ * @returns {Function} Cleanup function to remove trap
143
+ */
144
+ export function trapFocus(container) {
145
+ if (!container || !(container instanceof HTMLElement)) {
146
+ throw new Error('trapFocus: container must be a valid HTMLElement');
147
+ }
148
+
149
+ const focusableElements = getFocusableElements(container);
150
+ if (focusableElements.length === 0) {
151
+ return () => { };
152
+ }
153
+
154
+ const firstElement = focusableElements[0];
155
+ const lastElement = focusableElements[focusableElements.length - 1];
156
+
157
+ const handleTabKey = (e) => {
158
+ if (e.key !== 'Tab') {
159
+ return;
160
+ }
161
+
162
+ if (e.shiftKey) {
163
+ if (document.activeElement === firstElement) {
164
+ e.preventDefault();
165
+ lastElement.focus();
166
+ }
167
+ } else {
168
+ if (document.activeElement === lastElement) {
169
+ e.preventDefault();
170
+ firstElement.focus();
171
+ }
172
+ }
173
+ };
174
+
175
+ container.addEventListener('keydown', handleTabKey);
176
+ firstElement.focus();
177
+
178
+ return () => {
179
+ container.removeEventListener('keydown', handleTabKey);
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Format a number as a percentage string.
185
+ * @param {number} value - Value to format (0-1 if isDecimal=true, 0-100 if isDecimal=false)
186
+ * @param {boolean} isDecimal - True if value is 0-1, false if 0-100
187
+ * @returns {string} Formatted percentage (e.g., "75%")
188
+ */
189
+ export function formatPercentage(value, isDecimal = true) {
190
+ if (typeof value !== 'number' || isNaN(value)) {
191
+ return '0%';
192
+ }
193
+ const percent = isDecimal ? value * 100 : value;
194
+ return `${Math.round(percent)}%`;
195
+ }
196
+
197
+ /**
198
+ * Wait for a condition to be true, checking at regular intervals.
199
+ * @param {Function} condition - Function that returns true when condition is met
200
+ * @param {number} timeout - Maximum time to wait in milliseconds
201
+ * @param {number} interval - Check interval in milliseconds
202
+ * @returns {Promise<void>} Resolves when condition is met, rejects on timeout
203
+ */
204
+ export function waitFor(condition, timeout = 5000, interval = 100) {
205
+ if (!condition || typeof condition !== 'function') {
206
+ return Promise.reject(new Error('waitFor: condition must be a function'));
207
+ }
208
+ if (timeout <= 0) {
209
+ return Promise.reject(new Error('waitFor: timeout must be positive'));
210
+ }
211
+ if (interval <= 0) {
212
+ return Promise.reject(new Error('waitFor: interval must be positive'));
213
+ }
214
+
215
+ return new Promise((resolve, reject) => {
216
+ const startTime = Date.now();
217
+
218
+ const check = () => {
219
+ try {
220
+ if (condition()) {
221
+ resolve();
222
+ } else if (Date.now() - startTime >= timeout) {
223
+ reject(new Error('Timeout waiting for condition'));
224
+ } else {
225
+ setTimeout(check, interval);
226
+ }
227
+ } catch (error) {
228
+ reject(new Error(`Condition check failed: ${error.message}`));
229
+ }
230
+ };
231
+
232
+ check();
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Deep merge multiple objects (mutates target object).
238
+ * Plain objects are merged recursively; other values are overwritten.
239
+ * @param {Object} target - The target object to merge into (will be mutated)
240
+ * @param {...Object} sources - The source objects to merge from
241
+ * @returns {Object} The merged target object
242
+ */
243
+ export function deepMerge(target, ...sources) {
244
+ if (!target || typeof target !== 'object' || Array.isArray(target)) {
245
+ throw new Error('deepMerge: target must be a plain object');
246
+ }
247
+
248
+ if (sources.length === 0) {
249
+ return target;
250
+ }
251
+
252
+ const source = sources.shift();
253
+
254
+ if (source && typeof source === 'object' && !Array.isArray(source)) {
255
+ for (const key in source) {
256
+ if (source.hasOwnProperty(key)) {
257
+ const sourceValue = source[key];
258
+ const targetValue = target[key];
259
+
260
+ if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
261
+ // Recursive merge for plain objects
262
+ if (!targetValue || typeof targetValue !== 'object' || Array.isArray(targetValue)) {
263
+ target[key] = {};
264
+ }
265
+ deepMerge(target[key], sourceValue);
266
+ } else {
267
+ // Direct assignment for primitives, arrays, and other types
268
+ target[key] = sourceValue;
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ return deepMerge(target, ...sources);
275
+ }
276
+
277
+ /**
278
+ * Shuffle an array using Fisher-Yates algorithm (creates new array).
279
+ * @param {Array} array - Array to shuffle
280
+ * @returns {Array} New shuffled array
281
+ */
282
+ export function shuffleArray(array) {
283
+ if (!Array.isArray(array)) {
284
+ throw new Error('shuffleArray: input must be an array');
285
+ }
286
+
287
+ const shuffled = [...array];
288
+ for (let i = shuffled.length - 1; i > 0; i--) {
289
+ const j = Math.floor(Math.random() * (i + 1));
290
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
291
+ }
292
+ return shuffled;
293
+ }
@@ -0,0 +1,227 @@
1
+ import { eventBus } from '../core/event-bus.js';
2
+ import { logger } from './logger.js';
3
+ import { validateRenderedHTML } from '../validation/html-validators.js';
4
+ import { initializeDeclarativeComponents } from './ui-initializer.js';
5
+ import { courseConfig } from '../../../course/course-config.js';
6
+
7
+ /**
8
+ * Creates a view manager for a given container element.
9
+ * @param {HTMLElement} container - The container element for the views.
10
+ * @param {string} [scope='local'] - The scope of this ViewManager ('main' for main navigation, 'assessment' for assessment internal views, etc.)
11
+ * @returns {object} A view manager instance.
12
+ */
13
+ export function createViewManager(container, scope = 'local') {
14
+ if (!container || !(container instanceof HTMLElement)) {
15
+ throw new Error('ViewManager: A valid container element is required.');
16
+ }
17
+
18
+ const views = {};
19
+ let currentViewName = null;
20
+
21
+ /**
22
+ * Registers a view with lifecycle hooks.
23
+ * @param {string} name - The name of the view.
24
+ * @param {object} viewObject - An object defining the view.
25
+ * @param {Function} viewObject.render - A function that returns an HTMLElement. Called every time the view is shown.
26
+ * @param {Function} [viewObject.onShow] - A function called every time the view is shown, after rendering.
27
+ * @param {Function} [viewObject.onHide] - A function called every time the view is hidden, before removal from DOM.
28
+ */
29
+ function registerView(name, viewObject) {
30
+ if (!name) {
31
+ throw new Error('ViewManager: View name is required.');
32
+ }
33
+ if (!viewObject || typeof viewObject.render !== 'function') {
34
+ throw new Error(`ViewManager: View '${name}' must be an object with a render function.`);
35
+ }
36
+ views[name] = { ...viewObject };
37
+ }
38
+
39
+ /**
40
+ * Shows a view by name.
41
+ * @param {string} name - The name of the view to show.
42
+ * @param {object} [options] - Options to pass to the render and onShow functions.
43
+ */
44
+ async function showView(name, options = {}) {
45
+ if (!views[name]) {
46
+ throw new Error(`ViewManager: View '${name}' not found.`);
47
+ }
48
+
49
+ // Hide the current view and call its onHide hook
50
+ let oldElement = null;
51
+ if (currentViewName && views[currentViewName]) {
52
+ const oldView = views[currentViewName];
53
+ // Get the current element from the container
54
+ oldElement = container.firstElementChild;
55
+ if (oldElement && typeof oldView.onHide === 'function') {
56
+ oldView.onHide(oldElement);
57
+ }
58
+ }
59
+
60
+ const newView = views[name];
61
+
62
+ // Emit before-change event to allow cleanup (e.g., clearing interaction registry)
63
+ // Include scope to distinguish main navigation from component-internal navigation
64
+ eventBus.emit('view:before-change', {
65
+ oldView: currentViewName,
66
+ newView: name,
67
+ scope: scope,
68
+ context: options
69
+ });
70
+
71
+ // Always render fresh to ensure current data is displayed
72
+ let newElement = await newView.render(options);
73
+
74
+ if (!(newElement instanceof HTMLElement)) {
75
+ throw new Error(`ViewManager: View '${name}' render function must return an HTMLElement.`);
76
+ }
77
+
78
+ // Auto-wrap content with content-width class if configured and not already wrapped
79
+ newElement = autoWrapContentIfNeeded(newElement, name);
80
+
81
+ // Validate rendered HTML for common issues BEFORE adding to DOM
82
+ validateRenderedContent(newElement, name);
83
+
84
+ // Clear container and add new view
85
+ container.innerHTML = '';
86
+ container.appendChild(newElement);
87
+
88
+ // Initialize any declarative components within the new view
89
+ initializeDeclarativeComponents(newElement);
90
+
91
+ // Call onShow hook
92
+ if (typeof newView.onShow === 'function') {
93
+ newView.onShow(newElement, options);
94
+ }
95
+
96
+ currentViewName = name;
97
+ eventBus.emit('view:change', { view: name, context: options });
98
+ }
99
+
100
+ /**
101
+ * Auto-wraps content with a content-width class if configured and not already wrapped.
102
+ * Allows per-slide override using data-content-width attribute.
103
+ * @param {HTMLElement} element - The rendered element
104
+ * @param {string} viewName - The name of the view being rendered
105
+ * @returns {HTMLElement} The element, potentially wrapped
106
+ * @private
107
+ */
108
+ function autoWrapContentIfNeeded(element, _viewName) {
109
+ // Canvas layout: author owns all styling, no auto-wrapping
110
+ if (courseConfig?.layout === 'canvas') {
111
+ return element;
112
+ }
113
+
114
+ // Check for per-slide override via data-content-width attribute
115
+ const dataAttrWidth = element.getAttribute('data-content-width');
116
+ const slideConfigWidth = dataAttrWidth;
117
+
118
+ // Determine which width to use: per-slide override > global config > no wrapping
119
+ const configWidth = slideConfigWidth || courseConfig?.slideDefaults?.contentWidth;
120
+
121
+ if (!configWidth) {
122
+ // No wrapping configured
123
+ return element;
124
+ }
125
+
126
+ // Check if already wrapped with a content-width class
127
+ if (hasContentWidthClass(element)) {
128
+ // Already wrapped, don't double-wrap
129
+ return element;
130
+ }
131
+
132
+ // Create wrapper with appropriate content-width class
133
+ const wrapper = document.createElement('div');
134
+ wrapper.className = `content-${configWidth}`;
135
+ wrapper.appendChild(element);
136
+ return wrapper;
137
+ }
138
+
139
+ /**
140
+ * Checks if an element or its children already have a content-width class.
141
+ * @param {HTMLElement} element - The element to check
142
+ * @returns {boolean} True if element or children have content-width class
143
+ * @private
144
+ */
145
+ function hasContentWidthClass(element) {
146
+ const contentWidthClasses = ['content-narrow', 'content-medium', 'content-wide', 'content-full'];
147
+
148
+ // Check the element itself
149
+ if (element.classList && contentWidthClasses.some(cls => element.classList.contains(cls))) {
150
+ return true;
151
+ }
152
+
153
+ // Check immediate children (common pattern: wrapper div around content)
154
+ if (element.children && element.children.length > 0) {
155
+ for (const child of element.children) {
156
+ if (child.classList && contentWidthClasses.some(cls => child.classList.contains(cls))) {
157
+ return true;
158
+ }
159
+ }
160
+ }
161
+
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Validates rendered content for common issues that can cause page reloads or errors.
167
+ * @param {HTMLElement} element - The rendered element to validate
168
+ * @param {string} viewName - The name of the view being rendered
169
+ * @private
170
+ */
171
+ function validateRenderedContent(element, viewName) {
172
+ // Use centralized validation from html-validators.js
173
+ const validation = validateRenderedHTML(element, viewName);
174
+
175
+ if (!validation.valid) {
176
+ // Process each error and emit events
177
+ validation.errors.forEach(error => {
178
+ const message = `View "${viewName}" [${error.type}]: ${error.message}`;
179
+ logger.error(`[ViewManager] ${message}`, { domain: 'view', operation: 'validateRenderedContent', ...error.context });
180
+ });
181
+
182
+ // Throw with a summary of all errors
183
+ const errorSummary = validation.errors.map(e => `[${e.type}] ${e.message}`).join('; ');
184
+ throw new Error(`[ViewManager] View "${viewName}" has ${validation.errors.length} validation error(s): ${errorSummary}`);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Gets the name of the currently visible view.
190
+ * @returns {string|null} The name of the current view.
191
+ */
192
+ function getCurrentView() {
193
+ return currentViewName;
194
+ }
195
+
196
+ /**
197
+ * Gets the container element.
198
+ * @returns {HTMLElement} The container element.
199
+ */
200
+ function getContainer() {
201
+ return container;
202
+ }
203
+
204
+ /**
205
+ * Destroys the view manager and cleans up the container.
206
+ */
207
+ function destroy() {
208
+ // Call onHide for current view if exists
209
+ if (currentViewName && views[currentViewName]) {
210
+ const currentElement = container.firstElementChild;
211
+ const currentView = views[currentViewName];
212
+ if (currentElement && typeof currentView.onHide === 'function') {
213
+ currentView.onHide(currentElement);
214
+ }
215
+ }
216
+ container.innerHTML = '';
217
+ currentViewName = null;
218
+ }
219
+
220
+ return {
221
+ registerView,
222
+ showView,
223
+ getCurrentView,
224
+ getContainer,
225
+ destroy,
226
+ };
227
+ }