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,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file accordion.js
|
|
3
|
+
* @description Accessible accordion interface with keyboard navigation and engagement tracking.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { initAccordion } from '../framework/js/components/ui-components/accordion.js';
|
|
7
|
+
*
|
|
8
|
+
* initAccordion(root, {
|
|
9
|
+
* id: 'faq-accordion',
|
|
10
|
+
* mode: 'single', // 'single' or 'multi'
|
|
11
|
+
* items: [
|
|
12
|
+
* { id: 'panel1', title: 'Question 1', content: '<p>Answer...</p>' },
|
|
13
|
+
* { id: 'panel2', title: 'Question 2', content: '<p>Answer...</p>' }
|
|
14
|
+
* ],
|
|
15
|
+
* defaultOpen: ['panel1']
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* Data Attributes:
|
|
19
|
+
* data-mode="single" - Only one panel open at a time
|
|
20
|
+
* data-mode="multi" - Multiple panels can be open (default)
|
|
21
|
+
* data-always-open - Prevent closing the last open panel (requires at least one open)
|
|
22
|
+
*
|
|
23
|
+
* Note: Audio is NOT supported on accordions due to multi-panel complexity.
|
|
24
|
+
* Use tabs (single panel visible) or modals for audio content.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { announceToScreenReader } from './index.js';
|
|
28
|
+
import engagementManager from '../../engagement/engagement-manager.js';
|
|
29
|
+
import * as NavigationState from '../../navigation/NavigationState.js';
|
|
30
|
+
import { eventBus } from '../../core/event-bus.js';
|
|
31
|
+
import { logger } from '../../utilities/logger.js';
|
|
32
|
+
|
|
33
|
+
// Schema for validation, linting, and AI-assisted authoring
|
|
34
|
+
export const schema = {
|
|
35
|
+
type: 'accordion',
|
|
36
|
+
description: 'Accessible accordion with keyboard navigation and engagement tracking',
|
|
37
|
+
example: `<div data-component="accordion" id="preview-accordion" data-mode="multi">
|
|
38
|
+
<div class="accordion-item" data-panel-id="getting-started">
|
|
39
|
+
<button class="accordion-button" data-panel="getting-started" data-action="toggle-accordion-panel" aria-expanded="true" aria-controls="preview-accordion-panel-getting-started"><span class="accordion-title">Getting Started</span><span class="accordion-icon"></span></button>
|
|
40
|
+
<div id="preview-accordion-panel-getting-started" class="accordion-content show" role="region"><div class="accordion-body">Learn the basics of course authoring with step-by-step guidance.</div></div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="accordion-item" data-panel-id="advanced">
|
|
43
|
+
<button class="accordion-button collapsed" data-panel="advanced" data-action="toggle-accordion-panel" aria-expanded="false" aria-controls="preview-accordion-panel-advanced"><span class="accordion-title">Advanced Features</span><span class="accordion-icon"></span></button>
|
|
44
|
+
<div id="preview-accordion-panel-advanced" class="accordion-content" role="region" hidden><div class="accordion-body">Explore components, interactions, and layout patterns.</div></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="accordion-item" data-panel-id="publishing">
|
|
47
|
+
<button class="accordion-button collapsed" data-panel="publishing" data-action="toggle-accordion-panel" aria-expanded="false" aria-controls="preview-accordion-panel-publishing"><span class="accordion-title">Publishing</span><span class="accordion-icon"></span></button>
|
|
48
|
+
<div id="preview-accordion-panel-publishing" class="accordion-content" role="region" hidden><div class="accordion-body">Export your course for LMS deployment.</div></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>`,
|
|
51
|
+
properties: {
|
|
52
|
+
mode: { type: 'string', enum: ['single', 'multi'], default: 'multi', description: 'single = one panel open, multi = multiple open' },
|
|
53
|
+
alwaysOpen: { type: 'boolean', default: false, description: 'Prevent closing last open panel (data-always-open attribute)' }
|
|
54
|
+
},
|
|
55
|
+
structure: {
|
|
56
|
+
container: '[data-component="accordion"]',
|
|
57
|
+
children: {
|
|
58
|
+
item: { selector: '.accordion-item', required: true, minItems: 1 },
|
|
59
|
+
button: { selector: '.accordion-button', parent: '.accordion-item', required: true },
|
|
60
|
+
content: { selector: '.accordion-content', parent: '.accordion-item', required: true }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const metadata = {
|
|
66
|
+
category: 'ui-component',
|
|
67
|
+
cssFile: 'components/accordions.css',
|
|
68
|
+
engagementTracking: 'viewAllPanels',
|
|
69
|
+
emitsEvents: ['accordion:toggled']
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates and initializes an accordion component
|
|
75
|
+
* @param {HTMLElement|string} root - Container element or selector
|
|
76
|
+
* @param {Object} options - Configuration options
|
|
77
|
+
* @param {string} options.id - Unique accordion ID
|
|
78
|
+
* @param {string} [options.mode='multi'] - 'single' (one open) or 'multi' (multiple open)
|
|
79
|
+
* @param {Array} options.items - Array of {id, title, content}
|
|
80
|
+
* @param {Array} [options.defaultOpen=[]] - Array of panel IDs to open by default
|
|
81
|
+
* @param {Function} [options.onToggle] - Callback when panel toggles
|
|
82
|
+
* @returns {Object} Accordion API
|
|
83
|
+
*/
|
|
84
|
+
export function init(root, options = {}) {
|
|
85
|
+
const accordionElement = typeof root === 'string' ? document.querySelector(root) : root;
|
|
86
|
+
if (!accordionElement) {
|
|
87
|
+
logger.fatal('UIComponents.initAccordion: container not found', { domain: 'ui', operation: 'initAccordion' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Configuration is read from the element's ID and data attributes
|
|
92
|
+
const id = accordionElement.id;
|
|
93
|
+
const mode = accordionElement.dataset.mode || 'multi';
|
|
94
|
+
const alwaysOpen = accordionElement.hasAttribute('data-always-open');
|
|
95
|
+
const onToggle = typeof options.onToggle === 'function' ? options.onToggle : null;
|
|
96
|
+
|
|
97
|
+
if (!id) {
|
|
98
|
+
logger.fatal('UIComponents.initAccordion: The accordion container element must have an ID.', { domain: 'ui', operation: 'initAccordion' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (mode !== 'single' && mode !== 'multi') {
|
|
102
|
+
logger.fatal('UIComponents.initAccordion: data-mode attribute must be "single" or "multi"', { domain: 'ui', operation: 'initAccordion' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// HYDRATION: Support simplified syntax
|
|
107
|
+
// If no .accordion-item elements exist, check for children with data-title
|
|
108
|
+
if (accordionElement.querySelectorAll('.accordion-item').length === 0) {
|
|
109
|
+
const simpleItems = Array.from(accordionElement.children).filter(el => el.hasAttribute('data-title'));
|
|
110
|
+
|
|
111
|
+
if (simpleItems.length > 0) {
|
|
112
|
+
simpleItems.forEach((item, index) => {
|
|
113
|
+
const title = item.getAttribute('data-title');
|
|
114
|
+
// Preserve existing content
|
|
115
|
+
const content = item.innerHTML;
|
|
116
|
+
// Generate a unique ID suffix for this panel
|
|
117
|
+
const uniqueSuffix = `item-${index + 1}`;
|
|
118
|
+
|
|
119
|
+
const wrapper = document.createElement('div');
|
|
120
|
+
wrapper.className = 'accordion-item';
|
|
121
|
+
wrapper.setAttribute('data-panel-id', uniqueSuffix);
|
|
122
|
+
|
|
123
|
+
wrapper.innerHTML = `
|
|
124
|
+
<button class="accordion-button collapsed" data-panel="${uniqueSuffix}" data-action="toggle-accordion-panel">
|
|
125
|
+
<span class="accordion-title">${title}</span>
|
|
126
|
+
<span class="accordion-icon"></span>
|
|
127
|
+
</button>
|
|
128
|
+
<div id="${id}-panel-${uniqueSuffix}" class="accordion-content" hidden>
|
|
129
|
+
<div class="accordion-body">${content}</div>
|
|
130
|
+
</div>
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
accordionElement.replaceChild(wrapper, item);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const buttons = Array.from(accordionElement.querySelectorAll('.accordion-button'));
|
|
139
|
+
const items = Array.from(accordionElement.querySelectorAll('.accordion-item')).map(itemEl => {
|
|
140
|
+
// Try to get ID from the item wrapper first, then fallback to the button inside
|
|
141
|
+
const button = itemEl.querySelector('.accordion-button');
|
|
142
|
+
const id = itemEl.dataset.panelId || button?.dataset.panel;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
title: itemEl.querySelector('.accordion-title')?.textContent || '',
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!buttons.length) {
|
|
151
|
+
// No buttons found, nothing to initialize.
|
|
152
|
+
return {
|
|
153
|
+
destroy: () => {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Validate structure: Check if all buttons have corresponding panels
|
|
158
|
+
const errors = [];
|
|
159
|
+
buttons.forEach((btn, index) => {
|
|
160
|
+
const panelId = btn.dataset.panel;
|
|
161
|
+
if (!panelId) {
|
|
162
|
+
errors.push(`Button ${index + 1} is missing data-panel attribute.`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
|
|
166
|
+
if (!content) {
|
|
167
|
+
errors.push(`Panel content not found for button ${index + 1} (expected id="${id}-panel-${panelId}").`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (errors.length > 0) {
|
|
172
|
+
logger.fatal(`UIComponents.initAccordion: Invalid structure in #${id}:\n${errors.join('\n')}`, { domain: 'ui', operation: 'initAccordion' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Determine initially open panels by inspecting the DOM
|
|
177
|
+
const openPanels = new Set(
|
|
178
|
+
buttons.filter(btn => btn.getAttribute('aria-expanded') === 'true').map(btn => btn.dataset.panel)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Initialize ARIA attributes
|
|
182
|
+
buttons.forEach(btn => {
|
|
183
|
+
const panelId = btn.dataset.panel;
|
|
184
|
+
const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
|
|
185
|
+
|
|
186
|
+
if (!btn.hasAttribute('aria-expanded')) {
|
|
187
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
188
|
+
}
|
|
189
|
+
if (!btn.hasAttribute('aria-controls') && content) {
|
|
190
|
+
btn.setAttribute('aria-controls', content.id);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Ensure content has role="region" and aria-labelledby
|
|
194
|
+
if (content) {
|
|
195
|
+
if (!content.hasAttribute('role')) {
|
|
196
|
+
content.setAttribute('role', 'region');
|
|
197
|
+
}
|
|
198
|
+
if (!content.hasAttribute('aria-labelledby') && btn.id) {
|
|
199
|
+
content.setAttribute('aria-labelledby', btn.id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// If alwaysOpen is set and no panels are open, open the first one
|
|
205
|
+
if (alwaysOpen && openPanels.size === 0 && buttons.length > 0) {
|
|
206
|
+
const firstPanelId = buttons[0].dataset.panel;
|
|
207
|
+
const firstContent = accordionElement.querySelector(`#${id}-panel-${firstPanelId}`);
|
|
208
|
+
if (firstPanelId && firstContent) {
|
|
209
|
+
openPanels.add(firstPanelId);
|
|
210
|
+
buttons[0].classList.remove('collapsed');
|
|
211
|
+
buttons[0].setAttribute('aria-expanded', 'true');
|
|
212
|
+
firstContent.classList.add('show');
|
|
213
|
+
firstContent.removeAttribute('hidden');
|
|
214
|
+
logger.debug(`[Accordion] Auto-opened first panel for data-always-open: ${firstPanelId}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Register with engagement tracking
|
|
219
|
+
const currentSlideId = NavigationState.getCurrentSlideId();
|
|
220
|
+
if (currentSlideId) {
|
|
221
|
+
const panelIds = items.map(item => item.id);
|
|
222
|
+
engagementManager.registerAccordion(currentSlideId, panelIds);
|
|
223
|
+
|
|
224
|
+
// Track initially open panels for engagement progress.
|
|
225
|
+
// Users who see open content should get credit for viewing it.
|
|
226
|
+
// This tracks panels that are open when the accordion initializes.
|
|
227
|
+
if (openPanels.size > 0) {
|
|
228
|
+
openPanels.forEach(panelId => {
|
|
229
|
+
engagementManager.trackAccordionPanel(currentSlideId, panelId);
|
|
230
|
+
});
|
|
231
|
+
logger.debug(`[Accordion] Tracked ${openPanels.size} initially open panels for engagement: ${Array.from(openPanels).join(', ')}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Track pending transitions to prevent overlapping animations
|
|
236
|
+
let transitionTimeout = null;
|
|
237
|
+
|
|
238
|
+
// Update locked state for always-open accordions
|
|
239
|
+
// When only one panel is open and alwaysOpen is set, mark it as locked (can't close)
|
|
240
|
+
function updateLockedState() {
|
|
241
|
+
if (!alwaysOpen) return;
|
|
242
|
+
|
|
243
|
+
buttons.forEach(btn => {
|
|
244
|
+
const panelId = btn.dataset.panel;
|
|
245
|
+
const isOpen = openPanels.has(panelId);
|
|
246
|
+
const isLastOpen = isOpen && openPanels.size === 1;
|
|
247
|
+
|
|
248
|
+
btn.classList.toggle('accordion-locked', isLastOpen);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Initialize locked state
|
|
253
|
+
updateLockedState();
|
|
254
|
+
|
|
255
|
+
function togglePanel(panelId) {
|
|
256
|
+
const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
|
|
257
|
+
const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
|
|
258
|
+
|
|
259
|
+
if (!button || !content) return;
|
|
260
|
+
|
|
261
|
+
// Cancel any pending open actions from rapid clicks
|
|
262
|
+
if (transitionTimeout) {
|
|
263
|
+
clearTimeout(transitionTimeout);
|
|
264
|
+
transitionTimeout = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const isCurrentlyOpen = openPanels.has(panelId);
|
|
268
|
+
|
|
269
|
+
if (isCurrentlyOpen) {
|
|
270
|
+
// Case 1: Closing the currently open panel
|
|
271
|
+
// If alwaysOpen is set and this is the last open panel, prevent closing
|
|
272
|
+
if (alwaysOpen && openPanels.size === 1) {
|
|
273
|
+
logger.debug('[Accordion] Cannot close last panel - data-always-open is set');
|
|
274
|
+
announceToScreenReader('At least one panel must remain open');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// Action: Close immediately
|
|
278
|
+
closePanel(panelId);
|
|
279
|
+
notifyToggle(panelId, false);
|
|
280
|
+
} else {
|
|
281
|
+
// Case 2: Opening a new panel
|
|
282
|
+
if (mode === 'single' && openPanels.size > 0) {
|
|
283
|
+
// Crossfade: Open new and close old simultaneously
|
|
284
|
+
// Both panels animate at the same time for smooth transition
|
|
285
|
+
const panelsToClose = Array.from(openPanels);
|
|
286
|
+
|
|
287
|
+
// Start opening new panel first (so it's visible during transition)
|
|
288
|
+
openPanel(panelId);
|
|
289
|
+
notifyToggle(panelId, true);
|
|
290
|
+
|
|
291
|
+
// Close old panels (animated close is now built into closePanel)
|
|
292
|
+
// Notify for each closed panel so linked components (e.g. interactive-image) can update
|
|
293
|
+
panelsToClose.forEach(openId => {
|
|
294
|
+
closePanel(openId, false);
|
|
295
|
+
notifyToggle(openId, false);
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
// Case 3: Opening panel (Multi mode OR Single mode with nothing open)
|
|
299
|
+
// Action: Open immediately
|
|
300
|
+
openPanel(panelId);
|
|
301
|
+
notifyToggle(panelId, true);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function notifyToggle(panelId, isOpen) {
|
|
307
|
+
if (currentSlideId && isOpen) {
|
|
308
|
+
engagementManager.trackAccordionPanel(currentSlideId, panelId);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (onToggle) {
|
|
312
|
+
onToggle({ panelId, isOpen, mode });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
eventBus.emit('accordion:toggled', {
|
|
316
|
+
accordionId: id,
|
|
317
|
+
panelId: panelId,
|
|
318
|
+
isOpen: isOpen
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function openPanel(panelId, announce = true) {
|
|
323
|
+
const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
|
|
324
|
+
const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
|
|
325
|
+
|
|
326
|
+
if (!button || !content) return;
|
|
327
|
+
|
|
328
|
+
openPanels.add(panelId);
|
|
329
|
+
button.classList.remove('collapsed');
|
|
330
|
+
button.setAttribute('aria-expanded', 'true');
|
|
331
|
+
content.classList.add('show');
|
|
332
|
+
content.removeAttribute('hidden');
|
|
333
|
+
|
|
334
|
+
updateLockedState();
|
|
335
|
+
|
|
336
|
+
if (announce) {
|
|
337
|
+
const title = button.querySelector('.accordion-title')?.textContent || 'Panel';
|
|
338
|
+
announceToScreenReader(`${title} expanded`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function closePanel(panelId, announce = true) {
|
|
343
|
+
const button = accordionElement.querySelector(`[data-panel="${panelId}"]`);
|
|
344
|
+
const content = accordionElement.querySelector(`#${id}-panel-${panelId}`);
|
|
345
|
+
|
|
346
|
+
if (!button || !content) return;
|
|
347
|
+
|
|
348
|
+
openPanels.delete(panelId);
|
|
349
|
+
button.classList.add('collapsed');
|
|
350
|
+
button.setAttribute('aria-expanded', 'false');
|
|
351
|
+
content.classList.remove('show'); // Starts shrinking + fade animation
|
|
352
|
+
// Don't add hidden yet - let it animate first
|
|
353
|
+
|
|
354
|
+
updateLockedState();
|
|
355
|
+
|
|
356
|
+
if (announce) {
|
|
357
|
+
const title = button.querySelector('.accordion-title')?.textContent || 'Panel';
|
|
358
|
+
announceToScreenReader(`${title} collapsed`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add hidden after transition completes (matches CSS transition duration)
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
// Only add hidden if panel is still closed (wasn't re-opened)
|
|
364
|
+
if (!content.classList.contains('show')) {
|
|
365
|
+
content.setAttribute('hidden', 'hidden');
|
|
366
|
+
}
|
|
367
|
+
}, 350); // Matches CSS transition duration
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function openAll() {
|
|
371
|
+
if (mode === 'single') {
|
|
372
|
+
throw new Error('UIComponents.initAccordion: openAll() is not available in single mode. Use mode="multi" or call togglePanel() individually.');
|
|
373
|
+
}
|
|
374
|
+
items.forEach(item => openPanel(item.id, false));
|
|
375
|
+
announceToScreenReader('All panels expanded');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function closeAll() {
|
|
379
|
+
items.forEach(item => closePanel(item.id, false));
|
|
380
|
+
announceToScreenReader('All panels collapsed');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const clickHandler = (event) => {
|
|
384
|
+
const button = event.target.closest('[data-action="toggle-accordion-panel"]');
|
|
385
|
+
if (!button) return;
|
|
386
|
+
|
|
387
|
+
event.preventDefault();
|
|
388
|
+
const panelId = button.dataset.panel;
|
|
389
|
+
if (panelId) {
|
|
390
|
+
togglePanel(panelId);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const keydownHandler = (event) => {
|
|
395
|
+
const button = event.target.closest('.accordion-button');
|
|
396
|
+
if (!button) return;
|
|
397
|
+
|
|
398
|
+
const currentIndex = buttons.indexOf(button);
|
|
399
|
+
|
|
400
|
+
switch (event.key) {
|
|
401
|
+
case 'ArrowDown':
|
|
402
|
+
case 'ArrowRight':
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
const nextIndex = (currentIndex + 1) % buttons.length;
|
|
405
|
+
buttons[nextIndex].focus();
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'ArrowUp':
|
|
409
|
+
case 'ArrowLeft':
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
const prevIndex = (currentIndex - 1 + buttons.length) % buttons.length;
|
|
412
|
+
buttons[prevIndex].focus();
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case 'Home':
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
buttons[0].focus();
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'End':
|
|
421
|
+
event.preventDefault();
|
|
422
|
+
buttons[buttons.length - 1].focus();
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
accordionElement.addEventListener('click', clickHandler);
|
|
428
|
+
accordionElement.addEventListener('keydown', keydownHandler);
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
togglePanel,
|
|
432
|
+
openPanel,
|
|
433
|
+
closePanel,
|
|
434
|
+
openAll,
|
|
435
|
+
closeAll,
|
|
436
|
+
getOpenPanels: () => Array.from(openPanels),
|
|
437
|
+
destroy: () => {
|
|
438
|
+
accordionElement.removeEventListener('click', clickHandler);
|
|
439
|
+
accordionElement.removeEventListener('keydown', keydownHandler);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file alert.js
|
|
3
|
+
* @description Handles dismissible, in-page alerts.
|
|
4
|
+
*
|
|
5
|
+
* Usage (Declarative):
|
|
6
|
+
* <div class="alert alert-warning" data-component="alert">
|
|
7
|
+
* <p>This is a warning message.</p>
|
|
8
|
+
* <button class="alert-close" data-action="dismiss-alert" aria-label="Dismiss alert">×</button>
|
|
9
|
+
* </div>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const schema = {
|
|
13
|
+
type: 'alert',
|
|
14
|
+
description: 'Dismissible in-page alert message',
|
|
15
|
+
example: `<div class="alert alert-warning" data-component="alert">
|
|
16
|
+
<p><strong>Important:</strong> Please review the course requirements before proceeding.</p>
|
|
17
|
+
<button class="alert-close" data-action="dismiss-alert" aria-label="Dismiss alert">×</button>
|
|
18
|
+
</div>`,
|
|
19
|
+
properties: {},
|
|
20
|
+
structure: {
|
|
21
|
+
container: '[data-component="alert"]',
|
|
22
|
+
children: {
|
|
23
|
+
close: { selector: '[data-action="dismiss-alert"]', required: false }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const metadata = {
|
|
29
|
+
category: 'ui-component',
|
|
30
|
+
cssFile: 'components/callouts.css',
|
|
31
|
+
engagementTracking: null,
|
|
32
|
+
emitsEvents: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
import { iconManager } from '../../utilities/icons.js';
|
|
36
|
+
import { logger } from '../../utilities/logger.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initializes a dismissible alert component.
|
|
40
|
+
* @param {HTMLElement} alertElement - The alert container element.
|
|
41
|
+
* @returns {object} An object with a `destroy` method.
|
|
42
|
+
*/
|
|
43
|
+
export function init(alertElement) {
|
|
44
|
+
if (!alertElement) {
|
|
45
|
+
logger.fatal('initAlert: alertElement not found.', { domain: 'ui', operation: 'initAlert' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const closeButton = alertElement.querySelector('[data-action="dismiss-alert"]');
|
|
50
|
+
|
|
51
|
+
// Inject standardize icon if button exists
|
|
52
|
+
if (closeButton) {
|
|
53
|
+
closeButton.innerHTML = iconManager.getIcon('x');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!closeButton) {
|
|
57
|
+
// If there's no close button, there's nothing to initialize.
|
|
58
|
+
return {
|
|
59
|
+
destroy: () => { }
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const dismiss = () => {
|
|
64
|
+
alertElement.style.opacity = '0';
|
|
65
|
+
alertElement.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out, max-height 0.3s 0.1s, padding 0.3s 0.1s, margin 0.3s 0.1s';
|
|
66
|
+
alertElement.style.transform = 'scaleY(0.8)';
|
|
67
|
+
alertElement.style.maxHeight = '0';
|
|
68
|
+
alertElement.style.paddingTop = '0';
|
|
69
|
+
alertElement.style.paddingBottom = '0';
|
|
70
|
+
alertElement.style.marginTop = '0';
|
|
71
|
+
alertElement.style.marginBottom = '0';
|
|
72
|
+
alertElement.style.borderWidth = '0';
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Remove from DOM after transition
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
alertElement.remove();
|
|
78
|
+
}, 400);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
closeButton.addEventListener('click', dismiss);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
destroy: () => {
|
|
85
|
+
closeButton.removeEventListener('click', dismiss);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|