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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- package/template/vite.config.js +339 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
// NOTE: Both SCORM 1.2 and SCORM 2004 drivers use pipwerks wrapper (dynamically imported)
|
|
2
|
+
// cmi5 uses @xapi/cmi5. No script loading needed here.
|
|
3
|
+
|
|
4
|
+
// Import core framework modules
|
|
5
|
+
import { eventBus } from './core/event-bus.js';
|
|
6
|
+
|
|
7
|
+
import * as CourseHelpers from './utilities/course-helpers.js';
|
|
8
|
+
import { createViewManager } from './utilities/view-manager.js';
|
|
9
|
+
import { courseConfig } from '../../course/course-config.js';
|
|
10
|
+
import { customIcons } from '../../course/icons.js';
|
|
11
|
+
|
|
12
|
+
// Import the central interaction type catalog (auto-discovers built-in + custom interactions)
|
|
13
|
+
import { getCreator, getRegisteredTypes } from './core/interaction-catalog.js';
|
|
14
|
+
|
|
15
|
+
// Import managers
|
|
16
|
+
import stateManager from './state/index.js';
|
|
17
|
+
|
|
18
|
+
import objectiveManager from './managers/objective-manager.js';
|
|
19
|
+
import interactionManager from './managers/interaction-manager.js';
|
|
20
|
+
import interactionRegistry from './managers/interaction-registry.js';
|
|
21
|
+
import * as AssessmentManager from './managers/assessment-manager.js';
|
|
22
|
+
import flagManager from './managers/flag-manager.js';
|
|
23
|
+
import accessibilityManager from './managers/accessibility-manager.js';
|
|
24
|
+
import engagementManager from './engagement/engagement-manager.js';
|
|
25
|
+
|
|
26
|
+
import commentManager from './managers/comment-manager.js';
|
|
27
|
+
import audioManager from './managers/audio-manager.js';
|
|
28
|
+
import videoManager from './managers/video-manager.js';
|
|
29
|
+
import * as NavigationActions from './navigation/NavigationActions.js';
|
|
30
|
+
import * as DocumentGallery from './navigation/document-gallery.js';
|
|
31
|
+
import * as AppState from './app/AppState.js';
|
|
32
|
+
import * as AppUI from './app/AppUI.js';
|
|
33
|
+
import * as AppActions from './app/AppActions.js';
|
|
34
|
+
|
|
35
|
+
// Interaction creators are auto-discovered via interaction-catalog.js
|
|
36
|
+
// No explicit imports needed - use getCreator('type') or window.CourseCode.createTypeQuestion
|
|
37
|
+
|
|
38
|
+
// Import UI components (programmatic APIs only - initialization handled by component-catalog)
|
|
39
|
+
import * as Modal from './components/ui-components/modal.js';
|
|
40
|
+
import * as AudioPlayer from './components/ui-components/audio-player.js';
|
|
41
|
+
import { announceToScreenReader } from './components/ui-components/index.js';
|
|
42
|
+
import { showNotification } from './components/ui-components/notifications.js';
|
|
43
|
+
import { updateProgress } from './components/ui-components/progress.js';
|
|
44
|
+
|
|
45
|
+
// Import utilities
|
|
46
|
+
import { ScrollTracker } from './utilities/scroll-tracker.js';
|
|
47
|
+
import { logger } from './utilities/logger.js';
|
|
48
|
+
import { iconManager } from './utilities/icons.js';
|
|
49
|
+
import { breakpointManager } from './utilities/breakpoint-manager.js';
|
|
50
|
+
import { initErrorReporter } from './utilities/error-reporter.js';
|
|
51
|
+
import { initDataReporter } from './utilities/data-reporter.js';
|
|
52
|
+
import { initCourseChannel } from './utilities/course-channel.js';
|
|
53
|
+
import { canvasSlide } from './utilities/canvas-slide.js';
|
|
54
|
+
|
|
55
|
+
// Expose framework modules globally IMMEDIATELY for bundled course slides
|
|
56
|
+
// This MUST happen before any slide code executes (which happens during glob import)
|
|
57
|
+
window.logger = logger;
|
|
58
|
+
window.CourseCode = {
|
|
59
|
+
// Managers
|
|
60
|
+
stateManager,
|
|
61
|
+
objectiveManager,
|
|
62
|
+
interactionManager,
|
|
63
|
+
interactionRegistry,
|
|
64
|
+
AssessmentManager,
|
|
65
|
+
flagManager,
|
|
66
|
+
accessibilityManager,
|
|
67
|
+
commentManager,
|
|
68
|
+
audioManager,
|
|
69
|
+
videoManager,
|
|
70
|
+
scoreManager: null, // Will be set during initialization if scoring is configured
|
|
71
|
+
|
|
72
|
+
// Actions
|
|
73
|
+
NavigationActions,
|
|
74
|
+
AppActions,
|
|
75
|
+
AppState,
|
|
76
|
+
|
|
77
|
+
// Interaction creators (dynamically from catalog - includes built-in + custom)
|
|
78
|
+
...Object.fromEntries(
|
|
79
|
+
getRegisteredTypes()
|
|
80
|
+
.filter(type => type !== 'multiple-choice-single') // Skip alias
|
|
81
|
+
.map(type => {
|
|
82
|
+
const pascalName = type.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
83
|
+
.replace(/^[a-z]/, c => c.toUpperCase());
|
|
84
|
+
return [`create${pascalName}Question`, getCreator(type)];
|
|
85
|
+
})
|
|
86
|
+
),
|
|
87
|
+
|
|
88
|
+
// UI components (programmatic APIs)
|
|
89
|
+
Modal,
|
|
90
|
+
announceToScreenReader,
|
|
91
|
+
showNotification,
|
|
92
|
+
updateProgress,
|
|
93
|
+
|
|
94
|
+
// Utilities
|
|
95
|
+
iconManager,
|
|
96
|
+
breakpointManager,
|
|
97
|
+
canvasSlide,
|
|
98
|
+
|
|
99
|
+
// Core
|
|
100
|
+
eventBus,
|
|
101
|
+
courseConfig
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// --- Conditional Automation Module Loading ---
|
|
105
|
+
// The automation API is ONLY loaded when explicitly enabled via course config.
|
|
106
|
+
// During production builds (vite build), Vite replaces import.meta.env.MODE with 'production'
|
|
107
|
+
// and tree-shaking removes this entire block if the condition is always false.
|
|
108
|
+
//
|
|
109
|
+
// Safety: When import.meta.env is undefined (non-Vite environments), we allow automation
|
|
110
|
+
// only if explicitly enabled in config. This supports SCORM desktop testing apps.
|
|
111
|
+
const buildMode = import.meta?.env?.MODE;
|
|
112
|
+
const isProductionBuild = buildMode === 'production';
|
|
113
|
+
const automationEnabled = courseConfig.environment?.automation?.enabled === true;
|
|
114
|
+
|
|
115
|
+
// Store automation initialization promise for coordination
|
|
116
|
+
let automationInitPromise = null;
|
|
117
|
+
|
|
118
|
+
// Only load automation if:
|
|
119
|
+
// 1. NOT a production build AND
|
|
120
|
+
// 2. Explicitly enabled in course config
|
|
121
|
+
if (!isProductionBuild && automationEnabled) {
|
|
122
|
+
logger.debug('[Framework] Automation mode enabled (MODE:', buildMode || 'undefined', ')');
|
|
123
|
+
|
|
124
|
+
// Dynamic import ensures the automation code is loaded on-demand
|
|
125
|
+
// Store the promise so initializeCourseApplication can wait for it
|
|
126
|
+
automationInitPromise = import('./automation/index.js').then(({ initializeAutomation }) => {
|
|
127
|
+
initializeAutomation();
|
|
128
|
+
logger.debug('[Framework] Automation initialization complete');
|
|
129
|
+
}).catch(error => {
|
|
130
|
+
logger.error('[Framework] Failed to load automation module:', error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Global Form Submission Guard ---
|
|
135
|
+
// This listener prevents accidental form submissions, which cause a full page reload
|
|
136
|
+
// and lead to SCORM re-initialization errors (error 103). This is a critical
|
|
137
|
+
// safeguard for the single-page application architecture of the course.
|
|
138
|
+
window.addEventListener('submit', (event) => {
|
|
139
|
+
// Prevent the default submission behavior that reloads the page.
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
|
|
142
|
+
const form = event.target;
|
|
143
|
+
const submitter = event.submitter;
|
|
144
|
+
|
|
145
|
+
// Construct submitter context for developer debugging.
|
|
146
|
+
let submitterInfo = 'N/A (Submission not triggered by a button)';
|
|
147
|
+
if (submitter) {
|
|
148
|
+
const type = submitter.getAttribute('type');
|
|
149
|
+
const tag = submitter.tagName.toLowerCase();
|
|
150
|
+
submitterInfo = `Tag: <${tag}>, Type: "${type || 'submit (default)'}", Text: "${submitter.textContent.trim()}"`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// preventDefault() already blocks the dangerous action above.
|
|
154
|
+
// logger.fatal throws in DEV for visibility, logs warning in PROD to avoid crashing.
|
|
155
|
+
logger.fatal('Form submission blocked to prevent SCORM data corruption.', {
|
|
156
|
+
domain: 'framework',
|
|
157
|
+
operation: 'formSubmissionGuard',
|
|
158
|
+
form: form.id || '(no id)',
|
|
159
|
+
submitter: submitterInfo,
|
|
160
|
+
fix: 'Ensure all <button> elements inside <form> have type="button". Use data-action pattern instead of form submissions.'
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// --- Global Anchor Tag Click Guard ---
|
|
165
|
+
// This listener prevents accidental navigation from <a> tags, which would also
|
|
166
|
+
// cause a page reload and corrupt the SCORM session.
|
|
167
|
+
window.addEventListener('click', (event) => {
|
|
168
|
+
const anchor = event.target.closest('a');
|
|
169
|
+
|
|
170
|
+
// If the click was not on an anchor tag, do nothing.
|
|
171
|
+
if (!anchor) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Allow links designed to open in a new tab.
|
|
176
|
+
if (anchor.target === '_blank') {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Allow lightbox triggers - they handle their own click behavior
|
|
181
|
+
if (anchor.dataset.component === 'lightbox') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get the href to check its value.
|
|
186
|
+
const href = anchor.getAttribute('href');
|
|
187
|
+
|
|
188
|
+
// Allow hash-only anchors (in-page scrolling, e.g., #features)
|
|
189
|
+
if (href && href.startsWith('#')) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Allow links inside lightbox containers (e.g., markdown content links)
|
|
194
|
+
if (anchor.closest('.lightbox-markdown') || anchor.closest('.lightbox-content')) {
|
|
195
|
+
// For external links, force open in new tab for safety
|
|
196
|
+
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// If the href is empty, '#' or a real URL, and it's not a new tab, it's a problem.
|
|
204
|
+
// We prevent the default action to stop the navigation/reload.
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
|
|
207
|
+
// preventDefault() already blocks the dangerous action above.
|
|
208
|
+
// logger.fatal throws in DEV for visibility, logs warning in PROD to avoid crashing.
|
|
209
|
+
logger.fatal('Anchor tag navigation blocked to prevent SCORM data corruption.', {
|
|
210
|
+
domain: 'framework',
|
|
211
|
+
operation: 'anchorClickGuard',
|
|
212
|
+
href: href || '(not set)',
|
|
213
|
+
text: anchor.textContent.trim(),
|
|
214
|
+
fix: 'Use NavigationActions.goToSlide(slideId) for internal navigation, or <button type="button"> with data-action pattern.'
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
function reportInitializationError(error) {
|
|
219
|
+
// Store error in AppState if initialized (defensive - AppState might not be initialized yet)
|
|
220
|
+
try {
|
|
221
|
+
if (AppState.isInitialized()) {
|
|
222
|
+
AppState.setInitializationError(error);
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// AppState not initialized, that's ok - initialization failed early
|
|
226
|
+
if (import.meta.env.DEV) {
|
|
227
|
+
logger.debug('AppState not initialized during error reporting:', e.message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if this is a user-facing error (e.g., expired session)
|
|
232
|
+
// These should be shown to the user but NOT reported to error tracking
|
|
233
|
+
const isUserFacing = error.userFacing === true;
|
|
234
|
+
|
|
235
|
+
// Report via unified logger (unless user-facing)
|
|
236
|
+
if (!isUserFacing) {
|
|
237
|
+
logger.error(error.message, { domain: 'initialization', operation: 'initializeCourseApplication', stack: error.stack });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Determine error type for appropriate messaging
|
|
241
|
+
const isSessionExpired = error.isSessionExpired === true;
|
|
242
|
+
const errorTitle = isSessionExpired ? 'Session Expired' : 'Initialization Error';
|
|
243
|
+
const errorMessage = isSessionExpired
|
|
244
|
+
? error.message // Already user-friendly
|
|
245
|
+
: `Failed to initialize course: ${error.message}`;
|
|
246
|
+
const actionMessage = isSessionExpired
|
|
247
|
+
? 'Close this window and launch the course again from your learning portal.'
|
|
248
|
+
: 'Please refresh the page.';
|
|
249
|
+
|
|
250
|
+
// Try to use the error modal if AppUI is initialized
|
|
251
|
+
// Otherwise fall back to inline HTML for early initialization errors
|
|
252
|
+
try {
|
|
253
|
+
if (AppUI.showErrorModal) {
|
|
254
|
+
AppUI.showErrorModal({
|
|
255
|
+
title: errorTitle,
|
|
256
|
+
message: errorMessage,
|
|
257
|
+
details: import.meta.env.DEV && !isSessionExpired ? error.stack : null,
|
|
258
|
+
showRefresh: !isSessionExpired, // Don't show refresh for expired sessions
|
|
259
|
+
showClose: false
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
} catch (_e) {
|
|
264
|
+
// Modal system not available, fall back to inline HTML
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fallback: Inline HTML for early errors before modal system is ready
|
|
268
|
+
const supportEmail = courseConfig.support?.email;
|
|
269
|
+
const supportHtml = !isSessionExpired && supportEmail
|
|
270
|
+
? `<p>If the problem persists, contact support at <a href="mailto:${supportEmail}">${supportEmail}</a>.</p>`
|
|
271
|
+
: !isSessionExpired ? '<p>If the problem persists, contact support.</p>' : '';
|
|
272
|
+
|
|
273
|
+
// Use info styling for session expired (expected behavior), error for real errors
|
|
274
|
+
const calloutClass = isSessionExpired ? 'callout-info' : 'callout-danger';
|
|
275
|
+
|
|
276
|
+
const content = document.getElementById('content');
|
|
277
|
+
if (content) {
|
|
278
|
+
content.innerHTML = `
|
|
279
|
+
<div class="p-6 callout ${calloutClass}" role="alert" aria-live="assertive">
|
|
280
|
+
<h2>${errorTitle}</h2>
|
|
281
|
+
<p>${errorMessage}</p>
|
|
282
|
+
<p>${actionMessage}</p>
|
|
283
|
+
${supportHtml}
|
|
284
|
+
</div>
|
|
285
|
+
`;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Attempt to resize the browser window to configured dimensions.
|
|
291
|
+
* Only works for popup windows - browsers block resize for main windows.
|
|
292
|
+
* Disabled when environment.autoResizeWindow is false.
|
|
293
|
+
*/
|
|
294
|
+
function resizeWindowToConfig() {
|
|
295
|
+
const autoResize = courseConfig.environment?.autoResizeWindow;
|
|
296
|
+
|
|
297
|
+
// Skip if explicitly disabled
|
|
298
|
+
if (autoResize === false) {
|
|
299
|
+
logger.debug('[CourseInit] Window auto-resize disabled by config');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Get dimensions from config object or use defaults
|
|
304
|
+
const width = typeof autoResize === 'object' ? autoResize.width : 1024;
|
|
305
|
+
const height = typeof autoResize === 'object' ? autoResize.height : 768;
|
|
306
|
+
|
|
307
|
+
if (!width || !height) return;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
// Check if we're in a popup window (opener exists) or undersized window
|
|
311
|
+
const isPopup = window.opener !== null ||
|
|
312
|
+
(window.outerWidth < width || window.outerHeight < height);
|
|
313
|
+
|
|
314
|
+
if (isPopup) {
|
|
315
|
+
window.resizeTo(width, height);
|
|
316
|
+
// Center the window on screen after resize
|
|
317
|
+
const left = Math.max(0, (screen.width - width) / 2);
|
|
318
|
+
const top = Math.max(0, (screen.height - height) / 2);
|
|
319
|
+
window.moveTo(left, top);
|
|
320
|
+
logger.debug(`[CourseInit] Resized window to ${width}x${height}`);
|
|
321
|
+
}
|
|
322
|
+
} catch (_e) {
|
|
323
|
+
// Browser may block resize - this is expected for security
|
|
324
|
+
logger.debug('[CourseInit] Window resize blocked by browser (expected for non-popup windows)');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Apply theme variant tokens as data attributes on <html>
|
|
330
|
+
*
|
|
331
|
+
* This bridges CSS custom properties to data attributes, enabling themes to configure
|
|
332
|
+
* global component styles (tabs, accordions, cards, etc.) via CSS tokens.
|
|
333
|
+
*
|
|
334
|
+
* Themes set tokens like: --tab-style: pills;
|
|
335
|
+
* This function reads them and applies: data-tab-style="pills" on <html>
|
|
336
|
+
*
|
|
337
|
+
* HTML-level overrides take precedence (existing data attributes are preserved).
|
|
338
|
+
*/
|
|
339
|
+
function applyThemeVariants() {
|
|
340
|
+
const html = document.documentElement;
|
|
341
|
+
const styles = getComputedStyle(html);
|
|
342
|
+
|
|
343
|
+
// Apply course layout from config (before theme variants)
|
|
344
|
+
// Layouts: 'article' (default), 'traditional', 'focused', 'presentation', 'canvas'
|
|
345
|
+
const layout = courseConfig.layout || 'article';
|
|
346
|
+
if (!html.hasAttribute('data-layout')) {
|
|
347
|
+
html.setAttribute('data-layout', layout);
|
|
348
|
+
logger.debug(`[Layout] Applied data-layout="${layout}" from course config`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Apply sidebar enabled state from config
|
|
352
|
+
// For 'traditional' layout, sidebar is always enabled
|
|
353
|
+
// For other layouts, it's controlled by navigation.sidebar.enabled
|
|
354
|
+
const sidebarEnabled = layout === 'traditional'
|
|
355
|
+
? true
|
|
356
|
+
: (courseConfig.navigation?.sidebar?.enabled ?? false);
|
|
357
|
+
html.setAttribute('data-sidebar-enabled', sidebarEnabled ? 'true' : 'false');
|
|
358
|
+
logger.debug(`[Layout] Applied data-sidebar-enabled="${sidebarEnabled}" from course config`);
|
|
359
|
+
|
|
360
|
+
// Nav button visibility — traditional layout always shows buttons
|
|
361
|
+
const showNavButtons = layout === 'traditional'
|
|
362
|
+
? true
|
|
363
|
+
: (courseConfig.navigation?.footer?.showButtons ?? true);
|
|
364
|
+
html.setAttribute('data-nav-buttons', showNavButtons ? 'true' : 'false');
|
|
365
|
+
logger.debug(`[Layout] Applied data-nav-buttons="${showNavButtons}" from course config`);
|
|
366
|
+
|
|
367
|
+
// Header visibility — canvas layout always hides header
|
|
368
|
+
const headerEnabled = layout === 'canvas'
|
|
369
|
+
? false
|
|
370
|
+
: (courseConfig.navigation?.header?.enabled ?? true);
|
|
371
|
+
html.setAttribute('data-header-enabled', headerEnabled ? 'true' : 'false');
|
|
372
|
+
logger.debug(`[Layout] Applied data-header-enabled="${headerEnabled}" from course config`);
|
|
373
|
+
|
|
374
|
+
// Map of CSS token names to their corresponding data attribute names
|
|
375
|
+
const variantTokens = [
|
|
376
|
+
{ token: '--tab-style', attr: 'data-tab-style' },
|
|
377
|
+
{ token: '--accordion-style', attr: 'data-accordion-style' },
|
|
378
|
+
{ token: '--button-shape', attr: 'data-button-shape' },
|
|
379
|
+
{ token: '--card-style', attr: 'data-card-style' },
|
|
380
|
+
{ token: '--callout-style', attr: 'data-callout-style' },
|
|
381
|
+
{ token: '--header-style', attr: 'data-header-style' },
|
|
382
|
+
{ token: '--sidebar-style', attr: 'data-sidebar-style' },
|
|
383
|
+
{ token: '--footer-style', attr: 'data-footer-style' }
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
for (const { token, attr } of variantTokens) {
|
|
387
|
+
// Only apply if not already set in HTML (HTML overrides theme)
|
|
388
|
+
if (!html.hasAttribute(attr)) {
|
|
389
|
+
const value = styles.getPropertyValue(token).trim();
|
|
390
|
+
if (value) {
|
|
391
|
+
html.setAttribute(attr, value);
|
|
392
|
+
logger.debug(`[ThemeVariants] Applied ${attr}="${value}" from theme token`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function initializeCourseApplication() {
|
|
399
|
+
logger.debug('[CourseInit] Initializing course modules...');
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
// 0. Set document title and description from course config
|
|
403
|
+
if (courseConfig.metadata?.title) {
|
|
404
|
+
document.title = courseConfig.metadata.title;
|
|
405
|
+
const titleElement = document.getElementById('page-title');
|
|
406
|
+
if (titleElement) titleElement.textContent = courseConfig.metadata.title;
|
|
407
|
+
}
|
|
408
|
+
if (courseConfig.metadata?.description) {
|
|
409
|
+
const descElement = document.getElementById('page-description');
|
|
410
|
+
if (descElement) descElement.setAttribute('content', courseConfig.metadata.description);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 0a. Initialize error reporter (if configured) - must be early to catch init errors
|
|
414
|
+
initErrorReporter(courseConfig);
|
|
415
|
+
|
|
416
|
+
// 0b. Initialize data reporter (if configured) - must be early to capture all events
|
|
417
|
+
initDataReporter(courseConfig);
|
|
418
|
+
|
|
419
|
+
// 0c. Initialize course channel (if configured) - pub/sub transport for course-to-course comms
|
|
420
|
+
initCourseChannel(courseConfig);
|
|
421
|
+
|
|
422
|
+
// 0d. Validate access control (for external hosting / multi-tenant CDN)
|
|
423
|
+
// This MUST run early before any LMS initialization to reject unauthorized clients
|
|
424
|
+
if (courseConfig.accessControl?.clients) {
|
|
425
|
+
const { validateAccess, showUnauthorizedScreen } = await import('./utilities/access-control.js');
|
|
426
|
+
const accessResult = validateAccess();
|
|
427
|
+
if (!accessResult.valid) {
|
|
428
|
+
logger.warn('[AccessControl] Access denied:', accessResult.error);
|
|
429
|
+
showUnauthorizedScreen(accessResult.error);
|
|
430
|
+
return; // Halt initialization
|
|
431
|
+
}
|
|
432
|
+
if (accessResult.clientId) {
|
|
433
|
+
logger.debug(`[AccessControl] Client authorized: ${accessResult.clientId}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 0e. Initialize breakpoint manager (must be early - before components render)
|
|
438
|
+
// Applies responsive .bp-* classes to <html> based on viewport width
|
|
439
|
+
breakpointManager.init();
|
|
440
|
+
|
|
441
|
+
// 0f. Attempt to resize window to configured dimensions (LMS popup windows)
|
|
442
|
+
resizeWindowToConfig();
|
|
443
|
+
|
|
444
|
+
// 0g. Apply theme variant tokens as data attributes (before components render)
|
|
445
|
+
applyThemeVariants();
|
|
446
|
+
|
|
447
|
+
// 0h. Register custom icons
|
|
448
|
+
if (customIcons) {
|
|
449
|
+
iconManager.registerAll(customIcons);
|
|
450
|
+
logger.debug('[IconManager] Registered custom icons');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 0i. Run static validation in development mode BEFORE any initialization
|
|
454
|
+
// Uses __DEV__ (replaced at build time) instead of runtime check to enable
|
|
455
|
+
// tree-shaking - Rollup sees `if (false)` in prod and eliminates the entire block
|
|
456
|
+
// The typeof check prevents ReferenceError if __DEV__ wasn't defined at build time
|
|
457
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
458
|
+
try {
|
|
459
|
+
const { lintCourse } = await import('./dev/runtime-linter.js');
|
|
460
|
+
await lintCourse(courseConfig);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
// Linter throws with formatted error message - show it and halt
|
|
463
|
+
reportInitializationError(error);
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 0j. Wait for automation to initialize (if enabled) before proceeding
|
|
469
|
+
// This ensures interactions can register when they're created
|
|
470
|
+
if (automationInitPromise) {
|
|
471
|
+
logger.debug('[CourseInit] Waiting for automation initialization...');
|
|
472
|
+
await automationInitPromise;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 1. LMS connection (required - no fallback)
|
|
476
|
+
// Handles driver init, lifecycle handlers, and xAPI service setup
|
|
477
|
+
stateManager.setCompatibilityMode(courseConfig.environment?.lmsCompatibilityMode || 'auto');
|
|
478
|
+
await stateManager.initializeConnection();
|
|
479
|
+
|
|
480
|
+
// 2. Set up state validation config BEFORE initializing stateManager
|
|
481
|
+
// This enables validation of stored LMS data against current course structure.
|
|
482
|
+
// In dev: throws on mismatch to catch stale data issues
|
|
483
|
+
// In prod: gracefully recovers to handle course updates
|
|
484
|
+
stateManager.setCourseValidationConfig({
|
|
485
|
+
structure: courseConfig.structure,
|
|
486
|
+
objectives: courseConfig.objectives,
|
|
487
|
+
version: courseConfig.metadata?.version
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// 3. Initialize state manager (hydrates from LMS with validation)
|
|
491
|
+
stateManager.initialize();
|
|
492
|
+
|
|
493
|
+
// 4. Initialize all managers with their configurations
|
|
494
|
+
objectiveManager.initialize(courseConfig.objectives);
|
|
495
|
+
interactionManager.initialize();
|
|
496
|
+
accessibilityManager.initialize();
|
|
497
|
+
flagManager.initialize();
|
|
498
|
+
commentManager.initialize();
|
|
499
|
+
engagementManager.initialize(courseConfig); // Pass courseConfig for requirement lookups
|
|
500
|
+
audioManager.initialize(); // Initialize audio manager for narration support
|
|
501
|
+
videoManager.initialize(); // Initialize video manager for embedded video support
|
|
502
|
+
|
|
503
|
+
// Initialize score manager (if course-level scoring is configured)
|
|
504
|
+
// Must happen AFTER objectiveManager to allow loading existing scores
|
|
505
|
+
if (courseConfig.scoring) {
|
|
506
|
+
const scoreManagerModule = await import('./managers/score-manager.js');
|
|
507
|
+
const scoreManager = scoreManagerModule.default;
|
|
508
|
+
scoreManager.initialize(courseConfig.scoring);
|
|
509
|
+
// Expose scoreManager globally for course authors
|
|
510
|
+
window.CourseCode.scoreManager = scoreManager;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 5. Initialize course helpers with the course configuration
|
|
514
|
+
CourseHelpers.init(courseConfig);
|
|
515
|
+
|
|
516
|
+
// 6. Load course structure
|
|
517
|
+
const slides = await CourseHelpers.getFlattenedSlides();
|
|
518
|
+
const menuTree = await CourseHelpers.getMenuTree();
|
|
519
|
+
const assessmentConfigs = await CourseHelpers.getAssessmentConfigs();
|
|
520
|
+
|
|
521
|
+
// 7. Initialize View Manager
|
|
522
|
+
const slideContainer = document.getElementById('slide-container');
|
|
523
|
+
if (!slideContainer) {
|
|
524
|
+
throw new Error('Framework error: #slide-container not found.');
|
|
525
|
+
}
|
|
526
|
+
const viewManager = createViewManager(slideContainer, 'main');
|
|
527
|
+
|
|
528
|
+
// Register all slides as views
|
|
529
|
+
slides.forEach(slide => {
|
|
530
|
+
const component = slide.component;
|
|
531
|
+
|
|
532
|
+
// REQUIRED: Validate engagement config exists in structure
|
|
533
|
+
if (!slide.engagement) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`Slide "${slide.id}" missing required 'engagement' configuration in course-config.js structure. ` +
|
|
536
|
+
'Add "engagement: { required: false }" to the slide definition in courseConfig.structure.'
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
viewManager.registerView(slide.id, {
|
|
541
|
+
render: async (options) => {
|
|
542
|
+
// 1. Clear registry and initialize engagement for the new slide
|
|
543
|
+
interactionRegistry.clear();
|
|
544
|
+
engagementManager.initSlide(slide.id, slide.engagement);
|
|
545
|
+
|
|
546
|
+
const renderContext = {
|
|
547
|
+
...options,
|
|
548
|
+
slideId: slide.id,
|
|
549
|
+
title: slide.title
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// 2. The slide's render function is now responsible for creating and returning its own element
|
|
553
|
+
let slideElement;
|
|
554
|
+
try {
|
|
555
|
+
slideElement = await component.render(null, renderContext);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
// Add slide context to error message
|
|
558
|
+
throw new Error(`Slide "${slide.id}" render() failed: ${err.message}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!slideElement) {
|
|
562
|
+
throw new Error(`Slide "${slide.id}" render() returned null/undefined. Must return a DOM element.`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 3. Declarative UI components will be initialized by ViewManager after render
|
|
566
|
+
// (Removed duplicate call here - ViewManager handles it in view-manager.js:84)
|
|
567
|
+
|
|
568
|
+
// 4. Finalize the interaction registry now that rendering is complete
|
|
569
|
+
interactionRegistry.setReady();
|
|
570
|
+
|
|
571
|
+
// 5. Return the element created by the slide
|
|
572
|
+
return slideElement;
|
|
573
|
+
},
|
|
574
|
+
onShow: (element, options) => {
|
|
575
|
+
// Original onShow logic
|
|
576
|
+
const tracker = new ScrollTracker('main#content', slide.id);
|
|
577
|
+
element._scrollTracker = tracker;
|
|
578
|
+
if (component.onShow) component.onShow(element, options);
|
|
579
|
+
},
|
|
580
|
+
onHide: (element) => {
|
|
581
|
+
// Cleanup scroll tracker
|
|
582
|
+
if (element._scrollTracker) {
|
|
583
|
+
element._scrollTracker.destroy();
|
|
584
|
+
element._scrollTracker = null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Cleanup engagement before hiding
|
|
588
|
+
engagementManager.cleanupSlide(slide.id);
|
|
589
|
+
if (component.onHide) component.onHide(element);
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// 8. Initialize the main app controller and UI
|
|
595
|
+
AppState.initAppState();
|
|
596
|
+
AppUI.initAppUI();
|
|
597
|
+
AppActions.initAppActions();
|
|
598
|
+
AudioPlayer.setup(); // Initialize audio player UI in footer
|
|
599
|
+
|
|
600
|
+
// Listen for view changes to log them
|
|
601
|
+
eventBus.on('view:change', ({ view, context }) => {
|
|
602
|
+
logger.debug(`[ViewManager] View changed to '${view}'`, context);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Listen for navigation changes to load/unload slide audio
|
|
606
|
+
// IMPORTANT: Must be registered BEFORE NavigationActions.init() to catch first slide
|
|
607
|
+
let slideAudioCompletedHandler = null;
|
|
608
|
+
|
|
609
|
+
eventBus.on('navigation:changed', async ({ toSlideId }) => {
|
|
610
|
+
// Clean up previous slide's audio completion listener
|
|
611
|
+
if (slideAudioCompletedHandler) {
|
|
612
|
+
eventBus.off('audio:completed', slideAudioCompletedHandler);
|
|
613
|
+
slideAudioCompletedHandler = null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Find the slide configuration
|
|
617
|
+
const slide = slides.find(s => s.id === toSlideId);
|
|
618
|
+
if (!slide) return;
|
|
619
|
+
|
|
620
|
+
// Check if slide has audio configuration
|
|
621
|
+
if (slide.audio && slide.audio.src) {
|
|
622
|
+
try {
|
|
623
|
+
await audioManager.load(slide.audio, toSlideId, 'slide');
|
|
624
|
+
logger.debug(`[AudioManager] Loaded audio for slide: ${toSlideId}`);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
logger.error(`[AudioManager] Failed to load audio for slide ${toSlideId}:`, error);
|
|
627
|
+
// Continue - don't let audio errors break navigation
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Check if slideAudioComplete is required in engagement config
|
|
631
|
+
// This is the new pattern - audio gating is configured via engagement requirements
|
|
632
|
+
const hasSlideAudioRequirement = slide.engagement?.requirements?.some(
|
|
633
|
+
req => req.type === 'slideAudioComplete'
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
if (hasSlideAudioRequirement) {
|
|
637
|
+
slideAudioCompletedHandler = ({ contextId }) => {
|
|
638
|
+
if (contextId === toSlideId) {
|
|
639
|
+
engagementManager.trackSlideAudioComplete(toSlideId);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
eventBus.on('audio:completed', slideAudioCompletedHandler);
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
// No audio for this slide - unload current audio
|
|
646
|
+
audioManager.unload();
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Unload audio before navigating away from a slide
|
|
651
|
+
eventBus.on('navigation:beforeChange', () => {
|
|
652
|
+
// Save position and pause (don't fully unload yet - navigation:changed will handle)
|
|
653
|
+
if (audioManager.hasAudio()) {
|
|
654
|
+
audioManager.pause();
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// 9. Initialize Navigation (this will trigger the first slide load)
|
|
659
|
+
await NavigationActions.init(slides, viewManager, menuTree, assessmentConfigs);
|
|
660
|
+
|
|
661
|
+
// 10. Initialize Document Gallery (after navigation renders the menu)
|
|
662
|
+
await DocumentGallery.init(courseConfig);
|
|
663
|
+
|
|
664
|
+
// 11. Initialize Breadcrumbs (after navigation to catch first slide)
|
|
665
|
+
const { init: initBreadcrumbs } = await import('./navigation/Breadcrumbs.js');
|
|
666
|
+
initBreadcrumbs();
|
|
667
|
+
|
|
668
|
+
logger.debug('[CourseInit] Initialization complete');
|
|
669
|
+
|
|
670
|
+
// Signal to automation consumers (headless browser) that the framework is fully ready
|
|
671
|
+
if (window.CourseCodeAutomation) {
|
|
672
|
+
window.CourseCodeAutomation.ready = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Hide loading indicator
|
|
676
|
+
AppUI.hideLoadingIndicator();
|
|
677
|
+
|
|
678
|
+
// --- Layer 3: SCORM Best Practice - Passive Beforeunload Warning ---
|
|
679
|
+
// This listener provides a warning but does not block reloads.
|
|
680
|
+
// It is enabled by default in production and disabled by default in development.
|
|
681
|
+
window.addEventListener('beforeunload', (event) => {
|
|
682
|
+
const isProduction = import.meta?.env?.MODE === 'production';
|
|
683
|
+
let guardEnabled = isProduction; // ON in production, OFF in development by default
|
|
684
|
+
|
|
685
|
+
// Allow course config to explicitly override the default
|
|
686
|
+
if (courseConfig.environment?.disableBeforeUnloadGuard === true) {
|
|
687
|
+
guardEnabled = false;
|
|
688
|
+
} else if (courseConfig.environment?.disableBeforeUnloadGuard === false) {
|
|
689
|
+
guardEnabled = true; // Explicitly enable it even in dev
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Automation config can also disable it
|
|
693
|
+
if (courseConfig.environment?.automation?.enabled &&
|
|
694
|
+
courseConfig.environment?.automation?.disableBeforeUnloadGuard) {
|
|
695
|
+
guardEnabled = false;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// If the guard is disabled for any reason, do nothing.
|
|
699
|
+
if (!guardEnabled) {
|
|
700
|
+
logger.debug('[Framework] Beforeunload warning is disabled.');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// If the exit is intentional (via Exit button), allow it silently.
|
|
705
|
+
if (AppState.isExitIntentional()) {
|
|
706
|
+
logger.debug('[Framework] Intentional exit detected, allowing page unload.');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// For unintentional exits (F5, browser close, etc.), trigger the browser's native confirmation dialog.
|
|
711
|
+
event.preventDefault();
|
|
712
|
+
event.returnValue = ''; // Required by modern browsers to trigger the dialog.
|
|
713
|
+
logger.warn('[Framework] Unintentional page unload detected. Showing browser confirmation.');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
} catch (error) {
|
|
717
|
+
reportInitializationError(error);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Start initialization when DOM is ready
|
|
723
|
+
if (document.readyState === 'loading') {
|
|
724
|
+
document.addEventListener('DOMContentLoaded', initializeCourseApplication);
|
|
725
|
+
} else {
|
|
726
|
+
initializeCourseApplication();
|
|
727
|
+
}
|