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,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file objective-manager.js
|
|
3
|
+
* @description Manages learning objectives. It is the single source of truth for
|
|
4
|
+
* all objective-related data and persists its state through the StateManager.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { deepClone } from '../utilities/utilities.js';
|
|
8
|
+
import { logger } from '../utilities/logger.js';
|
|
9
|
+
import stateManager from '../state/index.js';
|
|
10
|
+
import { eventBus } from '../core/event-bus.js';
|
|
11
|
+
|
|
12
|
+
class ObjectiveManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.objectives = {};
|
|
15
|
+
this.isInitialized = false;
|
|
16
|
+
this.earlyQueue = []; // Queue for calls made before initialization
|
|
17
|
+
this.DOMAIN_KEY = 'objectives';
|
|
18
|
+
this.SOURCE = 'objective-manager';
|
|
19
|
+
this.criteriaConfig = []; // Store objectives with criteria
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initializes the manager by loading state from the StateManager and setting up objectives from config.
|
|
24
|
+
* @param {Array<object>} [objectivesConfig] - Optional array of objective configurations from course-config.js
|
|
25
|
+
* @throws {Error} If already initialized
|
|
26
|
+
*/
|
|
27
|
+
initialize(objectivesConfig = []) {
|
|
28
|
+
if (this.isInitialized) {
|
|
29
|
+
throw new Error('ObjectiveManager: Already initialized. Do not call initialize() more than once.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Build set of configured objective IDs for validation
|
|
33
|
+
const configuredIds = new Set(objectivesConfig.map(obj => obj.id));
|
|
34
|
+
|
|
35
|
+
// Load from stateManager domain (transparently routed to CMI by stateManager)
|
|
36
|
+
const storedObjectives = stateManager.getDomainState(this.DOMAIN_KEY);
|
|
37
|
+
if (storedObjectives && typeof storedObjectives === 'object') {
|
|
38
|
+
// Validate stored objectives against current config
|
|
39
|
+
const storedIds = Object.keys(storedObjectives);
|
|
40
|
+
const orphanedIds = storedIds.filter(id => !configuredIds.has(id));
|
|
41
|
+
|
|
42
|
+
if (orphanedIds.length > 0 && objectivesConfig.length > 0) {
|
|
43
|
+
// Found objectives in storage that aren't in current config
|
|
44
|
+
const message = `Found ${orphanedIds.length} stored objective(s) not in current config: ${orphanedIds.join(', ')}. ` +
|
|
45
|
+
'These may be from a previous version of the course.';
|
|
46
|
+
|
|
47
|
+
if (import.meta.env.DEV) {
|
|
48
|
+
// Dev mode: Warn about orphaned objectives (could indicate config issue)
|
|
49
|
+
logger.warn(`[ObjectiveManager] ${message}`);
|
|
50
|
+
}
|
|
51
|
+
// In both modes, we keep orphaned objectives in CMI (can't remove from LMS)
|
|
52
|
+
// but they won't affect course logic since they're not in config
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.objectives = storedObjectives;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mark the manager initialized before seeding new objectives so SCORM syncs work immediately
|
|
59
|
+
this.isInitialized = true;
|
|
60
|
+
|
|
61
|
+
// Initialize objectives from config if provided (only if they don't exist)
|
|
62
|
+
if (objectivesConfig.length > 0) {
|
|
63
|
+
objectivesConfig.forEach(objective => {
|
|
64
|
+
if (!this.objectives[objective.id]) {
|
|
65
|
+
// OPTIMIZATION: Do NOT store description - it's in course-config.js
|
|
66
|
+
this.setObjective({
|
|
67
|
+
id: objective.id,
|
|
68
|
+
completion_status: objective.initialCompletion ?? 'incomplete',
|
|
69
|
+
success_status: objective.initialSuccess ?? 'unknown',
|
|
70
|
+
score: objective.initialScore ?? null
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
logger.debug(`[ObjectiveManager] Initialized ${objectivesConfig.length} objective(s) from configuration.`);
|
|
75
|
+
|
|
76
|
+
// Enable automatic criteria tracking
|
|
77
|
+
this.enableCriteriaTracking(objectivesConfig);
|
|
78
|
+
}
|
|
79
|
+
logger.debug('[ObjectiveManager] Initialized with objectives:', this.objectives);
|
|
80
|
+
|
|
81
|
+
// Process any calls that were queued before initialization was complete
|
|
82
|
+
this._processEarlyQueue();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Processes and executes method calls that were queued before the manager was initialized.
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
_processEarlyQueue() {
|
|
90
|
+
if (this.earlyQueue.length > 0) {
|
|
91
|
+
logger.debug(`[ObjectiveManager] Processing ${this.earlyQueue.length} early-queued call(s).`);
|
|
92
|
+
this.earlyQueue.forEach(({ method, args }) => {
|
|
93
|
+
if (typeof this[method] === 'function') {
|
|
94
|
+
this[method](...args);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.earlyQueue = []; // Clear the queue
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Retrieves all objectives.
|
|
103
|
+
* @returns {Array<object>} A deep-cloned array of all objective objects.
|
|
104
|
+
*/
|
|
105
|
+
getObjectives() {
|
|
106
|
+
return deepClone(Object.values(this.objectives));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retrieves a single objective by its ID.
|
|
111
|
+
* @param {string} id - The unique identifier for the objective.
|
|
112
|
+
* @returns {object|null} A deep-cloned objective object or null if not found.
|
|
113
|
+
*/
|
|
114
|
+
getObjective(id) {
|
|
115
|
+
if (!id || !this.objectives[id]) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return deepClone(this.objectives[id]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates or updates an objective.
|
|
123
|
+
* @param {object} objectiveData - The data for the objective.
|
|
124
|
+
* @param {string} objectiveData.id - The unique identifier for the objective.
|
|
125
|
+
* @param {string} [objectiveData.success_status] - The success status ('passed', 'failed', 'unknown').
|
|
126
|
+
* @param {string} [objectiveData.completion_status] - The completion status ('completed', 'incomplete').
|
|
127
|
+
* @param {number} [objectiveData.score] - The score (0-100).
|
|
128
|
+
* @param {string} [objectiveData.description] - A description of the objective.
|
|
129
|
+
* @returns {object|undefined} The updated objective object, or undefined if queued before initialization.
|
|
130
|
+
* @throws {Error} If objectiveData is missing or id is not provided
|
|
131
|
+
*/
|
|
132
|
+
setObjective(objectiveData) {
|
|
133
|
+
if (!this.isInitialized) {
|
|
134
|
+
this.earlyQueue.push({ method: 'setObjective', args: [objectiveData] });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { id } = objectiveData;
|
|
139
|
+
if (!id) {
|
|
140
|
+
throw new Error('ObjectiveManager: setObjective requires an id.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const existing = this.objectives[id] || { id };
|
|
144
|
+
const updated = { ...existing, ...objectiveData };
|
|
145
|
+
|
|
146
|
+
this.objectives[id] = updated;
|
|
147
|
+
|
|
148
|
+
// Persist via stateManager domain (transparently routed to CMI by stateManager)
|
|
149
|
+
stateManager.setDomainState(this.DOMAIN_KEY, this.objectives, { source: this.SOURCE });
|
|
150
|
+
|
|
151
|
+
eventBus.emit('objective:updated', updated);
|
|
152
|
+
|
|
153
|
+
// Emit specific score event if score was updated (for ScoreManager)
|
|
154
|
+
if (typeof updated.score === 'number') {
|
|
155
|
+
eventBus.emit('objective:score:updated', {
|
|
156
|
+
objectiveId: id,
|
|
157
|
+
score: updated.score
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return deepClone(updated);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Valid success status values per SCORM spec.
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
static VALID_SUCCESS_STATUSES = ['passed', 'failed', 'unknown'];
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Valid completion status values per SCORM spec.
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
static VALID_COMPLETION_STATUSES = ['completed', 'incomplete'];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A helper method to specifically update the success status of an objective.
|
|
178
|
+
* @param {string} id - The objective ID.
|
|
179
|
+
* @param {string} success_status - The success status ('passed', 'failed', 'unknown').
|
|
180
|
+
* @param {number|null} [score] - An optional score to set along with the status.
|
|
181
|
+
* @throws {Error} If objective with given ID is not found
|
|
182
|
+
* @throws {Error} If success_status is not a valid value
|
|
183
|
+
*/
|
|
184
|
+
setSuccessStatus(id, success_status, score = null) {
|
|
185
|
+
if (!this.isInitialized) {
|
|
186
|
+
this.earlyQueue.push({ method: 'setSuccessStatus', args: [id, success_status, score] });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!ObjectiveManager.VALID_SUCCESS_STATUSES.includes(success_status)) {
|
|
190
|
+
throw new Error(`ObjectiveManager: Invalid success_status "${success_status}". Must be one of: ${ObjectiveManager.VALID_SUCCESS_STATUSES.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
const objective = this.getObjective(id);
|
|
193
|
+
if (!objective) {
|
|
194
|
+
throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
|
|
195
|
+
}
|
|
196
|
+
objective.success_status = success_status;
|
|
197
|
+
this.setObjective(objective);
|
|
198
|
+
if (score !== null) {
|
|
199
|
+
// Validate score through setScore's guard (which will persist separately)
|
|
200
|
+
this.setScore(id, score);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A helper method to specifically update the completion status of an objective.
|
|
206
|
+
* @param {string} id - The objective ID.
|
|
207
|
+
* @param {string} completion_status - The completion status ('completed', 'incomplete').
|
|
208
|
+
* @throws {Error} If objective with given ID is not found
|
|
209
|
+
* @throws {Error} If completion_status is not a valid value
|
|
210
|
+
*/
|
|
211
|
+
setCompletionStatus(id, completion_status) {
|
|
212
|
+
if (!this.isInitialized) {
|
|
213
|
+
this.earlyQueue.push({ method: 'setCompletionStatus', args: [id, completion_status] });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!ObjectiveManager.VALID_COMPLETION_STATUSES.includes(completion_status)) {
|
|
217
|
+
throw new Error(`ObjectiveManager: Invalid completion_status "${completion_status}". Must be one of: ${ObjectiveManager.VALID_COMPLETION_STATUSES.join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
const objective = this.getObjective(id);
|
|
220
|
+
if (!objective) {
|
|
221
|
+
throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
|
|
222
|
+
}
|
|
223
|
+
objective.completion_status = completion_status;
|
|
224
|
+
this.setObjective(objective);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A helper method to specifically update the score of an objective.
|
|
229
|
+
* @param {string} id - The objective ID.
|
|
230
|
+
* @param {number} score - The score (0-100).
|
|
231
|
+
* @throws {Error} If objective with given ID is not found
|
|
232
|
+
* @throws {Error} If score is not a number or out of range
|
|
233
|
+
*/
|
|
234
|
+
setScore(id, score) {
|
|
235
|
+
if (!this.isInitialized) {
|
|
236
|
+
this.earlyQueue.push({ method: 'setScore', args: [id, score] });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const objective = this.getObjective(id);
|
|
240
|
+
if (!objective) {
|
|
241
|
+
throw new Error(`ObjectiveManager: Objective with id "${id}" not found.`);
|
|
242
|
+
}
|
|
243
|
+
if (typeof score !== 'number' || isNaN(score) || score < 0 || score > 100) {
|
|
244
|
+
throw new Error(`ObjectiveManager: Score must be a number between 0 and 100, got ${score}`);
|
|
245
|
+
}
|
|
246
|
+
objective.score = score;
|
|
247
|
+
this.setObjective(objective);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Enables automatic criteria tracking for objectives with built-in criteria.
|
|
252
|
+
* @param {Array<object>} objectivesConfig - Array of objective configurations from course-config.js
|
|
253
|
+
*/
|
|
254
|
+
enableCriteriaTracking(objectivesConfig = []) {
|
|
255
|
+
this.criteriaConfig = objectivesConfig.filter(obj => obj.criteria);
|
|
256
|
+
|
|
257
|
+
if (this.criteriaConfig.length === 0) {
|
|
258
|
+
logger.debug('[ObjectiveManager] No objectives with built-in criteria found.');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Guard against duplicate listener registration
|
|
263
|
+
if (this._criteriaTrackingEnabled) {
|
|
264
|
+
logger.debug('[ObjectiveManager] Criteria tracking already enabled, updating config only.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this._criteriaTrackingEnabled = true;
|
|
268
|
+
|
|
269
|
+
// Listen for view changes to track slide visits
|
|
270
|
+
eventBus.on('view:change', ({ view }) => {
|
|
271
|
+
this._handleSlideVisit(view);
|
|
272
|
+
this._checkTimeBasedObjectives(view);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Listen for flag changes to track flag-based objectives
|
|
276
|
+
eventBus.on('flag:updated', ({ key, value }) => {
|
|
277
|
+
this._checkFlagBasedObjectives(key, value);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Also listen for flag removals — a removed flag is effectively undefined/falsy
|
|
281
|
+
eventBus.on('flag:removed', ({ key }) => {
|
|
282
|
+
this._checkFlagBasedObjectives(key, undefined);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
logger.debug(`[ObjectiveManager] Criteria tracking enabled for ${this.criteriaConfig.length} objective(s).`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handles slide visit event and checks objectives with visit-based criteria.
|
|
290
|
+
* @private
|
|
291
|
+
* @param {string} slideId - The ID of the visited slide
|
|
292
|
+
*/
|
|
293
|
+
_handleSlideVisit(slideId) {
|
|
294
|
+
// Get visited slides from state manager
|
|
295
|
+
const navigationState = stateManager.getDomainState('navigation');
|
|
296
|
+
const visitedSlides = navigationState?.visitedSlides || [];
|
|
297
|
+
|
|
298
|
+
// Create a new set of visited slides including the current one for this check
|
|
299
|
+
const allVisitedSlides = new Set([...visitedSlides, slideId]);
|
|
300
|
+
|
|
301
|
+
this.criteriaConfig.forEach(objective => {
|
|
302
|
+
// Skip if already completed
|
|
303
|
+
const current = this.objectives[objective.id];
|
|
304
|
+
if (current && current.completion_status === 'completed') {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { criteria } = objective;
|
|
309
|
+
|
|
310
|
+
switch (criteria.type) {
|
|
311
|
+
case 'slideVisited': {
|
|
312
|
+
if (criteria.slideId === slideId) {
|
|
313
|
+
this.setCompletionStatus(objective.id, 'completed');
|
|
314
|
+
logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: slideVisited`);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'allSlidesVisited': {
|
|
320
|
+
const requiredSlides = criteria.slideIds || [];
|
|
321
|
+
const allVisited = requiredSlides.every(sid => allVisitedSlides.has(sid));
|
|
322
|
+
if (allVisited) {
|
|
323
|
+
this.setCompletionStatus(objective.id, 'completed');
|
|
324
|
+
logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: allSlidesVisited`);
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Checks if any time-based objectives are met for a given slide.
|
|
334
|
+
* Uses centralized timing data from AppActions (stored in sessionData domain).
|
|
335
|
+
* @private
|
|
336
|
+
* @param {string} slideId - The ID of the slide
|
|
337
|
+
*/
|
|
338
|
+
_checkTimeBasedObjectives(slideId) {
|
|
339
|
+
// Get slide durations from centralized session data
|
|
340
|
+
const sessionData = stateManager.getDomainState('sessionData');
|
|
341
|
+
const slideDurations = sessionData?.slideDurations || {};
|
|
342
|
+
|
|
343
|
+
this.criteriaConfig.forEach(objective => {
|
|
344
|
+
// Skip if already completed
|
|
345
|
+
const current = this.objectives[objective.id];
|
|
346
|
+
if (current && current.completion_status === 'completed') {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const { criteria } = objective;
|
|
351
|
+
|
|
352
|
+
if (criteria.type === 'timeOnSlide' && criteria.slideId === slideId) {
|
|
353
|
+
const totalMilliseconds = slideDurations[slideId] || 0;
|
|
354
|
+
const totalSeconds = totalMilliseconds / 1000;
|
|
355
|
+
|
|
356
|
+
if (totalSeconds >= (criteria.minSeconds || 0)) {
|
|
357
|
+
this.setCompletionStatus(objective.id, 'completed');
|
|
358
|
+
logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: timeOnSlide (${totalSeconds.toFixed(1)}s)`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Checks if any flag-based objectives are met.
|
|
366
|
+
* @private
|
|
367
|
+
* @param {string} flagKey - The key of the flag that was updated
|
|
368
|
+
* @param {any} flagValue - The new value of the flag
|
|
369
|
+
*/
|
|
370
|
+
_checkFlagBasedObjectives(flagKey, flagValue) {
|
|
371
|
+
// Get all current flags
|
|
372
|
+
const flags = stateManager.getDomainState('flags') || {};
|
|
373
|
+
|
|
374
|
+
this.criteriaConfig.forEach(objective => {
|
|
375
|
+
// Skip if already completed
|
|
376
|
+
const current = this.objectives[objective.id];
|
|
377
|
+
if (current && current.completion_status === 'completed') {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const { criteria } = objective;
|
|
382
|
+
|
|
383
|
+
if (criteria.type === 'flag') {
|
|
384
|
+
// Single flag check
|
|
385
|
+
if (criteria.key === flagKey) {
|
|
386
|
+
let isMet = false;
|
|
387
|
+
|
|
388
|
+
if (criteria.equals !== undefined) {
|
|
389
|
+
isMet = flagValue === criteria.equals;
|
|
390
|
+
} else {
|
|
391
|
+
// Default: check if flag is truthy
|
|
392
|
+
isMet = !!flagValue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (isMet) {
|
|
396
|
+
this.setCompletionStatus(objective.id, 'completed');
|
|
397
|
+
logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: flag "${flagKey}" = ${flagValue}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} else if (criteria.type === 'allFlags') {
|
|
401
|
+
// Multiple flags check - all must be truthy (or match equals values)
|
|
402
|
+
const requiredFlags = criteria.flags || [];
|
|
403
|
+
const allMet = requiredFlags.every(flagConfig => {
|
|
404
|
+
const key = typeof flagConfig === 'string' ? flagConfig : flagConfig.key;
|
|
405
|
+
const value = flags[key];
|
|
406
|
+
|
|
407
|
+
if (typeof flagConfig === 'object' && flagConfig.equals !== undefined) {
|
|
408
|
+
return value === flagConfig.equals;
|
|
409
|
+
}
|
|
410
|
+
return !!value;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (allMet) {
|
|
414
|
+
this.setCompletionStatus(objective.id, 'completed');
|
|
415
|
+
logger.debug(`[ObjectiveManager] Objective "${objective.id}" completed: allFlags`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const instance = new ObjectiveManager();
|
|
423
|
+
export default instance;
|