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,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tooltip.js
|
|
3
|
+
* @description Enhanced tooltip system using event delegation.
|
|
4
|
+
*
|
|
5
|
+
* ARCHITECTURE:
|
|
6
|
+
* Uses event delegation on document.body - no initialization needed!
|
|
7
|
+
* Just add data-tooltip attribute to any element and it works automatically.
|
|
8
|
+
*
|
|
9
|
+
* FEATURES:
|
|
10
|
+
* - Automatic - no init() calls needed
|
|
11
|
+
* - Works with dynamically added/changed elements
|
|
12
|
+
* - Smart show delay (500ms default, configurable via data-tooltip-delay)
|
|
13
|
+
* - Position control (data-tooltip-position)
|
|
14
|
+
* - Theme variants (data-tooltip-theme)
|
|
15
|
+
* - Multi-line support (use \\n or <br> in content)
|
|
16
|
+
* - Max-width control (data-tooltip-width)
|
|
17
|
+
* - Accessible (keyboard focusable, aria-describedby)
|
|
18
|
+
* - Smooth position-aware animations
|
|
19
|
+
* - Auto-hides on scroll, resize, and Escape key
|
|
20
|
+
*
|
|
21
|
+
* USAGE:
|
|
22
|
+
* Basic:
|
|
23
|
+
* <button data-tooltip="Click to submit">Submit</button>
|
|
24
|
+
*
|
|
25
|
+
* With options:
|
|
26
|
+
* <span data-tooltip="Line 1\\nLine 2"
|
|
27
|
+
* data-tooltip-position="top"
|
|
28
|
+
* data-tooltip-delay="500"
|
|
29
|
+
* data-tooltip-theme="light"
|
|
30
|
+
* data-tooltip-width="300">Hover me</span>
|
|
31
|
+
*
|
|
32
|
+
* Instant tooltip (no delay):
|
|
33
|
+
* <span data-tooltip="Instant!" data-tooltip-delay="0">Hover</span>
|
|
34
|
+
*
|
|
35
|
+
* POSITIONS: top, bottom, left, right (default: top)
|
|
36
|
+
* THEMES: dark (default), light
|
|
37
|
+
* DELAY: milliseconds (default: 500ms, use 0 for instant)
|
|
38
|
+
* WIDTH: pixels (default: 280)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export const schema = {
|
|
42
|
+
type: 'tooltip',
|
|
43
|
+
description: 'Declarative tooltip via data-tooltip attribute',
|
|
44
|
+
example: `<p>Hover over any of the highlighted terms below:</p>
|
|
45
|
+
<p>
|
|
46
|
+
<span data-tooltip="HyperText Markup Language" data-tooltip-position="top" style="text-decoration: underline; cursor: help;">HTML</span> provides structure,
|
|
47
|
+
<span data-tooltip="Cascading Style Sheets" data-tooltip-position="bottom" style="text-decoration: underline; cursor: help;">CSS</span> handles styling, and
|
|
48
|
+
<span data-tooltip="A programming language for the web" data-tooltip-position="right" style="text-decoration: underline; cursor: help;">JavaScript</span> adds interactivity.
|
|
49
|
+
</p>
|
|
50
|
+
<p>Icon tooltips work inline with text<span class="tooltip-icon" data-tooltip="This is a tooltip icon — great for contextual help."></span> and scale with font size.</p>
|
|
51
|
+
<h3 style="margin-top: 1em;">Heading with info<span class="tooltip-icon" data-tooltip="Icons inherit the surrounding font size."></span></h3>`,
|
|
52
|
+
properties: {
|
|
53
|
+
tooltip: { type: 'string', required: true, dataAttribute: 'data-tooltip' },
|
|
54
|
+
position: { type: 'string', enum: ['top', 'bottom', 'left', 'right'], default: 'top', dataAttribute: 'data-tooltip-position' },
|
|
55
|
+
theme: { type: 'string', enum: ['dark', 'light'], default: 'dark', dataAttribute: 'data-tooltip-theme' },
|
|
56
|
+
delay: { type: 'number', default: 500, dataAttribute: 'data-tooltip-delay' },
|
|
57
|
+
width: { type: 'number', default: 280, dataAttribute: 'data-tooltip-width' }
|
|
58
|
+
},
|
|
59
|
+
structure: {
|
|
60
|
+
container: '[data-tooltip]',
|
|
61
|
+
children: {}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const metadata = {
|
|
66
|
+
category: 'ui-component',
|
|
67
|
+
cssFile: 'components/tooltip.css',
|
|
68
|
+
engagementTracking: null,
|
|
69
|
+
emitsEvents: []
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let tooltipElement = null;
|
|
73
|
+
let arrowElement = null;
|
|
74
|
+
let showTimeout = null;
|
|
75
|
+
let hideTimeout = null;
|
|
76
|
+
let currentTarget = null;
|
|
77
|
+
let isInitialized = false;
|
|
78
|
+
let suppressUntil = 0; // Timestamp until which tooltips are suppressed
|
|
79
|
+
|
|
80
|
+
// Default timing constants (ms)
|
|
81
|
+
const DEFAULT_SHOW_DELAY = 500; // Time before tooltip appears
|
|
82
|
+
const DEFAULT_HIDE_DELAY = 0; // Hide immediately (was 100ms, felt laggy)
|
|
83
|
+
const POPUP_SUPPRESS_DURATION = 300; // Suppress tooltips briefly after popup opens
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates the shared tooltip element and appends it to the body.
|
|
87
|
+
*/
|
|
88
|
+
function ensureTooltipElement() {
|
|
89
|
+
if (tooltipElement) return;
|
|
90
|
+
|
|
91
|
+
tooltipElement = document.createElement('div');
|
|
92
|
+
tooltipElement.className = 'tooltip-container';
|
|
93
|
+
tooltipElement.setAttribute('role', 'tooltip');
|
|
94
|
+
tooltipElement.id = 'shared-tooltip';
|
|
95
|
+
|
|
96
|
+
// Create arrow element
|
|
97
|
+
arrowElement = document.createElement('div');
|
|
98
|
+
arrowElement.className = 'tooltip-arrow';
|
|
99
|
+
tooltipElement.appendChild(arrowElement);
|
|
100
|
+
|
|
101
|
+
document.body.appendChild(tooltipElement);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Finds the closest ancestor (or self) with a data-tooltip attribute.
|
|
106
|
+
* @param {HTMLElement} element - Starting element
|
|
107
|
+
* @returns {HTMLElement|null} Element with data-tooltip or null
|
|
108
|
+
*/
|
|
109
|
+
function findTooltipTarget(element) {
|
|
110
|
+
return element?.closest('[data-tooltip]');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Processes tooltip text for line breaks.
|
|
115
|
+
* Converts \n to <br> for multi-line display.
|
|
116
|
+
* @param {string} text - The tooltip text
|
|
117
|
+
* @returns {string} HTML-safe text with line breaks
|
|
118
|
+
*/
|
|
119
|
+
function processTooltipText(text) {
|
|
120
|
+
if (!text) return '';
|
|
121
|
+
|
|
122
|
+
// Escape HTML to prevent XSS, then convert line breaks
|
|
123
|
+
const escaped = text
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/</g, '<')
|
|
126
|
+
.replace(/>/g, '>')
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
.replace(/'/g, ''');
|
|
129
|
+
|
|
130
|
+
// Convert \n to <br> for multi-line support
|
|
131
|
+
return escaped.replace(/\\n|\n/g, '<br>');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Shows and positions the tooltip.
|
|
136
|
+
* @param {HTMLElement} targetElement - The element the tooltip is for.
|
|
137
|
+
*/
|
|
138
|
+
function showTooltip(targetElement) {
|
|
139
|
+
if (!tooltipElement) ensureTooltipElement();
|
|
140
|
+
|
|
141
|
+
// Clear any pending hide
|
|
142
|
+
if (hideTimeout) {
|
|
143
|
+
clearTimeout(hideTimeout);
|
|
144
|
+
hideTimeout = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const text = targetElement.getAttribute('data-tooltip');
|
|
148
|
+
if (!text) return;
|
|
149
|
+
|
|
150
|
+
currentTarget = targetElement;
|
|
151
|
+
|
|
152
|
+
// Apply theme
|
|
153
|
+
const theme = targetElement.dataset.tooltipTheme || 'dark';
|
|
154
|
+
tooltipElement.classList.remove('tooltip-theme-dark', 'tooltip-theme-light');
|
|
155
|
+
tooltipElement.classList.add(`tooltip-theme-${theme}`);
|
|
156
|
+
|
|
157
|
+
// Apply custom width if specified
|
|
158
|
+
const customWidth = targetElement.dataset.tooltipWidth;
|
|
159
|
+
if (customWidth) {
|
|
160
|
+
tooltipElement.style.maxWidth = `${customWidth}px`;
|
|
161
|
+
} else {
|
|
162
|
+
tooltipElement.style.maxWidth = ''; // Reset to CSS default
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Set content with line break support (preserve arrow element)
|
|
166
|
+
// Create a text node or use innerHTML for the content, then append arrow
|
|
167
|
+
const contentHtml = processTooltipText(text);
|
|
168
|
+
tooltipElement.innerHTML = contentHtml;
|
|
169
|
+
|
|
170
|
+
// Re-create and append arrow element (since innerHTML replaced it)
|
|
171
|
+
arrowElement = document.createElement('div');
|
|
172
|
+
arrowElement.className = 'tooltip-arrow';
|
|
173
|
+
tooltipElement.appendChild(arrowElement);
|
|
174
|
+
|
|
175
|
+
tooltipElement.classList.add('active');
|
|
176
|
+
|
|
177
|
+
// Link tooltip to target for accessibility
|
|
178
|
+
targetElement.setAttribute('aria-describedby', 'shared-tooltip');
|
|
179
|
+
|
|
180
|
+
positionTooltip(targetElement);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Schedules the tooltip to show after the configured delay.
|
|
185
|
+
* @param {HTMLElement} targetElement - The element the tooltip is for.
|
|
186
|
+
*/
|
|
187
|
+
function scheduleShow(targetElement) {
|
|
188
|
+
// Clear any pending hide (user moved back to element)
|
|
189
|
+
if (hideTimeout) {
|
|
190
|
+
clearTimeout(hideTimeout);
|
|
191
|
+
hideTimeout = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If already showing for this target, don't re-schedule
|
|
195
|
+
if (currentTarget === targetElement && tooltipElement?.classList.contains('active')) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Clear any pending show
|
|
200
|
+
if (showTimeout) {
|
|
201
|
+
clearTimeout(showTimeout);
|
|
202
|
+
showTimeout = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Use custom delay or default
|
|
206
|
+
const delay = parseInt(targetElement.dataset.tooltipDelay, 10);
|
|
207
|
+
const actualDelay = !isNaN(delay) ? delay : DEFAULT_SHOW_DELAY;
|
|
208
|
+
|
|
209
|
+
if (actualDelay > 0) {
|
|
210
|
+
showTimeout = setTimeout(() => {
|
|
211
|
+
showTooltip(targetElement);
|
|
212
|
+
}, actualDelay);
|
|
213
|
+
} else {
|
|
214
|
+
showTooltip(targetElement);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Hides the tooltip with optional delay.
|
|
220
|
+
* @param {boolean} immediate - If true, hide immediately without delay
|
|
221
|
+
*/
|
|
222
|
+
function hideTooltip(immediate = false) {
|
|
223
|
+
// Clear any pending show
|
|
224
|
+
if (showTimeout) {
|
|
225
|
+
clearTimeout(showTimeout);
|
|
226
|
+
showTimeout = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Clear any existing hide timeout
|
|
230
|
+
if (hideTimeout) {
|
|
231
|
+
clearTimeout(hideTimeout);
|
|
232
|
+
hideTimeout = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!tooltipElement) return;
|
|
236
|
+
|
|
237
|
+
const doHide = () => {
|
|
238
|
+
tooltipElement.classList.remove('active');
|
|
239
|
+
|
|
240
|
+
// Clean up aria reference
|
|
241
|
+
if (currentTarget) {
|
|
242
|
+
currentTarget.removeAttribute('aria-describedby');
|
|
243
|
+
currentTarget = null;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (immediate) {
|
|
248
|
+
doHide();
|
|
249
|
+
} else {
|
|
250
|
+
// Brief delay before hiding prevents flicker when moving between elements
|
|
251
|
+
hideTimeout = setTimeout(doHide, DEFAULT_HIDE_DELAY);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Calculates and sets the position of the tooltip relative to the target element.
|
|
257
|
+
* @param {HTMLElement} targetElement - The element to position the tooltip against.
|
|
258
|
+
*/
|
|
259
|
+
function positionTooltip(targetElement) {
|
|
260
|
+
const targetRect = targetElement.getBoundingClientRect();
|
|
261
|
+
const tooltipRect = tooltipElement.getBoundingClientRect();
|
|
262
|
+
const preferredPosition = targetElement.dataset.tooltipPosition || 'top';
|
|
263
|
+
const gap = 12; // Space between target and tooltip (includes arrow)
|
|
264
|
+
|
|
265
|
+
let top, left;
|
|
266
|
+
let finalPosition = preferredPosition;
|
|
267
|
+
|
|
268
|
+
// Calculate position based on preferred position
|
|
269
|
+
switch (preferredPosition) {
|
|
270
|
+
case 'bottom':
|
|
271
|
+
top = targetRect.bottom + gap;
|
|
272
|
+
left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
|
|
273
|
+
break;
|
|
274
|
+
case 'right':
|
|
275
|
+
top = targetRect.top + (targetRect.height / 2) - (tooltipRect.height / 2);
|
|
276
|
+
left = targetRect.right + gap;
|
|
277
|
+
break;
|
|
278
|
+
case 'left':
|
|
279
|
+
top = targetRect.top + (targetRect.height / 2) - (tooltipRect.height / 2);
|
|
280
|
+
left = targetRect.left - tooltipRect.width - gap;
|
|
281
|
+
break;
|
|
282
|
+
case 'top':
|
|
283
|
+
default:
|
|
284
|
+
top = targetRect.top - tooltipRect.height - gap;
|
|
285
|
+
left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
|
|
286
|
+
finalPosition = 'top';
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Adjust if off-screen
|
|
291
|
+
const viewportWidth = window.innerWidth;
|
|
292
|
+
const viewportHeight = window.innerHeight;
|
|
293
|
+
|
|
294
|
+
if (left < gap) left = gap;
|
|
295
|
+
if (left + tooltipRect.width > viewportWidth - gap) {
|
|
296
|
+
left = viewportWidth - tooltipRect.width - gap;
|
|
297
|
+
}
|
|
298
|
+
if (top < gap) top = gap;
|
|
299
|
+
if (top + tooltipRect.height > viewportHeight - gap) {
|
|
300
|
+
top = viewportHeight - tooltipRect.height - gap;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
tooltipElement.style.top = `${top + window.scrollY}px`;
|
|
304
|
+
tooltipElement.style.left = `${left + window.scrollX}px`;
|
|
305
|
+
|
|
306
|
+
// Set position attribute for arrow styling
|
|
307
|
+
tooltipElement.setAttribute('data-position', finalPosition);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Handles mouseenter/focus events via delegation.
|
|
312
|
+
* @param {Event} event - The mouseenter or focus event
|
|
313
|
+
*/
|
|
314
|
+
function handleShow(event) {
|
|
315
|
+
// Skip if tooltips are temporarily suppressed (e.g., popup just opened)
|
|
316
|
+
if (Date.now() < suppressUntil) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const target = findTooltipTarget(event.target);
|
|
321
|
+
if (target) {
|
|
322
|
+
scheduleShow(target);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Suppresses tooltips briefly. Called when popups/menus open.
|
|
328
|
+
* Prevents tooltips from appearing when content appears under the cursor.
|
|
329
|
+
*/
|
|
330
|
+
function suppressTooltips() {
|
|
331
|
+
suppressUntil = Date.now() + POPUP_SUPPRESS_DURATION;
|
|
332
|
+
// Also hide any currently showing tooltip
|
|
333
|
+
hideTooltip(true);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Sets up a MutationObserver to detect when popup menus become visible.
|
|
338
|
+
* This suppresses tooltips briefly to prevent them from appearing
|
|
339
|
+
* when a menu opens under the cursor.
|
|
340
|
+
*/
|
|
341
|
+
function observePopupVisibility() {
|
|
342
|
+
const observer = new MutationObserver((mutations) => {
|
|
343
|
+
for (const mutation of mutations) {
|
|
344
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') {
|
|
345
|
+
const target = mutation.target;
|
|
346
|
+
// If hidden attribute was removed (element became visible)
|
|
347
|
+
if (!target.hidden && target.classList.contains('popup-menu')) {
|
|
348
|
+
suppressTooltips();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Observe the entire document for hidden attribute changes
|
|
355
|
+
observer.observe(document.body, {
|
|
356
|
+
attributes: true,
|
|
357
|
+
attributeFilter: ['hidden'],
|
|
358
|
+
subtree: true
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Handles mouseleave/blur events via delegation.
|
|
364
|
+
* @param {Event} event - The mouseleave or blur event
|
|
365
|
+
*/
|
|
366
|
+
function handleHide(event) {
|
|
367
|
+
const target = findTooltipTarget(event.target);
|
|
368
|
+
|
|
369
|
+
// Always cancel pending show when leaving any tooltip target
|
|
370
|
+
// This prevents tooltip appearing after mouse has already left
|
|
371
|
+
if (target || showTimeout) {
|
|
372
|
+
if (showTimeout) {
|
|
373
|
+
clearTimeout(showTimeout);
|
|
374
|
+
showTimeout = null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Hide if leaving current target OR if no target (mouse left window)
|
|
379
|
+
if ((target && target === currentTarget) || (!target && currentTarget)) {
|
|
380
|
+
hideTooltip();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Handles scroll events - hides tooltip immediately.
|
|
386
|
+
*/
|
|
387
|
+
function handleScroll() {
|
|
388
|
+
if (currentTarget || showTimeout) {
|
|
389
|
+
hideTooltip(true);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Handles resize events - hides tooltip immediately.
|
|
395
|
+
*/
|
|
396
|
+
function handleResize() {
|
|
397
|
+
if (currentTarget || showTimeout) {
|
|
398
|
+
hideTooltip(true);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Handles keydown events - Escape hides tooltip.
|
|
404
|
+
* @param {KeyboardEvent} event
|
|
405
|
+
*/
|
|
406
|
+
function handleKeyDown(event) {
|
|
407
|
+
if (event.key === 'Escape' && (currentTarget || showTimeout)) {
|
|
408
|
+
hideTooltip(true);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Initializes the delegated event listeners on document.body.
|
|
414
|
+
* Called automatically on module load.
|
|
415
|
+
*/
|
|
416
|
+
function initDelegatedListeners() {
|
|
417
|
+
if (isInitialized) return;
|
|
418
|
+
|
|
419
|
+
ensureTooltipElement();
|
|
420
|
+
|
|
421
|
+
// Use capture phase for mouseenter/mouseleave since they don't bubble
|
|
422
|
+
document.body.addEventListener('mouseenter', handleShow, true);
|
|
423
|
+
document.body.addEventListener('mouseleave', handleHide, true);
|
|
424
|
+
|
|
425
|
+
// Focus/blur also need capture for delegation
|
|
426
|
+
document.body.addEventListener('focusin', handleShow, true);
|
|
427
|
+
document.body.addEventListener('focusout', handleHide, true);
|
|
428
|
+
|
|
429
|
+
// Hide on scroll, resize, or Escape (use passive for scroll/resize)
|
|
430
|
+
window.addEventListener('scroll', handleScroll, { passive: true, capture: true });
|
|
431
|
+
window.addEventListener('resize', handleResize, { passive: true });
|
|
432
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
433
|
+
|
|
434
|
+
// Watch for popup menus becoming visible to suppress immediate tooltips
|
|
435
|
+
observePopupVisibility();
|
|
436
|
+
|
|
437
|
+
isInitialized = true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* No-op. Tooltip uses event delegation — no per-element init needed.
|
|
442
|
+
* Kept for catalog compliance (components must export init).
|
|
443
|
+
*/
|
|
444
|
+
export function init(_container) {
|
|
445
|
+
// Ensure delegated listeners are set up (idempotent)
|
|
446
|
+
initDelegatedListeners();
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
destroy: () => { }
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Auto-initialize when DOM is ready
|
|
454
|
+
if (document.readyState === 'loading') {
|
|
455
|
+
document.addEventListener('DOMContentLoaded', initDelegatedListeners);
|
|
456
|
+
} else {
|
|
457
|
+
initDelegatedListeners();
|
|
458
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file value-display.js
|
|
3
|
+
* @description A generic component that updates its text content based on events from another element.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <div data-component="value-display"
|
|
7
|
+
* data-source="#my-input"
|
|
8
|
+
* data-event="change"
|
|
9
|
+
* data-key="value"
|
|
10
|
+
* data-format="You selected: {value}">
|
|
11
|
+
* </div>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const schema = {
|
|
15
|
+
type: 'value-display',
|
|
16
|
+
description: 'Updates text content based on events from another element',
|
|
17
|
+
example: `<div>
|
|
18
|
+
<label for="preview-slider" style="display: block; margin-bottom: 8px; font-weight: 500;">Adjust value:</label>
|
|
19
|
+
<input type="range" id="preview-slider" min="0" max="100" value="50" style="width: 100%;">
|
|
20
|
+
<div data-component="value-display" data-source="#preview-slider" data-event="input" data-key="value" data-format="Current value: {value}%" style="margin-top: 8px; color: #64748b;">Current value: 50%</div>
|
|
21
|
+
</div>`,
|
|
22
|
+
properties: {
|
|
23
|
+
source: { type: 'string', required: true, dataAttribute: 'data-source' },
|
|
24
|
+
event: { type: 'string', default: 'change', dataAttribute: 'data-event' },
|
|
25
|
+
key: { type: 'string', default: 'value', dataAttribute: 'data-key' },
|
|
26
|
+
format: { type: 'string', default: '{value}', dataAttribute: 'data-format' },
|
|
27
|
+
hideEmpty: { type: 'boolean', default: false, dataAttribute: 'data-hide-empty' }
|
|
28
|
+
},
|
|
29
|
+
structure: {
|
|
30
|
+
container: '[data-component="value-display"]',
|
|
31
|
+
children: {}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const metadata = {
|
|
36
|
+
category: 'ui-component',
|
|
37
|
+
cssFile: null,
|
|
38
|
+
engagementTracking: null,
|
|
39
|
+
emitsEvents: []
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
import { logger } from '../../utilities/logger.js';
|
|
43
|
+
|
|
44
|
+
export function init(element) {
|
|
45
|
+
const sourceSelector = element.dataset.source;
|
|
46
|
+
if (!sourceSelector) {
|
|
47
|
+
logger.warn('[ValueDisplay] No data-source provided', element);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// We need to find the source. If it's not in the DOM yet, we might need to wait or look in the same container.
|
|
52
|
+
// For now, assume document-level query or closest container.
|
|
53
|
+
// Using document.querySelector allows linking across the page.
|
|
54
|
+
let sourceElement = null;
|
|
55
|
+
if (sourceSelector.startsWith('#')) {
|
|
56
|
+
sourceElement = document.getElementById(sourceSelector.substring(1));
|
|
57
|
+
} else {
|
|
58
|
+
sourceElement = document.querySelector(sourceSelector);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!sourceElement) {
|
|
62
|
+
logger.fatal(`[ValueDisplay] Source element not found: ${sourceSelector}`, { domain: 'ui', operation: 'initValueDisplay' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const eventName = element.dataset.event || 'change';
|
|
67
|
+
const valueKey = element.dataset.key || 'value';
|
|
68
|
+
const format = element.dataset.format || '{value}';
|
|
69
|
+
const hideEmpty = element.dataset.hideEmpty === 'true';
|
|
70
|
+
|
|
71
|
+
const updateText = (data) => {
|
|
72
|
+
if ((data === '' || data === null || data === undefined) && hideEmpty) {
|
|
73
|
+
element.textContent = '';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let text = format;
|
|
78
|
+
|
|
79
|
+
if (typeof data === 'object' && data !== null) {
|
|
80
|
+
// Replace all keys in the format string
|
|
81
|
+
Object.keys(data).forEach(key => {
|
|
82
|
+
const val = data[key];
|
|
83
|
+
// Handle arrays by joining them
|
|
84
|
+
const displayVal = Array.isArray(val) ? val.join(', ') : val;
|
|
85
|
+
text = text.replace(new RegExp(`{${key}}`, 'g'), displayVal);
|
|
86
|
+
});
|
|
87
|
+
// Also try to replace {value} with the object itself if it wasn't an object with keys
|
|
88
|
+
// but here we assume data IS the object containing keys.
|
|
89
|
+
} else {
|
|
90
|
+
text = text.replace('{value}', data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
element.textContent = text;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleEvent = (e) => {
|
|
97
|
+
let data;
|
|
98
|
+
|
|
99
|
+
// Priority 1: Event Detail (Custom Events)
|
|
100
|
+
if (e.detail) {
|
|
101
|
+
// If valueKey is specified and exists in detail, use it.
|
|
102
|
+
// Otherwise, use the whole detail object to allow multi-key formatting.
|
|
103
|
+
if (valueKey !== 'value' && e.detail[valueKey] !== undefined) {
|
|
104
|
+
data = e.detail[valueKey];
|
|
105
|
+
} else {
|
|
106
|
+
data = e.detail;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Priority 2: Event Target Property (Native Inputs)
|
|
110
|
+
else if (e.target && e.target[valueKey] !== undefined) {
|
|
111
|
+
data = e.target[valueKey];
|
|
112
|
+
}
|
|
113
|
+
// Priority 3: Direct property on source element (fallback)
|
|
114
|
+
else if (sourceElement[valueKey] !== undefined) {
|
|
115
|
+
data = sourceElement[valueKey];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
updateText(data);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
sourceElement.addEventListener(eventName, handleEvent);
|
|
122
|
+
|
|
123
|
+
// Initial state check?
|
|
124
|
+
// If the source has a value property, we can try to set it initially.
|
|
125
|
+
// But for custom components, the value might not be on the DOM element directly.
|
|
126
|
+
// We'll skip initial set for now unless we want to read DOM attributes.
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
destroy: () => {
|
|
130
|
+
sourceElement.removeEventListener(eventName, handleEvent);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|