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,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file interaction-base.js
|
|
3
|
+
* @description Shared utilities and patterns for all interaction components.
|
|
4
|
+
* Eliminates duplication and ensures consistent error handling across interactions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import InteractionManager from '../../managers/interaction-manager.js';
|
|
8
|
+
import interactionRegistry from '../../managers/interaction-registry.js';
|
|
9
|
+
import stateManager from '../../state/index.js';
|
|
10
|
+
|
|
11
|
+
import engagementManager from '../../engagement/engagement-manager.js';
|
|
12
|
+
import * as NavigationState from '../../navigation/NavigationState.js';
|
|
13
|
+
import { iconManager } from '../../utilities/icons.js';
|
|
14
|
+
import { logger } from '../../utilities/logger.js';
|
|
15
|
+
import { escapeHTML } from '../../utilities/utilities.js';
|
|
16
|
+
import { formatLearnerResponseForScorm } from '../../validation/scorm-validators.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate a configuration object against an interaction schema.
|
|
20
|
+
* Each interaction passes its own schema directly to avoid circular imports.
|
|
21
|
+
* @param {object} config - The interaction configuration to validate
|
|
22
|
+
* @param {object} interactionSchema - The schema object (from the interaction's export)
|
|
23
|
+
* @param {object} [baseProps] - Optional base schema properties to merge (defaults to baseSchema)
|
|
24
|
+
* @throws {Error} If validation fails
|
|
25
|
+
* @returns {true} If validation passes
|
|
26
|
+
*/
|
|
27
|
+
export function validateAgainstSchema(config, interactionSchema, baseProps = null) {
|
|
28
|
+
if (!config || typeof config !== 'object') {
|
|
29
|
+
throw new Error(`Invalid interaction config: expected object, got ${typeof config}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!interactionSchema || !interactionSchema.properties) {
|
|
33
|
+
throw new Error('Invalid schema: must have properties object');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Merge base properties with interaction-specific properties
|
|
37
|
+
const schema = {
|
|
38
|
+
...interactionSchema,
|
|
39
|
+
properties: {
|
|
40
|
+
...baseProps,
|
|
41
|
+
...interactionSchema.properties
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
// Validate each property defined in the schema
|
|
48
|
+
for (const [propName, propDef] of Object.entries(schema.properties)) {
|
|
49
|
+
const value = config[propName];
|
|
50
|
+
const isPresent = value !== undefined && value !== null && value !== '';
|
|
51
|
+
|
|
52
|
+
// Check required
|
|
53
|
+
if (propDef.required && !isPresent) {
|
|
54
|
+
// Handle requiredUnless condition
|
|
55
|
+
if (propDef.requiredUnless && config[propDef.requiredUnless]) {
|
|
56
|
+
continue; // Skip - alternative condition satisfied
|
|
57
|
+
}
|
|
58
|
+
errors.push(`Missing required property: "${propName}"`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Skip validation if not present and not required
|
|
63
|
+
if (!isPresent) continue;
|
|
64
|
+
|
|
65
|
+
// Type checking
|
|
66
|
+
const expectedTypes = Array.isArray(propDef.type) ? propDef.type : [propDef.type];
|
|
67
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
68
|
+
|
|
69
|
+
if (!expectedTypes.includes(actualType)) {
|
|
70
|
+
errors.push(`Property "${propName}" expected ${expectedTypes.join(' or ')}, got ${actualType}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Array-specific validation
|
|
75
|
+
if (actualType === 'array') {
|
|
76
|
+
if (propDef.minItems && value.length < propDef.minItems) {
|
|
77
|
+
errors.push(`Property "${propName}" must have at least ${propDef.minItems} items`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Enum validation
|
|
82
|
+
if (propDef.enum && !propDef.enum.includes(value)) {
|
|
83
|
+
errors.push(`Property "${propName}" must be one of: ${propDef.enum.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
throw new Error(`Invalid ${type} configuration:\n - ${errors.join('\n - ')}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Domain key for storing live interaction responses in suspend_data
|
|
95
|
+
const RESPONSES_DOMAIN = 'interactionResponses';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Saves the current response state for an interaction.
|
|
99
|
+
* Called on every response change (selection, input, etc.) for live state tracking.
|
|
100
|
+
* Silently skips if stateManager is not initialized (e.g., during static validation).
|
|
101
|
+
* @param {string} id - The interaction ID
|
|
102
|
+
* @param {*} response - The current response value
|
|
103
|
+
* @param {boolean} submitted - Whether the interaction has been submitted (Check Answer clicked)
|
|
104
|
+
*/
|
|
105
|
+
export function saveInteractionState(id, response, submitted = false) {
|
|
106
|
+
// Silently skip if stateManager not initialized (static validation, early creation)
|
|
107
|
+
if (!stateManager.isInitialized) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const state = stateManager.getDomainState(RESPONSES_DOMAIN) || {};
|
|
112
|
+
state[id] = { response, submitted };
|
|
113
|
+
stateManager.setDomainState(RESPONSES_DOMAIN, state, { source: 'interaction-base' });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.warn(`[interaction-base] Failed to save interaction state for "${id}":`, error.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Retrieves the saved response state for an interaction.
|
|
121
|
+
* Used during render to restore previous selections.
|
|
122
|
+
* Returns null if stateManager is not initialized (e.g., during static validation).
|
|
123
|
+
* @param {string} id - The interaction ID
|
|
124
|
+
* @returns {{ response: *, submitted: boolean } | null} The saved state or null if none exists
|
|
125
|
+
*/
|
|
126
|
+
export function getInteractionState(id) {
|
|
127
|
+
// Silently return null if stateManager not initialized (static validation, early creation)
|
|
128
|
+
if (!stateManager.isInitialized) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const state = stateManager.getDomainState(RESPONSES_DOMAIN) || {};
|
|
133
|
+
return state[id] || null;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.warn(`[interaction-base] Failed to get interaction state for "${id}":`, error.message);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates that required config properties exist.
|
|
142
|
+
* @throws {Error} If required properties are missing
|
|
143
|
+
*/
|
|
144
|
+
export function validateInteractionConfig(config, requiredProps) {
|
|
145
|
+
if (!config || typeof config !== 'object') {
|
|
146
|
+
throw new Error('Interaction config must be an object');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!config.id || typeof config.id !== 'string') {
|
|
150
|
+
throw new Error('Interaction must have a valid string id');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!config.prompt || typeof config.prompt !== 'string') {
|
|
154
|
+
throw new Error(`Interaction "${config.id}" must have a valid prompt`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const prop of requiredProps) {
|
|
158
|
+
if (config[prop] === undefined) {
|
|
159
|
+
throw new Error(`Interaction "${config.id}" is missing required property: ${prop}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Creates a standardized interaction event handler that uses event delegation.
|
|
166
|
+
* Handles check-answer, reset, and custom actions.
|
|
167
|
+
*/
|
|
168
|
+
export function createInteractionEventHandler(questionObj, config, customHandlers = {}) {
|
|
169
|
+
return function handleInteractionEvent(event) {
|
|
170
|
+
const actionTarget = event.target.closest('[data-action]');
|
|
171
|
+
if (!actionTarget) return;
|
|
172
|
+
|
|
173
|
+
const action = actionTarget.dataset.action;
|
|
174
|
+
|
|
175
|
+
// Only handle actions for this interaction
|
|
176
|
+
if (actionTarget.dataset.interaction !== config.id) return;
|
|
177
|
+
|
|
178
|
+
switch (action) {
|
|
179
|
+
case 'check-answer':
|
|
180
|
+
const evaluation = questionObj.checkAnswer();
|
|
181
|
+
if (!evaluation) return; // checkAnswer handles error display
|
|
182
|
+
|
|
183
|
+
// NEW: Track for engagement
|
|
184
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
185
|
+
if (currentSlideId) {
|
|
186
|
+
engagementManager.trackInteraction(
|
|
187
|
+
currentSlideId,
|
|
188
|
+
config.id,
|
|
189
|
+
true, // completed
|
|
190
|
+
evaluation.correct
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Only record to InteractionManager if NOT in controlled mode
|
|
195
|
+
if (!config.controlled) {
|
|
196
|
+
recordInteractionResult(config, evaluation);
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'reset':
|
|
201
|
+
questionObj.reset();
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'show-hint':
|
|
205
|
+
if (questionObj.showHint) {
|
|
206
|
+
questionObj.showHint();
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
// Check for custom handlers
|
|
212
|
+
if (customHandlers[action]) {
|
|
213
|
+
customHandlers[action](event, actionTarget);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Records interaction result to InteractionManager with proper SCORM type mapping.
|
|
222
|
+
* Also marks the interaction as submitted in the response state.
|
|
223
|
+
*
|
|
224
|
+
* Formats the learner_response according to SCORM 2004 4th Edition requirements.
|
|
225
|
+
* Each interaction type has specific format requirements:
|
|
226
|
+
* - true-false: "true" or "false"
|
|
227
|
+
* - matching: "source[.]target[,]source[.]target"
|
|
228
|
+
* - sequencing: "item[,]item[,]item"
|
|
229
|
+
* - choice: "a[,]b[,]c"
|
|
230
|
+
* - fill-in: plain text
|
|
231
|
+
*/
|
|
232
|
+
export function recordInteractionResult(config, evaluation) {
|
|
233
|
+
// Mark as submitted in state (for restoration purposes)
|
|
234
|
+
saveInteractionState(config.id, evaluation.response, true);
|
|
235
|
+
|
|
236
|
+
// Format the response according to SCORM 2004 requirements
|
|
237
|
+
const scormType = config.scormType || 'other';
|
|
238
|
+
const formattedResponse = formatLearnerResponseForScorm(scormType, evaluation.response);
|
|
239
|
+
|
|
240
|
+
// Format correct_responses using the same SCORM format as learner_response
|
|
241
|
+
// This is critical for types like 'matching' which require source[.]target format
|
|
242
|
+
let formattedCorrectResponses;
|
|
243
|
+
if (evaluation.correctResponses) {
|
|
244
|
+
formattedCorrectResponses = evaluation.correctResponses;
|
|
245
|
+
} else if (config.correctPattern) {
|
|
246
|
+
// Format the correctPattern using the same formatter as learner_response
|
|
247
|
+
formattedCorrectResponses = [formatLearnerResponseForScorm(scormType, config.correctPattern)];
|
|
248
|
+
} else {
|
|
249
|
+
formattedCorrectResponses = [''];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const interactionData = {
|
|
253
|
+
id: config.id,
|
|
254
|
+
type: scormType,
|
|
255
|
+
learner_response: formattedResponse,
|
|
256
|
+
result: evaluation.correct ? 'correct' : 'incorrect',
|
|
257
|
+
correct_responses: formattedCorrectResponses,
|
|
258
|
+
description: config.prompt
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
InteractionManager.record(interactionData);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
logger.error(`Failed to record interaction "${config.id}": ${error.message}`, { domain: 'interaction', operation: 'record', stack: error.stack, interactionId: config.id });
|
|
265
|
+
throw error; // Re-throw to prevent silent failures
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Creates standard interaction controls HTML.
|
|
271
|
+
* Uses utility classes for layout: .flex .flex-wrap .justify-center .gap-3
|
|
272
|
+
* Note: No margin needed - parent .interaction uses gap for spacing
|
|
273
|
+
*/
|
|
274
|
+
export function renderInteractionControls(id, controlled = false, customButtons = []) {
|
|
275
|
+
if (controlled) return '';
|
|
276
|
+
|
|
277
|
+
const buttons = [
|
|
278
|
+
`<button type="button" class="btn btn-success" data-action="check-answer" data-interaction="${id}" data-testid="${id}-check-answer">Check Answer</button>`,
|
|
279
|
+
`<button type="button" class="btn btn-reset" data-action="reset" data-interaction="${id}" data-testid="${id}-reset">Reset</button>`,
|
|
280
|
+
...customButtons
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
return `<div class="flex flex-wrap justify-center gap-3" data-testid="${id}-controls">${buttons.join('')}</div>`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Creates a feedback container with proper ARIA attributes.
|
|
288
|
+
*/
|
|
289
|
+
export function renderFeedbackContainer(id) {
|
|
290
|
+
return `<div id="${id}_feedback" class="feedback" aria-live="polite" data-testid="${id}-feedback"></div>`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Displays feedback in the interaction's feedback element.
|
|
295
|
+
*/
|
|
296
|
+
export function displayFeedback(container, id, message, type = 'info') {
|
|
297
|
+
if (!container) {
|
|
298
|
+
throw new Error(`Cannot display feedback: container is null for interaction "${id}"`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const feedbackEl = container.querySelector(`#${id}_feedback, .feedback, .overall-feedback`);
|
|
302
|
+
if (!feedbackEl) {
|
|
303
|
+
throw new Error(`Feedback element not found for interaction "${id}"`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const icon = type === 'correct' ? iconManager.getIcon('check') : type === 'incorrect' ? iconManager.getIcon('x') : '';
|
|
307
|
+
feedbackEl.innerHTML = `<div class="feedback ${type}">${icon} ${escapeHTML(message)}</div>`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clears feedback from an interaction.
|
|
312
|
+
*/
|
|
313
|
+
export function clearFeedback(container, id) {
|
|
314
|
+
if (!container) return;
|
|
315
|
+
|
|
316
|
+
const feedbackEl = container.querySelector(`#${id}_feedback, .feedback, .overall-feedback`);
|
|
317
|
+
if (feedbackEl) {
|
|
318
|
+
feedbackEl.innerHTML = '';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validates and normalizes initial response for rendering.
|
|
324
|
+
* Returns null if invalid/empty, or the normalized response.
|
|
325
|
+
*/
|
|
326
|
+
export function normalizeInitialResponse(response) {
|
|
327
|
+
if (response === null || response === undefined || response === '') {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return response;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Safely escapes a string for use in a CSS selector.
|
|
335
|
+
* @throws {Error} If CSS.escape is not available and value contains special characters
|
|
336
|
+
*/
|
|
337
|
+
export function escapeCssSelector(value) {
|
|
338
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
339
|
+
return CSS.escape(value);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// No fallback - if CSS.escape doesn't exist, throw error
|
|
343
|
+
throw new Error('CSS.escape is not available and is required for safe selector escaping');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Ensures container is valid before performing operations.
|
|
348
|
+
* @throws {Error} If container is null or not an Element
|
|
349
|
+
*/
|
|
350
|
+
export function validateContainer(container, interactionId) {
|
|
351
|
+
if (!container) {
|
|
352
|
+
throw new Error(`Container is null for interaction "${interactionId}". Ensure render() was called first.`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!(container instanceof Element)) {
|
|
356
|
+
throw new Error(`Container must be a DOM Element for interaction "${interactionId}"`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Parses a response that could be a string, array, or object.
|
|
362
|
+
* Returns normalized data structure or throws error.
|
|
363
|
+
*/
|
|
364
|
+
export function parseResponse(response, expectedType = 'any') {
|
|
365
|
+
if (response === null || response === undefined) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If it's already the expected type, return it
|
|
370
|
+
if (expectedType === 'array' && Array.isArray(response)) {
|
|
371
|
+
return response;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (expectedType === 'object' && typeof response === 'object' && !Array.isArray(response)) {
|
|
375
|
+
return response;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Try parsing string as JSON
|
|
379
|
+
if (typeof response === 'string') {
|
|
380
|
+
const trimmed = response.trim();
|
|
381
|
+
if (!trimmed) return null;
|
|
382
|
+
|
|
383
|
+
// Try JSON parse
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(trimmed);
|
|
386
|
+
if (expectedType === 'array' && !Array.isArray(parsed)) {
|
|
387
|
+
throw new Error(`Expected array, got ${typeof parsed}`);
|
|
388
|
+
}
|
|
389
|
+
if (expectedType === 'object' && (typeof parsed !== 'object' || Array.isArray(parsed))) {
|
|
390
|
+
throw new Error(`Expected object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);
|
|
391
|
+
}
|
|
392
|
+
return parsed;
|
|
393
|
+
} catch (parseError) {
|
|
394
|
+
// If JSON parse fails and we need array/object, throw error
|
|
395
|
+
if (expectedType === 'array' || expectedType === 'object') {
|
|
396
|
+
throw new Error(`Failed to parse response as JSON: ${parseError.message}`);
|
|
397
|
+
}
|
|
398
|
+
// For 'any' or 'string', return the string value
|
|
399
|
+
return trimmed;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return response;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Creates evaluation error result for invalid responses.
|
|
408
|
+
*/
|
|
409
|
+
export function createInvalidEvaluation(_interactionType) {
|
|
410
|
+
return {
|
|
411
|
+
score: 0,
|
|
412
|
+
correct: false,
|
|
413
|
+
response: '',
|
|
414
|
+
error: 'Invalid or missing response'
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Debounce delay for auto-saving response state (ms)
|
|
419
|
+
const RESPONSE_SAVE_DEBOUNCE_MS = 300;
|
|
420
|
+
|
|
421
|
+
// Track debounce timers per interaction ID
|
|
422
|
+
const _responseDebounceTimers = new Map();
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Registers an uncontrolled interaction with the InteractionRegistry.
|
|
426
|
+
* This is the single registration point for all standalone interactions.
|
|
427
|
+
* The registry tracks currently rendered interactions for engagement and automation.
|
|
428
|
+
* Also restores any previously saved response state and sets up auto-save on changes.
|
|
429
|
+
* @param {object} config - The interaction configuration.
|
|
430
|
+
* @param {object} questionObj - The live interaction instance.
|
|
431
|
+
*/
|
|
432
|
+
export function registerCoreInteraction(config, questionObj) {
|
|
433
|
+
// Delegate to the InteractionRegistry (separate from persistence manager)
|
|
434
|
+
interactionRegistry.register(config, questionObj);
|
|
435
|
+
|
|
436
|
+
// Defer state restoration and auto-save setup to next frame
|
|
437
|
+
// This ensures the DOM container exists after render() completes
|
|
438
|
+
requestAnimationFrame(() => {
|
|
439
|
+
// Restore previously saved response state if it exists
|
|
440
|
+
const savedState = getInteractionState(config.id);
|
|
441
|
+
if (savedState && savedState.response !== null && savedState.response !== undefined) {
|
|
442
|
+
try {
|
|
443
|
+
if (typeof questionObj.setResponse === 'function') {
|
|
444
|
+
questionObj.setResponse(savedState.response);
|
|
445
|
+
logger.debug(`[interaction-base] Restored state for interaction "${config.id}"`);
|
|
446
|
+
|
|
447
|
+
// If it was previously submitted, also restore the feedback state
|
|
448
|
+
if (savedState.submitted && typeof questionObj.checkAnswer === 'function') {
|
|
449
|
+
questionObj.checkAnswer();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
// Silently ignore - container may not exist for controlled interactions
|
|
454
|
+
logger.debug(`[interaction-base] Could not restore state for "${config.id}" (may be controlled):`, error.message);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Set up auto-save on response changes (debounced)
|
|
459
|
+
// This listens for change/input events on the interaction container
|
|
460
|
+
// and saves the current response to state for restoration on re-render
|
|
461
|
+
_setupResponseAutoSave(config.id, questionObj);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Sets up debounced auto-save of response state when the user interacts.
|
|
467
|
+
* Listens for change and input events on the interaction container.
|
|
468
|
+
* @private
|
|
469
|
+
* @param {string} id - The interaction ID
|
|
470
|
+
* @param {object} questionObj - The live interaction instance
|
|
471
|
+
*/
|
|
472
|
+
function _setupResponseAutoSave(id, questionObj) {
|
|
473
|
+
const container = document.querySelector(`[data-interaction-id="${id}"]`);
|
|
474
|
+
if (!container) {
|
|
475
|
+
// Container not found - this is expected for controlled interactions (assessments)
|
|
476
|
+
// which manage their own state, or during static validation
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Debounced save function
|
|
481
|
+
const debouncedSave = () => {
|
|
482
|
+
// Clear any existing timer
|
|
483
|
+
if (_responseDebounceTimers.has(id)) {
|
|
484
|
+
clearTimeout(_responseDebounceTimers.get(id));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Set new timer
|
|
488
|
+
const timer = setTimeout(() => {
|
|
489
|
+
try {
|
|
490
|
+
if (typeof questionObj.getResponse === 'function') {
|
|
491
|
+
const response = questionObj.getResponse();
|
|
492
|
+
if (response !== null && response !== undefined) {
|
|
493
|
+
// Get current state to preserve submitted flag
|
|
494
|
+
const currentState = getInteractionState(id);
|
|
495
|
+
const submitted = currentState?.submitted || false;
|
|
496
|
+
saveInteractionState(id, response, submitted);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
logger.warn(`[interaction-base] Auto-save failed for "${id}":`, error.message);
|
|
501
|
+
}
|
|
502
|
+
_responseDebounceTimers.delete(id);
|
|
503
|
+
}, RESPONSE_SAVE_DEBOUNCE_MS);
|
|
504
|
+
|
|
505
|
+
_responseDebounceTimers.set(id, timer);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Listen for both change (radios, checkboxes, selects) and input (text fields)
|
|
509
|
+
container.addEventListener('change', debouncedSave);
|
|
510
|
+
container.addEventListener('input', debouncedSave);
|
|
511
|
+
}
|