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,195 @@
1
+ /**
2
+ * stub-player/app-viewer.js - Viewer-only Entry Point
3
+ *
4
+ * Lightweight stub player for export previews and cloud previews.
5
+ * Excludes all authoring/edit functionality: debug panel, config panel,
6
+ * interactions panel, catalog, outline mode, edit mode.
7
+ *
8
+ * Only imports: lms-api, header-bar, content-viewer, login-screen.
9
+ */
10
+
11
+ import {
12
+ initializeLMS
13
+ } from './lms-api.js';
14
+
15
+ import { createHeaderBarHandlers } from './header-bar.js';
16
+ import { createContentViewerHandlers } from './content-viewer.js';
17
+ import { createLoginHandlers } from './login-screen.js';
18
+
19
+ // Global Configuration (injected by server into window.STUB_CONFIG)
20
+ const config = window.STUB_CONFIG || {};
21
+ const LAUNCH_URL = config.launchUrl || '/';
22
+ const START_SLIDE = config.startSlide || null;
23
+
24
+ // State
25
+ let isInitialized = false;
26
+ let contentLoaded = false;
27
+
28
+ // =============================================================================
29
+ // INITIALIZATION
30
+ // =============================================================================
31
+
32
+ function init() {
33
+ // Initialize LMS APIs on window (SCORM 1.2, 2004, cmi5)
34
+ initializeLMS();
35
+
36
+ // Login Screen
37
+ createLoginHandlers({
38
+ onLogin: () => loadCourse()
39
+ });
40
+
41
+ // Content (Review) Panel
42
+ const contentPanel = document.getElementById('stub-player-content-panel');
43
+ const contentHandlers = contentPanel
44
+ ? createContentViewerHandlers({ initialContent: config.courseContent || null })
45
+ : null;
46
+
47
+ // Header Bar — viewer mode: Review + More menu (skip gating, reset)
48
+ createHeaderBarHandlers({
49
+ onToggle: () => {},
50
+ onContent: () => {
51
+ if (contentPanel) {
52
+ contentPanel.classList.toggle('visible');
53
+ if (contentPanel.classList.contains('visible') && !contentLoaded && contentHandlers) {
54
+ contentHandlers.loadContent();
55
+ contentLoaded = true;
56
+ }
57
+ }
58
+ },
59
+ onReset: () => doReset(),
60
+ onSkipGating: (enabled) => {
61
+ if (enabled) {
62
+ loadCourse();
63
+ } else {
64
+ doReset();
65
+ }
66
+ }
67
+ });
68
+
69
+ // Load course if no login screen active
70
+ if (!document.getElementById('stub-player-login-screen')?.classList.contains('visible')) {
71
+ loadCourse();
72
+ }
73
+
74
+ // Outside click to close panels
75
+ setupOutsideClickListener(contentPanel);
76
+ }
77
+
78
+ // =============================================================================
79
+ // COURSE LOADING & NAVIGATION
80
+ // =============================================================================
81
+
82
+ function loadCourse() {
83
+ const frame = document.getElementById('stub-player-course-frame');
84
+ const skipGating = document.getElementById('stub-player-skip-gating')?.checked;
85
+ let url = LAUNCH_URL;
86
+ if (skipGating) url += (url.includes('?') ? '&' : '?') + 'skipGating=true';
87
+ frame.src = url;
88
+ frame.addEventListener('load', handleFrameLoad);
89
+ }
90
+
91
+ function handleFrameLoad() {
92
+ try {
93
+ const frame = document.getElementById('stub-player-course-frame');
94
+ const win = frame.contentWindow;
95
+
96
+ // Inject LMS APIs
97
+ win.API_1484_11 = window.API_1484_11;
98
+ win.API = window.API;
99
+ win.cmi5 = window.cmi5;
100
+ win.lti = window.lti;
101
+
102
+ if (document.getElementById('stub-player-skip-gating')?.checked) {
103
+ win.__SCORM_PREVIEW_SKIP_GATING = true;
104
+ }
105
+
106
+ // Initial navigation (URL param or config)
107
+ if (!isInitialized) {
108
+ const urlSlide = new URLSearchParams(window.location.search).get('slide');
109
+ const target = urlSlide || START_SLIDE;
110
+ if (target) navigateToSlide(win, target);
111
+ isInitialized = true;
112
+ }
113
+ } catch (e) {
114
+ console.error('[Viewer] Failed to inject API:', e);
115
+ }
116
+ }
117
+
118
+ function navigateToSlide(contentWindow, slideIdOrIndex) {
119
+ const maxAttempts = 50;
120
+ let attempts = 0;
121
+
122
+ function tryNavigate() {
123
+ attempts++;
124
+ try {
125
+ const fw = contentWindow.CourseCode;
126
+ const nav = fw && fw.NavigationActions;
127
+
128
+ if (nav && typeof nav.isReady === 'function' && nav.isReady() && typeof nav.goToSlide === 'function') {
129
+ const slideId = resolveSlideId(contentWindow, slideIdOrIndex);
130
+ if (slideId) {
131
+ nav.goToSlide(slideId, { source: 'preview-url' });
132
+ return;
133
+ }
134
+ }
135
+
136
+ if (attempts < maxAttempts) setTimeout(tryNavigate, 100);
137
+ } catch (_e) {
138
+ if (attempts < maxAttempts) setTimeout(tryNavigate, 100);
139
+ }
140
+ }
141
+ setTimeout(tryNavigate, 300);
142
+ }
143
+
144
+ function resolveSlideId(contentWindow, slideIdOrIndex) {
145
+ const asNumber = Number(slideIdOrIndex);
146
+ if (!isNaN(asNumber) && Number.isInteger(asNumber) && asNumber >= 0) {
147
+ if (contentWindow.CourseCode?.NavigationActions?.getAllSlides) {
148
+ const slides = contentWindow.CourseCode.NavigationActions.getAllSlides();
149
+ if (slides && slides[asNumber]) return slides[asNumber].id;
150
+ }
151
+ return null;
152
+ }
153
+ return slideIdOrIndex;
154
+ }
155
+
156
+ // =============================================================================
157
+ // UTILS
158
+ // =============================================================================
159
+
160
+ function doReset() {
161
+ // Static/cloud export: clear LMS localStorage and reload (no server route)
162
+ const storageKey = config.storageKey;
163
+ if (storageKey) {
164
+ try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
165
+ }
166
+ window.location.reload();
167
+ }
168
+
169
+ function setupOutsideClickListener(contentPanel) {
170
+ document.addEventListener('click', (e) => {
171
+ if (contentPanel?.classList.contains('visible')) {
172
+ const clickedInPanel = e.target.closest('#stub-player-content-panel');
173
+ const clickedToggle = e.target.closest('button[id*="-btn"]');
174
+ const clickedHeader = e.target.closest('#stub-player-header');
175
+
176
+ if (!clickedInPanel && !clickedToggle && !clickedHeader) {
177
+ contentPanel.classList.remove('visible');
178
+ }
179
+ }
180
+ });
181
+
182
+ const iframe = document.getElementById('stub-player-course-frame');
183
+ if (iframe) {
184
+ window.addEventListener('blur', () => {
185
+ setTimeout(() => {
186
+ if (document.activeElement === iframe) {
187
+ contentPanel?.classList.remove('visible');
188
+ }
189
+ }, 0);
190
+ });
191
+ }
192
+ }
193
+
194
+ // Start
195
+ init();
@@ -0,0 +1,370 @@
1
+ /**
2
+ * stub-player/app.js - Main Application Entry Point
3
+ *
4
+ * Orchestrates all stub player components, initializes LMS API,
5
+ * and handles global state/events.
6
+ */
7
+
8
+ import {
9
+ cmiData,
10
+ logError,
11
+ logXapiStatement,
12
+ setUiCallbacks,
13
+ initializeLMS
14
+ } from './lms-api.js';
15
+
16
+ import { createHeaderBarHandlers, updateSlideId } from './header-bar.js';
17
+ import { createDebugPanelHandlers } from './debug-panel.js';
18
+ import { createConfigPanelHandlers } from './config-panel.js';
19
+ import { createContentViewerHandlers } from './content-viewer.js';
20
+ import { createInteractionsPanelHandlers } from './interactions-panel.js';
21
+ import { createCatalogPanelHandlers } from './catalog-panel.js';
22
+ import { createOutlineModeHandlers } from './outline-mode.js';
23
+ import { createLoginHandlers } from './login-screen.js';
24
+ import { createEditModeHandlers } from './edit-mode.js';
25
+ import { initInteractionEditor } from './interaction-editor.js';
26
+
27
+ // Global Configuration (injected by server into window.STUB_CONFIG)
28
+ const config = window.STUB_CONFIG || {};
29
+ const IS_LIVE = config.isLive || false;
30
+ const LAUNCH_URL = config.launchUrl || '/';
31
+ const START_SLIDE = config.startSlide || null;
32
+ // State
33
+ let isInitialized = false;
34
+
35
+ // =============================================================================
36
+ // INITIALIZATION
37
+ // =============================================================================
38
+
39
+ function init() {
40
+ console.log('[App] Initializing Stub Player...');
41
+
42
+ // Initialize LMS APIs on window (SCORM 1.2, 2004, cmi5)
43
+ initializeLMS();
44
+
45
+ // Login Screen
46
+ createLoginHandlers({
47
+ onLogin: () => {
48
+ loadCourse();
49
+ }
50
+ });
51
+
52
+ // If no login required (or already handled), load course immediately handled by login-screen logic normally
53
+ // But login-screen.js might need to be checked if it auto-hides.
54
+ // Actually, stub-player.js logic was: if login needed, show it, else loadCourse.
55
+ // For now, let's assume login handlers handle the "Start" flow.
56
+ // Check if we need to auto-start if no login required?
57
+ // login-screen.js handles this check usually.
58
+ // Wait, createLoginHandlers usually returns success check?
59
+ // Let's rely on callback.
60
+
61
+
62
+ // UI Panels & Exclusivity Management
63
+ const panels = {
64
+ debug: { el: document.getElementById('stub-player-debug-panel'), load: null },
65
+ config: { el: document.getElementById('stub-player-config-panel'), load: () => handlers.config.loadConfig() },
66
+ content: { el: document.getElementById('stub-player-content-panel'), load: () => handlers.content.loadContent() },
67
+ interactions: { el: document.getElementById('stub-player-interactions-panel'), load: () => handlers.interactions.loadInteractions() },
68
+ catalog: { el: document.getElementById('stub-player-catalog-panel'), load: () => handlers.catalog.loadCatalog() }
69
+ };
70
+
71
+ function closeAllPanels(except = null) {
72
+ for (const [key, panel] of Object.entries(panels)) {
73
+ if (key !== except && panel.el) {
74
+ panel.el.classList.remove('visible');
75
+ }
76
+ }
77
+ }
78
+
79
+ function togglePanel(name) {
80
+ closeAllPanels(name);
81
+ if (panels[name] && panels[name].el) {
82
+ panels[name].el.classList.toggle('visible');
83
+ if (panels[name].el.classList.contains('visible') && panels[name].load) {
84
+ panels[name].load();
85
+ }
86
+ }
87
+ }
88
+
89
+ // Context for child modules
90
+ const context = {
91
+ getCmiData: () => cmiData,
92
+ navigateToSlide: (id) => {
93
+ const frame = document.getElementById('stub-player-course-frame');
94
+ if (frame && frame.contentWindow) {
95
+ navigateToSlide(frame.contentWindow, id);
96
+ }
97
+ },
98
+ isLive: IS_LIVE,
99
+ loadCourse: () => loadCourse()
100
+ };
101
+
102
+ // Initialize Module Handlers
103
+ const handlers = {
104
+ debug: createDebugPanelHandlers(), // Debug panel handles its own internal logic, we trigger visibility via header
105
+ config: createConfigPanelHandlers ? createConfigPanelHandlers(context) : null,
106
+ content: createContentViewerHandlers ? createContentViewerHandlers(context) : null,
107
+ interactions: createInteractionsPanelHandlers ? createInteractionsPanelHandlers(context) : null,
108
+ catalog: createCatalogPanelHandlers ? createCatalogPanelHandlers(context) : null,
109
+ outline: createOutlineModeHandlers ? createOutlineModeHandlers(context) : null
110
+ };
111
+
112
+ // Stage-aware: check if we should show dashboard instead of course
113
+ if (IS_LIVE && handlers.outline) {
114
+ handlers.outline.checkStage().then(active => {
115
+ // If dashboard didn't activate, load course now
116
+ if (!active && !document.getElementById('stub-player-login-screen')?.classList.contains('visible')) {
117
+ loadCourse();
118
+ }
119
+ });
120
+ }
121
+
122
+ // Header Bar
123
+ createHeaderBarHandlers({
124
+ onToggle: (isCollapsed) => {
125
+ if (isCollapsed) closeAllPanels();
126
+ },
127
+ onDebug: () => togglePanel('debug'),
128
+ onConfig: () => togglePanel('config'),
129
+ onContent: () => togglePanel('content'),
130
+ onInteract: () => togglePanel('interactions'),
131
+ onCatalog: () => togglePanel('catalog'),
132
+ onEdit: () => { /* Edit logic internal to stub-player.js? Or moved? */ },
133
+ onReset: () => doReset(),
134
+ onSkipGating: (enabled) => {
135
+ if (enabled) {
136
+ loadCourse(); // Reload to apply skip
137
+ } else {
138
+ doReset(); // Reset to enforce gating
139
+ }
140
+ },
141
+ onStatus: () => {
142
+ if (handlers.outline) handlers.outline.toggle();
143
+ }
144
+ });
145
+
146
+ // Register LMS API Callbacks to update UI
147
+ setUiCallbacks({
148
+ onSlideNavigation: (slideId) => {
149
+ // Update header badge when cmi.location changes
150
+ updateSlideId(slideId);
151
+ }
152
+ });
153
+
154
+ // Initial Course Load (if no login screen or dashboard active)
155
+ if (!IS_LIVE && !document.getElementById('stub-player-login-screen')?.classList.contains('visible')) {
156
+ loadCourse();
157
+ }
158
+
159
+ // Setup outside click listener
160
+ setupOutsideClickListener(panels);
161
+
162
+ // Setup Edit Mode logic (if live)
163
+ if (IS_LIVE) setupEditMode();
164
+ }
165
+
166
+ // =============================================================================
167
+ // COURSE LOADING & NAVIGATION
168
+ // =============================================================================
169
+
170
+ function loadCourse() {
171
+ const frame = document.getElementById('stub-player-course-frame');
172
+ const skipGating = document.getElementById('stub-player-skip-gating')?.checked;
173
+ let url = LAUNCH_URL;
174
+ if (skipGating) url += (url.includes('?') ? '&' : '?') + 'skipGating=true';
175
+ frame.src = url;
176
+
177
+ // Inject API on load
178
+ frame.addEventListener('load', handleFrameLoad);
179
+ }
180
+
181
+ function handleFrameLoad() {
182
+ try {
183
+ const frame = document.getElementById('stub-player-course-frame');
184
+ const win = frame.contentWindow;
185
+
186
+ // Inject APIs (attached to window.* by initializeLMS() in init())
187
+ win.API_1484_11 = window.API_1484_11;
188
+ win.API = window.API;
189
+ win.cmi5 = window.cmi5;
190
+ win.lti = window.lti;
191
+
192
+ if (document.getElementById('stub-player-skip-gating')?.checked) {
193
+ win.__SCORM_PREVIEW_SKIP_GATING = true;
194
+ }
195
+
196
+ // Attach console interceptors
197
+ attachConsoleInterceptors(win);
198
+
199
+ // Attach xAPI listener
200
+ attachXapiListener(win);
201
+
202
+ // Initial Nav
203
+ if (!isInitialized) {
204
+ const urlSlide = new URLSearchParams(window.location.search).get('slide');
205
+ const target = urlSlide || START_SLIDE;
206
+ if (target) {
207
+ navigateToSlide(win, target);
208
+ }
209
+ isInitialized = true;
210
+ }
211
+
212
+ // Listen for slide changes to update header
213
+ // We can poll or hook into framework?
214
+ // Framework events are best.
215
+ // See attachXapiListener for pattern.
216
+
217
+ } catch (e) {
218
+ console.error('[App] Failed to inject API:', e);
219
+ }
220
+ }
221
+
222
+ // ... Navigation helpers (navigateToSlide, resolveSlideId) ...
223
+ function navigateToSlide(contentWindow, slideIdOrIndex) {
224
+ const maxAttempts = 50;
225
+ let attempts = 0;
226
+
227
+ function tryNavigate() {
228
+ attempts++;
229
+ try {
230
+ const fw = contentWindow.CourseCode;
231
+ const nav = fw && fw.NavigationActions;
232
+
233
+ if (nav && typeof nav.isReady === 'function' && nav.isReady() && typeof nav.goToSlide === 'function') {
234
+ const slideId = resolveSlideId(contentWindow, slideIdOrIndex);
235
+ if (slideId) {
236
+ nav.goToSlide(slideId, { source: 'preview-url' });
237
+ // Update header badge
238
+ updateSlideId(slideId);
239
+ return;
240
+ }
241
+ }
242
+
243
+ if (attempts < maxAttempts) setTimeout(tryNavigate, 100);
244
+ } catch (_e) {
245
+ if (attempts < maxAttempts) setTimeout(tryNavigate, 100);
246
+ }
247
+ }
248
+ setTimeout(tryNavigate, 300);
249
+ }
250
+
251
+ function resolveSlideId(contentWindow, slideIdOrIndex) {
252
+ // ... same implementation as before ...
253
+ const asNumber = Number(slideIdOrIndex);
254
+ if (!isNaN(asNumber) && Number.isInteger(asNumber) && asNumber >= 0) {
255
+ if (contentWindow.CourseCode?.NavigationActions?.getAllSlides) {
256
+ const slides = contentWindow.CourseCode.NavigationActions.getAllSlides();
257
+ if (slides && slides[asNumber]) return slides[asNumber].id;
258
+ }
259
+ return null;
260
+ }
261
+ return slideIdOrIndex;
262
+ }
263
+
264
+ // =============================================================================
265
+ // UTILS
266
+ // =============================================================================
267
+
268
+ function doReset() {
269
+ window.location.href = '/__reset';
270
+ }
271
+
272
+ function setupOutsideClickListener(panels) {
273
+ // Standard click-outside for clicks in parent document
274
+ document.addEventListener('click', (e) => {
275
+ const visiblePanel = Object.values(panels).find(p => p.el && p.el.classList.contains('visible'));
276
+ if (visiblePanel) {
277
+ const clickedInPanel = e.target.closest('#' + visiblePanel.el.id);
278
+ const clickedToggle = e.target.closest('button[id*="-btn"]');
279
+ const clickedHeader = e.target.closest('#stub-player-header');
280
+
281
+ if (!clickedInPanel && !clickedToggle && !clickedHeader) {
282
+ visiblePanel.el.classList.remove('visible');
283
+ }
284
+ }
285
+ });
286
+
287
+ // Iframe focus detection - when user clicks inside iframe, it gets focus
288
+ // This handles the case where the iframe covers most of the viewport
289
+ const iframe = document.getElementById('stub-player-course-frame');
290
+ if (iframe) {
291
+ // Use focusin on window to detect when iframe gets focus
292
+ window.addEventListener('blur', () => {
293
+ // Window loses focus when iframe gains it
294
+ setTimeout(() => {
295
+ if (document.activeElement === iframe) {
296
+ // User clicked inside iframe - close all panels
297
+ Object.values(panels).forEach(p => {
298
+ if (p.el) p.el.classList.remove('visible');
299
+ });
300
+ }
301
+ }, 0);
302
+ });
303
+ }
304
+ }
305
+
306
+ function setupEditMode() {
307
+ createEditModeHandlers({
308
+ getCmiData: () => cmiData
309
+ });
310
+
311
+ // Initialize the popup interaction editor
312
+ initInteractionEditor({
313
+ onSave: () => {
314
+ // Refresh the course iframe after saving an interaction
315
+ const frame = document.getElementById('stub-player-course-frame');
316
+ if (frame && frame.contentWindow) {
317
+ frame.contentWindow.location.reload();
318
+ }
319
+ }
320
+ });
321
+ }
322
+
323
+ function attachConsoleInterceptors(win) {
324
+ const output = win.console;
325
+ const origError = output.error;
326
+ const origWarn = output.warn;
327
+
328
+ output.error = function (...args) {
329
+ const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
330
+ logError('Course Error', msg.substring(0, 500), '', false);
331
+ origError.apply(output, args);
332
+ };
333
+
334
+ output.warn = function (...args) {
335
+ const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ');
336
+ const isCV = msg.includes('COURSE VALIDATION');
337
+ logError('Course Warning', msg.substring(0, isCV ? 10000 : 500), '', true);
338
+ origWarn.apply(output, args);
339
+ };
340
+
341
+ win.addEventListener('error', function (e) {
342
+ logError('Course Uncaught', e.message || 'Unknown', e.filename ? e.filename.split('/').pop() + ':' + e.lineno : '', false);
343
+ });
344
+ }
345
+
346
+ function attachXapiListener(win) {
347
+ function tryAttach() {
348
+ const fw = win.CourseCode;
349
+ if (fw && fw.eventBus) {
350
+ fw.eventBus.on('xapi:statement', logXapiStatement);
351
+ // Also listen for navigation to trigger config panel update
352
+ fw.eventBus.on('navigation:change', (data) => {
353
+ if (data.to && window.__refreshSlideTab) window.__refreshSlideTab();
354
+ });
355
+ return true;
356
+ }
357
+ return false;
358
+ }
359
+
360
+ if (!tryAttach()) {
361
+ let retries = 0;
362
+ const iv = setInterval(() => {
363
+ if (tryAttach() || ++retries > 20) clearInterval(iv);
364
+ }, 200);
365
+ }
366
+ }
367
+
368
+
369
+ // Start
370
+ init();