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
|
+
import {
|
|
2
|
+
validateAgainstSchema,
|
|
3
|
+
createInteractionEventHandler,
|
|
4
|
+
renderInteractionControls,
|
|
5
|
+
renderFeedbackContainer,
|
|
6
|
+
displayFeedback,
|
|
7
|
+
clearFeedback,
|
|
8
|
+
normalizeInitialResponse,
|
|
9
|
+
validateContainer,
|
|
10
|
+
registerCoreInteraction
|
|
11
|
+
} from './interaction-base.js';
|
|
12
|
+
|
|
13
|
+
// Metadata for sequencing interaction type
|
|
14
|
+
export const metadata = {
|
|
15
|
+
creator: 'createSequencingQuestion',
|
|
16
|
+
scormType: 'sequencing',
|
|
17
|
+
showCheckAnswer: true,
|
|
18
|
+
isAnswered: (response) => {
|
|
19
|
+
return Array.isArray(response) && response.length > 0;
|
|
20
|
+
},
|
|
21
|
+
getCorrectAnswer: (config) => {
|
|
22
|
+
return JSON.stringify(config.correctOrder || []);
|
|
23
|
+
},
|
|
24
|
+
formatCorrectAnswer: (question, correctAnswer) => {
|
|
25
|
+
let html = '<ol class="list-decimal pl-4 m-0">';
|
|
26
|
+
const correctOrder = Array.isArray(correctAnswer) ? correctAnswer : [];
|
|
27
|
+
const items = question.items || [];
|
|
28
|
+
|
|
29
|
+
correctOrder.forEach(itemId => {
|
|
30
|
+
const item = items.find(i => i.id === itemId);
|
|
31
|
+
if (item) {
|
|
32
|
+
html += `<li>${item.text}</li>`;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
html += '</ol>';
|
|
36
|
+
return html;
|
|
37
|
+
},
|
|
38
|
+
formatUserResponse: (question, response) => {
|
|
39
|
+
let html = '<ol class="list-decimal pl-4 m-0">';
|
|
40
|
+
const userOrder = Array.isArray(response) ? response : [];
|
|
41
|
+
const items = question.items || [];
|
|
42
|
+
|
|
43
|
+
userOrder.forEach(itemId => {
|
|
44
|
+
const item = items.find(i => i.id === itemId);
|
|
45
|
+
if (item) {
|
|
46
|
+
html += `<li>${item.text}</li>`;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
html += '</ol>';
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Schema for validation, linting, and AI-assisted authoring
|
|
55
|
+
export const schema = {
|
|
56
|
+
type: 'sequencing',
|
|
57
|
+
description: 'Drag-to-reorder items into correct sequence',
|
|
58
|
+
scormType: 'sequencing',
|
|
59
|
+
example: `<div class="interaction sequencing" data-interaction-id="demo-seq">
|
|
60
|
+
<div class="question-prompt"><h3>Arrange these steps in order:</h3></div>
|
|
61
|
+
<div class="sequencing-layout">
|
|
62
|
+
<div class="sequence-track" aria-hidden="true"><span class="sequence-label sequence-label-start">First</span><div class="sequence-track-line"></div><span class="sequence-label sequence-label-end">Last</span></div>
|
|
63
|
+
<div class="sequencing-list" role="list">
|
|
64
|
+
<div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Design</span></div>
|
|
65
|
+
<div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Develop</span></div>
|
|
66
|
+
<div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Test</span></div>
|
|
67
|
+
<div class="sequence-item" draggable="true" role="listitem" tabindex="0"><span class="item-text">Deploy</span></div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
|
|
71
|
+
</div>`,
|
|
72
|
+
properties: {
|
|
73
|
+
items: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
required: true,
|
|
76
|
+
minItems: 2,
|
|
77
|
+
description: 'Items to sequence',
|
|
78
|
+
itemSchema: {
|
|
79
|
+
id: { type: 'string', required: true },
|
|
80
|
+
text: { type: 'string', required: true }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
correctOrder: {
|
|
84
|
+
type: 'array',
|
|
85
|
+
required: true,
|
|
86
|
+
description: 'Array of item IDs in correct order'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function createSequencingQuestion(config) {
|
|
92
|
+
validateAgainstSchema(config, schema);
|
|
93
|
+
|
|
94
|
+
const { id, prompt, items, correctOrder, controlled = false, sequenceLabels = null } = config;
|
|
95
|
+
|
|
96
|
+
// Validate items and correctOrder
|
|
97
|
+
if (!Array.isArray(items) || items.length < 2) {
|
|
98
|
+
throw new Error(`Sequencing question "${id}" must have at least two items`);
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(correctOrder) || correctOrder.length !== items.length) {
|
|
101
|
+
throw new Error(`Sequencing question "${id}" correctOrder length must match items length`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let _container = null;
|
|
105
|
+
let _currentOrder = [];
|
|
106
|
+
|
|
107
|
+
const questionObj = {
|
|
108
|
+
id,
|
|
109
|
+
type: 'sequencing',
|
|
110
|
+
|
|
111
|
+
render: (container, initialResponse = null) => {
|
|
112
|
+
validateContainer(container, id);
|
|
113
|
+
_container = container;
|
|
114
|
+
|
|
115
|
+
// Determine initial order: use saved response or default (shuffled or as provided)
|
|
116
|
+
// For now, we'll use the order provided in 'items' as the initial display order.
|
|
117
|
+
// Ideally, we should shuffle them if no initial response exists.
|
|
118
|
+
let displayOrderIds = [];
|
|
119
|
+
|
|
120
|
+
const initialValue = normalizeInitialResponse(initialResponse);
|
|
121
|
+
if (Array.isArray(initialValue) && initialValue.length === items.length) {
|
|
122
|
+
displayOrderIds = initialValue;
|
|
123
|
+
} else {
|
|
124
|
+
// Default to items order (author should provide them shuffled or we shuffle here)
|
|
125
|
+
// Let's shuffle by default to ensure it's a challenge
|
|
126
|
+
displayOrderIds = items.map(i => i.id).sort(() => Math.random() - 0.5);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_currentOrder = [...displayOrderIds];
|
|
130
|
+
|
|
131
|
+
// Always show a direction cue; use provided labels or fall back to First/Last
|
|
132
|
+
const effectiveSequenceLabels = (sequenceLabels && Array.isArray(sequenceLabels) && sequenceLabels.length >= 2)
|
|
133
|
+
? sequenceLabels
|
|
134
|
+
: ['First', 'Last'];
|
|
135
|
+
|
|
136
|
+
// Build sequence track with start label at top, line in middle, end label at bottom
|
|
137
|
+
const startLabel = effectiveSequenceLabels[0];
|
|
138
|
+
const endLabel = effectiveSequenceLabels[effectiveSequenceLabels.length - 1];
|
|
139
|
+
const sequenceTrackHtml = `
|
|
140
|
+
<div class="sequence-track" aria-hidden="true">
|
|
141
|
+
<span class="sequence-label sequence-label-start">${startLabel}</span>
|
|
142
|
+
<div class="sequence-track-line"></div>
|
|
143
|
+
<span class="sequence-label sequence-label-end">${endLabel}</span>
|
|
144
|
+
</div>
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
let html = `
|
|
148
|
+
<div class="interaction sequencing" data-interaction-id="${id}">
|
|
149
|
+
<div class="question-prompt">
|
|
150
|
+
<h3>${prompt}</h3>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="sequencing-layout">
|
|
153
|
+
${sequenceTrackHtml}
|
|
154
|
+
<div class="sequencing-list" role="list">
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
displayOrderIds.forEach((itemId, _index) => {
|
|
158
|
+
const item = items.find(i => i.id === itemId);
|
|
159
|
+
if (item) {
|
|
160
|
+
html += `
|
|
161
|
+
<div class="sequence-item"
|
|
162
|
+
draggable="true"
|
|
163
|
+
data-item-id="${item.id}"
|
|
164
|
+
role="listitem"
|
|
165
|
+
tabindex="0"
|
|
166
|
+
aria-label="${item.text}. Press Up or Down arrow to reorder.">
|
|
167
|
+
<span class="item-text">${item.text}</span>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
html += `
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
${renderFeedbackContainer(id)}
|
|
177
|
+
${renderInteractionControls(id, controlled)}
|
|
178
|
+
</div>
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
container.innerHTML = html;
|
|
182
|
+
|
|
183
|
+
// Attach event listeners
|
|
184
|
+
const listContainer = container.querySelector('.sequencing-list');
|
|
185
|
+
attachDragAndDropListeners(listContainer);
|
|
186
|
+
attachKeyboardListeners(listContainer);
|
|
187
|
+
|
|
188
|
+
// Attach standard interaction handlers
|
|
189
|
+
container.addEventListener('click', createInteractionEventHandler(questionObj, config));
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
getResponse: () => {
|
|
193
|
+
return [..._currentOrder];
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
evaluate: (response) => {
|
|
197
|
+
const orderToCheck = Array.isArray(response) ? response : _currentOrder;
|
|
198
|
+
const isCorrect = JSON.stringify(orderToCheck) === JSON.stringify(correctOrder);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
correct: isCorrect,
|
|
202
|
+
score: isCorrect ? 1 : 0,
|
|
203
|
+
response: orderToCheck
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
setResponse: (response) => {
|
|
208
|
+
if (!Array.isArray(response)) return;
|
|
209
|
+
_currentOrder = [...response];
|
|
210
|
+
// Re-render to reflect new order
|
|
211
|
+
questionObj.render(_container, _currentOrder);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
checkAnswer: () => {
|
|
215
|
+
const evaluation = questionObj.evaluate(_currentOrder);
|
|
216
|
+
const feedbackMsg = evaluation.correct
|
|
217
|
+
? (config.feedback?.correct || 'Correct sequence!')
|
|
218
|
+
: (config.feedback?.incorrect || 'Incorrect sequence. Try again.');
|
|
219
|
+
|
|
220
|
+
displayFeedback(_container, id, feedbackMsg, evaluation.correct ? 'correct' : 'incorrect');
|
|
221
|
+
return { ...evaluation, feedback: feedbackMsg };
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
reset: () => {
|
|
225
|
+
clearFeedback(_container);
|
|
226
|
+
// Re-shuffle or reset to initial state
|
|
227
|
+
// For simplicity, we'll just re-render with a new shuffle
|
|
228
|
+
questionObj.render(_container, null);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
showHint: () => {
|
|
232
|
+
// Optional: Highlight the first incorrect item
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
getCorrectAnswer: () => {
|
|
236
|
+
return [...correctOrder];
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// For uncontrolled interactions, register with the central registry for lifecycle mgmt
|
|
241
|
+
if (!controlled) {
|
|
242
|
+
registerCoreInteraction(config, questionObj);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Helper to update internal order state based on DOM
|
|
246
|
+
function updateOrderFromDOM(listContainer) {
|
|
247
|
+
const itemElements = Array.from(listContainer.querySelectorAll('.sequence-item'));
|
|
248
|
+
_currentOrder = itemElements.map(el => el.dataset.itemId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function attachDragAndDropListeners(list) {
|
|
252
|
+
let draggedItem = null;
|
|
253
|
+
|
|
254
|
+
list.addEventListener('dragstart', (e) => {
|
|
255
|
+
draggedItem = e.target.closest('.sequence-item');
|
|
256
|
+
if (!draggedItem) {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
261
|
+
e.dataTransfer.setData('text/plain', draggedItem.dataset.itemId);
|
|
262
|
+
draggedItem.classList.add('dragging');
|
|
263
|
+
// Accessibility
|
|
264
|
+
draggedItem.setAttribute('aria-grabbed', 'true');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
list.addEventListener('dragend', (_e) => {
|
|
268
|
+
if (draggedItem) {
|
|
269
|
+
draggedItem.classList.remove('dragging');
|
|
270
|
+
draggedItem.setAttribute('aria-grabbed', 'false');
|
|
271
|
+
draggedItem = null;
|
|
272
|
+
}
|
|
273
|
+
// Remove all drop indicators
|
|
274
|
+
list.querySelectorAll('.sequence-item').forEach(item => {
|
|
275
|
+
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
updateOrderFromDOM(list);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Required for drop to work - must prevent default on dragenter
|
|
282
|
+
list.addEventListener('dragenter', (e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
e.dataTransfer.dropEffect = 'move';
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
list.addEventListener('dragover', (e) => {
|
|
288
|
+
e.preventDefault(); // Allow drop
|
|
289
|
+
e.dataTransfer.dropEffect = 'move'; // Show move cursor, not 🚫
|
|
290
|
+
|
|
291
|
+
// If no valid dragged item, skip indicator logic
|
|
292
|
+
if (!draggedItem) return;
|
|
293
|
+
|
|
294
|
+
// Clear all previous indicators
|
|
295
|
+
list.querySelectorAll('.sequence-item').forEach(item => {
|
|
296
|
+
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const items = Array.from(list.querySelectorAll('.sequence-item'));
|
|
300
|
+
if (items.length === 0) return;
|
|
301
|
+
|
|
302
|
+
// Find the item we should indicate drop position for
|
|
303
|
+
let targetItem = null;
|
|
304
|
+
let position = 'bottom'; // 'top' or 'bottom'
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < items.length; i++) {
|
|
307
|
+
const item = items[i];
|
|
308
|
+
if (item === draggedItem) continue;
|
|
309
|
+
|
|
310
|
+
const rect = item.getBoundingClientRect();
|
|
311
|
+
const midY = rect.top + rect.height / 2;
|
|
312
|
+
|
|
313
|
+
if (e.clientY < midY) {
|
|
314
|
+
targetItem = item;
|
|
315
|
+
position = 'top';
|
|
316
|
+
break;
|
|
317
|
+
} else {
|
|
318
|
+
targetItem = item;
|
|
319
|
+
position = 'bottom';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (targetItem && targetItem !== draggedItem) {
|
|
324
|
+
targetItem.classList.add(position === 'top' ? 'drag-over-top' : 'drag-over-bottom');
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
list.addEventListener('dragleave', (e) => {
|
|
329
|
+
// Only clear if leaving the list entirely
|
|
330
|
+
if (!list.contains(e.relatedTarget)) {
|
|
331
|
+
list.querySelectorAll('.sequence-item').forEach(item => {
|
|
332
|
+
item.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
list.addEventListener('drop', (e) => {
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
|
|
340
|
+
const items = Array.from(list.querySelectorAll('.sequence-item'));
|
|
341
|
+
let targetItem = null;
|
|
342
|
+
let position = 'bottom';
|
|
343
|
+
|
|
344
|
+
for (let i = 0; i < items.length; i++) {
|
|
345
|
+
const item = items[i];
|
|
346
|
+
if (item === draggedItem) continue;
|
|
347
|
+
|
|
348
|
+
const rect = item.getBoundingClientRect();
|
|
349
|
+
const midY = rect.top + rect.height / 2;
|
|
350
|
+
|
|
351
|
+
if (e.clientY < midY) {
|
|
352
|
+
targetItem = item;
|
|
353
|
+
position = 'top';
|
|
354
|
+
break;
|
|
355
|
+
} else {
|
|
356
|
+
targetItem = item;
|
|
357
|
+
position = 'bottom';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (targetItem && draggedItem && targetItem !== draggedItem) {
|
|
362
|
+
// Capture reference before it gets nullified in dragend
|
|
363
|
+
const droppedItem = draggedItem;
|
|
364
|
+
|
|
365
|
+
if (position === 'top') {
|
|
366
|
+
list.insertBefore(droppedItem, targetItem);
|
|
367
|
+
} else {
|
|
368
|
+
list.insertBefore(droppedItem, targetItem.nextSibling);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Add settling animation to the dropped item
|
|
372
|
+
droppedItem.classList.add('settling');
|
|
373
|
+
droppedItem.addEventListener('animationend', () => {
|
|
374
|
+
droppedItem.classList.remove('settling');
|
|
375
|
+
}, { once: true });
|
|
376
|
+
|
|
377
|
+
targetItem.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
378
|
+
updateOrderFromDOM(list);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function attachKeyboardListeners(list) {
|
|
384
|
+
list.addEventListener('keydown', (e) => {
|
|
385
|
+
const item = e.target.closest('.sequence-item');
|
|
386
|
+
if (!item) return;
|
|
387
|
+
|
|
388
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
|
|
391
|
+
if (e.key === 'ArrowUp') {
|
|
392
|
+
const prev = item.previousElementSibling;
|
|
393
|
+
if (prev) {
|
|
394
|
+
// Capture reference for animationend callback
|
|
395
|
+
const movedItem = item;
|
|
396
|
+
list.insertBefore(movedItem, prev);
|
|
397
|
+
movedItem.classList.add('settling');
|
|
398
|
+
movedItem.addEventListener('animationend', () => {
|
|
399
|
+
movedItem.classList.remove('settling');
|
|
400
|
+
}, { once: true });
|
|
401
|
+
movedItem.focus();
|
|
402
|
+
updateOrderFromDOM(list);
|
|
403
|
+
}
|
|
404
|
+
} else if (e.key === 'ArrowDown') {
|
|
405
|
+
const next = item.nextElementSibling;
|
|
406
|
+
if (next) {
|
|
407
|
+
// Capture reference for animationend callback
|
|
408
|
+
const movedItem = item;
|
|
409
|
+
list.insertBefore(movedItem, next.nextSibling);
|
|
410
|
+
movedItem.classList.add('settling');
|
|
411
|
+
movedItem.addEventListener('animationend', () => {
|
|
412
|
+
movedItem.classList.remove('settling');
|
|
413
|
+
}, { once: true });
|
|
414
|
+
movedItem.focus();
|
|
415
|
+
updateOrderFromDOM(list);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return questionObj;
|
|
423
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateAgainstSchema,
|
|
3
|
+
createInteractionEventHandler,
|
|
4
|
+
renderInteractionControls,
|
|
5
|
+
renderFeedbackContainer,
|
|
6
|
+
displayFeedback,
|
|
7
|
+
clearFeedback,
|
|
8
|
+
normalizeInitialResponse,
|
|
9
|
+
validateContainer,
|
|
10
|
+
registerCoreInteraction
|
|
11
|
+
} from './interaction-base.js';
|
|
12
|
+
import engagementManager from '../../engagement/engagement-manager.js';
|
|
13
|
+
import * as NavigationState from '../../navigation/NavigationState.js';
|
|
14
|
+
|
|
15
|
+
// Metadata for true-false interaction type
|
|
16
|
+
export const metadata = {
|
|
17
|
+
creator: 'createTrueFalseQuestion',
|
|
18
|
+
scormType: 'true-false',
|
|
19
|
+
showCheckAnswer: true,
|
|
20
|
+
isAnswered: (response) => {
|
|
21
|
+
return response !== null && response !== undefined;
|
|
22
|
+
},
|
|
23
|
+
getCorrectAnswer: (config) => config.correctAnswer.toString(),
|
|
24
|
+
formatCorrectAnswer: (question, correctAnswer) => `<p class="correct-item">${correctAnswer}</p>`,
|
|
25
|
+
formatUserResponse: (question, response) => `<p class="response-item">${response}</p>`
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Schema for validation, linting, and AI-assisted authoring
|
|
29
|
+
export const schema = {
|
|
30
|
+
type: 'true-false',
|
|
31
|
+
description: 'Binary true/false question with radio buttons',
|
|
32
|
+
scormType: 'true-false',
|
|
33
|
+
example: `<div class="interaction true-false" data-interaction-id="demo-tf">
|
|
34
|
+
<div class="question-prompt text-center"><h3>The Earth revolves around the Sun.</h3></div>
|
|
35
|
+
<div class="true-false-options">
|
|
36
|
+
<label><input type="radio" name="demo-tf" value="true"> <span class="tf-label">True</span></label>
|
|
37
|
+
<label><input type="radio" name="demo-tf" value="false"> <span class="tf-label">False</span></label>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
|
|
40
|
+
</div>`,
|
|
41
|
+
properties: {
|
|
42
|
+
correctAnswer: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
required: true,
|
|
45
|
+
description: 'The correct answer (true or false)'
|
|
46
|
+
},
|
|
47
|
+
autoCheck: {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Auto-evaluate on selection'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function createTrueFalseQuestion(config) {
|
|
56
|
+
// Validate config on creation
|
|
57
|
+
validateAgainstSchema(config, schema);
|
|
58
|
+
|
|
59
|
+
const { id, prompt, correctAnswer, controlled = false, autoCheck = false, feedback = {} } = config;
|
|
60
|
+
|
|
61
|
+
let _container = null;
|
|
62
|
+
|
|
63
|
+
const questionObj = {
|
|
64
|
+
id,
|
|
65
|
+
type: 'true-false',
|
|
66
|
+
|
|
67
|
+
render: (container, initialResponse = null) => {
|
|
68
|
+
validateContainer(container, id);
|
|
69
|
+
_container = container;
|
|
70
|
+
|
|
71
|
+
const initialValue = normalizeInitialResponse(initialResponse);
|
|
72
|
+
|
|
73
|
+
const html = `
|
|
74
|
+
<div class="interaction true-false" data-interaction-id="${id}">
|
|
75
|
+
<div class="question-prompt text-center">
|
|
76
|
+
<h3>${prompt}</h3>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="true-false-options">
|
|
79
|
+
<label>
|
|
80
|
+
<input type="radio" name="${id}_choice" value="true" ${initialValue === 'true' ? 'checked' : ''} data-testid="${id}-choice-true">
|
|
81
|
+
<span class="tf-label">True</span>
|
|
82
|
+
</label>
|
|
83
|
+
<label>
|
|
84
|
+
<input type="radio" name="${id}_choice" value="false" ${initialValue === 'false' ? 'checked' : ''} data-testid="${id}-choice-false">
|
|
85
|
+
<span class="tf-label">False</span>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
${autoCheck ? '' : renderInteractionControls(id, controlled)}
|
|
89
|
+
${renderFeedbackContainer(id)}
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
container.innerHTML = html;
|
|
94
|
+
|
|
95
|
+
// Attach event handler only in uncontrolled mode
|
|
96
|
+
if (!controlled) {
|
|
97
|
+
container.addEventListener('click', createInteractionEventHandler(questionObj, {
|
|
98
|
+
...config,
|
|
99
|
+
scormType: 'true-false',
|
|
100
|
+
correctPattern: correctAnswer.toString()
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Auto-check on radio selection if enabled
|
|
105
|
+
if (autoCheck && !controlled) {
|
|
106
|
+
const radios = container.querySelectorAll(`input[name="${id}_choice"]`);
|
|
107
|
+
radios.forEach(radio => {
|
|
108
|
+
radio.addEventListener('change', () => {
|
|
109
|
+
const evaluation = questionObj.checkAnswer();
|
|
110
|
+
|
|
111
|
+
// Track engagement when autoCheck triggers evaluation
|
|
112
|
+
if (evaluation) {
|
|
113
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
114
|
+
if (currentSlideId) {
|
|
115
|
+
engagementManager.trackInteraction(
|
|
116
|
+
currentSlideId,
|
|
117
|
+
id,
|
|
118
|
+
true, // completed
|
|
119
|
+
evaluation.correct
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (evaluation && !evaluation.correct) {
|
|
125
|
+
// Flash incorrect feedback, then reset after 2 seconds
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
questionObj.reset();
|
|
128
|
+
}, 2000);
|
|
129
|
+
}
|
|
130
|
+
// Correct answers stay green (don't reset)
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
evaluate: (response) => {
|
|
137
|
+
if (response === null || response === undefined || response === '') {
|
|
138
|
+
return {
|
|
139
|
+
score: 0,
|
|
140
|
+
correct: false,
|
|
141
|
+
response: '',
|
|
142
|
+
error: 'No answer provided'
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const correct = response === correctAnswer.toString();
|
|
147
|
+
return {
|
|
148
|
+
score: correct ? 1 : 0,
|
|
149
|
+
correct,
|
|
150
|
+
response
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
checkAnswer: () => {
|
|
155
|
+
validateContainer(_container, id);
|
|
156
|
+
|
|
157
|
+
const selected = _container.querySelector(`input[name="${id}_choice"]:checked`);
|
|
158
|
+
|
|
159
|
+
if (!selected) {
|
|
160
|
+
displayFeedback(_container, id, 'Please select an answer.', 'error');
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const response = selected.value;
|
|
165
|
+
const evaluation = questionObj.evaluate(response);
|
|
166
|
+
|
|
167
|
+
// Get the selected label for visual feedback
|
|
168
|
+
const selectedLabel = selected.closest('label');
|
|
169
|
+
|
|
170
|
+
if (evaluation.correct) {
|
|
171
|
+
const message = feedback.correct || `Correct! The answer is ${correctAnswer}.`;
|
|
172
|
+
displayFeedback(_container, id, message, 'correct');
|
|
173
|
+
if (selectedLabel) {
|
|
174
|
+
selectedLabel.classList.add('answer-correct');
|
|
175
|
+
selectedLabel.classList.remove('answer-incorrect');
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
const message = feedback.incorrect || `Incorrect. The correct answer is ${correctAnswer}.`;
|
|
179
|
+
displayFeedback(_container, id, message, 'incorrect');
|
|
180
|
+
if (selectedLabel) {
|
|
181
|
+
selectedLabel.classList.add('answer-incorrect');
|
|
182
|
+
selectedLabel.classList.remove('answer-correct');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return evaluation;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
reset: () => {
|
|
190
|
+
validateContainer(_container, id);
|
|
191
|
+
|
|
192
|
+
const radios = _container.querySelectorAll(`input[name="${id}_choice"]`);
|
|
193
|
+
const labels = _container.querySelectorAll('.true-false-options label');
|
|
194
|
+
|
|
195
|
+
radios.forEach(radio => {
|
|
196
|
+
radio.checked = false;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Clear answer state classes from labels
|
|
200
|
+
labels.forEach(label => {
|
|
201
|
+
label.classList.remove('answer-correct', 'answer-incorrect');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
clearFeedback(_container, id);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
setResponse: (response) => {
|
|
208
|
+
validateContainer(_container, id);
|
|
209
|
+
|
|
210
|
+
if (response === null || response === undefined) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const radio = _container.querySelector(`input[name="${id}_choice"][value="${response}"]`);
|
|
215
|
+
if (!radio) {
|
|
216
|
+
throw new Error(`Invalid response value "${response}" for true-false question "${id}". Must be "true" or "false".`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
radio.checked = true;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
getResponse: () => {
|
|
223
|
+
validateContainer(_container, id);
|
|
224
|
+
|
|
225
|
+
const selected = _container.querySelector(`input[name="${id}_choice"]:checked`);
|
|
226
|
+
return selected ? selected.value : null;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
getCorrectAnswer: () => {
|
|
230
|
+
return correctAnswer.toString();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// For uncontrolled interactions, register with the central registry for lifecycle mgmt
|
|
235
|
+
if (!controlled) {
|
|
236
|
+
registerCoreInteraction(config, questionObj);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return questionObj;
|
|
240
|
+
}
|
|
241
|
+
|