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,410 @@
|
|
|
1
|
+
import stateManager from '../state/index.js';
|
|
2
|
+
import * as CourseHelpers from '../utilities/course-helpers.js';
|
|
3
|
+
import * as NavigationActions from '../navigation/NavigationActions.js';
|
|
4
|
+
import * as NavigationState from '../navigation/NavigationState.js';
|
|
5
|
+
import * as AssessmentManager from '../managers/assessment-manager.js';
|
|
6
|
+
import commentManager from '../managers/comment-manager.js';
|
|
7
|
+
import { eventBus } from '../core/event-bus.js';
|
|
8
|
+
import { courseConfig } from '../../../course/course-config.js';
|
|
9
|
+
import { showNotification, getCompletionModalData } from './AppUI.js';
|
|
10
|
+
import { announceToScreenReader } from '../components/ui-components/index.js';
|
|
11
|
+
import * as AppState from './AppState.js';
|
|
12
|
+
|
|
13
|
+
import { shouldBypassGating } from '../navigation/navigation-helpers.js';
|
|
14
|
+
import { logger } from '../utilities/logger.js';
|
|
15
|
+
|
|
16
|
+
// Re-export UI functions for use by other modules
|
|
17
|
+
export { showNotification };
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initializes the AppActions module by setting up event handlers for exit buttons.
|
|
22
|
+
*/
|
|
23
|
+
export function initAppActions() {
|
|
24
|
+
_initEventListeners();
|
|
25
|
+
|
|
26
|
+
// Listen for navigation events to handle timing and completion
|
|
27
|
+
eventBus.on('navigation:beforeChange', ({ fromSlideId }) => endSessionTimer(fromSlideId));
|
|
28
|
+
eventBus.on('navigation:changed', ({ toSlideId }) => startSessionTimer(toSlideId));
|
|
29
|
+
eventBus.on('navigation:completeCheck', checkCompletion);
|
|
30
|
+
|
|
31
|
+
logger.debug('AppActions initialized.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Sets up a global event listener for all application-level actions.
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
function _initEventListeners() {
|
|
39
|
+
document.body.addEventListener('click', async (event) => {
|
|
40
|
+
const actionTarget = event.target.closest('[data-action]');
|
|
41
|
+
if (!actionTarget) return;
|
|
42
|
+
|
|
43
|
+
const action = actionTarget.dataset.action;
|
|
44
|
+
|
|
45
|
+
switch (action) {
|
|
46
|
+
case 'exit-course':
|
|
47
|
+
await _handleExitClick();
|
|
48
|
+
break;
|
|
49
|
+
case 'confirm-exit':
|
|
50
|
+
await exit();
|
|
51
|
+
break;
|
|
52
|
+
case 'confirm-complete':
|
|
53
|
+
await _completeAndExit();
|
|
54
|
+
break;
|
|
55
|
+
case 'confirm-restart':
|
|
56
|
+
await restartCourse();
|
|
57
|
+
break;
|
|
58
|
+
case 'refresh-page':
|
|
59
|
+
window.location.reload();
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'provide-feedback':
|
|
63
|
+
_handleProvideFeedback();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async function _handleProvideFeedback() {
|
|
72
|
+
|
|
73
|
+
const feedback = prompt('Please provide feedback about your learning experience:');
|
|
74
|
+
if (feedback) {
|
|
75
|
+
commentManager.addComment(feedback, 'course-feedback');
|
|
76
|
+
announceToScreenReader('Thank you for your feedback!');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Checks if the course completion criteria have been met.
|
|
83
|
+
* Delegates to appropriate managers to determine completion status.
|
|
84
|
+
*
|
|
85
|
+
* Criteria:
|
|
86
|
+
* 1. User is on the last slide or a summary slide (NavigationManager)
|
|
87
|
+
* 2. All assessments with completion requirements have met those requirements (AssessmentManager)
|
|
88
|
+
*
|
|
89
|
+
* If all criteria are met, it updates the state and shows the completion modal.
|
|
90
|
+
* @returns {Promise<boolean>} True if the course is complete, otherwise false.
|
|
91
|
+
*/
|
|
92
|
+
export async function checkCompletion() {
|
|
93
|
+
const onCompletionSlide = NavigationActions.isOnLastSlide();
|
|
94
|
+
const currentCompletionStatus = stateManager.getCompletion() || 'incomplete';
|
|
95
|
+
|
|
96
|
+
// If not on a completion slide, we don't process completion/success status yet.
|
|
97
|
+
if (!onCompletionSlide) {
|
|
98
|
+
const currentSuccessStatus = currentCompletionStatus === 'completed'
|
|
99
|
+
? (stateManager.getSuccess() || 'unknown')
|
|
100
|
+
: 'unknown';
|
|
101
|
+
|
|
102
|
+
eventBus.emit('course:statusChanged', {
|
|
103
|
+
completionStatus: currentCompletionStatus,
|
|
104
|
+
successStatus: currentSuccessStatus,
|
|
105
|
+
isOnLastSlide: false
|
|
106
|
+
});
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If already marked as completed, no need to re-evaluate.
|
|
111
|
+
if (currentCompletionStatus === 'completed') {
|
|
112
|
+
eventBus.emit('course:statusChanged', {
|
|
113
|
+
completionStatus: 'completed',
|
|
114
|
+
successStatus: stateManager.getSuccess() || 'unknown',
|
|
115
|
+
isOnLastSlide: true
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// On a completion slide, so evaluate final status.
|
|
121
|
+
const assessmentSlides = await CourseHelpers.getSlidesByType('assessment');
|
|
122
|
+
const assessmentConfigs = await CourseHelpers.getAssessmentConfigs();
|
|
123
|
+
|
|
124
|
+
// Completion is based on submission requirements.
|
|
125
|
+
const assessmentsRequiringSubmission = assessmentSlides.filter(s =>
|
|
126
|
+
assessmentConfigs.get(s.assessmentId)?.completionRequirements?.requireSubmission
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const allSubmitted = assessmentsRequiringSubmission.every(s =>
|
|
130
|
+
AssessmentManager.meetsCompletionRequirements(s.assessmentId, { requireSubmission: true })
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const completionStatus = allSubmitted ? 'completed' : 'incomplete';
|
|
134
|
+
|
|
135
|
+
// Success is based on passing requirements.
|
|
136
|
+
const assessmentsRequiringPass = assessmentSlides.filter(s =>
|
|
137
|
+
assessmentConfigs.get(s.assessmentId)?.completionRequirements?.requirePass
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
let successStatus = 'unknown';
|
|
141
|
+
if (assessmentsRequiringPass.length > 0) {
|
|
142
|
+
const allPassed = assessmentsRequiringPass.every(s =>
|
|
143
|
+
AssessmentManager.meetsCompletionRequirements(s.assessmentId, { requirePass: true })
|
|
144
|
+
);
|
|
145
|
+
successStatus = allPassed ? 'passed' : 'failed';
|
|
146
|
+
// Note: Course score reporting is handled by ScoreManager (configure in course-config.js)
|
|
147
|
+
} else if (completionStatus === 'completed') {
|
|
148
|
+
// If the course is completed and no assessments required passing, then success is 'passed'.
|
|
149
|
+
successStatus = 'passed';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Persist the determined completion and success statuses.
|
|
153
|
+
stateManager.reportCompletion(completionStatus);
|
|
154
|
+
stateManager.reportSuccess(successStatus);
|
|
155
|
+
|
|
156
|
+
// Flush critical completion status to LMS immediately
|
|
157
|
+
// Course completion is a critical action - don't rely on debounce
|
|
158
|
+
await stateManager.flush();
|
|
159
|
+
|
|
160
|
+
logger.debug(`[AppActions] checkCompletion: completionStatus=${completionStatus}, successStatus=${successStatus}`);
|
|
161
|
+
|
|
162
|
+
eventBus.emit('course:statusChanged', {
|
|
163
|
+
completionStatus,
|
|
164
|
+
successStatus,
|
|
165
|
+
isOnLastSlide: true
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Return true only if the course is actually complete
|
|
169
|
+
return completionStatus === 'completed';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sets the flag indicating a legitimate exit is occurring.
|
|
174
|
+
* This is called by any intentional exit action to signal to the 'beforeunload'
|
|
175
|
+
* guard that it should allow the page to unload.
|
|
176
|
+
*/
|
|
177
|
+
export function setExitFlag() {
|
|
178
|
+
AppState.setExitIntentional(true);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Finalizes the current slide's progress before exiting.
|
|
183
|
+
* Marks the current slide as visited and updates the progress measure.
|
|
184
|
+
* @private
|
|
185
|
+
*/
|
|
186
|
+
function _finalizeCurrentSlideProgress() {
|
|
187
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
188
|
+
if (currentSlideId) {
|
|
189
|
+
NavigationState.addVisitedSlide(currentSlideId);
|
|
190
|
+
|
|
191
|
+
// Update progress measure one final time before exit
|
|
192
|
+
const slides = NavigationActions.getAllSlides();
|
|
193
|
+
if (slides) {
|
|
194
|
+
const totalSequentialSlides = slides.filter(s =>
|
|
195
|
+
s.navigation?.sequence?.included !== false
|
|
196
|
+
).length;
|
|
197
|
+
stateManager.updateProgressMeasure(totalSequentialSlides);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Exits the course, saving the current state (suspend data).
|
|
204
|
+
*/
|
|
205
|
+
export async function exit() {
|
|
206
|
+
setExitFlag();
|
|
207
|
+
AppState.setExitInProgress(true);
|
|
208
|
+
|
|
209
|
+
// Mark the current slide as visited before exiting
|
|
210
|
+
// This ensures that if the user exits from the last slide, it counts toward progress
|
|
211
|
+
_finalizeCurrentSlideProgress();
|
|
212
|
+
|
|
213
|
+
const exitResult = await stateManager.exitCourseWithSuspend();
|
|
214
|
+
|
|
215
|
+
if (exitResult) {
|
|
216
|
+
eventBus.emit('ui:lockCourseForExit');
|
|
217
|
+
} else {
|
|
218
|
+
showNotification('We were unable to finalize the session automatically. Please close this window manually.', 'warning', 6000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Gathers final feedback/rating and exits the course after marking it as complete.
|
|
224
|
+
* This is called when the user explicitly clicks "Complete & Exit" in the completion modal.
|
|
225
|
+
*/
|
|
226
|
+
async function _completeAndExit() {
|
|
227
|
+
setExitFlag();
|
|
228
|
+
AppState.setExitInProgress(true);
|
|
229
|
+
|
|
230
|
+
// Mark the current slide as visited before exiting
|
|
231
|
+
// This ensures the final slide counts toward progress
|
|
232
|
+
_finalizeCurrentSlideProgress();
|
|
233
|
+
|
|
234
|
+
// CRITICAL: Ensure completion status is set to 'completed' when user explicitly completes
|
|
235
|
+
// This handles edge cases where checkCompletion() may not have set it (e.g., dev bypass)
|
|
236
|
+
const currentStatus = stateManager.getCompletion();
|
|
237
|
+
if (currentStatus !== 'completed') {
|
|
238
|
+
logger.debug('[AppActions] Setting completion status to completed on explicit user completion');
|
|
239
|
+
stateManager.reportCompletion('completed');
|
|
240
|
+
|
|
241
|
+
// Also set success status if not already set
|
|
242
|
+
const currentSuccess = stateManager.getSuccess();
|
|
243
|
+
if (!currentSuccess || currentSuccess === 'unknown') {
|
|
244
|
+
stateManager.reportSuccess('passed');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Gather feedback and rating from the completion modal
|
|
249
|
+
const { rating, comment } = getCompletionModalData();
|
|
250
|
+
|
|
251
|
+
if (rating) {
|
|
252
|
+
commentManager.addRating(rating);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (comment) {
|
|
256
|
+
commentManager.addComment(comment, 'course-completion');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// DEFENSIVE: Force an immediate commit after setting all completion data
|
|
260
|
+
// This ensures completion + feedback is persisted even if subsequent operations fail
|
|
261
|
+
await stateManager.flush();
|
|
262
|
+
|
|
263
|
+
// Show saving indicator and give LMS time to persist before terminating
|
|
264
|
+
_exitWithSavingDelay();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Performs the actual exit with a brief delay to ensure LMS persistence.
|
|
269
|
+
* Shows a "Saving..." indicator to prevent users from closing prematurely.
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
function _exitWithSavingDelay() {
|
|
273
|
+
// Show saving indicator
|
|
274
|
+
showNotification('Saving your progress...', 'info', 2000);
|
|
275
|
+
|
|
276
|
+
// Brief delay to allow LMS to process the commit
|
|
277
|
+
// This helps with LMSs that have slow backend persistence
|
|
278
|
+
setTimeout(async () => {
|
|
279
|
+
try {
|
|
280
|
+
const exitResult = await stateManager.exitCourseComplete();
|
|
281
|
+
|
|
282
|
+
if (exitResult) {
|
|
283
|
+
eventBus.emit('ui:lockCourseForExit');
|
|
284
|
+
} else {
|
|
285
|
+
showNotification('We were unable to finalize the session automatically. Please close this window manually.', 'warning', 6000);
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.error(`Failed to complete exit: ${error.message}`, { domain: 'app', operation: 'completeAndExit', stack: error.stack });
|
|
289
|
+
showNotification('We were unable to finalize the session automatically. Please close this window manually.', 'warning', 6000);
|
|
290
|
+
}
|
|
291
|
+
}, 500);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Handles the initial click on the main exit button.
|
|
296
|
+
* It checks for completion first; if not complete, it shows the exit confirmation modal.
|
|
297
|
+
* In dev mode with gating disabled, allows immediate completion from the last slide.
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
async function _handleExitClick() {
|
|
301
|
+
// Check completion status (handles normal completion flow)
|
|
302
|
+
if (await checkCompletion()) {
|
|
303
|
+
showCompletionModal();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Dev mode override: if gating is disabled and we're on the last slide,
|
|
308
|
+
// show completion modal even if requirements aren't met
|
|
309
|
+
if (shouldBypassGating() && NavigationActions.isOnLastSlide()) {
|
|
310
|
+
showCompletionModal();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
eventBus.emit('ui:showModal', 'exit');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Starts tracking time spent on a slide by recording start time in state.
|
|
320
|
+
* If a timer is already running for this slide, it ends the previous session first.
|
|
321
|
+
* @param {string} slideId - The ID of the slide to track.
|
|
322
|
+
*/
|
|
323
|
+
export function startSessionTimer(slideId) {
|
|
324
|
+
if (!slideId) {
|
|
325
|
+
throw new Error('[AppActions] startSessionTimer called without slideId');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const sessionData = stateManager.getDomainState('sessionData') || {};
|
|
329
|
+
sessionData.slideStartTimes = sessionData.slideStartTimes || {};
|
|
330
|
+
sessionData.slideDurations = sessionData.slideDurations || {};
|
|
331
|
+
|
|
332
|
+
// If there's already a start time, end that session first
|
|
333
|
+
const existingStartTime = sessionData.slideStartTimes[slideId];
|
|
334
|
+
if (existingStartTime) {
|
|
335
|
+
const duration = Date.now() - existingStartTime;
|
|
336
|
+
sessionData.slideDurations[slideId] = (sessionData.slideDurations[slideId] || 0) + duration;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Record new start time
|
|
340
|
+
sessionData.slideStartTimes[slideId] = Date.now();
|
|
341
|
+
stateManager.setDomainState('sessionData', sessionData);
|
|
342
|
+
|
|
343
|
+
logger.debug(`[AppActions] Session timer started for: ${slideId}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Ends the session timer for a slide and records the duration.
|
|
348
|
+
* @param {string} slideId - The ID of the slide to stop tracking.
|
|
349
|
+
* @returns {number|null} Duration in milliseconds, or null if no timer was active.
|
|
350
|
+
*/
|
|
351
|
+
export function endSessionTimer(slideId) {
|
|
352
|
+
if (!slideId) {
|
|
353
|
+
throw new Error('[AppActions] endSessionTimer called without slideId');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const sessionData = stateManager.getDomainState('sessionData') || {};
|
|
357
|
+
sessionData.slideStartTimes = sessionData.slideStartTimes || {};
|
|
358
|
+
sessionData.slideDurations = sessionData.slideDurations || {};
|
|
359
|
+
|
|
360
|
+
const startTime = sessionData.slideStartTimes[slideId];
|
|
361
|
+
if (!startTime) {
|
|
362
|
+
return null; // No timer was active - expected behavior
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const duration = Date.now() - startTime;
|
|
366
|
+
sessionData.slideDurations[slideId] = (sessionData.slideDurations[slideId] || 0) + duration;
|
|
367
|
+
delete sessionData.slideStartTimes[slideId];
|
|
368
|
+
|
|
369
|
+
stateManager.setDomainState('sessionData', sessionData);
|
|
370
|
+
logger.debug(`[AppActions] Session timer ended for: ${slideId}, duration: ${duration}ms`);
|
|
371
|
+
return duration;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Calculates the total session duration by summing all slide durations.
|
|
376
|
+
* @returns {number} Total duration in milliseconds.
|
|
377
|
+
*/
|
|
378
|
+
export function getSessionDuration() {
|
|
379
|
+
const sessionData = stateManager.getDomainState('sessionData') || {};
|
|
380
|
+
const slideDurations = sessionData.slideDurations || {};
|
|
381
|
+
return Object.values(slideDurations).reduce((total, duration) => total + duration, 0);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function showCompletionModal() {
|
|
385
|
+
const completionFeatures = courseConfig.completion || {};
|
|
386
|
+
eventBus.emit('ui:prepareCompletionModal', {
|
|
387
|
+
promptForComments: !!completionFeatures.promptForComments,
|
|
388
|
+
promptForRating: !!completionFeatures.promptForRating,
|
|
389
|
+
});
|
|
390
|
+
eventBus.emit('ui:showModal', 'complete');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Restarts the course by clearing all SCORM suspend data and reloading the page.
|
|
395
|
+
* This is typically used when a learner has exhausted all assessment attempts.
|
|
396
|
+
* WARNING: This cannot be undone - all progress will be lost.
|
|
397
|
+
*/
|
|
398
|
+
export async function restartCourse() {
|
|
399
|
+
try {
|
|
400
|
+
// Clear all suspend data
|
|
401
|
+
await stateManager.clearAllData();
|
|
402
|
+
|
|
403
|
+
// Reload the page to reinitialize from scratch
|
|
404
|
+
// The beforeunload guard will not trigger since we're explicitly restarting
|
|
405
|
+
window.location.reload();
|
|
406
|
+
} catch (error) {
|
|
407
|
+
logger.error(`Failed to restart course: ${error.message}`, { domain: 'app', operation: 'restartCourse', stack: error.stack });
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AppState.js
|
|
3
|
+
* @description Manages the global application state for the course.
|
|
4
|
+
* This is the "State" component of the State-UI-Actions pattern for the app/ directory.
|
|
5
|
+
* Holds in-memory application-level state (UI state, flags, etc.) - NOT course data.
|
|
6
|
+
* Course data persistence is handled by StateManager in managers/.
|
|
7
|
+
*
|
|
8
|
+
* @author Seth
|
|
9
|
+
* @version 3.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { deepClone } from '../utilities/utilities.js';
|
|
13
|
+
import { logger } from '../utilities/logger.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal application state object.
|
|
17
|
+
* All state is kept private and accessed through getter/setter functions.
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
const state = {
|
|
21
|
+
// Initialization
|
|
22
|
+
isInitialized: false,
|
|
23
|
+
initializationError: null,
|
|
24
|
+
|
|
25
|
+
// Modal Management
|
|
26
|
+
currentModal: null,
|
|
27
|
+
modalStack: [],
|
|
28
|
+
|
|
29
|
+
// UI State
|
|
30
|
+
loadingVisible: true,
|
|
31
|
+
sidebarCollapsed: true,
|
|
32
|
+
|
|
33
|
+
// Exit Flow
|
|
34
|
+
courseExitLocked: false,
|
|
35
|
+
exitInProgress: false,
|
|
36
|
+
isExitIntentional: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initializes the application state.
|
|
41
|
+
* @throws {Error} If already initialized
|
|
42
|
+
*/
|
|
43
|
+
export function initAppState() {
|
|
44
|
+
if (state.isInitialized) {
|
|
45
|
+
throw new Error('AppState: Already initialized. Do not call initAppState() more than once.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
state.isInitialized = true;
|
|
49
|
+
logger.debug('[AppState] Initialized');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gets the entire application state (deep clone for immutability).
|
|
54
|
+
* @returns {object} A deep clone of the current state
|
|
55
|
+
*/
|
|
56
|
+
export function getState() {
|
|
57
|
+
return deepClone(state);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Initialization State
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if the application has been initialized.
|
|
66
|
+
* @returns {boolean} True if initialized
|
|
67
|
+
*/
|
|
68
|
+
export function isInitialized() {
|
|
69
|
+
return state.isInitialized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sets an initialization error.
|
|
74
|
+
* @param {Error|string} error - The error that occurred during initialization
|
|
75
|
+
*/
|
|
76
|
+
export function setInitializationError(error) {
|
|
77
|
+
state.initializationError = error instanceof Error ? error : new Error(String(error));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Gets the initialization error if one occurred.
|
|
82
|
+
* @returns {Error|null} The initialization error or null
|
|
83
|
+
*/
|
|
84
|
+
export function getInitializationError() {
|
|
85
|
+
return state.initializationError;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Modal Management
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sets the currently active modal.
|
|
94
|
+
* @param {string|null} modalId - The ID of the modal or null to clear
|
|
95
|
+
*/
|
|
96
|
+
export function setCurrentModal(modalId) {
|
|
97
|
+
if (modalId && state.currentModal !== modalId) {
|
|
98
|
+
// Push previous modal to stack if exists
|
|
99
|
+
if (state.currentModal) {
|
|
100
|
+
state.modalStack.push(state.currentModal);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
state.currentModal = modalId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gets the currently active modal ID.
|
|
108
|
+
* @returns {string|null} The current modal ID or null
|
|
109
|
+
*/
|
|
110
|
+
export function getCurrentModal() {
|
|
111
|
+
return state.currentModal;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clears the current modal (on close).
|
|
116
|
+
*/
|
|
117
|
+
export function clearCurrentModal() {
|
|
118
|
+
state.currentModal = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Pops and returns the previous modal from the stack.
|
|
123
|
+
* @returns {string|null} The previous modal ID or null
|
|
124
|
+
*/
|
|
125
|
+
export function popModalStack() {
|
|
126
|
+
return state.modalStack.pop() || null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// UI State
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sets the loading indicator visibility state.
|
|
135
|
+
* @param {boolean} visible - Whether loading indicator should be visible
|
|
136
|
+
*/
|
|
137
|
+
export function setLoadingVisible(visible) {
|
|
138
|
+
state.loadingVisible = !!visible;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Gets the loading indicator visibility state.
|
|
143
|
+
* @returns {boolean} True if loading is visible
|
|
144
|
+
*/
|
|
145
|
+
export function isLoadingVisible() {
|
|
146
|
+
return state.loadingVisible;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sets the sidebar collapsed state.
|
|
151
|
+
* @param {boolean} collapsed - Whether sidebar should be collapsed
|
|
152
|
+
*/
|
|
153
|
+
export function setSidebarCollapsed(collapsed) {
|
|
154
|
+
state.sidebarCollapsed = !!collapsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gets the sidebar collapsed state.
|
|
159
|
+
* @returns {boolean} True if sidebar is collapsed
|
|
160
|
+
*/
|
|
161
|
+
export function isSidebarCollapsed() {
|
|
162
|
+
return state.sidebarCollapsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Toggles the sidebar collapsed state.
|
|
167
|
+
* @returns {boolean} The new collapsed state
|
|
168
|
+
*/
|
|
169
|
+
export function toggleSidebar() {
|
|
170
|
+
state.sidebarCollapsed = !state.sidebarCollapsed;
|
|
171
|
+
return state.sidebarCollapsed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// Exit Flow State
|
|
176
|
+
// =============================================================================
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Sets whether the course is locked for exit (no more navigation allowed).
|
|
180
|
+
* @param {boolean} locked - Whether course should be locked
|
|
181
|
+
*/
|
|
182
|
+
export function setCourseExitLocked(locked) {
|
|
183
|
+
state.courseExitLocked = !!locked;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets whether the course is locked for exit.
|
|
188
|
+
* @returns {boolean} True if locked for exit
|
|
189
|
+
*/
|
|
190
|
+
export function isCourseExitLocked() {
|
|
191
|
+
return state.courseExitLocked;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sets whether an exit is currently in progress.
|
|
196
|
+
* @param {boolean} inProgress - Whether exit is in progress
|
|
197
|
+
*/
|
|
198
|
+
export function setExitInProgress(inProgress) {
|
|
199
|
+
state.exitInProgress = !!inProgress;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gets whether an exit is in progress.
|
|
204
|
+
* @returns {boolean} True if exit in progress
|
|
205
|
+
*/
|
|
206
|
+
export function isExitInProgress() {
|
|
207
|
+
return state.exitInProgress;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sets whether the page unload is intentional.
|
|
212
|
+
* This is the flag for the 'beforeunload' guard.
|
|
213
|
+
* @param {boolean} isIntentional - Whether the exit is intentional
|
|
214
|
+
*/
|
|
215
|
+
export function setExitIntentional(isIntentional) {
|
|
216
|
+
state.isExitIntentional = !!isIntentional;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gets whether the page unload is intentional.
|
|
221
|
+
* @returns {boolean} True if the exit is intentional
|
|
222
|
+
*/
|
|
223
|
+
export function isExitIntentional() {
|
|
224
|
+
return state.isExitIntentional;
|
|
225
|
+
}
|