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