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,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ui-initializer.js
|
|
3
|
+
* @description Automatically initializes UI components based on data-attributes.
|
|
4
|
+
* Uses the component catalog for dynamic discovery — no hardcoded component list.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
import { getComponentInit, getComponentStyles, isComponentRegistered, getRegisteredComponentTypes } from '../core/component-catalog.js';
|
|
9
|
+
import { init as initNotificationTriggers } from '../components/ui-components/notifications.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import engagementManager from '../engagement/engagement-manager.js';
|
|
13
|
+
import * as NavigationState from '../navigation/NavigationState.js';
|
|
14
|
+
|
|
15
|
+
// Track which custom component styles have been injected
|
|
16
|
+
const injectedStyles = new Set();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inject custom component styles into the document head
|
|
20
|
+
* @param {string} type - Component type
|
|
21
|
+
* @param {string} styles - CSS string
|
|
22
|
+
*/
|
|
23
|
+
function injectStyles(type, styles) {
|
|
24
|
+
if (injectedStyles.has(type) || !styles) return;
|
|
25
|
+
|
|
26
|
+
const styleEl = document.createElement('style');
|
|
27
|
+
styleEl.setAttribute('data-component-styles', type);
|
|
28
|
+
styleEl.textContent = styles;
|
|
29
|
+
document.head.appendChild(styleEl);
|
|
30
|
+
injectedStyles.add(type);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scans a container element for declarative UI components and initializes them.
|
|
35
|
+
* @param {HTMLElement} container - The container element to scan.
|
|
36
|
+
*/
|
|
37
|
+
export function initializeDeclarativeComponents(container) {
|
|
38
|
+
if (!container || typeof container.querySelectorAll !== 'function') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const components = container.querySelectorAll('[data-component]');
|
|
43
|
+
|
|
44
|
+
components.forEach(element => {
|
|
45
|
+
const componentName = element.dataset.component;
|
|
46
|
+
|
|
47
|
+
if (!isComponentRegistered(componentName)) {
|
|
48
|
+
logger.warn(`[UI-Initializer] Unknown component: '${componentName}'. Registered: ${getRegisteredComponentTypes().join(', ')}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Inject styles for custom components (CSS-in-JS)
|
|
53
|
+
const styles = getComponentStyles(componentName);
|
|
54
|
+
if (styles) {
|
|
55
|
+
injectStyles(componentName, styles);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get and call init function from catalog
|
|
59
|
+
const initializer = getComponentInit(componentName);
|
|
60
|
+
if (initializer && typeof initializer === 'function') {
|
|
61
|
+
try {
|
|
62
|
+
initializer(element);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(`[UI-Initializer] Failed to initialize '${componentName}' component: ${error.message}`, { domain: 'ui', operation: 'initializeComponent', stack: error.stack, component: componentName });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// CSS-only components (no init or no-op init) handled purely by CSS
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Tooltips auto-initialize via event delegation - no call needed
|
|
71
|
+
|
|
72
|
+
// Register all flip cards with engagement manager (batch registration like tabs)
|
|
73
|
+
// This must happen AFTER all flip cards are initialized
|
|
74
|
+
registerFlipCardsForEngagement(container);
|
|
75
|
+
|
|
76
|
+
// Register all modals with engagement manager (batch registration)
|
|
77
|
+
// This must happen AFTER all modal triggers are initialized
|
|
78
|
+
registerModalsForEngagement(container);
|
|
79
|
+
|
|
80
|
+
// Initialize declarative notification triggers (event delegation pattern)
|
|
81
|
+
initNotificationTriggers(container);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// Register lightbox triggers with engagement manager (batch registration)
|
|
85
|
+
// This must happen AFTER all lightbox triggers are initialized by the catalog
|
|
86
|
+
registerLightboxesForEngagement(container);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registers all flip cards in the container with the engagement manager.
|
|
91
|
+
* This batch registration ensures flipCardsTotal is set correctly after all cards are found.
|
|
92
|
+
* @param {HTMLElement} container - The container to scan for flip cards
|
|
93
|
+
*/
|
|
94
|
+
function registerFlipCardsForEngagement(container) {
|
|
95
|
+
const flipCards = container.querySelectorAll('[data-component="flip-card"][data-flip-card-id]');
|
|
96
|
+
if (!flipCards.length) return;
|
|
97
|
+
|
|
98
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
99
|
+
if (!currentSlideId) return;
|
|
100
|
+
|
|
101
|
+
const cardIds = Array.from(flipCards).map(card => card.dataset.flipCardId).filter(Boolean);
|
|
102
|
+
if (cardIds.length > 0) {
|
|
103
|
+
engagementManager.registerFlipCards(currentSlideId, cardIds);
|
|
104
|
+
logger.debug(`[UI-Initializer] Registered ${cardIds.length} flip cards for engagement tracking`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Registers all modal triggers in the container with the engagement manager.
|
|
110
|
+
* This batch registration ensures modalsTotal is set correctly after all triggers are found.
|
|
111
|
+
* @param {HTMLElement} container - The container to scan for modal triggers
|
|
112
|
+
*/
|
|
113
|
+
function registerModalsForEngagement(container) {
|
|
114
|
+
const modalTriggers = container.querySelectorAll('[data-component="modal-trigger"][data-modal-id]');
|
|
115
|
+
if (!modalTriggers.length) return;
|
|
116
|
+
|
|
117
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
118
|
+
if (!currentSlideId) return;
|
|
119
|
+
|
|
120
|
+
const modalIds = Array.from(modalTriggers).map(trigger => trigger.dataset.modalId).filter(Boolean);
|
|
121
|
+
if (modalIds.length > 0) {
|
|
122
|
+
engagementManager.registerModals(currentSlideId, modalIds);
|
|
123
|
+
logger.debug(`[UI-Initializer] Registered ${modalIds.length} modals for engagement tracking`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Registers all lightbox triggers in the container with the engagement manager.
|
|
129
|
+
* This batch registration ensures lightboxesTotal is set correctly after all triggers are found.
|
|
130
|
+
* @param {HTMLElement} container - The container to scan for lightbox triggers
|
|
131
|
+
*/
|
|
132
|
+
function registerLightboxesForEngagement(container) {
|
|
133
|
+
const lightboxTriggers = container.querySelectorAll('[data-component="lightbox"]');
|
|
134
|
+
if (!lightboxTriggers.length) return;
|
|
135
|
+
|
|
136
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
137
|
+
if (!currentSlideId) return;
|
|
138
|
+
|
|
139
|
+
const lightboxIds = Array.from(lightboxTriggers)
|
|
140
|
+
.map(trigger => trigger.id || trigger.dataset.lightboxId)
|
|
141
|
+
.filter(Boolean);
|
|
142
|
+
if (lightboxIds.length > 0) {
|
|
143
|
+
engagementManager.registerLightbox(currentSlideId, lightboxIds);
|
|
144
|
+
logger.debug(`[UI-Initializer] Registered ${lightboxIds.length} lightboxes for engagement tracking`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file utilities.js
|
|
3
|
+
* @description Core utility functions for the SCORM framework.
|
|
4
|
+
* These are pure, stateless helper functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format duration in milliseconds to a human-readable string.
|
|
9
|
+
* @param {number} ms - Duration in milliseconds
|
|
10
|
+
* @returns {string} Formatted duration (e.g., "2m 30s")
|
|
11
|
+
*/
|
|
12
|
+
export function formatDuration(ms) {
|
|
13
|
+
if (!ms || ms < 0) {
|
|
14
|
+
return '0s';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const seconds = Math.floor(ms / 1000);
|
|
18
|
+
const minutes = Math.floor(seconds / 60);
|
|
19
|
+
const hours = Math.floor(minutes / 60);
|
|
20
|
+
|
|
21
|
+
if (hours > 0) {
|
|
22
|
+
return `${hours}h ${minutes % 60}m`;
|
|
23
|
+
} else if (minutes > 0) {
|
|
24
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
25
|
+
} else {
|
|
26
|
+
return `${seconds}s`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deep clone an object (handles primitives, arrays, objects, and Dates).
|
|
32
|
+
* @param {*} obj - Value to clone
|
|
33
|
+
* @returns {*} Cloned value
|
|
34
|
+
*/
|
|
35
|
+
export function deepClone(obj) {
|
|
36
|
+
if (obj === null || typeof obj !== 'object') {
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
if (obj instanceof Date) {
|
|
40
|
+
return new Date(obj.getTime());
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(obj)) {
|
|
43
|
+
return obj.map(item => deepClone(item));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const clonedObj = {};
|
|
47
|
+
for (const key in obj) {
|
|
48
|
+
if (obj.hasOwnProperty(key)) {
|
|
49
|
+
clonedObj[key] = deepClone(obj[key]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return clonedObj;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a unique ID with optional prefix.
|
|
57
|
+
* @param {string} prefix - Optional prefix for the ID
|
|
58
|
+
* @returns {string} Unique ID (format: prefix-timestamp-random)
|
|
59
|
+
*/
|
|
60
|
+
export function generateId(prefix = 'id') {
|
|
61
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Escape HTML entities in text.
|
|
66
|
+
* @param {string} text - Text to escape
|
|
67
|
+
* @returns {string} Text with HTML entities escaped
|
|
68
|
+
*/
|
|
69
|
+
export function escapeHTML(text) {
|
|
70
|
+
if (typeof text !== 'string') {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
const map = {
|
|
74
|
+
'&': '&',
|
|
75
|
+
'<': '<',
|
|
76
|
+
'>': '>',
|
|
77
|
+
'"': '"',
|
|
78
|
+
"'": '''
|
|
79
|
+
};
|
|
80
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if an element is fully visible in the viewport.
|
|
85
|
+
* @param {HTMLElement} element - Element to check
|
|
86
|
+
* @returns {boolean} True if element is fully visible within viewport
|
|
87
|
+
*/
|
|
88
|
+
export function isElementVisible(element) {
|
|
89
|
+
if (!element || !(element instanceof HTMLElement)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rect = element.getBoundingClientRect();
|
|
94
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
95
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
rect.top >= 0 &&
|
|
99
|
+
rect.left >= 0 &&
|
|
100
|
+
rect.bottom <= viewportHeight &&
|
|
101
|
+
rect.right <= viewportWidth
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scroll element into view with smooth behavior.
|
|
107
|
+
* @param {HTMLElement} element - Element to scroll to
|
|
108
|
+
* @param {Object} options - ScrollIntoView options
|
|
109
|
+
*/
|
|
110
|
+
export function scrollToElement(element, options = {}) {
|
|
111
|
+
if (!element || !(element instanceof HTMLElement)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const defaultOptions = {
|
|
116
|
+
behavior: 'smooth',
|
|
117
|
+
block: 'start',
|
|
118
|
+
inline: 'nearest'
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
element.scrollIntoView({ ...defaultOptions, ...options });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get all focusable elements within a container.
|
|
126
|
+
* @param {HTMLElement} container - Container element
|
|
127
|
+
* @returns {Array<HTMLElement>} Array of focusable elements
|
|
128
|
+
*/
|
|
129
|
+
export function getFocusableElements(container) {
|
|
130
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const selector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
135
|
+
return Array.from(container.querySelectorAll(selector));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Trap focus within a container (for modals, dialogs).
|
|
140
|
+
* Returns a cleanup function that must be called to remove the trap.
|
|
141
|
+
* @param {HTMLElement} container - Container to trap focus in
|
|
142
|
+
* @returns {Function} Cleanup function to remove trap
|
|
143
|
+
*/
|
|
144
|
+
export function trapFocus(container) {
|
|
145
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
146
|
+
throw new Error('trapFocus: container must be a valid HTMLElement');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const focusableElements = getFocusableElements(container);
|
|
150
|
+
if (focusableElements.length === 0) {
|
|
151
|
+
return () => { };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const firstElement = focusableElements[0];
|
|
155
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
156
|
+
|
|
157
|
+
const handleTabKey = (e) => {
|
|
158
|
+
if (e.key !== 'Tab') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (e.shiftKey) {
|
|
163
|
+
if (document.activeElement === firstElement) {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
lastElement.focus();
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
if (document.activeElement === lastElement) {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
firstElement.focus();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
container.addEventListener('keydown', handleTabKey);
|
|
176
|
+
firstElement.focus();
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
container.removeEventListener('keydown', handleTabKey);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Format a number as a percentage string.
|
|
185
|
+
* @param {number} value - Value to format (0-1 if isDecimal=true, 0-100 if isDecimal=false)
|
|
186
|
+
* @param {boolean} isDecimal - True if value is 0-1, false if 0-100
|
|
187
|
+
* @returns {string} Formatted percentage (e.g., "75%")
|
|
188
|
+
*/
|
|
189
|
+
export function formatPercentage(value, isDecimal = true) {
|
|
190
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
191
|
+
return '0%';
|
|
192
|
+
}
|
|
193
|
+
const percent = isDecimal ? value * 100 : value;
|
|
194
|
+
return `${Math.round(percent)}%`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Wait for a condition to be true, checking at regular intervals.
|
|
199
|
+
* @param {Function} condition - Function that returns true when condition is met
|
|
200
|
+
* @param {number} timeout - Maximum time to wait in milliseconds
|
|
201
|
+
* @param {number} interval - Check interval in milliseconds
|
|
202
|
+
* @returns {Promise<void>} Resolves when condition is met, rejects on timeout
|
|
203
|
+
*/
|
|
204
|
+
export function waitFor(condition, timeout = 5000, interval = 100) {
|
|
205
|
+
if (!condition || typeof condition !== 'function') {
|
|
206
|
+
return Promise.reject(new Error('waitFor: condition must be a function'));
|
|
207
|
+
}
|
|
208
|
+
if (timeout <= 0) {
|
|
209
|
+
return Promise.reject(new Error('waitFor: timeout must be positive'));
|
|
210
|
+
}
|
|
211
|
+
if (interval <= 0) {
|
|
212
|
+
return Promise.reject(new Error('waitFor: interval must be positive'));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const startTime = Date.now();
|
|
217
|
+
|
|
218
|
+
const check = () => {
|
|
219
|
+
try {
|
|
220
|
+
if (condition()) {
|
|
221
|
+
resolve();
|
|
222
|
+
} else if (Date.now() - startTime >= timeout) {
|
|
223
|
+
reject(new Error('Timeout waiting for condition'));
|
|
224
|
+
} else {
|
|
225
|
+
setTimeout(check, interval);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
reject(new Error(`Condition check failed: ${error.message}`));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
check();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Deep merge multiple objects (mutates target object).
|
|
238
|
+
* Plain objects are merged recursively; other values are overwritten.
|
|
239
|
+
* @param {Object} target - The target object to merge into (will be mutated)
|
|
240
|
+
* @param {...Object} sources - The source objects to merge from
|
|
241
|
+
* @returns {Object} The merged target object
|
|
242
|
+
*/
|
|
243
|
+
export function deepMerge(target, ...sources) {
|
|
244
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
|
245
|
+
throw new Error('deepMerge: target must be a plain object');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (sources.length === 0) {
|
|
249
|
+
return target;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const source = sources.shift();
|
|
253
|
+
|
|
254
|
+
if (source && typeof source === 'object' && !Array.isArray(source)) {
|
|
255
|
+
for (const key in source) {
|
|
256
|
+
if (source.hasOwnProperty(key)) {
|
|
257
|
+
const sourceValue = source[key];
|
|
258
|
+
const targetValue = target[key];
|
|
259
|
+
|
|
260
|
+
if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
|
|
261
|
+
// Recursive merge for plain objects
|
|
262
|
+
if (!targetValue || typeof targetValue !== 'object' || Array.isArray(targetValue)) {
|
|
263
|
+
target[key] = {};
|
|
264
|
+
}
|
|
265
|
+
deepMerge(target[key], sourceValue);
|
|
266
|
+
} else {
|
|
267
|
+
// Direct assignment for primitives, arrays, and other types
|
|
268
|
+
target[key] = sourceValue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return deepMerge(target, ...sources);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Shuffle an array using Fisher-Yates algorithm (creates new array).
|
|
279
|
+
* @param {Array} array - Array to shuffle
|
|
280
|
+
* @returns {Array} New shuffled array
|
|
281
|
+
*/
|
|
282
|
+
export function shuffleArray(array) {
|
|
283
|
+
if (!Array.isArray(array)) {
|
|
284
|
+
throw new Error('shuffleArray: input must be an array');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const shuffled = [...array];
|
|
288
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
289
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
290
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
291
|
+
}
|
|
292
|
+
return shuffled;
|
|
293
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { eventBus } from '../core/event-bus.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { validateRenderedHTML } from '../validation/html-validators.js';
|
|
4
|
+
import { initializeDeclarativeComponents } from './ui-initializer.js';
|
|
5
|
+
import { courseConfig } from '../../../course/course-config.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a view manager for a given container element.
|
|
9
|
+
* @param {HTMLElement} container - The container element for the views.
|
|
10
|
+
* @param {string} [scope='local'] - The scope of this ViewManager ('main' for main navigation, 'assessment' for assessment internal views, etc.)
|
|
11
|
+
* @returns {object} A view manager instance.
|
|
12
|
+
*/
|
|
13
|
+
export function createViewManager(container, scope = 'local') {
|
|
14
|
+
if (!container || !(container instanceof HTMLElement)) {
|
|
15
|
+
throw new Error('ViewManager: A valid container element is required.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const views = {};
|
|
19
|
+
let currentViewName = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a view with lifecycle hooks.
|
|
23
|
+
* @param {string} name - The name of the view.
|
|
24
|
+
* @param {object} viewObject - An object defining the view.
|
|
25
|
+
* @param {Function} viewObject.render - A function that returns an HTMLElement. Called every time the view is shown.
|
|
26
|
+
* @param {Function} [viewObject.onShow] - A function called every time the view is shown, after rendering.
|
|
27
|
+
* @param {Function} [viewObject.onHide] - A function called every time the view is hidden, before removal from DOM.
|
|
28
|
+
*/
|
|
29
|
+
function registerView(name, viewObject) {
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new Error('ViewManager: View name is required.');
|
|
32
|
+
}
|
|
33
|
+
if (!viewObject || typeof viewObject.render !== 'function') {
|
|
34
|
+
throw new Error(`ViewManager: View '${name}' must be an object with a render function.`);
|
|
35
|
+
}
|
|
36
|
+
views[name] = { ...viewObject };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shows a view by name.
|
|
41
|
+
* @param {string} name - The name of the view to show.
|
|
42
|
+
* @param {object} [options] - Options to pass to the render and onShow functions.
|
|
43
|
+
*/
|
|
44
|
+
async function showView(name, options = {}) {
|
|
45
|
+
if (!views[name]) {
|
|
46
|
+
throw new Error(`ViewManager: View '${name}' not found.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hide the current view and call its onHide hook
|
|
50
|
+
let oldElement = null;
|
|
51
|
+
if (currentViewName && views[currentViewName]) {
|
|
52
|
+
const oldView = views[currentViewName];
|
|
53
|
+
// Get the current element from the container
|
|
54
|
+
oldElement = container.firstElementChild;
|
|
55
|
+
if (oldElement && typeof oldView.onHide === 'function') {
|
|
56
|
+
oldView.onHide(oldElement);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const newView = views[name];
|
|
61
|
+
|
|
62
|
+
// Emit before-change event to allow cleanup (e.g., clearing interaction registry)
|
|
63
|
+
// Include scope to distinguish main navigation from component-internal navigation
|
|
64
|
+
eventBus.emit('view:before-change', {
|
|
65
|
+
oldView: currentViewName,
|
|
66
|
+
newView: name,
|
|
67
|
+
scope: scope,
|
|
68
|
+
context: options
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Always render fresh to ensure current data is displayed
|
|
72
|
+
let newElement = await newView.render(options);
|
|
73
|
+
|
|
74
|
+
if (!(newElement instanceof HTMLElement)) {
|
|
75
|
+
throw new Error(`ViewManager: View '${name}' render function must return an HTMLElement.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto-wrap content with content-width class if configured and not already wrapped
|
|
79
|
+
newElement = autoWrapContentIfNeeded(newElement, name);
|
|
80
|
+
|
|
81
|
+
// Validate rendered HTML for common issues BEFORE adding to DOM
|
|
82
|
+
validateRenderedContent(newElement, name);
|
|
83
|
+
|
|
84
|
+
// Clear container and add new view
|
|
85
|
+
container.innerHTML = '';
|
|
86
|
+
container.appendChild(newElement);
|
|
87
|
+
|
|
88
|
+
// Initialize any declarative components within the new view
|
|
89
|
+
initializeDeclarativeComponents(newElement);
|
|
90
|
+
|
|
91
|
+
// Call onShow hook
|
|
92
|
+
if (typeof newView.onShow === 'function') {
|
|
93
|
+
newView.onShow(newElement, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
currentViewName = name;
|
|
97
|
+
eventBus.emit('view:change', { view: name, context: options });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Auto-wraps content with a content-width class if configured and not already wrapped.
|
|
102
|
+
* Allows per-slide override using data-content-width attribute.
|
|
103
|
+
* @param {HTMLElement} element - The rendered element
|
|
104
|
+
* @param {string} viewName - The name of the view being rendered
|
|
105
|
+
* @returns {HTMLElement} The element, potentially wrapped
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
function autoWrapContentIfNeeded(element, _viewName) {
|
|
109
|
+
// Canvas layout: author owns all styling, no auto-wrapping
|
|
110
|
+
if (courseConfig?.layout === 'canvas') {
|
|
111
|
+
return element;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for per-slide override via data-content-width attribute
|
|
115
|
+
const dataAttrWidth = element.getAttribute('data-content-width');
|
|
116
|
+
const slideConfigWidth = dataAttrWidth;
|
|
117
|
+
|
|
118
|
+
// Determine which width to use: per-slide override > global config > no wrapping
|
|
119
|
+
const configWidth = slideConfigWidth || courseConfig?.slideDefaults?.contentWidth;
|
|
120
|
+
|
|
121
|
+
if (!configWidth) {
|
|
122
|
+
// No wrapping configured
|
|
123
|
+
return element;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if already wrapped with a content-width class
|
|
127
|
+
if (hasContentWidthClass(element)) {
|
|
128
|
+
// Already wrapped, don't double-wrap
|
|
129
|
+
return element;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Create wrapper with appropriate content-width class
|
|
133
|
+
const wrapper = document.createElement('div');
|
|
134
|
+
wrapper.className = `content-${configWidth}`;
|
|
135
|
+
wrapper.appendChild(element);
|
|
136
|
+
return wrapper;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Checks if an element or its children already have a content-width class.
|
|
141
|
+
* @param {HTMLElement} element - The element to check
|
|
142
|
+
* @returns {boolean} True if element or children have content-width class
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
function hasContentWidthClass(element) {
|
|
146
|
+
const contentWidthClasses = ['content-narrow', 'content-medium', 'content-wide', 'content-full'];
|
|
147
|
+
|
|
148
|
+
// Check the element itself
|
|
149
|
+
if (element.classList && contentWidthClasses.some(cls => element.classList.contains(cls))) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check immediate children (common pattern: wrapper div around content)
|
|
154
|
+
if (element.children && element.children.length > 0) {
|
|
155
|
+
for (const child of element.children) {
|
|
156
|
+
if (child.classList && contentWidthClasses.some(cls => child.classList.contains(cls))) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validates rendered content for common issues that can cause page reloads or errors.
|
|
167
|
+
* @param {HTMLElement} element - The rendered element to validate
|
|
168
|
+
* @param {string} viewName - The name of the view being rendered
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
function validateRenderedContent(element, viewName) {
|
|
172
|
+
// Use centralized validation from html-validators.js
|
|
173
|
+
const validation = validateRenderedHTML(element, viewName);
|
|
174
|
+
|
|
175
|
+
if (!validation.valid) {
|
|
176
|
+
// Process each error and emit events
|
|
177
|
+
validation.errors.forEach(error => {
|
|
178
|
+
const message = `View "${viewName}" [${error.type}]: ${error.message}`;
|
|
179
|
+
logger.error(`[ViewManager] ${message}`, { domain: 'view', operation: 'validateRenderedContent', ...error.context });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Throw with a summary of all errors
|
|
183
|
+
const errorSummary = validation.errors.map(e => `[${e.type}] ${e.message}`).join('; ');
|
|
184
|
+
throw new Error(`[ViewManager] View "${viewName}" has ${validation.errors.length} validation error(s): ${errorSummary}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets the name of the currently visible view.
|
|
190
|
+
* @returns {string|null} The name of the current view.
|
|
191
|
+
*/
|
|
192
|
+
function getCurrentView() {
|
|
193
|
+
return currentViewName;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets the container element.
|
|
198
|
+
* @returns {HTMLElement} The container element.
|
|
199
|
+
*/
|
|
200
|
+
function getContainer() {
|
|
201
|
+
return container;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Destroys the view manager and cleans up the container.
|
|
206
|
+
*/
|
|
207
|
+
function destroy() {
|
|
208
|
+
// Call onHide for current view if exists
|
|
209
|
+
if (currentViewName && views[currentViewName]) {
|
|
210
|
+
const currentElement = container.firstElementChild;
|
|
211
|
+
const currentView = views[currentViewName];
|
|
212
|
+
if (currentElement && typeof currentView.onHide === 'function') {
|
|
213
|
+
currentView.onHide(currentElement);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
container.innerHTML = '';
|
|
217
|
+
currentViewName = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
registerView,
|
|
222
|
+
showView,
|
|
223
|
+
getCurrentView,
|
|
224
|
+
getContainer,
|
|
225
|
+
destroy,
|
|
226
|
+
};
|
|
227
|
+
}
|