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,535 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createInteractionEventHandler,
|
|
3
|
+
renderInteractionControls,
|
|
4
|
+
normalizeInitialResponse,
|
|
5
|
+
validateContainer,
|
|
6
|
+
parseResponse,
|
|
7
|
+
registerCoreInteraction
|
|
8
|
+
} from './interaction-base.js';
|
|
9
|
+
import { escapeHTML } from '../../utilities/utilities.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes text for comparison: trims and collapses whitespace
|
|
13
|
+
* @param {string} text - The text to normalize
|
|
14
|
+
* @returns {string} Normalized text
|
|
15
|
+
*/
|
|
16
|
+
function normalizeText(text) {
|
|
17
|
+
return (text || '').trim().replace(/\s+/g, ' ');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calculates Levenshtein distance between two strings
|
|
22
|
+
* @param {string} a - First string
|
|
23
|
+
* @param {string} b - Second string
|
|
24
|
+
* @returns {number} Number of edits (insertions, deletions, substitutions)
|
|
25
|
+
*/
|
|
26
|
+
function levenshteinDistance(a, b) {
|
|
27
|
+
if (a.length === 0) return b.length;
|
|
28
|
+
if (b.length === 0) return a.length;
|
|
29
|
+
|
|
30
|
+
const matrix = Array(b.length + 1).fill(null)
|
|
31
|
+
.map(() => Array(a.length + 1).fill(null));
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
|
|
34
|
+
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
|
|
35
|
+
|
|
36
|
+
for (let j = 1; j <= b.length; j++) {
|
|
37
|
+
for (let i = 1; i <= a.length; i++) {
|
|
38
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
39
|
+
matrix[j][i] = Math.min(
|
|
40
|
+
matrix[j][i - 1] + 1, // deletion
|
|
41
|
+
matrix[j - 1][i] + 1, // insertion
|
|
42
|
+
matrix[j - 1][i - 1] + cost // substitution
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return matrix[b.length][a.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks if a response matches any correct answer
|
|
51
|
+
* @param {string} response - User's response
|
|
52
|
+
* @param {string|string[]} correct - Correct answer(s)
|
|
53
|
+
* @param {boolean} caseSensitive - Whether to match case
|
|
54
|
+
* @param {number} typoTolerance - Max Levenshtein distance to accept (0 = exact match)
|
|
55
|
+
* @returns {boolean} True if response matches any correct answer
|
|
56
|
+
*/
|
|
57
|
+
function matchesAnswer(response, correct, caseSensitive, typoTolerance = 0) {
|
|
58
|
+
const normalizedResponse = normalizeText(response);
|
|
59
|
+
const correctAnswers = Array.isArray(correct) ? correct : [correct];
|
|
60
|
+
|
|
61
|
+
return correctAnswers.some(answer => {
|
|
62
|
+
const normalizedAnswer = normalizeText(answer);
|
|
63
|
+
|
|
64
|
+
// Apply case normalization if not case sensitive
|
|
65
|
+
const compareResponse = caseSensitive ? normalizedResponse : normalizedResponse.toLowerCase();
|
|
66
|
+
const compareAnswer = caseSensitive ? normalizedAnswer : normalizedAnswer.toLowerCase();
|
|
67
|
+
|
|
68
|
+
// Exact match check first
|
|
69
|
+
if (compareResponse === compareAnswer) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If typo tolerance is set, check Levenshtein distance
|
|
74
|
+
if (typoTolerance > 0) {
|
|
75
|
+
const distance = levenshteinDistance(compareResponse, compareAnswer);
|
|
76
|
+
return distance <= typoTolerance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Metadata for fill-in interaction type
|
|
84
|
+
export const metadata = {
|
|
85
|
+
creator: 'createFillInQuestion',
|
|
86
|
+
scormType: 'fill-in',
|
|
87
|
+
showCheckAnswer: true,
|
|
88
|
+
isAnswered: (response) => {
|
|
89
|
+
if (!response || typeof response !== 'object') return false;
|
|
90
|
+
return Object.values(response).some(val => val && String(val).trim().length > 0);
|
|
91
|
+
},
|
|
92
|
+
getCorrectAnswer: (config) => {
|
|
93
|
+
if (!config.blanks || typeof config.blanks !== 'object') {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(Object.fromEntries(
|
|
97
|
+
Object.entries(config.blanks).map(([key, blank]) => [
|
|
98
|
+
`${config.id}_${key}`,
|
|
99
|
+
Array.isArray(blank.correct) ? blank.correct[0] : blank.correct
|
|
100
|
+
])
|
|
101
|
+
));
|
|
102
|
+
},
|
|
103
|
+
formatCorrectAnswer: (question, _correctAnswer) => {
|
|
104
|
+
let html = '<ul class="list-disc pl-4 m-0">';
|
|
105
|
+
if (question.blanks && typeof question.blanks === 'object') {
|
|
106
|
+
Object.entries(question.blanks).forEach(([key, blank]) => {
|
|
107
|
+
html += `<li class="correct-item"><strong>${key}:</strong> ${blank.correct}</li>`;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
html += '</ul>';
|
|
111
|
+
return html;
|
|
112
|
+
},
|
|
113
|
+
formatUserResponse: (question, response) => {
|
|
114
|
+
let html = '<ul class="list-disc pl-4 m-0">';
|
|
115
|
+
const responseObj = parseResponse(response, 'object') || {};
|
|
116
|
+
Object.entries(responseObj).forEach(([key, value]) => {
|
|
117
|
+
html += `<li class="response-item"><strong>${key}:</strong> ${value}</li>`;
|
|
118
|
+
});
|
|
119
|
+
html += '</ul>';
|
|
120
|
+
return html;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Schema for validation, linting, and AI-assisted authoring
|
|
125
|
+
export const schema = {
|
|
126
|
+
type: 'fill-in',
|
|
127
|
+
description: 'Text input with fill-in-the-blank support',
|
|
128
|
+
scormType: 'fill-in',
|
|
129
|
+
example: `<div class="interaction fill-in fill-in-inline" data-interaction-id="demo-fi">
|
|
130
|
+
<div class="fill-in-template">
|
|
131
|
+
<p>The capital of France is <input type="text" class="fill-in-inline-input" placeholder="..." style="min-width:80px;"> and the capital of Japan is <input type="text" class="fill-in-inline-input" placeholder="..." style="min-width:80px;">.</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="interaction-controls"><button class="btn btn-primary" disabled>Check Answer</button></div>
|
|
134
|
+
</div>`,
|
|
135
|
+
properties: {
|
|
136
|
+
blanks: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
required: true,
|
|
139
|
+
description: 'Map of blank IDs to their correct answers',
|
|
140
|
+
valueSchema: {
|
|
141
|
+
correct: { type: ['string', 'array'], required: true },
|
|
142
|
+
typoTolerance: { type: 'number', default: 0 },
|
|
143
|
+
hint: { type: 'string' }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
template: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'HTML template with {{blankId}} placeholders (inline mode)'
|
|
149
|
+
},
|
|
150
|
+
caseSensitive: {
|
|
151
|
+
type: 'boolean',
|
|
152
|
+
default: false,
|
|
153
|
+
description: 'Require exact case match'
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
notes: 'Requires either template (inline) or just blanks (stacked mode)'
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Creates a fill-in-the-blank question with inline inputs
|
|
161
|
+
* @param {Object} config - Configuration object
|
|
162
|
+
* @param {string} config.id - Unique identifier
|
|
163
|
+
* @param {string} config.template - Template string with {{blankId}} placeholders
|
|
164
|
+
* @param {Object} config.blanks - Object mapping blankId to { correct, placeholder? }
|
|
165
|
+
* @param {boolean} config.caseSensitive - Whether matching is case-sensitive (default: false)
|
|
166
|
+
* @param {string} config.feedback - Optional hint text
|
|
167
|
+
* @param {boolean} config.controlled - Whether to use controlled mode
|
|
168
|
+
* @returns {Object} Question object with render, evaluate, checkAnswer, reset, getResponse, setResponse methods
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* createFillInQuestion({
|
|
172
|
+
* id: 'capitals',
|
|
173
|
+
* template: 'The capital of {{country1}} is Paris and the capital of {{country2}} is Tokyo.',
|
|
174
|
+
* blanks: {
|
|
175
|
+
* country1: { correct: 'France', placeholder: 'country' },
|
|
176
|
+
* country2: { correct: 'Japan' }
|
|
177
|
+
* }
|
|
178
|
+
* });
|
|
179
|
+
*/
|
|
180
|
+
export function createFillInQuestion(config) {
|
|
181
|
+
// Custom validation for fill-in (supports both template and prompt modes)
|
|
182
|
+
if (!config || typeof config !== 'object') {
|
|
183
|
+
throw new Error('Interaction config must be an object');
|
|
184
|
+
}
|
|
185
|
+
if (!config.id || typeof config.id !== 'string') {
|
|
186
|
+
throw new Error('Interaction must have a valid string id');
|
|
187
|
+
}
|
|
188
|
+
if (!config.blanks || typeof config.blanks !== 'object') {
|
|
189
|
+
throw new Error(`Fill-in question "${config.id}" must have a blanks object`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Determine mode: inline (template) vs stacked (prompt)
|
|
193
|
+
const isInlineMode = !!config.template;
|
|
194
|
+
const isStackedMode = !!config.prompt && !config.template;
|
|
195
|
+
|
|
196
|
+
if (!isInlineMode && !isStackedMode) {
|
|
197
|
+
throw new Error(`Fill-in question "${config.id}" must have either a 'template' (inline mode) or 'prompt' (stacked mode)`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { id, template, prompt, blanks, feedback, caseSensitive = false, controlled = false } = config;
|
|
201
|
+
|
|
202
|
+
// Validate blanks object
|
|
203
|
+
if (!blanks || typeof blanks !== 'object' || Object.keys(blanks).length === 0) {
|
|
204
|
+
throw new Error(`Fill-in question "${id}" must have at least one blank in the blanks object`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let blankIds;
|
|
208
|
+
|
|
209
|
+
if (isInlineMode) {
|
|
210
|
+
// Validate that all template placeholders have corresponding blanks
|
|
211
|
+
const placeholderRegex = /\{\{(\w+)\}\}/g;
|
|
212
|
+
const placeholders = [...template.matchAll(placeholderRegex)].map(m => m[1]);
|
|
213
|
+
|
|
214
|
+
if (placeholders.length === 0) {
|
|
215
|
+
throw new Error(`Fill-in question "${id}" template must contain at least one {{blankId}} placeholder`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
placeholders.forEach(placeholder => {
|
|
219
|
+
if (!blanks[placeholder]) {
|
|
220
|
+
throw new Error(`Fill-in question "${id}" is missing blank definition for placeholder "{{${placeholder}}}"`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Store blank IDs in order they appear in template
|
|
225
|
+
blankIds = placeholders;
|
|
226
|
+
} else {
|
|
227
|
+
// Stacked mode: use blanks object keys in definition order
|
|
228
|
+
blankIds = Object.keys(blanks);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let _container = null;
|
|
232
|
+
|
|
233
|
+
const questionObj = {
|
|
234
|
+
id,
|
|
235
|
+
type: 'fill-in',
|
|
236
|
+
blanks,
|
|
237
|
+
blankIds,
|
|
238
|
+
|
|
239
|
+
render: (container, initialResponse = null) => {
|
|
240
|
+
validateContainer(container, id);
|
|
241
|
+
_container = container;
|
|
242
|
+
|
|
243
|
+
// Parse initial response as object
|
|
244
|
+
const initialValues = normalizeInitialResponse(initialResponse);
|
|
245
|
+
const initialObj = initialValues && typeof initialValues === 'object' ? initialValues : {};
|
|
246
|
+
|
|
247
|
+
let html;
|
|
248
|
+
|
|
249
|
+
if (isInlineMode) {
|
|
250
|
+
// INLINE MODE: Replace {{placeholders}} with inputs in flowing text
|
|
251
|
+
let templateHtml = template;
|
|
252
|
+
|
|
253
|
+
blankIds.forEach((blankId, index) => {
|
|
254
|
+
const blank = blanks[blankId];
|
|
255
|
+
const inputName = `${id}_${blankId}`;
|
|
256
|
+
const initialValue = initialObj[inputName] || '';
|
|
257
|
+
const placeholder = blank.placeholder || '...';
|
|
258
|
+
|
|
259
|
+
// Calculate approximate width based on correct answer length (use first if array)
|
|
260
|
+
const correctLength = Array.isArray(blank.correct)
|
|
261
|
+
? blank.correct[0].length
|
|
262
|
+
: blank.correct.length;
|
|
263
|
+
const minWidth = Math.max(60, Math.min(200, correctLength * 10 + 20));
|
|
264
|
+
|
|
265
|
+
const inputHtml = `<input
|
|
266
|
+
type="text"
|
|
267
|
+
class="fill-in-inline-input"
|
|
268
|
+
id="${id}_${blankId}"
|
|
269
|
+
name="${inputName}"
|
|
270
|
+
placeholder="${placeholder}"
|
|
271
|
+
value="${initialValue}"
|
|
272
|
+
data-blank-id="${blankId}"
|
|
273
|
+
data-case-sensitive="${caseSensitive}"
|
|
274
|
+
data-testid="${id}-blank-${index}"
|
|
275
|
+
style="min-width: ${minWidth}px;"
|
|
276
|
+
aria-label="${blankId}"
|
|
277
|
+
/>`;
|
|
278
|
+
|
|
279
|
+
templateHtml = templateHtml.replace(`{{${blankId}}}`, inputHtml);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
html = `
|
|
283
|
+
<div class="interaction fill-in fill-in-inline" data-interaction-id="${id}">
|
|
284
|
+
<div class="fill-in-template">
|
|
285
|
+
${templateHtml}
|
|
286
|
+
</div>
|
|
287
|
+
${renderInteractionControls(id, controlled, feedback ? [
|
|
288
|
+
`<button type="button" class="btn btn-info" data-action="show-hint" data-interaction="${id}">Show Hint</button>`
|
|
289
|
+
] : [])}
|
|
290
|
+
<div class="overall-feedback" id="${id}_overall_feedback" aria-live="polite"></div>
|
|
291
|
+
</div>
|
|
292
|
+
`;
|
|
293
|
+
} else {
|
|
294
|
+
// STACKED MODE: Question prompt followed by stacked input fields
|
|
295
|
+
let blanksHtml = '';
|
|
296
|
+
|
|
297
|
+
blankIds.forEach((blankId, index) => {
|
|
298
|
+
const blank = blanks[blankId];
|
|
299
|
+
const inputName = `${id}_${blankId}`;
|
|
300
|
+
const initialValue = initialObj[inputName] || '';
|
|
301
|
+
const placeholder = blank.placeholder || '';
|
|
302
|
+
const label = blank.label; // Optional - only show if explicitly provided
|
|
303
|
+
|
|
304
|
+
blanksHtml += `
|
|
305
|
+
<div class="fill-in-item">
|
|
306
|
+
${label ? `<label for="${id}_${blankId}">${label}</label>` : ''}
|
|
307
|
+
<input
|
|
308
|
+
type="text"
|
|
309
|
+
class="fill-in-stacked-input"
|
|
310
|
+
id="${id}_${blankId}"
|
|
311
|
+
name="${inputName}"
|
|
312
|
+
placeholder="${placeholder}"
|
|
313
|
+
value="${initialValue}"
|
|
314
|
+
data-blank-id="${blankId}"
|
|
315
|
+
data-case-sensitive="${caseSensitive}"
|
|
316
|
+
data-testid="${id}-blank-${index}"
|
|
317
|
+
aria-describedby="${id}_feedback_${index}"
|
|
318
|
+
${label ? '' : `aria-label="${blankId}"`}
|
|
319
|
+
/>
|
|
320
|
+
<div id="${id}_feedback_${index}" class="feedback" aria-live="polite"></div>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
html = `
|
|
326
|
+
<div class="interaction fill-in fill-in-stacked" data-interaction-id="${id}">
|
|
327
|
+
<div class="question-prompt">
|
|
328
|
+
<h3>${prompt}</h3>
|
|
329
|
+
</div>
|
|
330
|
+
<div class="fill-in-container">
|
|
331
|
+
${blanksHtml}
|
|
332
|
+
</div>
|
|
333
|
+
${renderInteractionControls(id, controlled, feedback ? [
|
|
334
|
+
`<button type="button" class="btn btn-info" data-action="show-hint" data-interaction="${id}">Show Hint</button>`
|
|
335
|
+
] : [])}
|
|
336
|
+
<div class="overall-feedback" id="${id}_overall_feedback" aria-live="polite"></div>
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
container.innerHTML = html;
|
|
342
|
+
|
|
343
|
+
// Attach event handler only in uncontrolled mode
|
|
344
|
+
if (!controlled) {
|
|
345
|
+
const correctPattern = JSON.stringify(
|
|
346
|
+
Object.fromEntries(Object.entries(blanks).map(([k, v]) => [k, v.correct]))
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
container.addEventListener('click', createInteractionEventHandler(questionObj, {
|
|
350
|
+
...config,
|
|
351
|
+
scormType: 'fill-in',
|
|
352
|
+
correctPattern
|
|
353
|
+
}, {
|
|
354
|
+
'show-hint': () => questionObj.showHint()
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
evaluate: (responses) => {
|
|
360
|
+
if (!responses || typeof responses !== 'object') {
|
|
361
|
+
return {
|
|
362
|
+
score: 0,
|
|
363
|
+
correct: false,
|
|
364
|
+
results: blankIds.map(blankId => ({
|
|
365
|
+
blankId,
|
|
366
|
+
response: '',
|
|
367
|
+
correct: false,
|
|
368
|
+
expected: blanks[blankId].correct
|
|
369
|
+
})),
|
|
370
|
+
response: JSON.stringify({}),
|
|
371
|
+
error: 'Invalid response format'
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let correctCount = 0;
|
|
376
|
+
const results = blankIds.map(blankId => {
|
|
377
|
+
const key = `${id}_${blankId}`;
|
|
378
|
+
const response = responses[key] || '';
|
|
379
|
+
const blank = blanks[blankId];
|
|
380
|
+
const correctAnswers = blank.correct;
|
|
381
|
+
const typoTolerance = blank.typoTolerance || 0;
|
|
382
|
+
|
|
383
|
+
const isCorrect = matchesAnswer(response, correctAnswers, caseSensitive, typoTolerance);
|
|
384
|
+
|
|
385
|
+
if (isCorrect) correctCount++;
|
|
386
|
+
|
|
387
|
+
// For display, show first correct answer
|
|
388
|
+
const displayAnswer = Array.isArray(correctAnswers) ? correctAnswers[0] : correctAnswers;
|
|
389
|
+
return { blankId, response, correct: isCorrect, expected: displayAnswer };
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
score: correctCount / blankIds.length,
|
|
394
|
+
correct: correctCount === blankIds.length,
|
|
395
|
+
results,
|
|
396
|
+
response: JSON.stringify(responses)
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
checkAnswer: () => {
|
|
401
|
+
validateContainer(_container, id);
|
|
402
|
+
|
|
403
|
+
const inputs = _container.querySelectorAll('input[type="text"]');
|
|
404
|
+
if (inputs.length === 0) {
|
|
405
|
+
throw new Error(`No input elements found for fill-in question "${id}"`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const responses = {};
|
|
409
|
+
|
|
410
|
+
inputs.forEach(input => {
|
|
411
|
+
responses[input.name] = input.value.trim();
|
|
412
|
+
const blankId = input.dataset.blankId;
|
|
413
|
+
const blank = blanks[blankId];
|
|
414
|
+
const correctAnswers = blank.correct;
|
|
415
|
+
const typoTolerance = blank.typoTolerance || 0;
|
|
416
|
+
const isCaseSensitive = input.dataset.caseSensitive === 'true';
|
|
417
|
+
|
|
418
|
+
const isCorrect = matchesAnswer(input.value, correctAnswers, isCaseSensitive, typoTolerance);
|
|
419
|
+
|
|
420
|
+
if (isCorrect) {
|
|
421
|
+
input.classList.add('correct');
|
|
422
|
+
input.classList.remove('incorrect');
|
|
423
|
+
} else {
|
|
424
|
+
input.classList.add('incorrect');
|
|
425
|
+
input.classList.remove('correct');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const evaluation = questionObj.evaluate(responses);
|
|
430
|
+
|
|
431
|
+
// Show overall feedback
|
|
432
|
+
const overallFeedback = _container.querySelector('.overall-feedback');
|
|
433
|
+
if (overallFeedback) {
|
|
434
|
+
if (evaluation.correct) {
|
|
435
|
+
overallFeedback.innerHTML = '<div class="feedback correct">✓ All answers correct!</div>';
|
|
436
|
+
} else {
|
|
437
|
+
const incorrectBlanks = evaluation.results
|
|
438
|
+
.filter(r => !r.correct)
|
|
439
|
+
.map(r => `<strong>${escapeHTML(r.blankId)}</strong>: "${escapeHTML(String(r.expected))}"`)
|
|
440
|
+
.join(', ');
|
|
441
|
+
overallFeedback.innerHTML = `<div class="feedback incorrect">✗ Incorrect. Expected: ${incorrectBlanks}</div>`;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return evaluation;
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
reset: () => {
|
|
449
|
+
validateContainer(_container, id);
|
|
450
|
+
|
|
451
|
+
const inputs = _container.querySelectorAll('input[type="text"]');
|
|
452
|
+
inputs.forEach(input => {
|
|
453
|
+
input.value = '';
|
|
454
|
+
input.classList.remove('correct', 'incorrect');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const overallFeedback = _container.querySelector('.overall-feedback');
|
|
458
|
+
if (overallFeedback) {
|
|
459
|
+
overallFeedback.innerHTML = '';
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
showHint: () => {
|
|
464
|
+
validateContainer(_container, id);
|
|
465
|
+
|
|
466
|
+
if (!feedback) return;
|
|
467
|
+
|
|
468
|
+
const overallFeedback = _container.querySelector('.overall-feedback');
|
|
469
|
+
if (!overallFeedback) {
|
|
470
|
+
throw new Error(`Overall feedback element not found for fill-in question "${id}"`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
overallFeedback.innerHTML = `<div class="hint">${escapeHTML(feedback)}</div>`;
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
setResponse: (response) => {
|
|
477
|
+
validateContainer(_container, id);
|
|
478
|
+
|
|
479
|
+
const responseObj = parseResponse(response, 'object');
|
|
480
|
+
if (!responseObj) return;
|
|
481
|
+
|
|
482
|
+
const inputs = _container.querySelectorAll('input[type="text"]');
|
|
483
|
+
if (inputs.length === 0) {
|
|
484
|
+
throw new Error(`No input elements found for fill-in question "${id}"`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
Object.keys(responseObj).forEach(name => {
|
|
488
|
+
const input = _container.querySelector(`input[name="${CSS.escape(name)}"]`);
|
|
489
|
+
if (input) {
|
|
490
|
+
input.value = responseObj[name];
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
getResponse: () => {
|
|
496
|
+
// Return empty response if not yet rendered (for automation API compatibility)
|
|
497
|
+
if (!_container) {
|
|
498
|
+
const emptyResponse = {};
|
|
499
|
+
blankIds.forEach(blankId => {
|
|
500
|
+
emptyResponse[`${id}_${blankId}`] = '';
|
|
501
|
+
});
|
|
502
|
+
return emptyResponse;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const responses = {};
|
|
506
|
+
const inputs = _container.querySelectorAll('input[type="text"]');
|
|
507
|
+
|
|
508
|
+
if (inputs.length === 0) {
|
|
509
|
+
throw new Error(`No input elements found for fill-in question "${id}"`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
inputs.forEach(input => {
|
|
513
|
+
responses[input.name] = input.value.trim();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return responses;
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
getCorrectAnswer: () => {
|
|
520
|
+
return Object.fromEntries(
|
|
521
|
+
Object.entries(blanks).map(([key, blank]) => [
|
|
522
|
+
`${id}_${key}`,
|
|
523
|
+
Array.isArray(blank.correct) ? blank.correct[0] : blank.correct
|
|
524
|
+
])
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// For uncontrolled interactions, register with the central registry for lifecycle mgmt
|
|
530
|
+
if (!controlled) {
|
|
531
|
+
registerCoreInteraction(config, questionObj);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return questionObj;
|
|
535
|
+
}
|