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,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file lightbox.js
|
|
3
|
+
* @description Declarative lightbox component for click-to-enlarge media viewing.
|
|
4
|
+
* Supports images, videos (native, YouTube, Vimeo), markdown files, and PDFs.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* <!-- Single image lightbox -->
|
|
8
|
+
* <a href="full-size.jpg" data-component="lightbox">
|
|
9
|
+
* <img src="thumbnail.jpg" alt="Description">
|
|
10
|
+
* </a>
|
|
11
|
+
*
|
|
12
|
+
* <!-- Video lightbox (YouTube, Vimeo, or native) -->
|
|
13
|
+
* <a href="https://youtu.be/dQw4w9WgXcQ" data-component="lightbox">
|
|
14
|
+
* <img src="video-thumbnail.jpg" alt="Watch Video">
|
|
15
|
+
* </a>
|
|
16
|
+
* <a href="video/demo.mp4" data-component="lightbox">
|
|
17
|
+
* <img src="video-poster.jpg" alt="Demo Video">
|
|
18
|
+
* </a>
|
|
19
|
+
*
|
|
20
|
+
* <!-- Gallery group with prev/next navigation -->
|
|
21
|
+
* <a href="img1.jpg" data-lightbox-gallery="my-gallery">
|
|
22
|
+
* <img src="thumb1.jpg" alt="Image 1">
|
|
23
|
+
* </a>
|
|
24
|
+
* <a href="img2.jpg" data-lightbox-gallery="my-gallery">
|
|
25
|
+
* <img src="thumb2.jpg" alt="Image 2">
|
|
26
|
+
* </a>
|
|
27
|
+
*
|
|
28
|
+
* Attributes:
|
|
29
|
+
* - href: Full-size media URL (for <a> elements)
|
|
30
|
+
* - data-lightbox-src: Full-size media URL (alternative to href)
|
|
31
|
+
* - data-lightbox-gallery: Gallery group ID for prev/next navigation
|
|
32
|
+
* - data-lightbox-caption: Caption text (falls back to alt text)
|
|
33
|
+
*
|
|
34
|
+
* Keyboard:
|
|
35
|
+
* - ESC: Close lightbox
|
|
36
|
+
* - Arrow Left/Right: Navigate gallery (if multiple items)
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const schema = {
|
|
40
|
+
type: 'lightbox',
|
|
41
|
+
description: 'Click-to-enlarge media viewer for images, videos, markdown, PDFs',
|
|
42
|
+
example: `<div style="display: flex; gap: 12px;">
|
|
43
|
+
<a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23dbeafe'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%233b82f6' font-family='system-ui' font-size='16'%3EPhoto 1%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="First image" style="cursor: zoom-in;">
|
|
44
|
+
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23dbeafe'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%233b82f6' font-family='system-ui' font-size='11'%3EPhoto 1%3C/text%3E%3C/svg%3E" alt="Photo 1">
|
|
45
|
+
</a>
|
|
46
|
+
<a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23dcfce7'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2322c55e' font-family='system-ui' font-size='16'%3EPhoto 2%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="Second image" style="cursor: zoom-in;">
|
|
47
|
+
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23dcfce7'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2322c55e' font-family='system-ui' font-size='11'%3EPhoto 2%3C/text%3E%3C/svg%3E" alt="Photo 2">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='300' fill='%23fef3c7'%3E%3Crect width='400' height='300' rx='8'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23f59e0b' font-family='system-ui' font-size='16'%3EPhoto 3%3C/text%3E%3C/svg%3E" data-component="lightbox" data-lightbox-gallery="demo" data-lightbox-caption="Third image" style="cursor: zoom-in;">
|
|
50
|
+
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80' fill='%23fef3c7'%3E%3Crect width='120' height='80' rx='4'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%23f59e0b' font-family='system-ui' font-size='11'%3EPhoto 3%3C/text%3E%3C/svg%3E" alt="Photo 3">
|
|
51
|
+
</a>
|
|
52
|
+
</div>`,
|
|
53
|
+
properties: {
|
|
54
|
+
gallery: { type: 'string', dataAttribute: 'data-lightbox-gallery' },
|
|
55
|
+
caption: { type: 'string', dataAttribute: 'data-lightbox-caption' },
|
|
56
|
+
src: { type: 'string', dataAttribute: 'data-lightbox-src' },
|
|
57
|
+
thumbnail: { type: 'string', dataAttribute: 'data-lightbox-thumbnail' }
|
|
58
|
+
},
|
|
59
|
+
structure: {
|
|
60
|
+
container: '[data-component="lightbox"]',
|
|
61
|
+
children: {}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const metadata = {
|
|
66
|
+
category: 'ui-component',
|
|
67
|
+
cssFile: 'components/lightbox.css',
|
|
68
|
+
engagementTracking: 'viewAllLightbox',
|
|
69
|
+
emitsEvents: ['lightbox:opened', 'lightbox:closed']
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
import { eventBus } from '../../core/event-bus.js';
|
|
73
|
+
import { iconManager } from '../../utilities/icons.js';
|
|
74
|
+
import { logger } from '../../utilities/logger.js';
|
|
75
|
+
import { escapeHTML } from '../../utilities/utilities.js';
|
|
76
|
+
import { fetchAndRenderMarkdown } from '../../utilities/markdown-renderer.js';
|
|
77
|
+
import engagementManager from '../../engagement/engagement-manager.js';
|
|
78
|
+
|
|
79
|
+
// Lightbox state
|
|
80
|
+
let lightboxElement = null;
|
|
81
|
+
let currentIndex = 0;
|
|
82
|
+
let currentGallery = [];
|
|
83
|
+
let isOpen = false;
|
|
84
|
+
let currentMediaType = 'image'; // 'image' | 'video' | 'markdown' | 'pdf'
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolves asset paths relative to course directory.
|
|
88
|
+
* Follows same pattern as embed-frame.js for consistency.
|
|
89
|
+
* @param {string} src - The source path
|
|
90
|
+
* @returns {string} Resolved path
|
|
91
|
+
*/
|
|
92
|
+
function _resolvePath(src) {
|
|
93
|
+
if (!src) return src;
|
|
94
|
+
// Already absolute URL or protocol-relative
|
|
95
|
+
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('//')) {
|
|
96
|
+
return src;
|
|
97
|
+
}
|
|
98
|
+
// Already has leading slash (absolute path from root)
|
|
99
|
+
if (src.startsWith('/')) {
|
|
100
|
+
return src;
|
|
101
|
+
}
|
|
102
|
+
// Already uses ./ or ../ relative paths
|
|
103
|
+
if (src.startsWith('./') || src.startsWith('../')) {
|
|
104
|
+
return src;
|
|
105
|
+
}
|
|
106
|
+
// Otherwise, assume relative to course/ directory
|
|
107
|
+
return `./course/${src}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the source URL from a trigger element with path resolution.
|
|
112
|
+
* Uses raw attribute to avoid browser pre-resolution, then applies path resolution.
|
|
113
|
+
* @param {HTMLElement} trigger
|
|
114
|
+
* @returns {string} Resolved source URL
|
|
115
|
+
*/
|
|
116
|
+
function getTriggerSrc(trigger) {
|
|
117
|
+
const rawSrc = trigger.getAttribute('href') || trigger.dataset.lightboxSrc;
|
|
118
|
+
return _resolvePath(rawSrc);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// Media URL Detection
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detects if URL is a PDF file.
|
|
127
|
+
* @param {string} url
|
|
128
|
+
* @returns {{ type: 'pdf' } | null}
|
|
129
|
+
*/
|
|
130
|
+
function detectPDF(url) {
|
|
131
|
+
if (!url) return null;
|
|
132
|
+
if (/\.pdf($|\?|#)/i.test(url)) return { type: 'pdf' };
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Detects if URL is a markdown file.
|
|
138
|
+
* @param {string} url
|
|
139
|
+
* @returns {{ type: 'markdown' } | null}
|
|
140
|
+
*/
|
|
141
|
+
function detectMarkdown(url) {
|
|
142
|
+
if (!url) return null;
|
|
143
|
+
if (/\.md($|\?|#)/i.test(url)) return { type: 'markdown' };
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Detects if a URL is a YouTube video and extracts the video ID.
|
|
149
|
+
* @param {string} url
|
|
150
|
+
* @returns {{ type: 'youtube', id: string } | null}
|
|
151
|
+
*/
|
|
152
|
+
function detectYouTube(url) {
|
|
153
|
+
if (!url) return null;
|
|
154
|
+
// youtube.com/watch?v=VIDEO_ID
|
|
155
|
+
const watchMatch = url.match(/(?:youtube\.com\/watch\?.*v=)([a-zA-Z0-9_-]{11})/);
|
|
156
|
+
if (watchMatch) return { type: 'youtube', id: watchMatch[1] };
|
|
157
|
+
// youtu.be/VIDEO_ID
|
|
158
|
+
const shortMatch = url.match(/(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/);
|
|
159
|
+
if (shortMatch) return { type: 'youtube', id: shortMatch[1] };
|
|
160
|
+
// youtube.com/embed/VIDEO_ID
|
|
161
|
+
const embedMatch = url.match(/(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
|
|
162
|
+
if (embedMatch) return { type: 'youtube', id: embedMatch[1] };
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Detects if a URL is a Vimeo video and extracts the video ID.
|
|
168
|
+
* @param {string} url
|
|
169
|
+
* @returns {{ type: 'vimeo', id: string } | null}
|
|
170
|
+
*/
|
|
171
|
+
function detectVimeo(url) {
|
|
172
|
+
if (!url) return null;
|
|
173
|
+
const match = url.match(/(?:vimeo\.com\/)([\d]+)/);
|
|
174
|
+
if (match) return { type: 'vimeo', id: match[1] };
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detects if URL is a native video file.
|
|
180
|
+
* @param {string} url
|
|
181
|
+
* @returns {{ type: 'native' } | null}
|
|
182
|
+
*/
|
|
183
|
+
function detectNativeVideo(url) {
|
|
184
|
+
if (!url) return null;
|
|
185
|
+
const videoExtensions = /\.(mp4|webm|ogg|mov|m4v)($|\?)/i;
|
|
186
|
+
if (videoExtensions.test(url)) return { type: 'native' };
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Detects if URL is any type of video.
|
|
192
|
+
* @param {string} url
|
|
193
|
+
* @returns {{ type: 'youtube' | 'vimeo' | 'native', id?: string } | null}
|
|
194
|
+
*/
|
|
195
|
+
function detectVideo(url) {
|
|
196
|
+
return detectYouTube(url) || detectVimeo(url) || detectNativeVideo(url);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Initialize a single lightbox trigger element.
|
|
201
|
+
* Called by the component catalog for each [data-component="lightbox"] element.
|
|
202
|
+
* @param {HTMLElement} element - The trigger element
|
|
203
|
+
*/
|
|
204
|
+
export function init(element) {
|
|
205
|
+
// Skip if already initialized
|
|
206
|
+
if (element.dataset.lightboxInitialized) return;
|
|
207
|
+
element.dataset.lightboxInitialized = 'true';
|
|
208
|
+
|
|
209
|
+
// Lazy-create the shared lightbox overlay
|
|
210
|
+
if (!lightboxElement) {
|
|
211
|
+
createLightboxElement();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
element.addEventListener('click', (e) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
openFromTrigger(element);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const src = getTriggerSrc(element);
|
|
220
|
+
const mediaType = getMediaType(src);
|
|
221
|
+
const customThumbnail = element.dataset.lightboxThumbnail;
|
|
222
|
+
const subtitle = element.dataset.lightboxSubtitle;
|
|
223
|
+
|
|
224
|
+
// Check for custom thumbnail override (works for any media type)
|
|
225
|
+
if (customThumbnail) {
|
|
226
|
+
renderCustomThumbnail(element, customThumbnail);
|
|
227
|
+
} else if (mediaType === 'markdown') {
|
|
228
|
+
// For markdown files, render a thumbnail preview
|
|
229
|
+
renderMarkdownThumbnail(element, src);
|
|
230
|
+
} else if (mediaType === 'pdf') {
|
|
231
|
+
// For PDF files, render a styled placeholder
|
|
232
|
+
renderPdfThumbnail(element, src);
|
|
233
|
+
} else {
|
|
234
|
+
// Add visual indicator if not already styled
|
|
235
|
+
element.style.cursor = 'zoom-in';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add subtitle if present - wrap trigger in a container
|
|
239
|
+
if (subtitle) {
|
|
240
|
+
wrapTriggerWithSubtitle(element, subtitle);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Render a custom thumbnail image inside a trigger element.
|
|
246
|
+
* @param {HTMLElement} trigger - The trigger element
|
|
247
|
+
* @param {string} thumbnailSrc - URL of the custom thumbnail image
|
|
248
|
+
*/
|
|
249
|
+
function renderCustomThumbnail(trigger, thumbnailSrc) {
|
|
250
|
+
trigger.classList.add('lightbox-custom-thumbnail');
|
|
251
|
+
trigger.style.cursor = 'zoom-in';
|
|
252
|
+
const resolvedSrc = _resolvePath(thumbnailSrc);
|
|
253
|
+
trigger.innerHTML = `<img src="${resolvedSrc}" alt="" class="lightbox-custom-thumbnail-img">`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Render a markdown thumbnail preview inside a trigger element.
|
|
258
|
+
* @param {HTMLElement} trigger - The trigger element
|
|
259
|
+
* @param {string} src - URL of the markdown file
|
|
260
|
+
*/
|
|
261
|
+
async function renderMarkdownThumbnail(trigger, src) {
|
|
262
|
+
// Add thumbnail container class
|
|
263
|
+
trigger.classList.add('lightbox-md-thumbnail');
|
|
264
|
+
|
|
265
|
+
// Show loading state
|
|
266
|
+
trigger.innerHTML = `
|
|
267
|
+
<div class="lightbox-md-thumbnail-loading">
|
|
268
|
+
${iconManager.getIcon('loader-2', { size: 'lg', class: 'icon-spin' })}
|
|
269
|
+
</div>
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const html = await fetchAndRenderMarkdown(src);
|
|
274
|
+
trigger.innerHTML = `<div class="lightbox-md-thumbnail-content">${html}</div>`;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
trigger.innerHTML = `
|
|
277
|
+
<div class="lightbox-md-thumbnail-danger">
|
|
278
|
+
${iconManager.getIcon('file-text', { size: 'xl' })}
|
|
279
|
+
<span>Failed to load</span>
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
logger.error('Failed to render markdown thumbnail', { src, error: error.message });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Render a PDF placeholder thumbnail inside a trigger element.
|
|
288
|
+
* @param {HTMLElement} trigger - The trigger element
|
|
289
|
+
* @param {string} src - URL of the PDF file
|
|
290
|
+
*/
|
|
291
|
+
function renderPdfThumbnail(trigger, src) {
|
|
292
|
+
trigger.classList.add('lightbox-pdf-thumbnail');
|
|
293
|
+
|
|
294
|
+
// Extract filename from path
|
|
295
|
+
const filename = src.split('/').pop().split('?')[0] || 'Document.pdf';
|
|
296
|
+
|
|
297
|
+
trigger.innerHTML = `
|
|
298
|
+
<div class="lightbox-pdf-thumbnail-content">
|
|
299
|
+
${iconManager.getIcon('file-text', { size: '2xl', class: 'lightbox-pdf-thumbnail-icon' })}
|
|
300
|
+
<span class="lightbox-pdf-thumbnail-badge">PDF</span>
|
|
301
|
+
<span class="lightbox-pdf-thumbnail-filename">${filename}</span>
|
|
302
|
+
</div>
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Wrap a trigger element with a container that includes a subtitle below.
|
|
308
|
+
* @param {HTMLElement} trigger - The trigger element
|
|
309
|
+
* @param {string} subtitle - The subtitle text
|
|
310
|
+
*/
|
|
311
|
+
function wrapTriggerWithSubtitle(trigger, subtitle) {
|
|
312
|
+
// Create wrapper container
|
|
313
|
+
const wrapper = document.createElement('div');
|
|
314
|
+
wrapper.className = 'lightbox-thumbnail-wrapper';
|
|
315
|
+
|
|
316
|
+
// Insert wrapper before trigger, then move trigger into wrapper
|
|
317
|
+
trigger.parentNode.insertBefore(wrapper, trigger);
|
|
318
|
+
wrapper.appendChild(trigger);
|
|
319
|
+
|
|
320
|
+
// Add subtitle element
|
|
321
|
+
const subtitleEl = document.createElement('span');
|
|
322
|
+
subtitleEl.className = 'lightbox-thumbnail-subtitle';
|
|
323
|
+
subtitleEl.textContent = subtitle;
|
|
324
|
+
wrapper.appendChild(subtitleEl);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create the lightbox DOM element.
|
|
329
|
+
*/
|
|
330
|
+
function createLightboxElement() {
|
|
331
|
+
// Find the course container for proper z-index stacking
|
|
332
|
+
const courseContainer = document.querySelector('.course-container') || document.body;
|
|
333
|
+
|
|
334
|
+
lightboxElement = document.createElement('div');
|
|
335
|
+
lightboxElement.className = 'lightbox';
|
|
336
|
+
lightboxElement.innerHTML = `
|
|
337
|
+
<div class="lightbox-backdrop"></div>
|
|
338
|
+
<div class="lightbox-content">
|
|
339
|
+
<button class="lightbox-close" aria-label="Close lightbox">
|
|
340
|
+
${iconManager.getIcon('x', { size: 'lg' })}
|
|
341
|
+
</button>
|
|
342
|
+
<div class="lightbox-media-wrapper">
|
|
343
|
+
<div class="lightbox-loading">
|
|
344
|
+
${iconManager.getIcon('loader-2', { size: '2xl', class: 'icon-spin' })}
|
|
345
|
+
</div>
|
|
346
|
+
<!-- Image container -->
|
|
347
|
+
<img class="lightbox-image" src="" alt="">
|
|
348
|
+
<!-- Video container (shown when media is video) -->
|
|
349
|
+
<div class="lightbox-video"></div>
|
|
350
|
+
<!-- Markdown container (shown when media is markdown) -->
|
|
351
|
+
<div class="lightbox-markdown"></div>
|
|
352
|
+
<!-- PDF container (shown when media is pdf) -->
|
|
353
|
+
<iframe class="lightbox-pdf" src="" title="PDF document"></iframe>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="lightbox-caption"></div>
|
|
356
|
+
<button class="lightbox-nav lightbox-prev" aria-label="Previous">
|
|
357
|
+
${iconManager.getIcon('chevron-left', { size: 'xl' })}
|
|
358
|
+
</button>
|
|
359
|
+
<button class="lightbox-nav lightbox-next" aria-label="Next">
|
|
360
|
+
${iconManager.getIcon('chevron-right', { size: 'xl' })}
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
// Event listeners
|
|
366
|
+
lightboxElement.querySelector('.lightbox-backdrop').addEventListener('click', close);
|
|
367
|
+
lightboxElement.querySelector('.lightbox-close').addEventListener('click', close);
|
|
368
|
+
lightboxElement.querySelector('.lightbox-prev').addEventListener('click', () => navigate(-1));
|
|
369
|
+
lightboxElement.querySelector('.lightbox-next').addEventListener('click', () => navigate(1));
|
|
370
|
+
|
|
371
|
+
// Handle hash anchor links inside markdown container (TOC links)
|
|
372
|
+
const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
|
|
373
|
+
markdownContainer.addEventListener('click', (e) => {
|
|
374
|
+
const anchor = e.target.closest('a');
|
|
375
|
+
if (!anchor) return;
|
|
376
|
+
|
|
377
|
+
const href = anchor.getAttribute('href');
|
|
378
|
+
if (href && href.startsWith('#')) {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
const targetId = href.slice(1);
|
|
381
|
+
const targetElement = markdownContainer.querySelector(`#${CSS.escape(targetId)}`);
|
|
382
|
+
if (targetElement) {
|
|
383
|
+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Keyboard navigation
|
|
389
|
+
document.addEventListener('keydown', handleKeydown);
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
courseContainer.appendChild(lightboxElement);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Handle keyboard events.
|
|
397
|
+
* @param {KeyboardEvent} e
|
|
398
|
+
*/
|
|
399
|
+
function handleKeydown(e) {
|
|
400
|
+
if (!isOpen) return;
|
|
401
|
+
|
|
402
|
+
switch (e.key) {
|
|
403
|
+
case 'Escape':
|
|
404
|
+
close();
|
|
405
|
+
break;
|
|
406
|
+
case 'ArrowLeft':
|
|
407
|
+
navigate(-1);
|
|
408
|
+
break;
|
|
409
|
+
case 'ArrowRight':
|
|
410
|
+
navigate(1);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Open lightbox from a trigger element.
|
|
417
|
+
* @param {HTMLElement} trigger
|
|
418
|
+
*/
|
|
419
|
+
function openFromTrigger(trigger) {
|
|
420
|
+
const src = getTriggerSrc(trigger);
|
|
421
|
+
const caption = trigger.dataset.lightboxCaption ||
|
|
422
|
+
trigger.querySelector('img')?.alt ||
|
|
423
|
+
trigger.getAttribute('aria-label') || '';
|
|
424
|
+
const galleryId = trigger.dataset.lightboxGallery;
|
|
425
|
+
const lightboxId = trigger.id || trigger.dataset.lightboxId;
|
|
426
|
+
|
|
427
|
+
// Build gallery array if part of a group
|
|
428
|
+
if (galleryId) {
|
|
429
|
+
const galleryTriggers = document.querySelectorAll(`[data-lightbox-gallery="${galleryId}"]`);
|
|
430
|
+
currentGallery = Array.from(galleryTriggers).map(t => {
|
|
431
|
+
const itemSrc = getTriggerSrc(t);
|
|
432
|
+
return {
|
|
433
|
+
src: itemSrc,
|
|
434
|
+
caption: t.dataset.lightboxCaption || t.querySelector('img')?.alt || '',
|
|
435
|
+
mediaType: getMediaType(itemSrc)
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
currentIndex = Array.from(galleryTriggers).indexOf(trigger);
|
|
439
|
+
} else {
|
|
440
|
+
currentGallery = [{ src, caption, mediaType: getMediaType(src) }];
|
|
441
|
+
currentIndex = 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Track lightbox view for engagement
|
|
445
|
+
const slideId = trigger.closest?.('[data-slide-id]')?.dataset?.slideId ||
|
|
446
|
+
document.querySelector('.slide.active')?.dataset?.slideId ||
|
|
447
|
+
null;
|
|
448
|
+
if (slideId && lightboxId) {
|
|
449
|
+
engagementManager.trackLightboxView(slideId, lightboxId);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
open(src, caption);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Determine media type from URL.
|
|
457
|
+
* @param {string} src
|
|
458
|
+
* @returns {'markdown' | 'video' | 'image'}
|
|
459
|
+
*/
|
|
460
|
+
function getMediaType(src) {
|
|
461
|
+
if (detectPDF(src)) return 'pdf';
|
|
462
|
+
if (detectMarkdown(src)) return 'markdown';
|
|
463
|
+
if (detectVideo(src)) return 'video';
|
|
464
|
+
return 'image';
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Open the lightbox with an image, video, or markdown file.
|
|
469
|
+
* @param {string} src - Media source URL
|
|
470
|
+
* @param {string} [caption=''] - Optional caption
|
|
471
|
+
*/
|
|
472
|
+
export async function open(src, caption = '') {
|
|
473
|
+
if (!lightboxElement) {
|
|
474
|
+
createLightboxElement();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const img = lightboxElement.querySelector('.lightbox-image');
|
|
478
|
+
const videoContainer = lightboxElement.querySelector('.lightbox-video');
|
|
479
|
+
const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
|
|
480
|
+
const pdfContainer = lightboxElement.querySelector('.lightbox-pdf');
|
|
481
|
+
const captionEl = lightboxElement.querySelector('.lightbox-caption');
|
|
482
|
+
const loading = lightboxElement.querySelector('.lightbox-loading');
|
|
483
|
+
const prevBtn = lightboxElement.querySelector('.lightbox-prev');
|
|
484
|
+
const nextBtn = lightboxElement.querySelector('.lightbox-next');
|
|
485
|
+
|
|
486
|
+
// Detect media type
|
|
487
|
+
currentMediaType = getMediaType(src);
|
|
488
|
+
|
|
489
|
+
// Clear previous content and hide all containers
|
|
490
|
+
videoContainer.innerHTML = '';
|
|
491
|
+
videoContainer.style.display = 'none';
|
|
492
|
+
markdownContainer.innerHTML = '';
|
|
493
|
+
markdownContainer.scrollTop = 0; // Reset scroll position before loading new content
|
|
494
|
+
markdownContainer.style.display = 'none';
|
|
495
|
+
pdfContainer.src = '';
|
|
496
|
+
pdfContainer.style.display = 'none';
|
|
497
|
+
img.style.display = 'none';
|
|
498
|
+
img.src = '';
|
|
499
|
+
|
|
500
|
+
if (currentMediaType === 'pdf') {
|
|
501
|
+
// Render PDF in iframe
|
|
502
|
+
loading.style.display = 'none';
|
|
503
|
+
pdfContainer.style.display = 'block';
|
|
504
|
+
pdfContainer.src = src;
|
|
505
|
+
logger.debug('Lightbox opened with PDF', { src });
|
|
506
|
+
} else if (currentMediaType === 'markdown') {
|
|
507
|
+
// Render markdown
|
|
508
|
+
loading.style.display = 'flex';
|
|
509
|
+
markdownContainer.style.display = 'block';
|
|
510
|
+
try {
|
|
511
|
+
const html = await fetchAndRenderMarkdown(src);
|
|
512
|
+
markdownContainer.innerHTML = html;
|
|
513
|
+
// Reset scroll position after DOM renders
|
|
514
|
+
requestAnimationFrame(() => {
|
|
515
|
+
markdownContainer.scrollTop = 0;
|
|
516
|
+
});
|
|
517
|
+
loading.style.display = 'none';
|
|
518
|
+
logger.debug('Lightbox opened with markdown', { src });
|
|
519
|
+
} catch (error) {
|
|
520
|
+
loading.style.display = 'none';
|
|
521
|
+
markdownContainer.innerHTML = `<div class="lightbox-danger">Failed to load markdown: ${escapeHTML(error.message)}</div>`;
|
|
522
|
+
logger.error('Failed to load lightbox markdown', { src, error: error.message });
|
|
523
|
+
}
|
|
524
|
+
} else if (currentMediaType === 'video') {
|
|
525
|
+
// Render video
|
|
526
|
+
const videoInfo = detectVideo(src);
|
|
527
|
+
loading.style.display = 'none';
|
|
528
|
+
videoContainer.style.display = 'block';
|
|
529
|
+
videoContainer.innerHTML = renderVideo(src, videoInfo);
|
|
530
|
+
logger.debug('Lightbox opened with video', { src, type: videoInfo.type });
|
|
531
|
+
} else {
|
|
532
|
+
// Render image (original behavior)
|
|
533
|
+
img.style.display = 'block';
|
|
534
|
+
loading.style.display = 'flex';
|
|
535
|
+
img.style.opacity = '0';
|
|
536
|
+
|
|
537
|
+
img.onload = () => {
|
|
538
|
+
loading.style.display = 'none';
|
|
539
|
+
img.style.opacity = '1';
|
|
540
|
+
};
|
|
541
|
+
img.onerror = () => {
|
|
542
|
+
loading.style.display = 'none';
|
|
543
|
+
logger.error('Failed to load lightbox image', { src });
|
|
544
|
+
};
|
|
545
|
+
img.src = src;
|
|
546
|
+
img.alt = caption || 'Enlarged image';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Caption
|
|
550
|
+
captionEl.textContent = caption;
|
|
551
|
+
captionEl.style.display = caption ? 'block' : 'none';
|
|
552
|
+
|
|
553
|
+
// Gallery navigation visibility
|
|
554
|
+
const isGallery = currentGallery.length > 1;
|
|
555
|
+
prevBtn.style.display = isGallery ? 'flex' : 'none';
|
|
556
|
+
nextBtn.style.display = isGallery ? 'flex' : 'none';
|
|
557
|
+
|
|
558
|
+
// Show lightbox
|
|
559
|
+
lightboxElement.classList.add('active');
|
|
560
|
+
isOpen = true;
|
|
561
|
+
|
|
562
|
+
// Trap focus
|
|
563
|
+
lightboxElement.querySelector('.lightbox-close').focus();
|
|
564
|
+
|
|
565
|
+
eventBus.emit('lightbox:opened', { src, caption, type: currentMediaType });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Render video HTML based on video type.
|
|
570
|
+
* @param {string} src - Video URL
|
|
571
|
+
* @param {{ type: 'youtube' | 'vimeo' | 'native', id?: string }} videoInfo
|
|
572
|
+
* @returns {string} HTML string
|
|
573
|
+
*/
|
|
574
|
+
function renderVideo(src, videoInfo) {
|
|
575
|
+
if (videoInfo.type === 'youtube') {
|
|
576
|
+
return `
|
|
577
|
+
<iframe
|
|
578
|
+
class="lightbox-video-embed"
|
|
579
|
+
src="https://www.youtube.com/embed/${videoInfo.id}?autoplay=1&rel=0"
|
|
580
|
+
title="YouTube video"
|
|
581
|
+
frameborder="0"
|
|
582
|
+
allow="autoplay; fullscreen">
|
|
583
|
+
</iframe>
|
|
584
|
+
`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (videoInfo.type === 'vimeo') {
|
|
588
|
+
return `
|
|
589
|
+
<iframe
|
|
590
|
+
class="lightbox-video-embed"
|
|
591
|
+
src="https://player.vimeo.com/video/${videoInfo.id}?autoplay=1"
|
|
592
|
+
title="Vimeo video"
|
|
593
|
+
frameborder="0"
|
|
594
|
+
allow="autoplay; fullscreen">
|
|
595
|
+
</iframe>
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Native video
|
|
600
|
+
return `
|
|
601
|
+
<video
|
|
602
|
+
class="lightbox-video-native"
|
|
603
|
+
src="${src}"
|
|
604
|
+
controls
|
|
605
|
+
autoplay>
|
|
606
|
+
Your browser does not support video playback.
|
|
607
|
+
</video>
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Close the lightbox.
|
|
613
|
+
*/
|
|
614
|
+
export function close() {
|
|
615
|
+
if (!lightboxElement || !isOpen) return;
|
|
616
|
+
|
|
617
|
+
lightboxElement.classList.remove('active');
|
|
618
|
+
isOpen = false;
|
|
619
|
+
|
|
620
|
+
// Clear media to stop loading/playback
|
|
621
|
+
const img = lightboxElement.querySelector('.lightbox-image');
|
|
622
|
+
const videoContainer = lightboxElement.querySelector('.lightbox-video');
|
|
623
|
+
const markdownContainer = lightboxElement.querySelector('.lightbox-markdown');
|
|
624
|
+
const pdfContainer = lightboxElement.querySelector('.lightbox-pdf');
|
|
625
|
+
img.src = '';
|
|
626
|
+
videoContainer.innerHTML = ''; // Stops video/iframe playback
|
|
627
|
+
markdownContainer.innerHTML = '';
|
|
628
|
+
pdfContainer.src = ''; // Clear PDF iframe
|
|
629
|
+
|
|
630
|
+
eventBus.emit('lightbox:closed');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Navigate to next/previous image in gallery.
|
|
635
|
+
* @param {number} direction - 1 for next, -1 for previous
|
|
636
|
+
*/
|
|
637
|
+
function navigate(direction) {
|
|
638
|
+
if (currentGallery.length <= 1) return;
|
|
639
|
+
|
|
640
|
+
currentIndex = (currentIndex + direction + currentGallery.length) % currentGallery.length;
|
|
641
|
+
const item = currentGallery[currentIndex];
|
|
642
|
+
open(item.src, item.caption);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Check if lightbox is currently open.
|
|
647
|
+
* @returns {boolean}
|
|
648
|
+
*/
|
|
649
|
+
export function isVisible() {
|
|
650
|
+
return isOpen;
|
|
651
|
+
}
|
|
652
|
+
|