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,616 @@
|
|
|
1
|
+
import * as Modal from '../components/ui-components/modal.js';
|
|
2
|
+
|
|
3
|
+
import { setup as initNotifications, showNotification as showNotificationComponent } from '../components/ui-components/notifications.js';
|
|
4
|
+
import { eventBus } from '../core/event-bus.js';
|
|
5
|
+
import { courseConfig } from '../../../course/course-config.js';
|
|
6
|
+
import { logger } from '../utilities/logger.js';
|
|
7
|
+
import * as AppState from './AppState.js';
|
|
8
|
+
import { createLikertQuestion } from '../components/interactions/likert.js';
|
|
9
|
+
import { iconManager } from '../utilities/icons.js';
|
|
10
|
+
import { shouldBypassGating } from '../navigation/navigation-helpers.js';
|
|
11
|
+
|
|
12
|
+
// The HTML for the completion modal's feedback section is complex, so it's defined here as a template.
|
|
13
|
+
const completionFeedbackTemplate = `
|
|
14
|
+
<div class="completion-feedback-container">
|
|
15
|
+
<div id="completion-rating-section" class="feedback-section" hidden>
|
|
16
|
+
<div id="completion-rating-container"></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div id="completion-comments-section" class="feedback-section" hidden>
|
|
19
|
+
<label for="completion-comments-textarea">Leave a comment (optional):</label>
|
|
20
|
+
<textarea id="completion-comments-textarea" class="feedback-textarea" rows="4" data-testid="completion-comments"></textarea>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
// Store the active rating interaction instance
|
|
26
|
+
let activeRatingInteraction = null;
|
|
27
|
+
|
|
28
|
+
const MODAL_DEFINITIONS = {
|
|
29
|
+
exit: {
|
|
30
|
+
title: 'Exit Course',
|
|
31
|
+
body: `
|
|
32
|
+
<p>Are you sure you want to exit the course?</p>
|
|
33
|
+
<p><strong>Your progress will be saved automatically.</strong> You can resume exactly where you left off when you return.</p>
|
|
34
|
+
`,
|
|
35
|
+
footer: `
|
|
36
|
+
<button class="btn btn-secondary" data-action="close-modal" data-testid="modal-exit-cancel">Cancel</button>
|
|
37
|
+
<button class="btn btn-primary" data-action="confirm-exit" data-testid="modal-exit-confirm">Exit Course</button>
|
|
38
|
+
`,
|
|
39
|
+
config: {
|
|
40
|
+
closeOnBackdrop: true,
|
|
41
|
+
closeOnEscape: true,
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
complete: {
|
|
45
|
+
title: 'Course Complete!',
|
|
46
|
+
body: `
|
|
47
|
+
<p><strong>Congratulations!</strong> You have successfully completed this course.</p>
|
|
48
|
+
<p>Your completion status and final score have been recorded in the Learning Management System.</p>
|
|
49
|
+
<p>When you click "Complete & Exit" below, this window will close and you will be returned to the LMS.</p>
|
|
50
|
+
${completionFeedbackTemplate}
|
|
51
|
+
`,
|
|
52
|
+
footer: `
|
|
53
|
+
<button class="btn btn-secondary" data-action="close-modal" data-testid="modal-complete-cancel">Cancel</button>
|
|
54
|
+
<button class="btn btn-primary" data-action="confirm-complete" data-testid="modal-complete-confirm">Complete & Exit</button>
|
|
55
|
+
`,
|
|
56
|
+
config: {
|
|
57
|
+
closeOnBackdrop: true,
|
|
58
|
+
closeOnEscape: true,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
postExit: {
|
|
62
|
+
title: 'Session Closed',
|
|
63
|
+
body: `
|
|
64
|
+
<p>Your progress has been saved in the LMS. It is now safe to close this window.</p>
|
|
65
|
+
<p>If this window does not close automatically, please close it manually and return to the LMS.</p>
|
|
66
|
+
`,
|
|
67
|
+
footer: '',
|
|
68
|
+
config: {
|
|
69
|
+
closeOnBackdrop: false,
|
|
70
|
+
closeOnEscape: false,
|
|
71
|
+
hideCloseButton: true,
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
restart: {
|
|
75
|
+
title: 'Restart Course',
|
|
76
|
+
body: `
|
|
77
|
+
<p class="font-bold text-error">Are you sure you want to restart the entire course?</p>
|
|
78
|
+
<p>This will permanently erase ALL of your progress, including assessment scores and engagement history. This action cannot be undone.</p>
|
|
79
|
+
`,
|
|
80
|
+
footer: `
|
|
81
|
+
<button class="btn btn-secondary" data-action="close-modal" data-testid="modal-restart-cancel">Cancel</button>
|
|
82
|
+
<button class="btn btn-primary" data-action="confirm-restart" data-testid="modal-restart-confirm">Restart Course</button>
|
|
83
|
+
`,
|
|
84
|
+
config: {
|
|
85
|
+
closeOnBackdrop: true,
|
|
86
|
+
closeOnEscape: true,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const appContainer = document.getElementById('app');
|
|
92
|
+
const loadingIndicator = document.getElementById('loading');
|
|
93
|
+
const footer = document.querySelector('.app-footer');
|
|
94
|
+
const exitButton = document.getElementById('exitBtn');
|
|
95
|
+
const prevButton = document.getElementById('prevBtn');
|
|
96
|
+
const nextButton = document.getElementById('nextBtn');
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Sidebar elements - cached after initialization
|
|
101
|
+
let sidebarToggle = null;
|
|
102
|
+
let sidebar = null;
|
|
103
|
+
let sidebarBackdrop = null;
|
|
104
|
+
|
|
105
|
+
// Footer display state - used to restore original display value
|
|
106
|
+
let originalFooterDisplay = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initializes the AppUI module. This should be called once the DOM is ready.
|
|
110
|
+
* It prepares the modal and notification systems.
|
|
111
|
+
*/
|
|
112
|
+
export function initAppUI() {
|
|
113
|
+
Modal.setup();
|
|
114
|
+
|
|
115
|
+
initNotifications();
|
|
116
|
+
|
|
117
|
+
logger.debug('AppUI initialized: Dynamic Modal and Notification systems are ready.');
|
|
118
|
+
|
|
119
|
+
_initSidebarToggle();
|
|
120
|
+
logger.debug('Sidebar toggle initialized.');
|
|
121
|
+
|
|
122
|
+
_initBranding();
|
|
123
|
+
logger.debug('Branding initialized.');
|
|
124
|
+
|
|
125
|
+
// Initialize footer button icons
|
|
126
|
+
_initFooterButtonIcons();
|
|
127
|
+
|
|
128
|
+
// Tooltips auto-initialize via event delegation - no manual init needed
|
|
129
|
+
|
|
130
|
+
// Listen for requests to prepare and show the completion modal
|
|
131
|
+
eventBus.on('ui:prepareCompletionModal', ({ promptForComments: _promptForComments, promptForRating: _promptForRating }) => {
|
|
132
|
+
// This event is now handled dynamically when the 'complete' modal is shown.
|
|
133
|
+
// The content is already part of the modal definition. We just need to show/hide sections.
|
|
134
|
+
// This logic will be triggered within the onOpen callback for the completion modal.
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
eventBus.on('ui:showModal', showModal);
|
|
138
|
+
eventBus.on('ui:hideModal', hideModal);
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
eventBus.on('ui:lockCourseForExit', _lockApplicationForExit);
|
|
143
|
+
|
|
144
|
+
// Listen for course status changes to update exit button appearance
|
|
145
|
+
eventBus.on('course:statusChanged', _handleCourseStatusChanged);
|
|
146
|
+
|
|
147
|
+
// Global Error Listener for SCORM Connection Issues
|
|
148
|
+
// This bridges the gap between low-level connection errors and user awareness.
|
|
149
|
+
eventBus.on('scorm:error', (errorData) => {
|
|
150
|
+
// Only notify for critical connection/save errors that affect data persistence
|
|
151
|
+
// We filter out minor warnings or handled recoveries to avoid noise.
|
|
152
|
+
const criticalOperations = ['Commit', 'SetValue', 'Initialize', 'Terminate'];
|
|
153
|
+
|
|
154
|
+
if (criticalOperations.includes(errorData.operation)) {
|
|
155
|
+
logger.error('[AppUI] Critical SCORM Error detected:', JSON.stringify(errorData, null, 2));
|
|
156
|
+
|
|
157
|
+
// Format a user-friendly message
|
|
158
|
+
let userMessage = 'Connection error: Your progress may not be saved.';
|
|
159
|
+
|
|
160
|
+
if (errorData.operation === 'Commit' || errorData.operation === 'SetValue') {
|
|
161
|
+
userMessage = 'Failed to save progress. Please check your internet connection.';
|
|
162
|
+
} else if (errorData.operation === 'Initialize') {
|
|
163
|
+
userMessage = 'Course failed to connect to the LMS. Progress will not be tracked.';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
showNotificationComponent(userMessage, 'error', 5000);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hides the loading indicator after the course has finished initializing.
|
|
175
|
+
*/
|
|
176
|
+
export function hideLoadingIndicator() {
|
|
177
|
+
if (loadingIndicator) {
|
|
178
|
+
loadingIndicator.style.display = 'none';
|
|
179
|
+
AppState.setLoadingVisible(false);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Displays a modal by its key from the modal definitions.
|
|
185
|
+
* @param {string} modalKey - The key of the modal in MODAL_DEFINITIONS (e.g., 'exit', 'complete').
|
|
186
|
+
*/
|
|
187
|
+
export function showModal(modalKey) {
|
|
188
|
+
const definition = MODAL_DEFINITIONS[modalKey];
|
|
189
|
+
if (!definition) {
|
|
190
|
+
throw new Error(`[AppUI] Modal definition not found for key: ${modalKey}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Special handling for completion modal to show/hide feedback sections
|
|
194
|
+
if (modalKey === 'complete') {
|
|
195
|
+
definition.onOpen = () => {
|
|
196
|
+
const completionFeatures = courseConfig.completion || {};
|
|
197
|
+
const ratingSection = document.getElementById('completion-rating-section');
|
|
198
|
+
const commentsSection = document.getElementById('completion-comments-section');
|
|
199
|
+
|
|
200
|
+
if (ratingSection && completionFeatures.promptForRating) {
|
|
201
|
+
ratingSection.hidden = false;
|
|
202
|
+
_initCompletionModal();
|
|
203
|
+
}
|
|
204
|
+
if (commentsSection && completionFeatures.promptForComments) {
|
|
205
|
+
commentsSection.hidden = false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
AppState.setCurrentModal(modalKey);
|
|
211
|
+
Modal.show(definition);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Closes the currently active modal.
|
|
216
|
+
*/
|
|
217
|
+
export function hideModal() {
|
|
218
|
+
AppState.clearCurrentModal();
|
|
219
|
+
Modal.hide();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Displays a notification message.
|
|
224
|
+
* @param {string} message - The message to display.
|
|
225
|
+
* @param {string} [type='info'] - The type of notification ('info', 'success', 'warning', 'error').
|
|
226
|
+
* @param {number} [duration=5000] - How long the notification should be visible (in ms).
|
|
227
|
+
*/
|
|
228
|
+
export function showNotification(message, type = 'info', duration = 5000) {
|
|
229
|
+
showNotificationComponent(message, type, duration);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Displays a user-friendly error modal with support contact information.
|
|
234
|
+
* This is designed for production use when users encounter errors that may affect their progress.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} options - Error modal configuration
|
|
237
|
+
* @param {string} [options.title='Something Went Wrong'] - Modal title
|
|
238
|
+
* @param {string} options.message - User-friendly error message
|
|
239
|
+
* @param {string} [options.details] - Technical details (shown in collapsible section in dev mode)
|
|
240
|
+
* @param {boolean} [options.showRefresh=true] - Whether to show refresh button
|
|
241
|
+
* @param {boolean} [options.showClose=false] - Whether to show close button (allows dismissing)
|
|
242
|
+
*/
|
|
243
|
+
export function showErrorModal(options = {}) {
|
|
244
|
+
const {
|
|
245
|
+
title = 'Something Went Wrong',
|
|
246
|
+
message = 'An unexpected error occurred.',
|
|
247
|
+
details = null,
|
|
248
|
+
showRefresh = true,
|
|
249
|
+
showClose = false
|
|
250
|
+
} = options;
|
|
251
|
+
|
|
252
|
+
// Get support email from course config
|
|
253
|
+
const supportEmail = courseConfig.support?.email || null;
|
|
254
|
+
const supportPhone = courseConfig.support?.phone || null;
|
|
255
|
+
|
|
256
|
+
// Build contact section
|
|
257
|
+
let contactHtml = '';
|
|
258
|
+
if (supportEmail || supportPhone) {
|
|
259
|
+
const contactItems = [];
|
|
260
|
+
if (supportEmail) {
|
|
261
|
+
contactItems.push(`<a href="mailto:${supportEmail}">${supportEmail}</a>`);
|
|
262
|
+
}
|
|
263
|
+
if (supportPhone) {
|
|
264
|
+
contactItems.push(`<a href="tel:${supportPhone}">${supportPhone}</a>`);
|
|
265
|
+
}
|
|
266
|
+
contactHtml = `
|
|
267
|
+
<p class="mt-4"><strong>Need help?</strong> Contact support: ${contactItems.join(' or ')}</p>
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Build details section (collapsible, only in dev mode or if explicitly requested)
|
|
272
|
+
let detailsHtml = '';
|
|
273
|
+
if (details && import.meta.env.DEV) {
|
|
274
|
+
detailsHtml = `
|
|
275
|
+
<details class="mt-4">
|
|
276
|
+
<summary class="cursor-pointer text-sm text-gray-600">Technical Details</summary>
|
|
277
|
+
<pre class="mt-2 p-3 bg-gray-100 rounded text-xs overflow-auto max-h-40">${details}</pre>
|
|
278
|
+
</details>
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Build footer buttons
|
|
283
|
+
const footerButtons = [];
|
|
284
|
+
if (showClose) {
|
|
285
|
+
footerButtons.push('<button class="btn btn-secondary" data-action="close-modal" data-testid="modal-error-close">Close</button>');
|
|
286
|
+
}
|
|
287
|
+
if (showRefresh) {
|
|
288
|
+
footerButtons.push('<button class="btn btn-primary" data-action="refresh-page" data-testid="modal-error-refresh">Refresh Page</button>');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Show the modal directly (no need for a static definition)
|
|
292
|
+
AppState.setCurrentModal('error');
|
|
293
|
+
Modal.show({
|
|
294
|
+
title,
|
|
295
|
+
body: `
|
|
296
|
+
<div class="callout callout-danger mb-4" role="alert">
|
|
297
|
+
<p>${message}</p>
|
|
298
|
+
</div>
|
|
299
|
+
${contactHtml}
|
|
300
|
+
${detailsHtml}
|
|
301
|
+
`,
|
|
302
|
+
footer: footerButtons.join('\n'),
|
|
303
|
+
config: {
|
|
304
|
+
closeOnBackdrop: showClose,
|
|
305
|
+
closeOnEscape: showClose,
|
|
306
|
+
hideCloseButton: !showClose,
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Retrieves the main application container element.
|
|
313
|
+
* @returns {HTMLElement} The application container element.
|
|
314
|
+
*/
|
|
315
|
+
export function getAppContainer() {
|
|
316
|
+
return appContainer;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Retrieves completion modal data (rating and comments) entered by the user.
|
|
321
|
+
* @returns {{rating: string|null, comment: string|null}} Object containing rating and comment values.
|
|
322
|
+
*/
|
|
323
|
+
export function getCompletionModalData() {
|
|
324
|
+
let rating = null;
|
|
325
|
+
|
|
326
|
+
if (activeRatingInteraction) {
|
|
327
|
+
const response = activeRatingInteraction.getResponse();
|
|
328
|
+
// Extract the value for the 'overall' question
|
|
329
|
+
if (response && response['overall']) {
|
|
330
|
+
rating = response['overall'];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const commentTextarea = document.getElementById('completion-comments-textarea');
|
|
335
|
+
const comment = (commentTextarea && commentTextarea.value.trim())
|
|
336
|
+
? commentTextarea.value
|
|
337
|
+
: null;
|
|
338
|
+
|
|
339
|
+
return { rating, comment };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Initializes the sidebar toggle functionality.
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
function _initSidebarToggle() {
|
|
347
|
+
sidebarToggle = document.getElementById('sidebar-toggle');
|
|
348
|
+
sidebar = document.getElementById('sidebar');
|
|
349
|
+
sidebarBackdrop = document.getElementById('sidebar-backdrop');
|
|
350
|
+
|
|
351
|
+
if (!sidebarToggle || !sidebar) {
|
|
352
|
+
throw new Error('Sidebar toggle elements not found.');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Inject menu icon
|
|
356
|
+
const toggleIcon = sidebarToggle.querySelector('.toggle-icon');
|
|
357
|
+
if (toggleIcon) {
|
|
358
|
+
toggleIcon.innerHTML = iconManager.getIcon('menu', { size: 'lg' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
sidebarToggle.addEventListener('click', toggleSidebar);
|
|
362
|
+
if (sidebarBackdrop) {
|
|
363
|
+
sidebarBackdrop.addEventListener('click', closeSidebar);
|
|
364
|
+
}
|
|
365
|
+
document.addEventListener('keydown', (e) => {
|
|
366
|
+
if (e.key === 'Escape' && !sidebar.classList.contains('collapsed')) {
|
|
367
|
+
closeSidebar();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Initializes the interactive elements within the completion modal.
|
|
374
|
+
* This needs to be called each time the completion modal is opened,
|
|
375
|
+
* as its content is now dynamic.
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
function _initCompletionModal() {
|
|
379
|
+
const container = document.getElementById('completion-rating-container');
|
|
380
|
+
if (!container) return;
|
|
381
|
+
|
|
382
|
+
// Clear previous content
|
|
383
|
+
container.innerHTML = '';
|
|
384
|
+
|
|
385
|
+
// Create the likert interaction
|
|
386
|
+
activeRatingInteraction = createLikertQuestion({
|
|
387
|
+
id: 'course-rating',
|
|
388
|
+
prompt: 'How would you rate this course?',
|
|
389
|
+
scale: [
|
|
390
|
+
{ value: '1', text: '1 Star' },
|
|
391
|
+
{ value: '2', text: '2 Stars' },
|
|
392
|
+
{ value: '3', text: '3 Stars' },
|
|
393
|
+
{ value: '4', text: '4 Stars' },
|
|
394
|
+
{ value: '5', text: '5 Stars' }
|
|
395
|
+
],
|
|
396
|
+
questions: [
|
|
397
|
+
{ id: 'overall', text: 'Overall Rating' }
|
|
398
|
+
],
|
|
399
|
+
// No correctAnswers means it's a survey (always correct)
|
|
400
|
+
feedback: {
|
|
401
|
+
correct: 'Thank you for your rating!'
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
activeRatingInteraction.render(container);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function toggleSidebar() {
|
|
409
|
+
const isCollapsed = sidebar.classList.toggle('collapsed');
|
|
410
|
+
AppState.setSidebarCollapsed(isCollapsed);
|
|
411
|
+
sidebarToggle.setAttribute('aria-expanded', String(!isCollapsed));
|
|
412
|
+
if (sidebarBackdrop) {
|
|
413
|
+
sidebarBackdrop.classList.toggle('visible', !isCollapsed);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function closeSidebar() {
|
|
418
|
+
sidebar.classList.add('collapsed');
|
|
419
|
+
AppState.setSidebarCollapsed(true);
|
|
420
|
+
sidebarToggle.setAttribute('aria-expanded', 'false');
|
|
421
|
+
if (sidebarBackdrop) {
|
|
422
|
+
sidebarBackdrop.classList.remove('visible');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function openSidebar() {
|
|
427
|
+
sidebar.classList.remove('collapsed');
|
|
428
|
+
sidebarToggle.setAttribute('aria-expanded', 'true');
|
|
429
|
+
if (sidebarBackdrop) {
|
|
430
|
+
sidebarBackdrop.classList.add('visible');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function showFooter() {
|
|
435
|
+
if (!footer) return;
|
|
436
|
+
if (originalFooterDisplay === undefined || originalFooterDisplay === null) {
|
|
437
|
+
originalFooterDisplay = '';
|
|
438
|
+
}
|
|
439
|
+
footer.style.display = originalFooterDisplay;
|
|
440
|
+
|
|
441
|
+
// Re-enable sidebar toggle when footer is shown
|
|
442
|
+
if (sidebarToggle) {
|
|
443
|
+
sidebarToggle.disabled = false;
|
|
444
|
+
sidebarToggle.setAttribute('aria-disabled', 'false');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function hideFooter() {
|
|
449
|
+
if (!footer) return;
|
|
450
|
+
if (originalFooterDisplay === undefined || originalFooterDisplay === null) {
|
|
451
|
+
originalFooterDisplay = footer.style.display || '';
|
|
452
|
+
}
|
|
453
|
+
footer.style.display = 'none';
|
|
454
|
+
|
|
455
|
+
// Disable sidebar toggle and close sidebar when footer is hidden
|
|
456
|
+
// This prevents navigation during assessment question/review views
|
|
457
|
+
if (sidebarToggle) {
|
|
458
|
+
sidebarToggle.disabled = true;
|
|
459
|
+
sidebarToggle.setAttribute('aria-disabled', 'true');
|
|
460
|
+
}
|
|
461
|
+
closeSidebar();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function _initBranding() {
|
|
465
|
+
const brandContainer = document.getElementById('brand');
|
|
466
|
+
if (!brandContainer) throw new Error('Brand container #brand not found.');
|
|
467
|
+
|
|
468
|
+
const { branding = {} } = courseConfig;
|
|
469
|
+
const { logo, logoAlt, courseTitle: brandTitle } = branding;
|
|
470
|
+
const courseTitle = brandTitle || courseConfig.metadata?.title || 'Course';
|
|
471
|
+
|
|
472
|
+
let brandHTML = '';
|
|
473
|
+
|
|
474
|
+
// For SVG logos, inline them so they can inherit color via currentColor
|
|
475
|
+
if (logo && logo.endsWith('.svg')) {
|
|
476
|
+
try {
|
|
477
|
+
const response = await fetch(logo);
|
|
478
|
+
if (response.ok) {
|
|
479
|
+
const svgText = await response.text();
|
|
480
|
+
// Wrap in a container for styling
|
|
481
|
+
brandHTML += `<span class="logo" role="img" aria-label="${logoAlt || branding.companyName || 'Logo'}">${svgText}</span>`;
|
|
482
|
+
} else {
|
|
483
|
+
// Fallback to img if fetch fails
|
|
484
|
+
brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
// Fallback to img if fetch fails
|
|
488
|
+
brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
|
|
489
|
+
}
|
|
490
|
+
} else if (logo) {
|
|
491
|
+
brandHTML += `<img src="${logo}" alt="${logoAlt || branding.companyName || 'Logo'}" class="logo" />`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (courseTitle) {
|
|
495
|
+
brandHTML += `<span class="brand-title">${courseTitle}</span>`;
|
|
496
|
+
}
|
|
497
|
+
brandContainer.innerHTML = brandHTML;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Initializes icons for footer buttons using iconManager.
|
|
502
|
+
* @private
|
|
503
|
+
*/
|
|
504
|
+
function _initFooterButtonIcons() {
|
|
505
|
+
// Exit button icon - insert at the start of button
|
|
506
|
+
if (exitButton) {
|
|
507
|
+
exitButton.insertAdjacentHTML('afterbegin', iconManager.getIcon('log-out'));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Handles course status changes to update the exit button appearance.
|
|
516
|
+
* When on the last slide with completion requirements met, shows "Complete Course" button.
|
|
517
|
+
* @private
|
|
518
|
+
* @param {object} data - Status change data
|
|
519
|
+
* @param {string} data.completionStatus - Current completion status
|
|
520
|
+
* @param {boolean} data.isOnLastSlide - Whether user is on the last slide
|
|
521
|
+
*/
|
|
522
|
+
function _handleCourseStatusChanged({ completionStatus, isOnLastSlide }) {
|
|
523
|
+
// In dev mode with gating disabled, always show completion button on last slide
|
|
524
|
+
const bypassGating = shouldBypassGating();
|
|
525
|
+
const showCompletionButton = isOnLastSlide && (completionStatus === 'completed' || bypassGating);
|
|
526
|
+
|
|
527
|
+
if (showCompletionButton) {
|
|
528
|
+
_setExitButtonToCompletionMode();
|
|
529
|
+
} else {
|
|
530
|
+
_setExitButtonToNormalMode();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Animates the exit button content change with a fade transition.
|
|
536
|
+
* @private
|
|
537
|
+
* @param {Function} updateFn - Function that performs the actual DOM updates
|
|
538
|
+
*/
|
|
539
|
+
function _animateExitButtonChange(updateFn) {
|
|
540
|
+
if (!exitButton) return;
|
|
541
|
+
|
|
542
|
+
// Add transition style and fade out
|
|
543
|
+
exitButton.style.transition = 'opacity 150ms ease-out, background-color 200ms ease, border-color 200ms ease';
|
|
544
|
+
exitButton.style.opacity = '0';
|
|
545
|
+
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
// Perform the update while hidden
|
|
548
|
+
updateFn();
|
|
549
|
+
|
|
550
|
+
// Fade back in
|
|
551
|
+
exitButton.style.opacity = '1';
|
|
552
|
+
|
|
553
|
+
// Clean up inline transition after animation completes
|
|
554
|
+
setTimeout(() => {
|
|
555
|
+
exitButton.style.transition = '';
|
|
556
|
+
}, 200);
|
|
557
|
+
}, 150);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Updates exit button to completion mode (green success style with trophy icon).
|
|
562
|
+
* @private
|
|
563
|
+
*/
|
|
564
|
+
function _setExitButtonToCompletionMode() {
|
|
565
|
+
if (!exitButton) return;
|
|
566
|
+
|
|
567
|
+
// Skip animation if already in completion mode
|
|
568
|
+
if (exitButton.classList.contains('btn-success')) return;
|
|
569
|
+
|
|
570
|
+
_animateExitButtonChange(() => {
|
|
571
|
+
exitButton.innerHTML = iconManager.getIcon('trophy') + 'Complete Course';
|
|
572
|
+
exitButton.classList.remove('btn-secondary');
|
|
573
|
+
exitButton.classList.add('btn-success');
|
|
574
|
+
exitButton.setAttribute('data-tooltip', 'Complete and exit the course');
|
|
575
|
+
exitButton.setAttribute('data-testid', 'nav-complete');
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Reverts exit button to normal mode (secondary style with log-out icon).
|
|
581
|
+
* @private
|
|
582
|
+
*/
|
|
583
|
+
function _setExitButtonToNormalMode() {
|
|
584
|
+
if (!exitButton) return;
|
|
585
|
+
|
|
586
|
+
// Skip animation if already in normal mode
|
|
587
|
+
if (exitButton.classList.contains('btn-secondary')) return;
|
|
588
|
+
|
|
589
|
+
_animateExitButtonChange(() => {
|
|
590
|
+
exitButton.innerHTML = iconManager.getIcon('log-out') + 'Exit Course';
|
|
591
|
+
exitButton.classList.remove('btn-success');
|
|
592
|
+
exitButton.classList.add('btn-secondary');
|
|
593
|
+
exitButton.setAttribute('data-tooltip', 'Save progress and exit');
|
|
594
|
+
exitButton.setAttribute('data-testid', 'nav-exit');
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function _lockApplicationForExit() {
|
|
599
|
+
AppState.setCourseExitLocked(true);
|
|
600
|
+
AppState.setExitInProgress(true);
|
|
601
|
+
|
|
602
|
+
if (exitButton) {
|
|
603
|
+
exitButton.removeAttribute('data-action');
|
|
604
|
+
exitButton.innerHTML = iconManager.getIcon('check-circle') + 'Course Complete';
|
|
605
|
+
exitButton.disabled = true;
|
|
606
|
+
exitButton.classList.remove('btn-secondary');
|
|
607
|
+
exitButton.classList.add('btn-success');
|
|
608
|
+
}
|
|
609
|
+
if (prevButton) prevButton.disabled = true;
|
|
610
|
+
if (nextButton) nextButton.disabled = true;
|
|
611
|
+
if (sidebarToggle) sidebarToggle.disabled = true;
|
|
612
|
+
|
|
613
|
+
closeSidebar();
|
|
614
|
+
hideModal(); // Hide any active modal
|
|
615
|
+
showModal('postExit'); // Show the final "safe to close" modal
|
|
616
|
+
}
|