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,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file NavigationUI.js
|
|
3
|
+
* @description Renders and manages the UI components for course navigation.
|
|
4
|
+
* @author Seth
|
|
5
|
+
* @version 1.2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logger } from '../utilities/logger.js';
|
|
9
|
+
import { iconManager } from '../utilities/icons.js';
|
|
10
|
+
|
|
11
|
+
// Cache DOM elements for performance
|
|
12
|
+
const navMenu = document.getElementById('menu');
|
|
13
|
+
const prevButton = document.getElementById('prevBtn');
|
|
14
|
+
const nextButton = document.getElementById('nextBtn');
|
|
15
|
+
const engagementIndicator = document.getElementById('engagement-indicator');
|
|
16
|
+
const engagementProgress = engagementIndicator?.querySelector('.engagement-progress');
|
|
17
|
+
const engagementCheckmark = engagementIndicator?.querySelector('.engagement-checkmark');
|
|
18
|
+
|
|
19
|
+
// Header progress elements
|
|
20
|
+
const headerProgress = document.getElementById('header-progress');
|
|
21
|
+
const headerProgressText = headerProgress?.querySelector('.header-progress-text');
|
|
22
|
+
const headerProgressFill = headerProgress?.querySelector('.header-progress-fill');
|
|
23
|
+
const headerProgressBar = headerProgress?.querySelector('.header-progress-bar');
|
|
24
|
+
|
|
25
|
+
// Track if DOM has been validated
|
|
26
|
+
let isDOMValidated = false;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates that required DOM elements are present.
|
|
30
|
+
* @private
|
|
31
|
+
* @throws {Error} If required DOM elements are missing
|
|
32
|
+
*/
|
|
33
|
+
function _ensureDOMReady() {
|
|
34
|
+
const missing = [];
|
|
35
|
+
if (!navMenu) missing.push('#menu');
|
|
36
|
+
if (!prevButton) missing.push('#prevBtn');
|
|
37
|
+
if (!nextButton) missing.push('#nextBtn');
|
|
38
|
+
|
|
39
|
+
if (missing.length > 0) {
|
|
40
|
+
logger.fatal(`NavigationUI: Required DOM elements not found: ${missing.join(', ')}. Check framework/index.html.`, { domain: 'navigation', operation: 'NavigationUI._ensureDOMReady' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Inject icons into prev/next buttons using iconManager
|
|
45
|
+
// Insert chevron-left at the start of prevButton
|
|
46
|
+
prevButton.insertAdjacentHTML('afterbegin', iconManager.getIcon('chevron-left'));
|
|
47
|
+
|
|
48
|
+
// Append chevron-right at the end of nextButton
|
|
49
|
+
nextButton.insertAdjacentHTML('beforeend', iconManager.getIcon('chevron-right'));
|
|
50
|
+
|
|
51
|
+
isDOMValidated = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Renders the navigation sidebar menu based on the hierarchical menu tree structure.
|
|
56
|
+
* @param {object[]} menuTree - The hierarchical menu tree from getMenuTree().
|
|
57
|
+
* @param {string[]} visitedSlides - An array of slide IDs that have been visited.
|
|
58
|
+
* @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
|
|
59
|
+
* @throws {Error} If parameters are invalid or DOM elements are missing
|
|
60
|
+
*/
|
|
61
|
+
export function renderMenu(menuTree, visitedSlides, accessibilityMap = new Map()) {
|
|
62
|
+
// Validate DOM on first use
|
|
63
|
+
if (!isDOMValidated) {
|
|
64
|
+
_ensureDOMReady();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate parameters
|
|
68
|
+
if (!Array.isArray(menuTree)) {
|
|
69
|
+
throw new Error('NavigationUI.renderMenu: menuTree must be an array');
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(visitedSlides)) {
|
|
72
|
+
throw new Error('NavigationUI.renderMenu: visitedSlides must be an array');
|
|
73
|
+
}
|
|
74
|
+
if (!(accessibilityMap instanceof Map)) {
|
|
75
|
+
throw new Error('NavigationUI.renderMenu: accessibilityMap must be a Map');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
navMenu.innerHTML = `<ul class="nav-list">${renderMenuItems(menuTree, visitedSlides, accessibilityMap)}</ul>`;
|
|
79
|
+
|
|
80
|
+
// Add or update sidebar footer with settings & exit button
|
|
81
|
+
const sidebar = navMenu.closest('.sidebar');
|
|
82
|
+
if (sidebar) {
|
|
83
|
+
let sidebarFooter = sidebar.querySelector('.nav-sidebar-footer');
|
|
84
|
+
if (!sidebarFooter) {
|
|
85
|
+
sidebarFooter = document.createElement('div');
|
|
86
|
+
sidebarFooter.className = 'nav-sidebar-footer';
|
|
87
|
+
|
|
88
|
+
// Horizontal footer bar: exit left, settings icons right
|
|
89
|
+
sidebarFooter.innerHTML = `
|
|
90
|
+
<button class="sidebar-exit-link" data-action="exit-course" data-testid="nav-sidebar-exit" data-tooltip="Exit Course">
|
|
91
|
+
${iconManager.getIcon('log-out', { size: 'sm' })}
|
|
92
|
+
<span>Exit</span>
|
|
93
|
+
</button>
|
|
94
|
+
<div class="sidebar-settings-icons">
|
|
95
|
+
<button class="sidebar-icon-btn" data-action="toggle-theme" data-testid="sidebar-theme-toggle" data-tooltip="Toggle Dark Mode">
|
|
96
|
+
${iconManager.getIcon('moon', { size: 'sm' })}
|
|
97
|
+
</button>
|
|
98
|
+
<button class="sidebar-icon-btn" data-action="toggle-font-size" data-testid="sidebar-font-size-toggle" data-tooltip="Toggle Font Size">
|
|
99
|
+
<span class="icon-text">A+</span>
|
|
100
|
+
</button>
|
|
101
|
+
<button class="sidebar-icon-btn" data-action="toggle-contrast" data-testid="sidebar-contrast-toggle" data-tooltip="Toggle High Contrast">
|
|
102
|
+
${iconManager.getIcon('contrast', { size: 'sm' })}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
sidebar.appendChild(sidebarFooter);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Tooltips auto-initialize via event delegation - no manual init needed
|
|
111
|
+
|
|
112
|
+
// Add event listeners for collapsible sections
|
|
113
|
+
const sectionToggles = navMenu.querySelectorAll('.section-toggle');
|
|
114
|
+
sectionToggles.forEach(toggle => {
|
|
115
|
+
toggle.addEventListener('click', (e) => {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
const section = toggle.closest('.nav-section');
|
|
118
|
+
const isExpanded = section.classList.toggle('expanded');
|
|
119
|
+
toggle.setAttribute('aria-expanded', isExpanded);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Recursively checks if all children (slides) in a section are locked.
|
|
126
|
+
* @private
|
|
127
|
+
* @param {object[]} children - Array of child items (can include nested sections).
|
|
128
|
+
* @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
|
|
129
|
+
* @returns {boolean} True if all children are locked, false otherwise.
|
|
130
|
+
*/
|
|
131
|
+
function areAllChildrenLocked(children, accessibilityMap) {
|
|
132
|
+
if (!children || children.length === 0) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let allLocked = true;
|
|
137
|
+
let hasSlides = false;
|
|
138
|
+
|
|
139
|
+
for (const child of children) {
|
|
140
|
+
if (child.type === 'section') {
|
|
141
|
+
// Recursively check nested sections
|
|
142
|
+
if (!areAllChildrenLocked(child.children || [], accessibilityMap)) {
|
|
143
|
+
allLocked = false;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// It's a slide - check if it's accessible
|
|
148
|
+
hasSlides = true;
|
|
149
|
+
const access = accessibilityMap.get(child.id);
|
|
150
|
+
if (!access || access.allowed !== false) {
|
|
151
|
+
// At least one child is accessible
|
|
152
|
+
allLocked = false;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Only return true if we found slides and they're all locked
|
|
159
|
+
return hasSlides && allLocked;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Recursively renders menu items (sections and slides).
|
|
164
|
+
* @private
|
|
165
|
+
* @param {object[]} items - Array of menu items (sections or slides).
|
|
166
|
+
* @param {string[]} visitedSlides - Array of visited slide IDs.
|
|
167
|
+
* @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
|
|
168
|
+
* @returns {string} HTML string for the menu items.
|
|
169
|
+
*/
|
|
170
|
+
function renderMenuItems(items, visitedSlides, accessibilityMap) {
|
|
171
|
+
return items.map(item => {
|
|
172
|
+
if (item.type === 'section') {
|
|
173
|
+
const expandedClass = item.defaultExpanded ? 'expanded' : '';
|
|
174
|
+
const collapsibleClass = item.collapsible !== false ? 'collapsible' : '';
|
|
175
|
+
|
|
176
|
+
// Check if all children are locked
|
|
177
|
+
const allChildrenLocked = areAllChildrenLocked(item.children || [], accessibilityMap);
|
|
178
|
+
const allLockedClass = allChildrenLocked ? 'all-children-locked' : '';
|
|
179
|
+
|
|
180
|
+
// Debug logging
|
|
181
|
+
if (allChildrenLocked) {
|
|
182
|
+
logger.debug(`[NavigationUI] Section "${item.label}" (${item.id}) has all children locked`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return `
|
|
186
|
+
<li class="nav-section ${expandedClass} ${collapsibleClass} ${allLockedClass}" data-section-id="${item.id}" data-testid="nav-section-${item.id}">
|
|
187
|
+
<button class="section-toggle" aria-expanded="${item.defaultExpanded}" aria-controls="section-${item.id}" data-testid="nav-section-toggle-${item.id}">
|
|
188
|
+
${item.icon ? `<span class="section-icon" aria-hidden="true">${iconManager.getIcon(item.icon)}</span>` : ''}
|
|
189
|
+
<span class="section-label">${item.label}</span>
|
|
190
|
+
<span class="toggle-indicator" aria-hidden="true">${iconManager.getIcon('chevron-right')}</span>
|
|
191
|
+
</button>
|
|
192
|
+
<ul class="section-children" id="section-${item.id}">
|
|
193
|
+
${renderMenuItems(item.children || [], visitedSlides, accessibilityMap)}
|
|
194
|
+
</ul>
|
|
195
|
+
</li>
|
|
196
|
+
`;
|
|
197
|
+
} else {
|
|
198
|
+
// Slide item
|
|
199
|
+
const visitedClass = visitedSlides.includes(item.id) ? 'visited' : '';
|
|
200
|
+
const access = accessibilityMap.get(item.id);
|
|
201
|
+
const isLocked = access && access.allowed === false;
|
|
202
|
+
const lockedClass = isLocked ? 'locked' : '';
|
|
203
|
+
const ariaDisabled = isLocked ? 'aria-disabled="true"' : '';
|
|
204
|
+
const tabIndex = isLocked ? 'tabindex="-1"' : '';
|
|
205
|
+
|
|
206
|
+
// Use JS tooltip via data-tooltip attribute (no .tooltip class needed)
|
|
207
|
+
const tooltipData = isLocked && access.message ? `data-tooltip="${access.message}"` : '';
|
|
208
|
+
const ariaLabel = isLocked && access.message
|
|
209
|
+
? `aria-label="Go to ${item.label} (${access.message})"`
|
|
210
|
+
: `aria-label="Go to ${item.label}"`;
|
|
211
|
+
|
|
212
|
+
return `
|
|
213
|
+
<li class="nav-item ${visitedClass} ${lockedClass}" data-slide-id="${item.id}" data-slide-index="${item.slideIndex}" data-action="nav-menu-item" data-testid="nav-menu-item-${item.id}">
|
|
214
|
+
<button type="button" ${ariaLabel} ${ariaDisabled} ${tabIndex} ${tooltipData}>
|
|
215
|
+
${item.icon ? `<span class="slide-icon" aria-hidden="true">${iconManager.getIcon(item.icon)}</span>` : ''}
|
|
216
|
+
<span class="slide-label">${item.label}</span>
|
|
217
|
+
${isLocked ? `<span class="lock-icon" aria-hidden="true">${iconManager.getIcon('lock')}</span>` : ''}
|
|
218
|
+
</button>
|
|
219
|
+
</li>
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
}).join('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Highlights the active slide in the navigation menu and manages section expansion.
|
|
227
|
+
* Collapses all collapsible sections except the one containing the active slide.
|
|
228
|
+
* @param {string} slideId - The ID of the slide to mark as active.
|
|
229
|
+
* @throws {Error} If slideId is invalid
|
|
230
|
+
*/
|
|
231
|
+
export function setActiveItem(slideId) {
|
|
232
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
233
|
+
throw new Error(`NavigationUI.setActiveItem: Invalid slideId: ${slideId}`);
|
|
234
|
+
}
|
|
235
|
+
if (!navMenu) return;
|
|
236
|
+
|
|
237
|
+
// Find the active item and its parent section
|
|
238
|
+
const activeItem = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
|
|
239
|
+
const activeSection = activeItem?.closest('.nav-section');
|
|
240
|
+
|
|
241
|
+
// Update active state on all items
|
|
242
|
+
const items = navMenu.querySelectorAll('.nav-item');
|
|
243
|
+
items.forEach(item => {
|
|
244
|
+
const isActive = item.dataset.slideId === slideId;
|
|
245
|
+
item.classList.toggle('active', isActive);
|
|
246
|
+
item.querySelector('button').setAttribute('aria-current', isActive ? 'page' : 'false');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Manage section expansion: collapse others, expand the active one
|
|
250
|
+
const sections = navMenu.querySelectorAll('.nav-section.collapsible');
|
|
251
|
+
sections.forEach(section => {
|
|
252
|
+
const toggle = section.querySelector('.section-toggle');
|
|
253
|
+
if (!toggle) return;
|
|
254
|
+
|
|
255
|
+
if (section === activeSection) {
|
|
256
|
+
// Expand the section containing the active slide
|
|
257
|
+
section.classList.add('expanded');
|
|
258
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
259
|
+
} else {
|
|
260
|
+
// Collapse other collapsible sections
|
|
261
|
+
section.classList.remove('expanded');
|
|
262
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Updates the enabled/disabled state and ARIA attributes of the previous and next buttons.
|
|
269
|
+
* @param {object} config - Navigation state with isFirstSlide, isLastSlide, nextBlocked, nextBlockedMessage, engagementProgress.
|
|
270
|
+
*/
|
|
271
|
+
export function updateNavButtonState(config) {
|
|
272
|
+
// New API: config object only
|
|
273
|
+
const { isFirstSlide, isLastSlide, nextBlocked = false, nextBlockedMessage = null, engagementProgress = null } = config;
|
|
274
|
+
|
|
275
|
+
// Update previous button (no tooltip - icon is self-explanatory)
|
|
276
|
+
if (prevButton) {
|
|
277
|
+
prevButton.disabled = isFirstSlide;
|
|
278
|
+
prevButton.setAttribute('aria-disabled', String(isFirstSlide));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update next button
|
|
282
|
+
if (nextButton) {
|
|
283
|
+
const shouldDisable = isLastSlide || nextBlocked;
|
|
284
|
+
nextButton.disabled = shouldDisable;
|
|
285
|
+
nextButton.setAttribute('aria-disabled', String(shouldDisable));
|
|
286
|
+
|
|
287
|
+
// Manage gated state for progressive ring indicator
|
|
288
|
+
// Only show ring when blocked due to engagement (has progress data and not complete)
|
|
289
|
+
if (nextBlocked && engagementProgress !== null && engagementProgress < 100) {
|
|
290
|
+
nextButton.classList.add('gated');
|
|
291
|
+
nextButton.classList.remove('engagement-complete');
|
|
292
|
+
nextButton.style.setProperty('--engagement-progress', engagementProgress);
|
|
293
|
+
} else if (!nextButton.classList.contains('engagement-complete')) {
|
|
294
|
+
// Clear gated state unless animation is playing
|
|
295
|
+
nextButton.classList.remove('gated');
|
|
296
|
+
nextButton.style.removeProperty('--engagement-progress');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Only show tooltip when blocked with a specific message (provides real value)
|
|
300
|
+
// Skip tooltips for normal states - arrow icon is universally understood
|
|
301
|
+
if (nextBlocked && nextBlockedMessage) {
|
|
302
|
+
nextButton.setAttribute('data-tooltip', nextBlockedMessage);
|
|
303
|
+
nextButton.setAttribute('aria-label', `Next (${nextBlockedMessage})`);
|
|
304
|
+
} else {
|
|
305
|
+
nextButton.removeAttribute('data-tooltip');
|
|
306
|
+
nextButton.setAttribute('aria-label', isLastSlide ? 'Next (No next slide)' : 'Next');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Triggers the completion animation on the next button.
|
|
313
|
+
* Called by the engagement:complete event handler.
|
|
314
|
+
*/
|
|
315
|
+
export function triggerEngagementCompleteAnimation() {
|
|
316
|
+
if (!nextButton) return;
|
|
317
|
+
|
|
318
|
+
nextButton.classList.add('gated', 'engagement-complete');
|
|
319
|
+
nextButton.style.setProperty('--engagement-progress', 100);
|
|
320
|
+
|
|
321
|
+
// Remove classes after animation completes
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
nextButton.classList.remove('gated', 'engagement-complete');
|
|
324
|
+
nextButton.style.removeProperty('--engagement-progress');
|
|
325
|
+
}, 500);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Adds a 'visited' class to a menu item.
|
|
330
|
+
* @param {string} slideId - The ID of the slide to mark as visited.
|
|
331
|
+
* @throws {Error} If slideId is invalid
|
|
332
|
+
*/
|
|
333
|
+
export function markAsVisited(slideId) {
|
|
334
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
335
|
+
throw new Error(`NavigationUI.markAsVisited: Invalid slideId: ${slideId}`);
|
|
336
|
+
}
|
|
337
|
+
if (!navMenu) return;
|
|
338
|
+
const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
|
|
339
|
+
if (item) {
|
|
340
|
+
item.classList.add('visited');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Removes the 'visited' class from a menu item.
|
|
346
|
+
* @param {string} slideId - The ID of the slide to mark as unvisited.
|
|
347
|
+
* @throws {Error} If slideId is invalid
|
|
348
|
+
*/
|
|
349
|
+
export function markAsUnvisited(slideId) {
|
|
350
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
351
|
+
throw new Error(`NavigationUI.markAsUnvisited: Invalid slideId: ${slideId}`);
|
|
352
|
+
}
|
|
353
|
+
if (!navMenu) return;
|
|
354
|
+
const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
|
|
355
|
+
if (item) {
|
|
356
|
+
item.classList.remove('visited');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Marks a slide as locked (inaccessible) in the navigation menu.
|
|
362
|
+
* @param {string} slideId - The ID of the slide to mark as locked.
|
|
363
|
+
* @throws {Error} If slideId is invalid
|
|
364
|
+
*/
|
|
365
|
+
export function markAsLocked(slideId) {
|
|
366
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
367
|
+
throw new Error(`NavigationUI.markAsLocked: Invalid slideId: ${slideId}`);
|
|
368
|
+
}
|
|
369
|
+
if (!navMenu) return;
|
|
370
|
+
const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
|
|
371
|
+
if (item) {
|
|
372
|
+
item.classList.add('locked');
|
|
373
|
+
const link = item.querySelector('button');
|
|
374
|
+
if (link) {
|
|
375
|
+
link.setAttribute('aria-disabled', 'true');
|
|
376
|
+
link.setAttribute('tabindex', '-1');
|
|
377
|
+
|
|
378
|
+
// Add lock icon if not already present
|
|
379
|
+
if (!link.querySelector('.lock-icon')) {
|
|
380
|
+
const lockIconSpan = document.createElement('span');
|
|
381
|
+
lockIconSpan.className = 'lock-icon';
|
|
382
|
+
lockIconSpan.setAttribute('aria-hidden', 'true');
|
|
383
|
+
lockIconSpan.innerHTML = iconManager.getIcon('lock');
|
|
384
|
+
link.appendChild(lockIconSpan);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Removes the locked state from a slide in the navigation menu.
|
|
392
|
+
* @param {string} slideId - The ID of the slide to unlock.
|
|
393
|
+
* @throws {Error} If slideId is invalid
|
|
394
|
+
*/
|
|
395
|
+
export function markAsUnlocked(slideId) {
|
|
396
|
+
if (!slideId || typeof slideId !== 'string') {
|
|
397
|
+
throw new Error(`NavigationUI.markAsUnlocked: Invalid slideId: ${slideId}`);
|
|
398
|
+
}
|
|
399
|
+
if (!navMenu) return;
|
|
400
|
+
const item = navMenu.querySelector(`.nav-item[data-slide-id="${slideId}"]`);
|
|
401
|
+
if (item) {
|
|
402
|
+
item.classList.remove('locked');
|
|
403
|
+
const link = item.querySelector('button');
|
|
404
|
+
if (link) {
|
|
405
|
+
link.removeAttribute('aria-disabled');
|
|
406
|
+
link.removeAttribute('tabindex');
|
|
407
|
+
|
|
408
|
+
// Remove lock icon if present
|
|
409
|
+
const lockIcon = link.querySelector('.lock-icon');
|
|
410
|
+
if (lockIcon) {
|
|
411
|
+
lockIcon.remove();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Updates the locked state of all sections based on child slide accessibility.
|
|
419
|
+
* A section gets the 'all-children-locked' class if all its child slides are locked.
|
|
420
|
+
* @param {Map<string, {allowed: boolean, message: string|null}>} accessibilityMap - A map of slide accessibility states.
|
|
421
|
+
*/
|
|
422
|
+
export function updateSectionStates(accessibilityMap) {
|
|
423
|
+
if (!navMenu) return;
|
|
424
|
+
if (!(accessibilityMap instanceof Map)) {
|
|
425
|
+
throw new Error('NavigationUI.updateSectionStates: accessibilityMap must be a Map');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const sections = navMenu.querySelectorAll('.nav-section');
|
|
429
|
+
sections.forEach(section => {
|
|
430
|
+
// Get all child slides in this section (including nested)
|
|
431
|
+
const childSlides = section.querySelectorAll('.nav-item[data-slide-id]');
|
|
432
|
+
|
|
433
|
+
if (childSlides.length === 0) {
|
|
434
|
+
// No slides in section, remove locked state
|
|
435
|
+
section.classList.remove('all-children-locked');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check if ALL child slides are locked
|
|
440
|
+
let allLocked = true;
|
|
441
|
+
childSlides.forEach(slideItem => {
|
|
442
|
+
const slideId = slideItem.dataset.slideId;
|
|
443
|
+
const access = accessibilityMap.get(slideId);
|
|
444
|
+
// If access is missing or allowed is not false, the slide is accessible
|
|
445
|
+
if (!access || access.allowed !== false) {
|
|
446
|
+
allLocked = false;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (allLocked) {
|
|
451
|
+
section.classList.add('all-children-locked');
|
|
452
|
+
} else {
|
|
453
|
+
section.classList.remove('all-children-locked');
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Shows the engagement indicator with current progress.
|
|
460
|
+
* @param {object} progress - Progress object from EngagementManager.getProgress()
|
|
461
|
+
* @param {number} progress.percentage - Completion percentage (0-100)
|
|
462
|
+
* @param {string} progress.tooltip - Pre-built tooltip text
|
|
463
|
+
* @throws {Error} If progress object is invalid
|
|
464
|
+
*/
|
|
465
|
+
export function showEngagementIndicator(progress) {
|
|
466
|
+
if (!progress || typeof progress.percentage !== 'number') {
|
|
467
|
+
throw new Error('NavigationUI.showEngagementIndicator: Invalid progress object');
|
|
468
|
+
}
|
|
469
|
+
if (!engagementIndicator) return;
|
|
470
|
+
|
|
471
|
+
// Show the indicator
|
|
472
|
+
engagementIndicator.hidden = false;
|
|
473
|
+
|
|
474
|
+
// Update progress
|
|
475
|
+
updateEngagementProgress(progress.percentage, progress.percentage === 100);
|
|
476
|
+
|
|
477
|
+
// Use tooltip from engagement manager
|
|
478
|
+
engagementIndicator.setAttribute('data-tooltip', progress.tooltip);
|
|
479
|
+
|
|
480
|
+
// Update aria-live announcement for screen readers
|
|
481
|
+
const percentText = `${progress.percentage}% of content requirements completed`;
|
|
482
|
+
engagementIndicator.setAttribute('aria-label', percentText);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Hides the engagement indicator.
|
|
487
|
+
*/
|
|
488
|
+
export function hideEngagementIndicator() {
|
|
489
|
+
if (!engagementIndicator) return;
|
|
490
|
+
engagementIndicator.hidden = true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Updates the circular progress indicator.
|
|
495
|
+
* @param {number} percentage - Completion percentage (0-100)
|
|
496
|
+
* @param {boolean} complete - Whether all requirements are met
|
|
497
|
+
*/
|
|
498
|
+
export function updateEngagementProgress(percentage, complete) {
|
|
499
|
+
if (!engagementProgress || !engagementCheckmark) return;
|
|
500
|
+
|
|
501
|
+
// Apply threshold: if percentage is below 15, display as 0 for visual fill
|
|
502
|
+
const displayPercentage = percentage < 5 ? 0 : percentage;
|
|
503
|
+
|
|
504
|
+
// Update progress circle (circumference = 2πr, r=14, so ~87.96)
|
|
505
|
+
const circumference = 2 * Math.PI * 14;
|
|
506
|
+
const offset = circumference - (displayPercentage / 100) * circumference;
|
|
507
|
+
|
|
508
|
+
engagementProgress.style.strokeDasharray = `${circumference} ${circumference}`;
|
|
509
|
+
engagementProgress.style.strokeDashoffset = `${offset}`;
|
|
510
|
+
|
|
511
|
+
// Show/hide checkmark
|
|
512
|
+
if (complete) {
|
|
513
|
+
engagementCheckmark.style.opacity = '1';
|
|
514
|
+
engagementProgress.classList.add('complete');
|
|
515
|
+
} else {
|
|
516
|
+
engagementCheckmark.style.opacity = '0';
|
|
517
|
+
engagementProgress.classList.remove('complete');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Update data attribute for CSS targeting
|
|
521
|
+
engagementIndicator?.setAttribute('data-progress', percentage);
|
|
522
|
+
engagementIndicator?.setAttribute('data-complete', complete ? 'true' : 'false');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Updates the header progress indicator with current slide position.
|
|
527
|
+
* @param {number} currentIndex - Current slide index (0-based)
|
|
528
|
+
* @param {number} totalSlides - Total number of slides in sequence
|
|
529
|
+
* @param {number} [visitedCount] - Optional: number of visited slides for progress bar
|
|
530
|
+
*/
|
|
531
|
+
export function updateHeaderProgress(currentIndex, totalSlides, visitedCount = null) {
|
|
532
|
+
if (!headerProgress) return;
|
|
533
|
+
|
|
534
|
+
// Show the progress indicator
|
|
535
|
+
headerProgress.hidden = false;
|
|
536
|
+
|
|
537
|
+
// Update text: "Slide X of Y" (1-based for display)
|
|
538
|
+
if (headerProgressText) {
|
|
539
|
+
headerProgressText.textContent = `Slide ${currentIndex + 1} of ${totalSlides}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Update progress bar
|
|
543
|
+
if (headerProgressFill && headerProgressBar) {
|
|
544
|
+
// Use visited count if provided, otherwise use current position
|
|
545
|
+
const progressValue = visitedCount !== null ? visitedCount : currentIndex + 1;
|
|
546
|
+
const percentage = totalSlides > 0 ? (progressValue / totalSlides) * 100 : 0;
|
|
547
|
+
headerProgressFill.style.width = `${percentage}%`;
|
|
548
|
+
|
|
549
|
+
// Update ARIA
|
|
550
|
+
headerProgressBar.setAttribute('aria-valuenow', Math.round(percentage));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Hides the header progress indicator.
|
|
556
|
+
*/
|
|
557
|
+
export function hideHeaderProgress() {
|
|
558
|
+
if (!headerProgress) return;
|
|
559
|
+
headerProgress.hidden = true;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Provides access to the cached DOM elements.
|
|
564
|
+
* @returns {{navMenu: HTMLElement, prevButton: HTMLElement, nextButton: HTMLElement, engagementIndicator: HTMLElement, headerProgress: HTMLElement}}
|
|
565
|
+
*/
|
|
566
|
+
export function getElements() {
|
|
567
|
+
return {
|
|
568
|
+
navMenu,
|
|
569
|
+
prevButton,
|
|
570
|
+
nextButton,
|
|
571
|
+
engagementIndicator,
|
|
572
|
+
headerProgress
|
|
573
|
+
};
|
|
574
|
+
}
|