synthos 0.8.0 → 0.10.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/README.md +1 -1
- package/default-pages/application/page.html +42 -0
- package/default-pages/application/page.json +10 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
- package/default-pages/elevenlabs_effects_studio/page.json +11 -0
- package/default-pages/elevenlabs_voice_studio/page.html +801 -0
- package/default-pages/elevenlabs_voice_studio/page.json +11 -0
- package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
- package/default-pages/json_tools/page.json +10 -0
- package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
- package/default-pages/my_notes/page.html +132 -0
- package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
- package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
- package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
- package/default-pages/neon_asteroids/files/effects.json +74 -0
- package/default-pages/neon_asteroids/page.html +1803 -0
- package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
- package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
- package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
- package/default-pages/retro_game_starter/page.html +1308 -0
- package/default-pages/retro_game_starter/page.json +12 -0
- package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
- package/default-pages/sidebar_page/page.json +10 -0
- package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
- package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
- package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
- package/default-pages/solar_tutorial/page.json +10 -0
- package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
- package/default-pages/two-panel_page/page.json +10 -0
- package/default-pages/{us_map.html → us_map/page.html} +193 -192
- package/default-pages/{us_map.json → us_map/page.json} +12 -12
- package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
- package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
- package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
- package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
- package/default-themes/aurora-dawn.json +19 -0
- package/default-themes/aurora-dawn.v3.css +198 -0
- package/default-themes/aurora-dusk.json +19 -0
- package/default-themes/aurora-dusk.v3.css +200 -0
- package/default-themes/cosmos-dawn.json +19 -0
- package/default-themes/cosmos-dawn.v3.css +198 -0
- package/default-themes/cosmos-dusk.json +19 -0
- package/default-themes/cosmos-dusk.v3.css +200 -0
- package/default-themes/high-contrast-dark.json +19 -0
- package/default-themes/high-contrast-dark.v3.css +200 -0
- package/default-themes/high-contrast-light.json +19 -0
- package/default-themes/high-contrast-light.v3.css +198 -0
- package/default-themes/nebula-dawn.v2.css +110 -0
- package/default-themes/nebula-dawn.v3.css +199 -0
- package/default-themes/nebula-dusk.v2.css +104 -0
- package/default-themes/nebula-dusk.v3.css +201 -0
- package/default-themes/solar-flare-dawn.json +19 -0
- package/default-themes/solar-flare-dawn.v3.css +198 -0
- package/default-themes/solar-flare-dusk.json +19 -0
- package/default-themes/solar-flare-dusk.v3.css +200 -0
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
- package/dist/agents/openclaw/gatewayManager.js +27 -11
- package/dist/agents/openclaw/gatewayManager.js.map +1 -1
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
- package/dist/agents/openclaw/openclawProvider.js +2 -4
- package/dist/agents/openclaw/openclawProvider.js.map +1 -1
- package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
- package/dist/agents/openclaw/sshTunnelManager.js +31 -12
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
- package/dist/builders/anthropic.d.ts +31 -0
- package/dist/builders/anthropic.d.ts.map +1 -0
- package/dist/builders/anthropic.js +227 -0
- package/dist/builders/anthropic.js.map +1 -0
- package/dist/builders/fireworksai.d.ts +9 -0
- package/dist/builders/fireworksai.d.ts.map +1 -0
- package/dist/builders/fireworksai.js +57 -0
- package/dist/builders/fireworksai.js.map +1 -0
- package/dist/builders/index.d.ts +13 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +31 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/builders/openai.d.ts +8 -0
- package/dist/builders/openai.d.ts.map +1 -0
- package/dist/builders/openai.js +87 -0
- package/dist/builders/openai.js.map +1 -0
- package/dist/builders/types.d.ts +54 -0
- package/dist/builders/types.d.ts.map +1 -0
- package/dist/builders/types.js +211 -0
- package/dist/builders/types.js.map +1 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +3 -2
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/registry.d.ts +2 -1
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +31 -8
- package/dist/connectors/registry.js.map +1 -1
- package/dist/customizer/Customizer.d.ts +62 -0
- package/dist/customizer/Customizer.d.ts.map +1 -0
- package/dist/customizer/Customizer.js +134 -0
- package/dist/customizer/Customizer.js.map +1 -0
- package/dist/customizer/index.d.ts +4 -0
- package/dist/customizer/index.d.ts.map +1 -0
- package/dist/customizer/index.js +9 -0
- package/dist/customizer/index.js.map +1 -0
- package/dist/files.d.ts +16 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +60 -1
- package/dist/files.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +12 -6
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +150 -133
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +23 -10
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +4 -2
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +33 -6
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/fireworksai.d.ts.map +1 -1
- package/dist/models/fireworksai.js +9 -1
- package/dist/models/fireworksai.js.map +1 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/openai.d.ts +1 -1
- package/dist/models/openai.d.ts.map +1 -1
- package/dist/models/openai.js +24 -3
- package/dist/models/openai.js.map +1 -1
- package/dist/models/types.d.ts +20 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +6 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +34 -10
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +229 -79
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +2 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +2 -2
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/requiresSettings.d.ts +2 -1
- package/dist/service/requiresSettings.d.ts.map +1 -1
- package/dist/service/requiresSettings.js +3 -3
- package/dist/service/requiresSettings.js.map +1 -1
- package/dist/service/server.d.ts +2 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +37 -8
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +47 -20
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +514 -293
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +2 -1
- package/dist/service/useAgentRoutes.d.ts.map +1 -1
- package/dist/service/useAgentRoutes.js +17 -14
- package/dist/service/useAgentRoutes.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts +2 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +287 -172
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.js +17 -17
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +13 -10
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useFileRoutes.d.ts +4 -0
- package/dist/service/useFileRoutes.d.ts.map +1 -0
- package/dist/service/useFileRoutes.js +122 -0
- package/dist/service/useFileRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +2 -1
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +671 -74
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts +4 -0
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
- package/dist/service/useSharedDataRoutes.js +107 -0
- package/dist/service/useSharedDataRoutes.js.map +1 -0
- package/dist/service/useSharedFileRoutes.d.ts +4 -0
- package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
- package/dist/service/useSharedFileRoutes.js +121 -0
- package/dist/service/useSharedFileRoutes.js.map +1 -0
- package/dist/settings.d.ts +5 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +12 -10
- package/dist/settings.js.map +1 -1
- package/dist/storage/FsStorageProvider.d.ts +25 -0
- package/dist/storage/FsStorageProvider.d.ts.map +1 -0
- package/dist/storage/FsStorageProvider.js +103 -0
- package/dist/storage/FsStorageProvider.js.map +1 -0
- package/dist/storage/StorageProvider.d.ts +31 -0
- package/dist/storage/StorageProvider.d.ts.map +1 -0
- package/dist/storage/StorageProvider.js +3 -0
- package/dist/storage/StorageProvider.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +4 -3
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +1 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +65 -28
- package/dist/themes.js.map +1 -1
- package/migration-rules/v1-to-v2.md +193 -0
- package/migration-rules/v2-to-v3.md +481 -0
- package/package.json +11 -10
- package/required-pages/builder/page.html +43 -0
- package/required-pages/builder/page.json +10 -0
- package/required-pages/{pages.html → pages/page.html} +238 -233
- package/required-pages/pages/page.json +10 -0
- package/required-pages/{settings.html → settings/page.html} +389 -275
- package/required-pages/settings/page.json +10 -0
- package/required-pages/synthos_apis/page.html +846 -0
- package/required-pages/synthos_apis/page.json +10 -0
- package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
- package/required-pages/synthos_scripts/page.json +10 -0
- package/src/agents/index.ts +1 -1
- package/src/agents/openclaw/gatewayManager.ts +22 -11
- package/src/agents/openclaw/openclawProvider.ts +2 -4
- package/src/agents/openclaw/sshTunnelManager.ts +19 -11
- package/src/builders/anthropic.ts +283 -0
- package/src/builders/fireworksai.ts +59 -0
- package/src/builders/index.ts +33 -0
- package/src/builders/openai.ts +89 -0
- package/src/builders/types.ts +261 -0
- package/src/connectors/index.ts +1 -1
- package/src/connectors/registry.ts +28 -8
- package/src/customizer/Customizer.ts +163 -0
- package/src/customizer/index.ts +5 -0
- package/src/files.ts +57 -0
- package/src/index.ts +3 -1
- package/src/init.ts +195 -145
- package/src/migrations.ts +30 -10
- package/src/models/anthropic.ts +40 -10
- package/src/models/fireworksai.ts +9 -2
- package/src/models/index.ts +1 -1
- package/src/models/openai.ts +26 -6
- package/src/models/types.ts +31 -1
- package/src/pages.ts +230 -77
- package/src/service/createCompletePrompt.ts +3 -2
- package/src/service/requiresSettings.ts +4 -3
- package/src/service/server.ts +36 -9
- package/src/service/transformPage.ts +557 -326
- package/src/service/useAgentRoutes.ts +19 -14
- package/src/service/useApiRoutes.ts +208 -84
- package/src/service/useConnectorRoutes.ts +18 -18
- package/src/service/useDataRoutes.ts +13 -10
- package/src/service/useFileRoutes.ts +128 -0
- package/src/service/usePageRoutes.ts +730 -81
- package/src/service/useSharedDataRoutes.ts +109 -0
- package/src/service/useSharedFileRoutes.ts +127 -0
- package/src/settings.ts +14 -10
- package/src/storage/FsStorageProvider.ts +87 -0
- package/src/storage/StorageProvider.ts +34 -0
- package/src/storage/index.ts +2 -0
- package/src/synthos-cli.ts +4 -3
- package/src/themes.ts +64 -27
- package/static-files/favicon.svg +12 -0
- package/static-files/fluentlm-instructions.llmd +868 -0
- package/static-files/fluentlm-instructions.md +1595 -0
- package/static-files/fluentlm.css +4844 -0
- package/static-files/fluentlm.js +3602 -0
- package/static-files/fluentlm.min.css +1 -0
- package/static-files/fluentlm.min.js +1 -0
- package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
- package/static-files/page.v3.js +1290 -0
- package/static-files/recommended-frameworks.llmd +81 -0
- package/static-files/recommended-frameworks.md +137 -0
- package/static-files/retro-game.js +877 -0
- package/static-files/shell.css +797 -0
- package/static-files/theme-dark.css +169 -0
- package/static-files/theme-light.css +169 -0
- package/tests/builders.spec.ts +139 -0
- package/tests/pages.spec.ts +54 -84
- package/tests/transformPage.spec.ts +299 -360
- package/default-pages/application.html +0 -40
- package/default-pages/application.json +0 -1
- package/default-pages/json_tools.json +0 -1
- package/default-pages/my_notes.html +0 -33
- package/default-pages/neon_asteroids.html +0 -77
- package/default-pages/sidebar_page.json +0 -1
- package/default-pages/solar_tutorial.json +0 -1
- package/default-pages/two-panel_page.json +0 -1
- package/dist/service/useGatewayRoutes.d.ts +0 -4
- package/dist/service/useGatewayRoutes.d.ts.map +0 -1
- package/dist/service/useGatewayRoutes.js +0 -168
- package/dist/service/useGatewayRoutes.js.map +0 -1
- package/page-scripts/page-v2.js +0 -656
- package/required-pages/builder.html +0 -48
- package/required-pages/builder.json +0 -1
- package/required-pages/pages.json +0 -1
- package/required-pages/settings.json +0 -1
- package/required-pages/synthos_apis.html +0 -327
- package/required-pages/synthos_apis.json +0 -1
- package/required-pages/synthos_scripts.json +0 -1
- package/src/connectors/airtable/connector.json +0 -27
- package/src/connectors/alpha-vantage/connector.json +0 -26
- package/src/connectors/brave-search/connector.json +0 -26
- package/src/connectors/cloudinary/connector.json +0 -27
- package/src/connectors/deepl/connector.json +0 -28
- package/src/connectors/elevenlabs/connector.json +0 -30
- package/src/connectors/giphy/connector.json +0 -27
- package/src/connectors/github/connector.json +0 -29
- package/src/connectors/huggingface/connector.json +0 -27
- package/src/connectors/imgur/connector.json +0 -29
- package/src/connectors/instagram/connector.json +0 -43
- package/src/connectors/jira/connector.json +0 -28
- package/src/connectors/mapbox/connector.json +0 -26
- package/src/connectors/nasa/connector.json +0 -27
- package/src/connectors/newsapi/connector.json +0 -27
- package/src/connectors/notion/connector.json +0 -28
- package/src/connectors/open-exchange-rates/connector.json +0 -27
- package/src/connectors/openweathermap/connector.json +0 -26
- package/src/connectors/pexels/connector.json +0 -27
- package/src/connectors/resend/connector.json +0 -29
- package/src/connectors/rss2json/connector.json +0 -27
- package/src/connectors/sendgrid/connector.json +0 -27
- package/src/connectors/spoonacular/connector.json +0 -28
- package/src/connectors/stability-ai/connector.json +0 -27
- package/src/connectors/twilio/connector.json +0 -28
- package/src/connectors/unsplash/connector.json +0 -27
- package/src/connectors/wolfram-alpha/connector.json +0 -26
- package/src/connectors/youtube-data/connector.json +0 -30
- /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
- /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
|
@@ -0,0 +1,3602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FluentLM — Main Runtime
|
|
3
|
+
*
|
|
4
|
+
* On DOMContentLoaded, initializes the theme and walks the DOM to
|
|
5
|
+
* enhance elements that need JS (icons, split buttons, toggles, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Components that are pure CSS (Stack, Text, Label, Link, Separator,
|
|
8
|
+
* Spinner, TextField, Checkbox, Dropdown, Breadcrumb, Image,
|
|
9
|
+
* ProgressIndicator, Persona, Overlay) require no JS initialization.
|
|
10
|
+
*
|
|
11
|
+
* Load order:
|
|
12
|
+
* 1. icons.js (icon registry — no deps)
|
|
13
|
+
* 2. components/*.js (component modules — depend on FluentIcons)
|
|
14
|
+
* 3. theme.js (theme manager — no deps)
|
|
15
|
+
* 4. fluentlm.js (this file — orchestrator)
|
|
16
|
+
*/
|
|
17
|
+
var FluentLM = (function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var initialized = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize all components on the page.
|
|
24
|
+
* Safe to call multiple times (idempotent per element).
|
|
25
|
+
* Optionally pass a root element to scope initialization.
|
|
26
|
+
*/
|
|
27
|
+
function init(root) {
|
|
28
|
+
// Theme
|
|
29
|
+
FluentTheme.init();
|
|
30
|
+
|
|
31
|
+
// Tier 1 components
|
|
32
|
+
FluentLMIconComponent.init(root);
|
|
33
|
+
FluentLMButtonComponent.init(root);
|
|
34
|
+
FluentLMToggleComponent.init(root);
|
|
35
|
+
FluentLMMessageBarComponent.init(root);
|
|
36
|
+
FluentLMDropdownComponent.init(root);
|
|
37
|
+
|
|
38
|
+
// Tier 2 components
|
|
39
|
+
FluentLMSearchBoxComponent.init(root);
|
|
40
|
+
FluentLMDialogComponent.init(root);
|
|
41
|
+
FluentLMPanelComponent.init(root);
|
|
42
|
+
FluentLMModalComponent.init(root);
|
|
43
|
+
FluentLMCalloutComponent.init(root);
|
|
44
|
+
FluentLMContextMenuComponent.init(root);
|
|
45
|
+
FluentLMNavComponent.init(root);
|
|
46
|
+
FluentLMPivotComponent.init(root);
|
|
47
|
+
FluentLMTooltipComponent.init(root);
|
|
48
|
+
FluentLMCommandBarComponent.init(root);
|
|
49
|
+
|
|
50
|
+
// Tier 3 components
|
|
51
|
+
FluentLMGroupedListComponent.init(root);
|
|
52
|
+
FluentLMRatingComponent.init(root);
|
|
53
|
+
FluentLMFacepileComponent.init(root);
|
|
54
|
+
FluentLMSwatchColorPickerComponent.init(root);
|
|
55
|
+
FluentLMDocumentCardComponent.init(root);
|
|
56
|
+
FluentLMSpinButtonComponent.init(root);
|
|
57
|
+
FluentLMSliderComponent.init(root);
|
|
58
|
+
FluentLMComboBoxComponent.init(root);
|
|
59
|
+
FluentLMTeachingBubbleComponent.init(root);
|
|
60
|
+
FluentLMHoverCardComponent.init(root);
|
|
61
|
+
FluentLMCoachmarkComponent.init(root);
|
|
62
|
+
FluentLMDatePickerComponent.init(root);
|
|
63
|
+
|
|
64
|
+
// Tier 4 components
|
|
65
|
+
FluentLMTagPickerComponent.init(root);
|
|
66
|
+
FluentLMOverflowSetComponent.init(root);
|
|
67
|
+
FluentLMTimePickerComponent.init(root);
|
|
68
|
+
|
|
69
|
+
initialized = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Re-initialize new elements added after page load.
|
|
74
|
+
* Call this after dynamically inserting HTML with flm-* components.
|
|
75
|
+
* Optionally scope to a container element.
|
|
76
|
+
*/
|
|
77
|
+
function refresh(root) {
|
|
78
|
+
FluentLMIconComponent.init(root);
|
|
79
|
+
FluentLMButtonComponent.init(root);
|
|
80
|
+
FluentLMToggleComponent.init(root);
|
|
81
|
+
FluentLMMessageBarComponent.init(root);
|
|
82
|
+
FluentLMDropdownComponent.init(root);
|
|
83
|
+
FluentLMSearchBoxComponent.init(root);
|
|
84
|
+
FluentLMDialogComponent.init(root);
|
|
85
|
+
FluentLMPanelComponent.init(root);
|
|
86
|
+
FluentLMModalComponent.init(root);
|
|
87
|
+
FluentLMCalloutComponent.init(root);
|
|
88
|
+
FluentLMContextMenuComponent.init(root);
|
|
89
|
+
FluentLMNavComponent.init(root);
|
|
90
|
+
FluentLMPivotComponent.init(root);
|
|
91
|
+
FluentLMTooltipComponent.init(root);
|
|
92
|
+
FluentLMCommandBarComponent.init(root);
|
|
93
|
+
|
|
94
|
+
// Tier 3 components
|
|
95
|
+
FluentLMGroupedListComponent.init(root);
|
|
96
|
+
FluentLMRatingComponent.init(root);
|
|
97
|
+
FluentLMFacepileComponent.init(root);
|
|
98
|
+
FluentLMSwatchColorPickerComponent.init(root);
|
|
99
|
+
FluentLMDocumentCardComponent.init(root);
|
|
100
|
+
FluentLMSpinButtonComponent.init(root);
|
|
101
|
+
FluentLMSliderComponent.init(root);
|
|
102
|
+
FluentLMComboBoxComponent.init(root);
|
|
103
|
+
FluentLMTeachingBubbleComponent.init(root);
|
|
104
|
+
FluentLMHoverCardComponent.init(root);
|
|
105
|
+
FluentLMCoachmarkComponent.init(root);
|
|
106
|
+
FluentLMDatePickerComponent.init(root);
|
|
107
|
+
|
|
108
|
+
// Tier 4 components
|
|
109
|
+
FluentLMTagPickerComponent.init(root);
|
|
110
|
+
FluentLMOverflowSetComponent.init(root);
|
|
111
|
+
FluentLMTimePickerComponent.init(root);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Register a custom theme.
|
|
116
|
+
* @param {string} name - Theme identifier (e.g. 'highcontrast')
|
|
117
|
+
* @param {string} className - CSS class applied to <html> (e.g. 'fluent-highcontrast')
|
|
118
|
+
*/
|
|
119
|
+
function registerTheme(name, className) {
|
|
120
|
+
FluentTheme.register(name, className);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set theme by name (e.g. 'light', 'dark', or any registered theme).
|
|
125
|
+
*/
|
|
126
|
+
function setTheme(theme) {
|
|
127
|
+
FluentTheme.setTheme(theme);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Toggle to the next theme. Returns new theme name.
|
|
132
|
+
*/
|
|
133
|
+
function toggleTheme() {
|
|
134
|
+
return FluentTheme.toggle();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get current theme name.
|
|
139
|
+
*/
|
|
140
|
+
function getTheme() {
|
|
141
|
+
return FluentTheme.current();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Auto-initialize on DOMContentLoaded
|
|
145
|
+
if (document.readyState === 'loading') {
|
|
146
|
+
document.addEventListener('DOMContentLoaded', function () { init(); });
|
|
147
|
+
} else {
|
|
148
|
+
// DOM already ready (script loaded with defer or at end of body)
|
|
149
|
+
init();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Expose sub-component APIs for direct use
|
|
153
|
+
return {
|
|
154
|
+
init: init,
|
|
155
|
+
refresh: refresh,
|
|
156
|
+
registerTheme: registerTheme,
|
|
157
|
+
setTheme: setTheme,
|
|
158
|
+
toggleTheme: toggleTheme,
|
|
159
|
+
getTheme: getTheme,
|
|
160
|
+
dialog: typeof FluentLMDialogComponent !== 'undefined' ? FluentLMDialogComponent : null,
|
|
161
|
+
panel: typeof FluentLMPanelComponent !== 'undefined' ? FluentLMPanelComponent : null,
|
|
162
|
+
modal: typeof FluentLMModalComponent !== 'undefined' ? FluentLMModalComponent : null,
|
|
163
|
+
callout: typeof FluentLMCalloutComponent !== 'undefined' ? FluentLMCalloutComponent : null,
|
|
164
|
+
contextMenu: typeof FluentLMContextMenuComponent !== 'undefined' ? FluentLMContextMenuComponent : null,
|
|
165
|
+
comboBox: typeof FluentLMComboBoxComponent !== 'undefined' ? FluentLMComboBoxComponent : null,
|
|
166
|
+
datePicker: typeof FluentLMDatePickerComponent !== 'undefined' ? FluentLMDatePickerComponent : null,
|
|
167
|
+
teachingBubble: typeof FluentLMTeachingBubbleComponent !== 'undefined' ? FluentLMTeachingBubbleComponent : null,
|
|
168
|
+
hoverCard: typeof FluentLMHoverCardComponent !== 'undefined' ? FluentLMHoverCardComponent : null,
|
|
169
|
+
tagPicker: typeof FluentLMTagPickerComponent !== 'undefined' ? FluentLMTagPickerComponent : null,
|
|
170
|
+
overflowSet: typeof FluentLMOverflowSetComponent !== 'undefined' ? FluentLMOverflowSetComponent : null,
|
|
171
|
+
timePicker: typeof FluentLMTimePickerComponent !== 'undefined' ? FluentLMTimePickerComponent : null
|
|
172
|
+
};
|
|
173
|
+
})();
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* FluentLM — Icon Registry
|
|
177
|
+
*
|
|
178
|
+
* SVG path data for commonly used Fluent UI icons.
|
|
179
|
+
* Each entry is a 16x16 viewBox SVG path string.
|
|
180
|
+
* Add icons as needed; the runtime looks them up by name via data-icon="Name".
|
|
181
|
+
*/
|
|
182
|
+
var FluentIcons = (function () {
|
|
183
|
+
'use strict';
|
|
184
|
+
|
|
185
|
+
// All paths drawn for 16x16 viewBox
|
|
186
|
+
var icons = {
|
|
187
|
+
// Navigation / UI
|
|
188
|
+
ChevronDown: 'M3.15 5.65a.5.5 0 0 1 .7 0L8 9.79l4.15-4.14a.5.5 0 0 1 .7.7l-4.5 4.5a.5.5 0 0 1-.7 0l-4.5-4.5a.5.5 0 0 1 0-.7z',
|
|
189
|
+
ChevronUp: 'M3.15 10.35a.5.5 0 0 1 0-.7L7.29 5.5a.5.5 0 0 1 .71 0l4.15 4.15a.5.5 0 0 1-.71.7L7.65 6.56l-3.8 3.79a.5.5 0 0 1-.7 0z',
|
|
190
|
+
ChevronRight: 'M5.65 3.15a.5.5 0 0 1 .7 0l4.5 4.5a.5.5 0 0 1 0 .7l-4.5 4.5a.5.5 0 0 1-.7-.7L9.79 8 5.65 3.85a.5.5 0 0 1 0-.7z',
|
|
191
|
+
ChevronLeft: 'M10.35 3.15a.5.5 0 0 1 0 .7L6.21 8l4.14 4.15a.5.5 0 0 1-.7.7l-4.5-4.5a.5.5 0 0 1 0-.7l4.5-4.5a.5.5 0 0 1 .7 0z',
|
|
192
|
+
Cancel: 'M2.59 2.59a.5.5 0 0 1 .7 0L8 7.29l4.71-4.7a.5.5 0 0 1 .7.7L8.71 8l4.7 4.71a.5.5 0 0 1-.7.7L8 8.71l-4.71 4.7a.5.5 0 0 1-.7-.7L7.29 8 2.59 3.29a.5.5 0 0 1 0-.7z',
|
|
193
|
+
More: 'M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0zm4.5 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0zm4.5 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0z',
|
|
194
|
+
Search: 'M6.5 1a5.5 5.5 0 0 1 4.38 8.82l3.15 3.15a.5.5 0 0 1-.7.7l-3.15-3.14A5.5 5.5 0 1 1 6.5 1zm0 1a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9z',
|
|
195
|
+
Filter: 'M1.5 2h13a.5.5 0 0 1 .37.83L10 8.72V13.5a.5.5 0 0 1-.28.45l-3 1.5A.5.5 0 0 1 6 15V8.72L1.13 2.83A.5.5 0 0 1 1.5 2z',
|
|
196
|
+
|
|
197
|
+
// Actions
|
|
198
|
+
Add: 'M8 1.5a.5.5 0 0 1 .5.5v5.5H14a.5.5 0 0 1 0 1H8.5V14a.5.5 0 0 1-1 0V8.5H2a.5.5 0 0 1 0-1h5.5V2a.5.5 0 0 1 .5-.5z',
|
|
199
|
+
Delete: 'M7 3h2a1 1 0 0 0-2 0zM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.56l-1.22 9.17A2 2 0 0 1 10.24 15H5.76a2 2 0 0 1-1.98-1.83L2.56 4H2a.5.5 0 0 1 0-1h4zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5zm3 0a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5z',
|
|
200
|
+
Edit: 'M12.92 2.08a2.5 2.5 0 0 0-3.54 0L3.15 8.31a1.5 1.5 0 0 0-.4.65l-.9 3.15a.5.5 0 0 0 .62.62l3.15-.9a1.5 1.5 0 0 0 .65-.4l6.23-6.23a2.5 2.5 0 0 0 0-3.54l-.58-.58z',
|
|
201
|
+
Save: 'M2 3a1 1 0 0 1 1-1h8.59a1.5 1.5 0 0 1 1.06.44l1.91 1.91a1.5 1.5 0 0 1 .44 1.06V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm3 9h6V9H5v3zm7 0v-3.5a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.5.5V12H3V3h1v2.5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5V3h.59l1.91 1.91V12z',
|
|
202
|
+
Copy: 'M4 4.09V10a2 2 0 0 0 2 2h5.91A2 2 0 0 1 10 13H6a3 3 0 0 1-3-3V6a2 2 0 0 1 1-1.91zM6 2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z',
|
|
203
|
+
Undo: 'M3.21 5.5H10a3.5 3.5 0 1 1 0 7H8a.5.5 0 0 1 0-1h2a2.5 2.5 0 0 0 0-5H3.21l2.15 2.15a.5.5 0 0 1-.71.7l-3-3a.5.5 0 0 1 0-.7l3-3a.5.5 0 1 1 .7.7L3.22 5.5z',
|
|
204
|
+
Redo: 'M12.79 5.5H6a3.5 3.5 0 1 0 0 7h2a.5.5 0 0 1 0 1H6a4.5 4.5 0 0 1 0-9h6.79l-2.15-2.15a.5.5 0 0 1 .71-.7l3 3a.5.5 0 0 1 0 .7l-3 3a.5.5 0 0 1-.7-.7L12.78 5.5z',
|
|
205
|
+
Share: 'M12 2.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 8a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zM5.5 8a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0z',
|
|
206
|
+
|
|
207
|
+
// Communication
|
|
208
|
+
Mail: 'M2 4.5A1.5 1.5 0 0 1 3.5 3h9A1.5 1.5 0 0 1 14 4.5v7a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 11.5v-7zM3.5 4a.5.5 0 0 0-.5.5v.3l5 3 5-3v-.3a.5.5 0 0 0-.5-.5h-9zM13 5.7 8.17 8.56a.5.5 0 0 1-.34 0L3 5.7v5.8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V5.7z',
|
|
209
|
+
Send: 'M1.72 1.26a.5.5 0 0 1 .53-.05l12.5 6.25a.5.5 0 0 1 0 .9l-12.5 6.24a.5.5 0 0 1-.7-.58L3.27 8.5H7.5a.5.5 0 0 0 0-1H3.27L1.55 1.84a.5.5 0 0 1 .17-.58z',
|
|
210
|
+
|
|
211
|
+
// Status
|
|
212
|
+
Info: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm0 2.5a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5zM8 7a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0v-4A.5.5 0 0 1 8 7z',
|
|
213
|
+
Warning: 'M7.56 1.53a.5.5 0 0 1 .88 0l6.5 12A.5.5 0 0 1 14.5 14h-13a.5.5 0 0 1-.44-.73l6.5-12zM8 5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3A.5.5 0 0 0 8 5zm0 5.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5z',
|
|
214
|
+
ErrorBadge: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm2.85 3.15a.5.5 0 0 1 0 .7L8.71 8l2.14 2.15a.5.5 0 0 1-.7.7L8 8.71l-2.15 2.14a.5.5 0 0 1-.7-.7L7.29 8 5.15 5.85a.5.5 0 0 1 .7-.7L8 7.29l2.15-2.14a.5.5 0 0 1 .7 0z',
|
|
215
|
+
Completed: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm3.35 3.65a.5.5 0 0 1 0 .7l-4 4a.5.5 0 0 1-.7 0l-2-2a.5.5 0 0 1 .7-.7L7 9.29l3.65-3.64a.5.5 0 0 1 .7 0z',
|
|
216
|
+
Blocked: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm3.46 2.54a.5.5 0 0 1 0 .7l-6.22 6.22a.5.5 0 0 1-.7-.7l6.22-6.22a.5.5 0 0 1 .7 0z',
|
|
217
|
+
Checkmark: 'M13.86 3.66a.5.5 0 0 1-.02.7l-7.93 7.48a.5.5 0 0 1-.7-.02L2.16 8.59a.5.5 0 0 1 .72-.7l2.7 2.88 7.58-7.13a.5.5 0 0 1 .7.02z',
|
|
218
|
+
|
|
219
|
+
// Objects
|
|
220
|
+
Settings: 'M7.2 1a.8.8 0 0 0-.79.65l-.28 1.5a5.53 5.53 0 0 0-1.18.68l-1.42-.57a.8.8 0 0 0-.97.33l-.8 1.38a.8.8 0 0 0 .18.99l1.14.94a5.6 5.6 0 0 0 0 1.36l-1.14.94a.8.8 0 0 0-.18.98l.8 1.38a.8.8 0 0 0 .97.34l1.42-.57c.36.27.76.5 1.18.68l.28 1.5a.8.8 0 0 0 .79.66h1.6a.8.8 0 0 0 .79-.65l.28-1.5a5.5 5.5 0 0 0 1.18-.69l1.42.57a.8.8 0 0 0 .97-.33l.8-1.38a.8.8 0 0 0-.18-.99l-1.14-.94a5.5 5.5 0 0 0 0-1.36l1.14-.94a.8.8 0 0 0 .18-.98l-.8-1.38a.8.8 0 0 0-.97-.34l-1.42.57a5.5 5.5 0 0 0-1.18-.68l-.28-1.5A.8.8 0 0 0 8.8 1H7.2zM8 5.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5z',
|
|
221
|
+
Home: 'M7.65 1.15a.5.5 0 0 1 .7 0l6 6a.5.5 0 0 1-.7.7l-.65-.64V13a1 1 0 0 1-1 1h-2.5a.5.5 0 0 1-.5-.5V10H7v3.5a.5.5 0 0 1-.5.5H4a1 1 0 0 1-1-1V7.21l-.65.64a.5.5 0 0 1-.7-.7l6-6z',
|
|
222
|
+
Calendar: 'M4.5 1a.5.5 0 0 1 .5.5V3h6V1.5a.5.5 0 0 1 1 0V3h1.5A1.5 1.5 0 0 1 15 4.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-8A1.5 1.5 0 0 1 2.5 3H4V1.5a.5.5 0 0 1 .5-.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z',
|
|
223
|
+
Person: 'M8 1a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm0 8c3.5 0 6 1.75 6 3.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-.5C2 10.75 4.5 9 8 9z',
|
|
224
|
+
Document: 'M4 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.41a1 1 0 0 0-.3-.7L9.3 1.28a1 1 0 0 0-.71-.29H4zm5 0v3.5a.5.5 0 0 0 .5.5H13',
|
|
225
|
+
Attach: 'M7.73 1.82a3.01 3.01 0 0 1 4.24 0 3.01 3.01 0 0 1 0 4.24l-5.3 5.3a1.75 1.75 0 0 1-2.47-2.47l5.3-5.3a.5.5 0 0 1 .7.7l-5.3 5.3a.75.75 0 1 0 1.07 1.07l5.3-5.3a2.01 2.01 0 0 0-2.84-2.84l-5.3 5.3a3.25 3.25 0 1 0 4.6 4.6l5.3-5.3a.5.5 0 0 1 .7.7l-5.3 5.3a4.25 4.25 0 0 1-6-6l5.3-5.3z',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Return an SVG element for the given icon name.
|
|
230
|
+
* Returns null if the name is not registered.
|
|
231
|
+
*/
|
|
232
|
+
function getSvg(name) {
|
|
233
|
+
var path = icons[name];
|
|
234
|
+
if (!path) return null;
|
|
235
|
+
|
|
236
|
+
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
237
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
238
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
239
|
+
svg.setAttribute('focusable', 'false');
|
|
240
|
+
svg.style.width = '1em';
|
|
241
|
+
svg.style.height = '1em';
|
|
242
|
+
svg.style.fill = 'currentColor';
|
|
243
|
+
|
|
244
|
+
var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
245
|
+
p.setAttribute('d', path);
|
|
246
|
+
svg.appendChild(p);
|
|
247
|
+
return svg;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register a custom icon (or override a built-in one).
|
|
252
|
+
*/
|
|
253
|
+
function register(name, pathData) {
|
|
254
|
+
icons[name] = pathData;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
getSvg: getSvg,
|
|
259
|
+
register: register
|
|
260
|
+
};
|
|
261
|
+
})();
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* FluentLM — Theme Manager
|
|
265
|
+
*
|
|
266
|
+
* Handles theme switching via class on <html>.
|
|
267
|
+
* Respects prefers-color-scheme on first load.
|
|
268
|
+
*
|
|
269
|
+
* Built-in themes: light, dark.
|
|
270
|
+
* Register custom themes with FluentTheme.register(name, className).
|
|
271
|
+
*/
|
|
272
|
+
var FluentTheme = (function () {
|
|
273
|
+
'use strict';
|
|
274
|
+
|
|
275
|
+
var TRANSITION = 'fluent-theme-transition';
|
|
276
|
+
var STORAGE_KEY = 'fluentlm-theme';
|
|
277
|
+
|
|
278
|
+
// Theme registry: name → CSS class
|
|
279
|
+
var themes = {
|
|
280
|
+
light: 'fluentlm',
|
|
281
|
+
dark: 'fluent-dark'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Ordered list of theme names for cycling
|
|
285
|
+
var themeOrder = ['light', 'dark'];
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Register a custom theme.
|
|
289
|
+
* @param {string} name - Theme identifier (used with setTheme/getTheme)
|
|
290
|
+
* @param {string} className - CSS class applied to <html>
|
|
291
|
+
*/
|
|
292
|
+
function register(name, className) {
|
|
293
|
+
if (!name || !className) { return; }
|
|
294
|
+
themes[name] = className;
|
|
295
|
+
if (themeOrder.indexOf(name) === -1) {
|
|
296
|
+
themeOrder.push(name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function allClasses() {
|
|
301
|
+
var classes = [];
|
|
302
|
+
for (var key in themes) {
|
|
303
|
+
if (themes.hasOwnProperty(key)) { classes.push(themes[key]); }
|
|
304
|
+
}
|
|
305
|
+
return classes;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function current() {
|
|
309
|
+
var html = document.documentElement;
|
|
310
|
+
for (var i = themeOrder.length - 1; i >= 0; i--) {
|
|
311
|
+
if (html.classList.contains(themes[themeOrder[i]])) {
|
|
312
|
+
return themeOrder[i];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return 'light';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function setTheme(theme) {
|
|
319
|
+
var className = themes[theme];
|
|
320
|
+
if (!className) { return; }
|
|
321
|
+
|
|
322
|
+
var html = document.documentElement;
|
|
323
|
+
html.classList.add(TRANSITION);
|
|
324
|
+
|
|
325
|
+
var classes = allClasses();
|
|
326
|
+
for (var i = 0; i < classes.length; i++) {
|
|
327
|
+
html.classList.remove(classes[i]);
|
|
328
|
+
}
|
|
329
|
+
html.classList.add(className);
|
|
330
|
+
|
|
331
|
+
try { localStorage.setItem(STORAGE_KEY, theme); } catch (e) { /* noop */ }
|
|
332
|
+
|
|
333
|
+
setTimeout(function () {
|
|
334
|
+
html.classList.remove(TRANSITION);
|
|
335
|
+
}, 250);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function toggle() {
|
|
339
|
+
var idx = themeOrder.indexOf(current());
|
|
340
|
+
var next = themeOrder[(idx + 1) % themeOrder.length];
|
|
341
|
+
setTheme(next);
|
|
342
|
+
return current();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function init() {
|
|
346
|
+
var html = document.documentElement;
|
|
347
|
+
|
|
348
|
+
// 1. Check localStorage
|
|
349
|
+
var stored;
|
|
350
|
+
try { stored = localStorage.getItem(STORAGE_KEY); } catch (e) { /* noop */ }
|
|
351
|
+
|
|
352
|
+
if (stored && themes[stored]) {
|
|
353
|
+
var classes = allClasses();
|
|
354
|
+
for (var i = 0; i < classes.length; i++) {
|
|
355
|
+
html.classList.remove(classes[i]);
|
|
356
|
+
}
|
|
357
|
+
html.classList.add(themes[stored]);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 2. Check OS preference (only if no theme class is already set)
|
|
362
|
+
var classes = allClasses();
|
|
363
|
+
var hasTheme = false;
|
|
364
|
+
for (var i = 0; i < classes.length; i++) {
|
|
365
|
+
if (html.classList.contains(classes[i])) { hasTheme = true; break; }
|
|
366
|
+
}
|
|
367
|
+
if (!hasTheme) {
|
|
368
|
+
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
369
|
+
html.classList.add(prefersDark ? themes.dark : themes.light);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
init: init,
|
|
375
|
+
current: current,
|
|
376
|
+
setTheme: setTheme,
|
|
377
|
+
toggle: toggle,
|
|
378
|
+
register: register
|
|
379
|
+
};
|
|
380
|
+
})();
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Button component JS — handles split buttons and icon injection.
|
|
384
|
+
* Icon injection is delegated to FluentLMIconComponent.
|
|
385
|
+
* This module handles data-split transformation.
|
|
386
|
+
*/
|
|
387
|
+
var FluentLMButtonComponent = (function () {
|
|
388
|
+
'use strict';
|
|
389
|
+
|
|
390
|
+
function init(root) {
|
|
391
|
+
var els = (root || document).querySelectorAll('.flm-button[data-split]');
|
|
392
|
+
for (var i = 0; i < els.length; i++) {
|
|
393
|
+
renderSplit(els[i]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function renderSplit(btn) {
|
|
398
|
+
// Skip if already rendered
|
|
399
|
+
if (btn.getAttribute('data-split-rendered')) return;
|
|
400
|
+
|
|
401
|
+
// Wrap button in a split container
|
|
402
|
+
var wrapper = document.createElement('div');
|
|
403
|
+
wrapper.className = 'flm-button-split';
|
|
404
|
+
if (btn.classList.contains('flm-button--primary')) {
|
|
405
|
+
wrapper.classList.add('flm-button-split--primary');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Create caret button
|
|
409
|
+
var caret = document.createElement('button');
|
|
410
|
+
caret.className = 'flm-button-split-caret';
|
|
411
|
+
caret.setAttribute('aria-label', 'See more options');
|
|
412
|
+
caret.setAttribute('aria-haspopup', 'true');
|
|
413
|
+
caret.type = 'button';
|
|
414
|
+
|
|
415
|
+
var chevron = FluentIcons.getSvg('ChevronDown');
|
|
416
|
+
if (chevron) {
|
|
417
|
+
caret.appendChild(chevron);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Insert wrapper and move button into it
|
|
421
|
+
btn.parentNode.insertBefore(wrapper, btn);
|
|
422
|
+
btn.removeAttribute('data-split');
|
|
423
|
+
wrapper.appendChild(btn);
|
|
424
|
+
wrapper.appendChild(caret);
|
|
425
|
+
|
|
426
|
+
btn.setAttribute('data-split-rendered', 'true');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { init: init };
|
|
430
|
+
})();
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Callout component JS — positions a callout relative to a target element.
|
|
434
|
+
*
|
|
435
|
+
* Usage:
|
|
436
|
+
* FluentLMCalloutComponent.show(calloutEl, targetEl)
|
|
437
|
+
* FluentLMCalloutComponent.hide(calloutEl)
|
|
438
|
+
*
|
|
439
|
+
* Or declarative: <button data-callout-toggle="my-callout">Toggle</button>
|
|
440
|
+
*/
|
|
441
|
+
var FluentLMCalloutComponent = (function () {
|
|
442
|
+
'use strict';
|
|
443
|
+
|
|
444
|
+
function init(root) {
|
|
445
|
+
var doc = root || document;
|
|
446
|
+
|
|
447
|
+
var triggers = doc.querySelectorAll('[data-callout-toggle]');
|
|
448
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
449
|
+
wireTrigger(triggers[i]);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function wireTrigger(btn) {
|
|
454
|
+
if (btn.getAttribute('data-callout-wired')) return;
|
|
455
|
+
btn.addEventListener('click', function (e) {
|
|
456
|
+
var id = btn.getAttribute('data-callout-toggle');
|
|
457
|
+
var callout = document.getElementById(id);
|
|
458
|
+
if (!callout) return;
|
|
459
|
+
|
|
460
|
+
if (callout.classList.contains('flm-callout--visible')) {
|
|
461
|
+
hide(callout);
|
|
462
|
+
} else {
|
|
463
|
+
show(callout, btn);
|
|
464
|
+
}
|
|
465
|
+
e.stopPropagation();
|
|
466
|
+
});
|
|
467
|
+
btn.setAttribute('data-callout-wired', 'true');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function show(callout, target) {
|
|
471
|
+
// Position relative to target
|
|
472
|
+
var rect = target.getBoundingClientRect();
|
|
473
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
474
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
475
|
+
|
|
476
|
+
callout.style.position = 'absolute';
|
|
477
|
+
callout.style.left = rect.left + scrollX + 'px';
|
|
478
|
+
callout.style.top = (rect.bottom + scrollY + 4) + 'px';
|
|
479
|
+
|
|
480
|
+
callout.classList.add('flm-callout--visible');
|
|
481
|
+
callout.classList.add('flm-callout--below');
|
|
482
|
+
|
|
483
|
+
// Check if callout goes off-screen bottom, flip above if needed
|
|
484
|
+
setTimeout(function () {
|
|
485
|
+
var calloutRect = callout.getBoundingClientRect();
|
|
486
|
+
if (calloutRect.bottom > window.innerHeight) {
|
|
487
|
+
callout.classList.remove('flm-callout--below');
|
|
488
|
+
callout.classList.add('flm-callout--above');
|
|
489
|
+
callout.style.top = (rect.top + scrollY - calloutRect.height - 4) + 'px';
|
|
490
|
+
}
|
|
491
|
+
}, 0);
|
|
492
|
+
|
|
493
|
+
// Click outside to dismiss
|
|
494
|
+
var outsideHandler = function (e) {
|
|
495
|
+
if (!callout.contains(e.target) && !target.contains(e.target)) {
|
|
496
|
+
hide(callout);
|
|
497
|
+
document.removeEventListener('click', outsideHandler);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
setTimeout(function () {
|
|
501
|
+
document.addEventListener('click', outsideHandler);
|
|
502
|
+
}, 0);
|
|
503
|
+
callout._outsideHandler = outsideHandler;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function hide(callout) {
|
|
507
|
+
callout.classList.remove('flm-callout--visible', 'flm-callout--below', 'flm-callout--above');
|
|
508
|
+
if (callout._outsideHandler) {
|
|
509
|
+
document.removeEventListener('click', callout._outsideHandler);
|
|
510
|
+
delete callout._outsideHandler;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { init: init, show: show, hide: hide };
|
|
515
|
+
})();
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Coachmark component JS — pulsing beacon that opens a TeachingBubble on click.
|
|
519
|
+
*
|
|
520
|
+
* Usage:
|
|
521
|
+
* <div class="flm-coachmark" data-teachingbubble-toggle="tb1">
|
|
522
|
+
* <div class="flm-coachmark-dot"></div>
|
|
523
|
+
* <div class="flm-coachmark-ring"></div>
|
|
524
|
+
* </div>
|
|
525
|
+
* <div class="flm-teachingbubble" id="tb1">...</div>
|
|
526
|
+
*
|
|
527
|
+
* Uses MutationObserver to auto-hide beacon when the teaching bubble is dismissed.
|
|
528
|
+
*/
|
|
529
|
+
var FluentLMCoachmarkComponent = (function () {
|
|
530
|
+
'use strict';
|
|
531
|
+
|
|
532
|
+
function init(root) {
|
|
533
|
+
var doc = root || document;
|
|
534
|
+
|
|
535
|
+
var coachmarks = doc.querySelectorAll('.flm-coachmark');
|
|
536
|
+
for (var i = 0; i < coachmarks.length; i++) {
|
|
537
|
+
wireCoachmark(coachmarks[i]);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function wireCoachmark(el) {
|
|
542
|
+
if (el.getAttribute('data-coachmark-wired')) return;
|
|
543
|
+
|
|
544
|
+
var bubbleId = el.getAttribute('data-teachingbubble-toggle');
|
|
545
|
+
if (!bubbleId) return;
|
|
546
|
+
|
|
547
|
+
// Click handler to open teaching bubble
|
|
548
|
+
el.addEventListener('click', function () {
|
|
549
|
+
var bubble = document.getElementById(bubbleId);
|
|
550
|
+
if (!bubble) return;
|
|
551
|
+
|
|
552
|
+
if (typeof FluentLMTeachingBubbleComponent !== 'undefined') {
|
|
553
|
+
FluentLMTeachingBubbleComponent.show(bubble, el);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Observe the teaching bubble for dismiss to hide beacon
|
|
558
|
+
var bubble = document.getElementById(bubbleId);
|
|
559
|
+
if (bubble && typeof MutationObserver !== 'undefined') {
|
|
560
|
+
var observer = new MutationObserver(function (mutations) {
|
|
561
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
562
|
+
if (mutations[i].attributeName === 'class') {
|
|
563
|
+
if (!bubble.classList.contains('flm-teachingbubble--visible')) {
|
|
564
|
+
el.classList.add('flm-coachmark--hidden');
|
|
565
|
+
observer.disconnect();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
observer.observe(bubble, { attributes: true, attributeFilter: ['class'] });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
el.setAttribute('data-coachmark-wired', 'true');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { init: init };
|
|
578
|
+
})();
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* ComboBox component JS — filterable dropdown with keyboard navigation.
|
|
582
|
+
*
|
|
583
|
+
* Usage:
|
|
584
|
+
* <div class="flm-combobox">
|
|
585
|
+
* <div class="flm-combobox-wrapper">
|
|
586
|
+
* <input class="flm-combobox-input" placeholder="Select...">
|
|
587
|
+
* <button class="flm-combobox-caret" data-icon="ChevronDown" aria-label="Toggle"></button>
|
|
588
|
+
* </div>
|
|
589
|
+
* <div class="flm-combobox-listbox">
|
|
590
|
+
* <div class="flm-combobox-option" data-value="a">Alpha</div>
|
|
591
|
+
* <div class="flm-combobox-option" data-value="b">Beta</div>
|
|
592
|
+
* </div>
|
|
593
|
+
* </div>
|
|
594
|
+
*
|
|
595
|
+
* Add data-multiselect on .flm-combobox for multi-select mode.
|
|
596
|
+
*/
|
|
597
|
+
var FluentLMComboBoxComponent = (function () {
|
|
598
|
+
'use strict';
|
|
599
|
+
|
|
600
|
+
function init(root) {
|
|
601
|
+
var doc = root || document;
|
|
602
|
+
|
|
603
|
+
var combos = doc.querySelectorAll('.flm-combobox');
|
|
604
|
+
for (var i = 0; i < combos.length; i++) {
|
|
605
|
+
wireCombo(combos[i]);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function wireCombo(el) {
|
|
610
|
+
if (el.getAttribute('data-combobox-wired')) return;
|
|
611
|
+
|
|
612
|
+
var input = el.querySelector('.flm-combobox-input');
|
|
613
|
+
var caret = el.querySelector('.flm-combobox-caret');
|
|
614
|
+
var listbox = el.querySelector('.flm-combobox-listbox');
|
|
615
|
+
|
|
616
|
+
if (!input || !listbox) return;
|
|
617
|
+
|
|
618
|
+
var multiselect = el.hasAttribute('data-multiselect');
|
|
619
|
+
var highlighted = -1;
|
|
620
|
+
|
|
621
|
+
function getOptions() {
|
|
622
|
+
return listbox.querySelectorAll('.flm-combobox-option:not(.flm-combobox-option--disabled):not(.flm-combobox-option--hidden)');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function isOpen() {
|
|
626
|
+
return listbox.classList.contains('flm-combobox-listbox--open');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function open() {
|
|
630
|
+
document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
|
|
631
|
+
listbox.classList.add('flm-combobox-listbox--open');
|
|
632
|
+
highlighted = -1;
|
|
633
|
+
flipIfNeeded();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function close() {
|
|
637
|
+
listbox.classList.remove('flm-combobox-listbox--open', 'flm-combobox-listbox--above');
|
|
638
|
+
highlighted = -1;
|
|
639
|
+
clearHighlight();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function flipIfNeeded() {
|
|
643
|
+
setTimeout(function () {
|
|
644
|
+
var rect = listbox.getBoundingClientRect();
|
|
645
|
+
if (rect.bottom > window.innerHeight) {
|
|
646
|
+
listbox.classList.add('flm-combobox-listbox--above');
|
|
647
|
+
} else {
|
|
648
|
+
listbox.classList.remove('flm-combobox-listbox--above');
|
|
649
|
+
}
|
|
650
|
+
}, 0);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function clearHighlight() {
|
|
654
|
+
var opts = listbox.querySelectorAll('.flm-combobox-option--highlighted');
|
|
655
|
+
for (var i = 0; i < opts.length; i++) {
|
|
656
|
+
opts[i].classList.remove('flm-combobox-option--highlighted');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function setHighlight(idx) {
|
|
661
|
+
clearHighlight();
|
|
662
|
+
var opts = getOptions();
|
|
663
|
+
if (idx >= 0 && idx < opts.length) {
|
|
664
|
+
highlighted = idx;
|
|
665
|
+
opts[idx].classList.add('flm-combobox-option--highlighted');
|
|
666
|
+
opts[idx].scrollIntoView({ block: 'nearest' });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function filterOptions() {
|
|
671
|
+
var text = input.value.toLowerCase();
|
|
672
|
+
var allOpts = listbox.querySelectorAll('.flm-combobox-option');
|
|
673
|
+
for (var i = 0; i < allOpts.length; i++) {
|
|
674
|
+
var optText = allOpts[i].textContent.toLowerCase();
|
|
675
|
+
if (text === '' || optText.indexOf(text) !== -1) {
|
|
676
|
+
allOpts[i].classList.remove('flm-combobox-option--hidden');
|
|
677
|
+
} else {
|
|
678
|
+
allOpts[i].classList.add('flm-combobox-option--hidden');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
highlighted = -1;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function selectOption(opt) {
|
|
685
|
+
var value = opt.getAttribute('data-value') || opt.textContent;
|
|
686
|
+
var text = opt.textContent;
|
|
687
|
+
|
|
688
|
+
if (multiselect) {
|
|
689
|
+
opt.classList.toggle('flm-combobox-option--selected');
|
|
690
|
+
// Build comma-separated value
|
|
691
|
+
var selected = listbox.querySelectorAll('.flm-combobox-option--selected');
|
|
692
|
+
var values = [];
|
|
693
|
+
for (var i = 0; i < selected.length; i++) {
|
|
694
|
+
values.push(selected[i].textContent);
|
|
695
|
+
}
|
|
696
|
+
input.value = values.join(', ');
|
|
697
|
+
} else {
|
|
698
|
+
// Clear previous selection
|
|
699
|
+
var prev = listbox.querySelectorAll('.flm-combobox-option--selected');
|
|
700
|
+
for (var j = 0; j < prev.length; j++) {
|
|
701
|
+
prev[j].classList.remove('flm-combobox-option--selected');
|
|
702
|
+
}
|
|
703
|
+
opt.classList.add('flm-combobox-option--selected');
|
|
704
|
+
input.value = text;
|
|
705
|
+
el.setAttribute('data-value', value);
|
|
706
|
+
close();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Fire change event
|
|
710
|
+
var evt = document.createEvent('Event');
|
|
711
|
+
evt.initEvent('change', true, true);
|
|
712
|
+
input.dispatchEvent(evt);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Input events
|
|
716
|
+
input.addEventListener('focus', function () {
|
|
717
|
+
if (!isOpen()) open();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
input.addEventListener('input', function () {
|
|
721
|
+
if (!isOpen()) open();
|
|
722
|
+
filterOptions();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// Caret toggle
|
|
726
|
+
if (caret) {
|
|
727
|
+
caret.addEventListener('click', function (e) {
|
|
728
|
+
e.stopPropagation();
|
|
729
|
+
if (isOpen()) {
|
|
730
|
+
close();
|
|
731
|
+
} else {
|
|
732
|
+
filterOptions();
|
|
733
|
+
open();
|
|
734
|
+
input.focus();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Keyboard navigation
|
|
740
|
+
input.addEventListener('keydown', function (e) {
|
|
741
|
+
var opts = getOptions();
|
|
742
|
+
var len = opts.length;
|
|
743
|
+
|
|
744
|
+
if (e.key === 'ArrowDown' || e.keyCode === 40) {
|
|
745
|
+
e.preventDefault();
|
|
746
|
+
if (!isOpen()) { open(); filterOptions(); }
|
|
747
|
+
setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
|
|
748
|
+
} else if (e.key === 'ArrowUp' || e.keyCode === 38) {
|
|
749
|
+
e.preventDefault();
|
|
750
|
+
if (!isOpen()) { open(); filterOptions(); }
|
|
751
|
+
setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
|
|
752
|
+
} else if (e.key === 'Enter' || e.keyCode === 13) {
|
|
753
|
+
e.preventDefault();
|
|
754
|
+
if (highlighted >= 0 && highlighted < len) {
|
|
755
|
+
selectOption(opts[highlighted]);
|
|
756
|
+
}
|
|
757
|
+
} else if (e.key === 'Escape' || e.keyCode === 27) {
|
|
758
|
+
close();
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Option click
|
|
763
|
+
listbox.addEventListener('click', function (e) {
|
|
764
|
+
var opt = e.target.closest('.flm-combobox-option');
|
|
765
|
+
if (opt && !opt.classList.contains('flm-combobox-option--disabled')) {
|
|
766
|
+
selectOption(opt);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Click outside
|
|
771
|
+
document.addEventListener('click', function (e) {
|
|
772
|
+
if (!el.contains(e.target) && isOpen()) {
|
|
773
|
+
close();
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Close when another picker opens
|
|
778
|
+
document.addEventListener('flm-dismiss-pickers', function (e) {
|
|
779
|
+
if (e.detail.source !== el && isOpen()) close();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
el.setAttribute('data-combobox-wired', 'true');
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return { init: init };
|
|
786
|
+
})();
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* CommandBar component JS — injects icons for command bar items with data-icon.
|
|
790
|
+
*/
|
|
791
|
+
var FluentLMCommandBarComponent = (function () {
|
|
792
|
+
'use strict';
|
|
793
|
+
|
|
794
|
+
function init(root) {
|
|
795
|
+
var doc = root || document;
|
|
796
|
+
|
|
797
|
+
// Inject icons into commandbar items that have data-icon
|
|
798
|
+
var items = doc.querySelectorAll('.flm-commandbar-item[data-icon]');
|
|
799
|
+
for (var i = 0; i < items.length; i++) {
|
|
800
|
+
FluentLMIconComponent.render(items[i]);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Inject overflow icon
|
|
804
|
+
var overflows = doc.querySelectorAll('.flm-commandbar-overflow');
|
|
805
|
+
for (var j = 0; j < overflows.length; j++) {
|
|
806
|
+
if (!overflows[j].getAttribute('data-icon-rendered')) {
|
|
807
|
+
var svg = FluentIcons.getSvg('More');
|
|
808
|
+
if (svg) {
|
|
809
|
+
overflows[j].innerHTML = '';
|
|
810
|
+
overflows[j].appendChild(svg);
|
|
811
|
+
overflows[j].setAttribute('data-icon-rendered', 'true');
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return { init: init };
|
|
818
|
+
})();
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* ContextualMenu component JS — show/hide, positioning, click-outside dismiss.
|
|
822
|
+
*
|
|
823
|
+
* Usage:
|
|
824
|
+
* FluentLMContextMenuComponent.show(menuEl, targetEl)
|
|
825
|
+
* FluentLMContextMenuComponent.hide(menuEl)
|
|
826
|
+
*
|
|
827
|
+
* Or: <button data-contextmenu-toggle="my-menu">Menu</button>
|
|
828
|
+
* Or: Right-click trigger: <div data-contextmenu="my-menu">Right-click me</div>
|
|
829
|
+
*/
|
|
830
|
+
var FluentLMContextMenuComponent = (function () {
|
|
831
|
+
'use strict';
|
|
832
|
+
|
|
833
|
+
function init(root) {
|
|
834
|
+
var doc = root || document;
|
|
835
|
+
|
|
836
|
+
// Button triggers
|
|
837
|
+
var triggers = doc.querySelectorAll('[data-contextmenu-toggle]');
|
|
838
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
839
|
+
wireTrigger(triggers[i]);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Right-click triggers
|
|
843
|
+
var contextTriggers = doc.querySelectorAll('[data-contextmenu]');
|
|
844
|
+
for (var j = 0; j < contextTriggers.length; j++) {
|
|
845
|
+
wireContextTrigger(contextTriggers[j]);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Wire menu item clicks for dismiss
|
|
849
|
+
var menus = doc.querySelectorAll('.flm-contextmenu');
|
|
850
|
+
for (var k = 0; k < menus.length; k++) {
|
|
851
|
+
wireMenuItems(menus[k]);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function wireTrigger(btn) {
|
|
856
|
+
if (btn.getAttribute('data-cm-wired')) return;
|
|
857
|
+
btn.addEventListener('click', function (e) {
|
|
858
|
+
var id = btn.getAttribute('data-contextmenu-toggle');
|
|
859
|
+
var menu = document.getElementById(id);
|
|
860
|
+
if (!menu) return;
|
|
861
|
+
if (menu.classList.contains('flm-contextmenu--visible')) {
|
|
862
|
+
hide(menu);
|
|
863
|
+
} else {
|
|
864
|
+
show(menu, btn);
|
|
865
|
+
}
|
|
866
|
+
e.stopPropagation();
|
|
867
|
+
});
|
|
868
|
+
btn.setAttribute('data-cm-wired', 'true');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function wireContextTrigger(el) {
|
|
872
|
+
if (el.getAttribute('data-cm-wired')) return;
|
|
873
|
+
el.addEventListener('contextmenu', function (e) {
|
|
874
|
+
e.preventDefault();
|
|
875
|
+
var id = el.getAttribute('data-contextmenu');
|
|
876
|
+
var menu = document.getElementById(id);
|
|
877
|
+
if (!menu) return;
|
|
878
|
+
showAt(menu, e.pageX, e.pageY);
|
|
879
|
+
});
|
|
880
|
+
el.setAttribute('data-cm-wired', 'true');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function wireMenuItems(menu) {
|
|
884
|
+
var items = menu.querySelectorAll('.flm-contextmenu-item');
|
|
885
|
+
for (var i = 0; i < items.length; i++) {
|
|
886
|
+
items[i].addEventListener('click', function () {
|
|
887
|
+
hide(menu);
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function show(menu, target) {
|
|
893
|
+
var rect = target.getBoundingClientRect();
|
|
894
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
895
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
896
|
+
showAt(menu, rect.left + scrollX, rect.bottom + scrollY + 2);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function showAt(menu, x, y) {
|
|
900
|
+
menu.style.position = 'absolute';
|
|
901
|
+
menu.style.left = x + 'px';
|
|
902
|
+
menu.style.top = y + 'px';
|
|
903
|
+
menu.classList.add('flm-contextmenu--visible');
|
|
904
|
+
|
|
905
|
+
// Reposition if off-screen
|
|
906
|
+
setTimeout(function () {
|
|
907
|
+
var menuRect = menu.getBoundingClientRect();
|
|
908
|
+
if (menuRect.right > window.innerWidth) {
|
|
909
|
+
menu.style.left = (x - menuRect.width) + 'px';
|
|
910
|
+
}
|
|
911
|
+
if (menuRect.bottom > window.innerHeight) {
|
|
912
|
+
menu.style.top = (y - menuRect.height) + 'px';
|
|
913
|
+
}
|
|
914
|
+
}, 0);
|
|
915
|
+
|
|
916
|
+
var outsideHandler = function (e) {
|
|
917
|
+
if (!menu.contains(e.target)) {
|
|
918
|
+
hide(menu);
|
|
919
|
+
document.removeEventListener('click', outsideHandler);
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
setTimeout(function () {
|
|
923
|
+
document.addEventListener('click', outsideHandler);
|
|
924
|
+
}, 0);
|
|
925
|
+
menu._outsideHandler = outsideHandler;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function hide(menu) {
|
|
929
|
+
menu.classList.remove('flm-contextmenu--visible');
|
|
930
|
+
if (menu._outsideHandler) {
|
|
931
|
+
document.removeEventListener('click', menu._outsideHandler);
|
|
932
|
+
delete menu._outsideHandler;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return { init: init, show: show, showAt: showAt, hide: hide };
|
|
937
|
+
})();
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* DatePicker component JS — generates calendar grid and handles date selection.
|
|
941
|
+
*
|
|
942
|
+
* Usage:
|
|
943
|
+
* <div class="flm-datepicker">
|
|
944
|
+
* <label class="flm-label" for="dp1">Date</label>
|
|
945
|
+
* <div class="flm-datepicker-wrapper">
|
|
946
|
+
* <input class="flm-datepicker-input" id="dp1" placeholder="MM/DD/YYYY">
|
|
947
|
+
* <button class="flm-datepicker-icon" data-icon="Calendar" aria-label="Open calendar"></button>
|
|
948
|
+
* </div>
|
|
949
|
+
* </div>
|
|
950
|
+
*
|
|
951
|
+
* Attributes on .flm-datepicker:
|
|
952
|
+
* data-min-date="MM/DD/YYYY"
|
|
953
|
+
* data-max-date="MM/DD/YYYY"
|
|
954
|
+
*/
|
|
955
|
+
var FluentLMDatePickerComponent = (function () {
|
|
956
|
+
'use strict';
|
|
957
|
+
|
|
958
|
+
var DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
959
|
+
var MONTHS = [
|
|
960
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
961
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
962
|
+
];
|
|
963
|
+
|
|
964
|
+
function init(root) {
|
|
965
|
+
var doc = root || document;
|
|
966
|
+
|
|
967
|
+
var pickers = doc.querySelectorAll('.flm-datepicker');
|
|
968
|
+
for (var i = 0; i < pickers.length; i++) {
|
|
969
|
+
wirePicker(pickers[i]);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function wirePicker(el) {
|
|
974
|
+
if (el.getAttribute('data-datepicker-wired')) return;
|
|
975
|
+
|
|
976
|
+
var input = el.querySelector('.flm-datepicker-input');
|
|
977
|
+
var iconBtn = el.querySelector('.flm-datepicker-icon');
|
|
978
|
+
if (!input) return;
|
|
979
|
+
|
|
980
|
+
var callout = null;
|
|
981
|
+
var viewYear, viewMonth, selectedDate;
|
|
982
|
+
|
|
983
|
+
// Parse min/max dates
|
|
984
|
+
var minDate = parseDate(el.getAttribute('data-min-date'));
|
|
985
|
+
var maxDate = parseDate(el.getAttribute('data-max-date'));
|
|
986
|
+
|
|
987
|
+
function parseDate(str) {
|
|
988
|
+
if (!str) return null;
|
|
989
|
+
var parts = str.split('/');
|
|
990
|
+
if (parts.length !== 3) return null;
|
|
991
|
+
return new Date(parseInt(parts[2], 10), parseInt(parts[0], 10) - 1, parseInt(parts[1], 10));
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function formatDate(d) {
|
|
995
|
+
var mm = (d.getMonth() + 1);
|
|
996
|
+
var dd = d.getDate();
|
|
997
|
+
var yyyy = d.getFullYear();
|
|
998
|
+
return (mm < 10 ? '0' : '') + mm + '/' + (dd < 10 ? '0' : '') + dd + '/' + yyyy;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function ensureCallout() {
|
|
1002
|
+
if (callout) return callout;
|
|
1003
|
+
|
|
1004
|
+
callout = document.createElement('div');
|
|
1005
|
+
callout.className = 'flm-datepicker-callout';
|
|
1006
|
+
el.appendChild(callout);
|
|
1007
|
+
return callout;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function isOpen() {
|
|
1011
|
+
return callout && callout.classList.contains('flm-datepicker-callout--open');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function open() {
|
|
1015
|
+
document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
|
|
1016
|
+
ensureCallout();
|
|
1017
|
+
var today = new Date();
|
|
1018
|
+
viewYear = selectedDate ? selectedDate.getFullYear() : today.getFullYear();
|
|
1019
|
+
viewMonth = selectedDate ? selectedDate.getMonth() : today.getMonth();
|
|
1020
|
+
renderCalendar();
|
|
1021
|
+
callout.classList.add('flm-datepicker-callout--open');
|
|
1022
|
+
flipIfNeeded();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function close() {
|
|
1026
|
+
if (callout) {
|
|
1027
|
+
callout.classList.remove('flm-datepicker-callout--open', 'flm-datepicker-callout--above');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function flipIfNeeded() {
|
|
1032
|
+
setTimeout(function () {
|
|
1033
|
+
if (!callout) return;
|
|
1034
|
+
var rect = callout.getBoundingClientRect();
|
|
1035
|
+
if (rect.bottom > window.innerHeight) {
|
|
1036
|
+
callout.classList.add('flm-datepicker-callout--above');
|
|
1037
|
+
} else {
|
|
1038
|
+
callout.classList.remove('flm-datepicker-callout--above');
|
|
1039
|
+
}
|
|
1040
|
+
}, 0);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function renderCalendar() {
|
|
1044
|
+
var html = '';
|
|
1045
|
+
|
|
1046
|
+
// Navigation
|
|
1047
|
+
html += '<div class="flm-datepicker-nav">';
|
|
1048
|
+
html += '<button class="flm-datepicker-nav-btn flm-datepicker-prev" data-icon="ChevronLeft" aria-label="Previous month"></button>';
|
|
1049
|
+
html += '<span class="flm-datepicker-month">' + MONTHS[viewMonth] + ' ' + viewYear + '</span>';
|
|
1050
|
+
html += '<button class="flm-datepicker-nav-btn flm-datepicker-next" data-icon="ChevronRight" aria-label="Next month"></button>';
|
|
1051
|
+
html += '</div>';
|
|
1052
|
+
|
|
1053
|
+
// Weekday headers
|
|
1054
|
+
html += '<div class="flm-datepicker-weekdays">';
|
|
1055
|
+
for (var d = 0; d < 7; d++) {
|
|
1056
|
+
html += '<span class="flm-datepicker-weekday">' + DAYS[d] + '</span>';
|
|
1057
|
+
}
|
|
1058
|
+
html += '</div>';
|
|
1059
|
+
|
|
1060
|
+
// Calendar grid (42 cells)
|
|
1061
|
+
html += '<div class="flm-datepicker-grid">';
|
|
1062
|
+
|
|
1063
|
+
var firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
|
1064
|
+
var daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
1065
|
+
var daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
|
|
1066
|
+
var today = new Date();
|
|
1067
|
+
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
1068
|
+
|
|
1069
|
+
var cellDate;
|
|
1070
|
+
for (var i = 0; i < 42; i++) {
|
|
1071
|
+
var dayNum;
|
|
1072
|
+
var isOutside = false;
|
|
1073
|
+
var cellYear = viewYear;
|
|
1074
|
+
var cellMonth = viewMonth;
|
|
1075
|
+
|
|
1076
|
+
if (i < firstDay) {
|
|
1077
|
+
// Previous month
|
|
1078
|
+
dayNum = daysInPrev - firstDay + i + 1;
|
|
1079
|
+
isOutside = true;
|
|
1080
|
+
cellMonth = viewMonth - 1;
|
|
1081
|
+
if (cellMonth < 0) { cellMonth = 11; cellYear--; }
|
|
1082
|
+
} else if (i - firstDay >= daysInMonth) {
|
|
1083
|
+
// Next month
|
|
1084
|
+
dayNum = i - firstDay - daysInMonth + 1;
|
|
1085
|
+
isOutside = true;
|
|
1086
|
+
cellMonth = viewMonth + 1;
|
|
1087
|
+
if (cellMonth > 11) { cellMonth = 0; cellYear++; }
|
|
1088
|
+
} else {
|
|
1089
|
+
dayNum = i - firstDay + 1;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
cellDate = new Date(cellYear, cellMonth, dayNum);
|
|
1093
|
+
|
|
1094
|
+
var classes = 'flm-datepicker-day';
|
|
1095
|
+
if (isOutside) classes += ' flm-datepicker-day--outside';
|
|
1096
|
+
if (cellDate.getTime() === today.getTime()) classes += ' flm-datepicker-day--today';
|
|
1097
|
+
if (selectedDate && cellDate.getTime() === selectedDate.getTime()) classes += ' flm-datepicker-day--selected';
|
|
1098
|
+
|
|
1099
|
+
var disabled = false;
|
|
1100
|
+
if (minDate && cellDate < minDate) disabled = true;
|
|
1101
|
+
if (maxDate && cellDate > maxDate) disabled = true;
|
|
1102
|
+
if (disabled) classes += ' flm-datepicker-day--disabled';
|
|
1103
|
+
|
|
1104
|
+
html += '<button class="' + classes + '" data-date="' + cellYear + '-' + cellMonth + '-' + dayNum + '"' +
|
|
1105
|
+
(disabled ? ' disabled' : '') + '>' + dayNum + '</button>';
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
html += '</div>';
|
|
1109
|
+
callout.innerHTML = html;
|
|
1110
|
+
|
|
1111
|
+
// Wire navigation buttons
|
|
1112
|
+
var prevBtn = callout.querySelector('.flm-datepicker-prev');
|
|
1113
|
+
var nextBtn = callout.querySelector('.flm-datepicker-next');
|
|
1114
|
+
|
|
1115
|
+
if (prevBtn) {
|
|
1116
|
+
// Disable prev button if at min date boundary
|
|
1117
|
+
if (minDate && viewYear === minDate.getFullYear() && viewMonth === minDate.getMonth()) {
|
|
1118
|
+
prevBtn.disabled = true;
|
|
1119
|
+
}
|
|
1120
|
+
prevBtn.addEventListener('click', function (e) {
|
|
1121
|
+
e.stopPropagation();
|
|
1122
|
+
var newMonth = viewMonth - 1;
|
|
1123
|
+
var newYear = viewYear;
|
|
1124
|
+
if (newMonth < 0) { newMonth = 11; newYear--; }
|
|
1125
|
+
// Don't navigate before the month containing minDate
|
|
1126
|
+
if (minDate) {
|
|
1127
|
+
var minMonth = minDate.getFullYear() * 12 + minDate.getMonth();
|
|
1128
|
+
var targetMonth = newYear * 12 + newMonth;
|
|
1129
|
+
if (targetMonth < minMonth) return;
|
|
1130
|
+
}
|
|
1131
|
+
viewMonth = newMonth;
|
|
1132
|
+
viewYear = newYear;
|
|
1133
|
+
renderCalendar();
|
|
1134
|
+
if (typeof FluentLMIconComponent !== 'undefined') {
|
|
1135
|
+
FluentLMIconComponent.init(callout);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (nextBtn) {
|
|
1140
|
+
// Disable next button if at max date boundary
|
|
1141
|
+
if (maxDate && viewYear === maxDate.getFullYear() && viewMonth === maxDate.getMonth()) {
|
|
1142
|
+
nextBtn.disabled = true;
|
|
1143
|
+
}
|
|
1144
|
+
nextBtn.addEventListener('click', function (e) {
|
|
1145
|
+
e.stopPropagation();
|
|
1146
|
+
var newMonth = viewMonth + 1;
|
|
1147
|
+
var newYear = viewYear;
|
|
1148
|
+
if (newMonth > 11) { newMonth = 0; newYear++; }
|
|
1149
|
+
// Don't navigate past the month containing maxDate
|
|
1150
|
+
if (maxDate) {
|
|
1151
|
+
var maxMonth = maxDate.getFullYear() * 12 + maxDate.getMonth();
|
|
1152
|
+
var targetMonth = newYear * 12 + newMonth;
|
|
1153
|
+
if (targetMonth > maxMonth) return;
|
|
1154
|
+
}
|
|
1155
|
+
viewMonth = newMonth;
|
|
1156
|
+
viewYear = newYear;
|
|
1157
|
+
renderCalendar();
|
|
1158
|
+
if (typeof FluentLMIconComponent !== 'undefined') {
|
|
1159
|
+
FluentLMIconComponent.init(callout);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Wire day clicks
|
|
1165
|
+
var days = callout.querySelectorAll('.flm-datepicker-day');
|
|
1166
|
+
for (var j = 0; j < days.length; j++) {
|
|
1167
|
+
days[j].addEventListener('click', function (e) {
|
|
1168
|
+
e.stopPropagation();
|
|
1169
|
+
var parts = this.getAttribute('data-date').split('-');
|
|
1170
|
+
selectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10));
|
|
1171
|
+
input.value = formatDate(selectedDate);
|
|
1172
|
+
close();
|
|
1173
|
+
|
|
1174
|
+
// Fire change event
|
|
1175
|
+
var evt = document.createEvent('Event');
|
|
1176
|
+
evt.initEvent('change', true, true);
|
|
1177
|
+
input.dispatchEvent(evt);
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Init icons for nav buttons
|
|
1182
|
+
if (typeof FluentLMIconComponent !== 'undefined') {
|
|
1183
|
+
FluentLMIconComponent.init(callout);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Toggle on icon button click
|
|
1188
|
+
if (iconBtn) {
|
|
1189
|
+
iconBtn.addEventListener('click', function (e) {
|
|
1190
|
+
e.stopPropagation();
|
|
1191
|
+
if (isOpen()) {
|
|
1192
|
+
close();
|
|
1193
|
+
} else {
|
|
1194
|
+
open();
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Open on input focus
|
|
1200
|
+
input.addEventListener('focus', function () {
|
|
1201
|
+
if (!isOpen()) open();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// Click outside to dismiss
|
|
1205
|
+
document.addEventListener('click', function (e) {
|
|
1206
|
+
if (!el.contains(e.target) && isOpen()) {
|
|
1207
|
+
close();
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Keyboard: Escape closes
|
|
1212
|
+
input.addEventListener('keydown', function (e) {
|
|
1213
|
+
if (e.key === 'Escape' || e.keyCode === 27) {
|
|
1214
|
+
close();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
// Close when another picker opens
|
|
1219
|
+
document.addEventListener('flm-dismiss-pickers', function (e) {
|
|
1220
|
+
if (e.detail.source !== el && isOpen()) close();
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
el.setAttribute('data-datepicker-wired', 'true');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return { init: init };
|
|
1227
|
+
})();
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Dialog component JS — open/close, Escape key, overlay click dismiss.
|
|
1231
|
+
*
|
|
1232
|
+
* Usage:
|
|
1233
|
+
* FluentLMDialogComponent.open('my-dialog')
|
|
1234
|
+
* FluentLMDialogComponent.close('my-dialog')
|
|
1235
|
+
*
|
|
1236
|
+
* Or wire a trigger button:
|
|
1237
|
+
* <button data-dialog-open="my-dialog">Open</button>
|
|
1238
|
+
*/
|
|
1239
|
+
var FluentLMDialogComponent = (function () {
|
|
1240
|
+
'use strict';
|
|
1241
|
+
|
|
1242
|
+
function init(root) {
|
|
1243
|
+
var doc = root || document;
|
|
1244
|
+
|
|
1245
|
+
// Wire trigger buttons
|
|
1246
|
+
var triggers = doc.querySelectorAll('[data-dialog-open]');
|
|
1247
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
1248
|
+
wireOpen(triggers[i]);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Wire close buttons inside dialogs
|
|
1252
|
+
var closeBtns = doc.querySelectorAll('.flm-dialog-close, [data-dialog-close]');
|
|
1253
|
+
for (var j = 0; j < closeBtns.length; j++) {
|
|
1254
|
+
wireClose(closeBtns[j]);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Wire overlay click-to-dismiss (light dismiss)
|
|
1258
|
+
var overlays = doc.querySelectorAll('.flm-dialog-overlay[data-light-dismiss]');
|
|
1259
|
+
for (var k = 0; k < overlays.length; k++) {
|
|
1260
|
+
wireOverlayDismiss(overlays[k]);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function wireOpen(btn) {
|
|
1265
|
+
if (btn.getAttribute('data-dialog-wired')) return;
|
|
1266
|
+
btn.addEventListener('click', function () {
|
|
1267
|
+
var id = btn.getAttribute('data-dialog-open');
|
|
1268
|
+
open(id);
|
|
1269
|
+
});
|
|
1270
|
+
btn.setAttribute('data-dialog-wired', 'true');
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function wireClose(btn) {
|
|
1274
|
+
if (btn.getAttribute('data-dialog-wired')) return;
|
|
1275
|
+
btn.addEventListener('click', function () {
|
|
1276
|
+
var overlay = btn.closest('.flm-dialog-overlay');
|
|
1277
|
+
if (overlay) {
|
|
1278
|
+
closeOverlay(overlay);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
btn.setAttribute('data-dialog-wired', 'true');
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function wireOverlayDismiss(overlay) {
|
|
1285
|
+
if (overlay.getAttribute('data-dismiss-wired')) return;
|
|
1286
|
+
overlay.addEventListener('click', function (e) {
|
|
1287
|
+
if (e.target === overlay) {
|
|
1288
|
+
closeOverlay(overlay);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
overlay.setAttribute('data-dismiss-wired', 'true');
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function open(id) {
|
|
1295
|
+
var overlay = document.getElementById(id);
|
|
1296
|
+
if (!overlay) return;
|
|
1297
|
+
overlay.classList.add('flm-dialog-overlay--open');
|
|
1298
|
+
document.body.style.overflow = 'hidden';
|
|
1299
|
+
|
|
1300
|
+
// Escape key listener
|
|
1301
|
+
var escHandler = function (e) {
|
|
1302
|
+
if (e.key === 'Escape') {
|
|
1303
|
+
closeOverlay(overlay);
|
|
1304
|
+
document.removeEventListener('keydown', escHandler);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
document.addEventListener('keydown', escHandler);
|
|
1308
|
+
overlay._escHandler = escHandler;
|
|
1309
|
+
|
|
1310
|
+
// Focus first focusable element
|
|
1311
|
+
setTimeout(function () {
|
|
1312
|
+
var focusable = overlay.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
1313
|
+
if (focusable) focusable.focus();
|
|
1314
|
+
}, 50);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function close(id) {
|
|
1318
|
+
var overlay = document.getElementById(id);
|
|
1319
|
+
if (overlay) closeOverlay(overlay);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function closeOverlay(overlay) {
|
|
1323
|
+
overlay.classList.remove('flm-dialog-overlay--open');
|
|
1324
|
+
document.body.style.overflow = '';
|
|
1325
|
+
if (overlay._escHandler) {
|
|
1326
|
+
document.removeEventListener('keydown', overlay._escHandler);
|
|
1327
|
+
delete overlay._escHandler;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return { init: init, open: open, close: close };
|
|
1332
|
+
})();
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* DocumentCard component JS — stub for icon injection via FluentLMIconComponent.
|
|
1336
|
+
*/
|
|
1337
|
+
var FluentLMDocumentCardComponent = (function () {
|
|
1338
|
+
'use strict';
|
|
1339
|
+
|
|
1340
|
+
function init(root) {
|
|
1341
|
+
var doc = root || document;
|
|
1342
|
+
|
|
1343
|
+
var cards = doc.querySelectorAll('.flm-documentcard');
|
|
1344
|
+
for (var i = 0; i < cards.length; i++) {
|
|
1345
|
+
wireCard(cards[i]);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function wireCard(card) {
|
|
1350
|
+
if (card.getAttribute('data-documentcard-wired')) return;
|
|
1351
|
+
|
|
1352
|
+
// Icons are handled by FluentLMIconComponent.
|
|
1353
|
+
// Wire up any action buttons for focus management.
|
|
1354
|
+
var actions = card.querySelectorAll('.flm-documentcard-actions .flm-button');
|
|
1355
|
+
for (var i = 0; i < actions.length; i++) {
|
|
1356
|
+
actions[i].addEventListener('click', function (e) {
|
|
1357
|
+
e.stopPropagation();
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
card.setAttribute('data-documentcard-wired', 'true');
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return { init: init };
|
|
1365
|
+
})();
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Dropdown component JS — custom dropdown with keyboard navigation.
|
|
1369
|
+
*
|
|
1370
|
+
* Usage:
|
|
1371
|
+
* <div class="flm-dropdown">
|
|
1372
|
+
* <label class="flm-label" for="dd1">Country</label>
|
|
1373
|
+
* <button class="flm-dropdown-trigger" id="dd1">
|
|
1374
|
+
* <span class="flm-dropdown-title flm-dropdown-title--placeholder">Select…</span>
|
|
1375
|
+
* <span class="flm-dropdown-caret" data-icon="ChevronDown"></span>
|
|
1376
|
+
* </button>
|
|
1377
|
+
* <div class="flm-dropdown-listbox">
|
|
1378
|
+
* <div class="flm-dropdown-option" data-value="us">United States</div>
|
|
1379
|
+
* <div class="flm-dropdown-option" data-value="gb">United Kingdom</div>
|
|
1380
|
+
* </div>
|
|
1381
|
+
* </div>
|
|
1382
|
+
*
|
|
1383
|
+
* Sets data-value on the root .flm-dropdown when an option is selected.
|
|
1384
|
+
* A hidden <input class="flm-dropdown-value"> is updated if present.
|
|
1385
|
+
*/
|
|
1386
|
+
var FluentLMDropdownComponent = (function () {
|
|
1387
|
+
'use strict';
|
|
1388
|
+
|
|
1389
|
+
function init(root) {
|
|
1390
|
+
var doc = root || document;
|
|
1391
|
+
|
|
1392
|
+
var dropdowns = doc.querySelectorAll('.flm-dropdown');
|
|
1393
|
+
for (var i = 0; i < dropdowns.length; i++) {
|
|
1394
|
+
wireDropdown(dropdowns[i]);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function wireDropdown(el) {
|
|
1399
|
+
if (el.getAttribute('data-dropdown-wired')) return;
|
|
1400
|
+
if (el.classList.contains('flm-dropdown--disabled')) return;
|
|
1401
|
+
|
|
1402
|
+
var trigger = el.querySelector('.flm-dropdown-trigger');
|
|
1403
|
+
var listbox = el.querySelector('.flm-dropdown-listbox');
|
|
1404
|
+
|
|
1405
|
+
if (!trigger || !listbox) return;
|
|
1406
|
+
|
|
1407
|
+
var titleEl = trigger.querySelector('.flm-dropdown-title');
|
|
1408
|
+
var hiddenInput = el.querySelector('.flm-dropdown-value');
|
|
1409
|
+
var placeholder = titleEl ? titleEl.textContent : '';
|
|
1410
|
+
var highlighted = -1;
|
|
1411
|
+
|
|
1412
|
+
function getOptions() {
|
|
1413
|
+
return listbox.querySelectorAll('.flm-dropdown-option:not(.flm-dropdown-option--disabled)');
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function isOpen() {
|
|
1417
|
+
return listbox.classList.contains('flm-dropdown-listbox--open');
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function open() {
|
|
1421
|
+
document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
|
|
1422
|
+
listbox.classList.add('flm-dropdown-listbox--open');
|
|
1423
|
+
highlighted = -1;
|
|
1424
|
+
|
|
1425
|
+
// Highlight current selection
|
|
1426
|
+
var opts = getOptions();
|
|
1427
|
+
for (var i = 0; i < opts.length; i++) {
|
|
1428
|
+
if (opts[i].classList.contains('flm-dropdown-option--selected')) {
|
|
1429
|
+
highlighted = i;
|
|
1430
|
+
opts[i].classList.add('flm-dropdown-option--highlighted');
|
|
1431
|
+
opts[i].scrollIntoView({ block: 'nearest' });
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
flipIfNeeded();
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function close() {
|
|
1440
|
+
listbox.classList.remove('flm-dropdown-listbox--open', 'flm-dropdown-listbox--above');
|
|
1441
|
+
highlighted = -1;
|
|
1442
|
+
clearHighlight();
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function flipIfNeeded() {
|
|
1446
|
+
setTimeout(function () {
|
|
1447
|
+
var rect = listbox.getBoundingClientRect();
|
|
1448
|
+
if (rect.bottom > window.innerHeight) {
|
|
1449
|
+
listbox.classList.add('flm-dropdown-listbox--above');
|
|
1450
|
+
} else {
|
|
1451
|
+
listbox.classList.remove('flm-dropdown-listbox--above');
|
|
1452
|
+
}
|
|
1453
|
+
}, 0);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function clearHighlight() {
|
|
1457
|
+
var opts = listbox.querySelectorAll('.flm-dropdown-option--highlighted');
|
|
1458
|
+
for (var i = 0; i < opts.length; i++) {
|
|
1459
|
+
opts[i].classList.remove('flm-dropdown-option--highlighted');
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function setHighlight(idx) {
|
|
1464
|
+
clearHighlight();
|
|
1465
|
+
var opts = getOptions();
|
|
1466
|
+
if (idx >= 0 && idx < opts.length) {
|
|
1467
|
+
highlighted = idx;
|
|
1468
|
+
opts[idx].classList.add('flm-dropdown-option--highlighted');
|
|
1469
|
+
opts[idx].scrollIntoView({ block: 'nearest' });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function selectOption(opt) {
|
|
1474
|
+
// Clear previous
|
|
1475
|
+
var prev = listbox.querySelectorAll('.flm-dropdown-option--selected');
|
|
1476
|
+
for (var i = 0; i < prev.length; i++) {
|
|
1477
|
+
prev[i].classList.remove('flm-dropdown-option--selected');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
opt.classList.add('flm-dropdown-option--selected');
|
|
1481
|
+
|
|
1482
|
+
var value = opt.getAttribute('data-value') || opt.textContent.trim();
|
|
1483
|
+
var text = opt.textContent.trim();
|
|
1484
|
+
|
|
1485
|
+
if (titleEl) {
|
|
1486
|
+
titleEl.textContent = text;
|
|
1487
|
+
titleEl.classList.remove('flm-dropdown-title--placeholder');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
el.setAttribute('data-value', value);
|
|
1491
|
+
|
|
1492
|
+
if (hiddenInput) {
|
|
1493
|
+
hiddenInput.value = value;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
close();
|
|
1497
|
+
|
|
1498
|
+
// Fire change event
|
|
1499
|
+
var evt = document.createEvent('Event');
|
|
1500
|
+
evt.initEvent('change', true, true);
|
|
1501
|
+
el.dispatchEvent(evt);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Toggle on click
|
|
1505
|
+
trigger.addEventListener('click', function (e) {
|
|
1506
|
+
e.stopPropagation();
|
|
1507
|
+
if (isOpen()) {
|
|
1508
|
+
close();
|
|
1509
|
+
} else {
|
|
1510
|
+
open();
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// Keyboard navigation
|
|
1515
|
+
trigger.addEventListener('keydown', function (e) {
|
|
1516
|
+
var opts = getOptions();
|
|
1517
|
+
var len = opts.length;
|
|
1518
|
+
|
|
1519
|
+
if (e.key === 'ArrowDown' || e.keyCode === 40) {
|
|
1520
|
+
e.preventDefault();
|
|
1521
|
+
if (!isOpen()) open();
|
|
1522
|
+
setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
|
|
1523
|
+
} else if (e.key === 'ArrowUp' || e.keyCode === 38) {
|
|
1524
|
+
e.preventDefault();
|
|
1525
|
+
if (!isOpen()) open();
|
|
1526
|
+
setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
|
|
1527
|
+
} else if (e.key === 'Enter' || e.keyCode === 13 || e.key === ' ' || e.keyCode === 32) {
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
if (!isOpen()) {
|
|
1530
|
+
open();
|
|
1531
|
+
} else if (highlighted >= 0 && highlighted < len) {
|
|
1532
|
+
selectOption(opts[highlighted]);
|
|
1533
|
+
}
|
|
1534
|
+
} else if (e.key === 'Escape' || e.keyCode === 27) {
|
|
1535
|
+
close();
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
// Option click
|
|
1540
|
+
listbox.addEventListener('click', function (e) {
|
|
1541
|
+
var opt = e.target.closest('.flm-dropdown-option');
|
|
1542
|
+
if (opt && !opt.classList.contains('flm-dropdown-option--disabled')) {
|
|
1543
|
+
selectOption(opt);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// Click outside
|
|
1548
|
+
document.addEventListener('click', function (e) {
|
|
1549
|
+
if (!el.contains(e.target) && isOpen()) {
|
|
1550
|
+
close();
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// Close when another picker opens
|
|
1555
|
+
document.addEventListener('flm-dismiss-pickers', function (e) {
|
|
1556
|
+
if (e.detail.source !== el && isOpen()) close();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
el.setAttribute('data-dropdown-wired', 'true');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return { init: init };
|
|
1563
|
+
})();
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* Facepile component JS — hides excess coins and injects +N overflow chip.
|
|
1567
|
+
*/
|
|
1568
|
+
var FluentLMFacepileComponent = (function () {
|
|
1569
|
+
'use strict';
|
|
1570
|
+
|
|
1571
|
+
function init(root) {
|
|
1572
|
+
var doc = root || document;
|
|
1573
|
+
|
|
1574
|
+
var facepiles = doc.querySelectorAll('.flm-facepile');
|
|
1575
|
+
for (var i = 0; i < facepiles.length; i++) {
|
|
1576
|
+
wireFacepile(facepiles[i]);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function wireFacepile(el) {
|
|
1581
|
+
if (el.getAttribute('data-facepile-wired')) return;
|
|
1582
|
+
|
|
1583
|
+
var max = parseInt(el.getAttribute('data-max'), 10);
|
|
1584
|
+
if (!max || isNaN(max)) {
|
|
1585
|
+
el.setAttribute('data-facepile-wired', 'true');
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
var members = el.querySelectorAll('.flm-facepile-member');
|
|
1590
|
+
if (members.length <= max) {
|
|
1591
|
+
el.setAttribute('data-facepile-wired', 'true');
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
var overflow = members.length - max;
|
|
1596
|
+
|
|
1597
|
+
// Hide excess members
|
|
1598
|
+
for (var i = max; i < members.length; i++) {
|
|
1599
|
+
members[i].style.display = 'none';
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Remove existing overflow chip if any
|
|
1603
|
+
var existing = el.querySelector('.flm-facepile-overflow');
|
|
1604
|
+
if (existing) {
|
|
1605
|
+
existing.parentNode.removeChild(existing);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Inject overflow chip
|
|
1609
|
+
var chip = document.createElement('span');
|
|
1610
|
+
chip.className = 'flm-facepile-overflow';
|
|
1611
|
+
chip.textContent = '+' + overflow;
|
|
1612
|
+
|
|
1613
|
+
// Insert after last visible member
|
|
1614
|
+
var lastVisible = members[max - 1];
|
|
1615
|
+
if (lastVisible.nextSibling) {
|
|
1616
|
+
el.insertBefore(chip, lastVisible.nextSibling);
|
|
1617
|
+
} else {
|
|
1618
|
+
el.appendChild(chip);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
el.setAttribute('data-facepile-wired', 'true');
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
return { init: init };
|
|
1625
|
+
})();
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* GroupedList component JS — collapsible groups with chevron rotation.
|
|
1629
|
+
*/
|
|
1630
|
+
var FluentLMGroupedListComponent = (function () {
|
|
1631
|
+
'use strict';
|
|
1632
|
+
|
|
1633
|
+
function init(root) {
|
|
1634
|
+
var doc = root || document;
|
|
1635
|
+
|
|
1636
|
+
var headers = doc.querySelectorAll('.flm-groupedlist-header');
|
|
1637
|
+
for (var i = 0; i < headers.length; i++) {
|
|
1638
|
+
wireHeader(headers[i]);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function wireHeader(header) {
|
|
1643
|
+
if (header.getAttribute('data-groupedlist-wired')) return;
|
|
1644
|
+
|
|
1645
|
+
header.addEventListener('click', function () {
|
|
1646
|
+
var items = header.nextElementSibling;
|
|
1647
|
+
if (!items || !items.classList.contains('flm-groupedlist-items')) return;
|
|
1648
|
+
|
|
1649
|
+
var chevron = header.querySelector('.flm-groupedlist-chevron');
|
|
1650
|
+
var collapsed = items.classList.contains('flm-groupedlist-items--collapsed');
|
|
1651
|
+
|
|
1652
|
+
if (collapsed) {
|
|
1653
|
+
items.classList.remove('flm-groupedlist-items--collapsed');
|
|
1654
|
+
if (chevron) chevron.classList.add('flm-groupedlist-chevron--expanded');
|
|
1655
|
+
} else {
|
|
1656
|
+
items.classList.add('flm-groupedlist-items--collapsed');
|
|
1657
|
+
if (chevron) chevron.classList.remove('flm-groupedlist-chevron--expanded');
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
header.setAttribute('data-groupedlist-wired', 'true');
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
return { init: init };
|
|
1665
|
+
})();
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* HoverCard component JS — two-phase hover: compact after 500ms, expanded after 1500ms.
|
|
1669
|
+
*
|
|
1670
|
+
* Usage:
|
|
1671
|
+
* <span class="flm-hovercard-host" data-hovercard-id="hc1">Hover over me</span>
|
|
1672
|
+
* <div class="flm-hovercard" id="hc1">
|
|
1673
|
+
* <div class="flm-hovercard-compact">Compact content</div>
|
|
1674
|
+
* <div class="flm-hovercard-expanded">Expanded content</div>
|
|
1675
|
+
* </div>
|
|
1676
|
+
*/
|
|
1677
|
+
var FluentLMHoverCardComponent = (function () {
|
|
1678
|
+
'use strict';
|
|
1679
|
+
|
|
1680
|
+
function init(root) {
|
|
1681
|
+
var doc = root || document;
|
|
1682
|
+
|
|
1683
|
+
var hosts = doc.querySelectorAll('[data-hovercard-id]');
|
|
1684
|
+
for (var i = 0; i < hosts.length; i++) {
|
|
1685
|
+
wireHost(hosts[i]);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function wireHost(host) {
|
|
1690
|
+
if (host.getAttribute('data-hovercard-wired')) return;
|
|
1691
|
+
|
|
1692
|
+
var showTimer = null;
|
|
1693
|
+
var expandTimer = null;
|
|
1694
|
+
var card = null;
|
|
1695
|
+
|
|
1696
|
+
function getCard() {
|
|
1697
|
+
if (!card) {
|
|
1698
|
+
var id = host.getAttribute('data-hovercard-id');
|
|
1699
|
+
card = document.getElementById(id);
|
|
1700
|
+
}
|
|
1701
|
+
return card;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
host.addEventListener('mouseenter', function () {
|
|
1705
|
+
showTimer = setTimeout(function () {
|
|
1706
|
+
showCompact(getCard(), host);
|
|
1707
|
+
|
|
1708
|
+
expandTimer = setTimeout(function () {
|
|
1709
|
+
showExpanded(getCard());
|
|
1710
|
+
}, 1000); // 1000ms after compact = 1500ms total
|
|
1711
|
+
}, 500);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
host.addEventListener('mouseleave', function () {
|
|
1715
|
+
clearTimeout(showTimer);
|
|
1716
|
+
clearTimeout(expandTimer);
|
|
1717
|
+
|
|
1718
|
+
// Delay hide to allow mouse to move to card
|
|
1719
|
+
var c = getCard();
|
|
1720
|
+
if (c) {
|
|
1721
|
+
setTimeout(function () {
|
|
1722
|
+
if (!c._hovered) {
|
|
1723
|
+
hide(c);
|
|
1724
|
+
}
|
|
1725
|
+
}, 100);
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
// Keep card open while mouse is over it
|
|
1730
|
+
var hoverCardEl = getCard();
|
|
1731
|
+
if (hoverCardEl) {
|
|
1732
|
+
hoverCardEl.addEventListener('mouseenter', function () {
|
|
1733
|
+
hoverCardEl._hovered = true;
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
hoverCardEl.addEventListener('mouseleave', function () {
|
|
1737
|
+
hoverCardEl._hovered = false;
|
|
1738
|
+
clearTimeout(expandTimer);
|
|
1739
|
+
hide(hoverCardEl);
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
host.setAttribute('data-hovercard-wired', 'true');
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function showCompact(card, host) {
|
|
1747
|
+
if (!card) return;
|
|
1748
|
+
|
|
1749
|
+
var rect = host.getBoundingClientRect();
|
|
1750
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
1751
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
1752
|
+
|
|
1753
|
+
card.style.position = 'absolute';
|
|
1754
|
+
card.style.left = rect.left + scrollX + 'px';
|
|
1755
|
+
card.style.top = (rect.bottom + scrollY + 4) + 'px';
|
|
1756
|
+
|
|
1757
|
+
card.classList.add('flm-hovercard--visible');
|
|
1758
|
+
|
|
1759
|
+
// Flip if off-screen
|
|
1760
|
+
setTimeout(function () {
|
|
1761
|
+
var cRect = card.getBoundingClientRect();
|
|
1762
|
+
if (cRect.bottom > window.innerHeight) {
|
|
1763
|
+
card.style.top = (rect.top + scrollY - cRect.height - 4) + 'px';
|
|
1764
|
+
}
|
|
1765
|
+
if (cRect.right > window.innerWidth) {
|
|
1766
|
+
card.style.left = (rect.right + scrollX - cRect.width) + 'px';
|
|
1767
|
+
}
|
|
1768
|
+
}, 0);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function showExpanded(card) {
|
|
1772
|
+
if (!card) return;
|
|
1773
|
+
var expanded = card.querySelector('.flm-hovercard-expanded');
|
|
1774
|
+
if (expanded) {
|
|
1775
|
+
expanded.classList.add('flm-hovercard-expanded--visible');
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function hide(card) {
|
|
1780
|
+
if (!card) return;
|
|
1781
|
+
card.classList.remove('flm-hovercard--visible');
|
|
1782
|
+
var expanded = card.querySelector('.flm-hovercard-expanded');
|
|
1783
|
+
if (expanded) {
|
|
1784
|
+
expanded.classList.remove('flm-hovercard-expanded--visible');
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
return { init: init };
|
|
1789
|
+
})();
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Icon component — resolves data-icon attributes to inline SVG.
|
|
1793
|
+
*/
|
|
1794
|
+
var FluentLMIconComponent = (function () {
|
|
1795
|
+
'use strict';
|
|
1796
|
+
|
|
1797
|
+
function init(root) {
|
|
1798
|
+
var els = (root || document).querySelectorAll('[data-icon]');
|
|
1799
|
+
for (var i = 0; i < els.length; i++) {
|
|
1800
|
+
render(els[i]);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function render(el) {
|
|
1805
|
+
// Skip if already rendered
|
|
1806
|
+
if (el.getAttribute('data-icon-rendered')) return;
|
|
1807
|
+
|
|
1808
|
+
var name = el.getAttribute('data-icon');
|
|
1809
|
+
if (!name) return;
|
|
1810
|
+
|
|
1811
|
+
var svg = FluentIcons.getSvg(name);
|
|
1812
|
+
if (!svg) return;
|
|
1813
|
+
|
|
1814
|
+
// For .flm-icon elements or inline icon elements (i, span with data-icon only), replace contents
|
|
1815
|
+
if (el.classList.contains('flm-icon') || ((el.tagName === 'I' || el.tagName === 'SPAN') && el.childNodes.length === 0)) {
|
|
1816
|
+
el.innerHTML = '';
|
|
1817
|
+
el.appendChild(svg);
|
|
1818
|
+
}
|
|
1819
|
+
// For buttons / other elements, prepend icon
|
|
1820
|
+
else if (el.classList.contains('flm-button') || el.tagName === 'BUTTON' || el.tagName === 'A') {
|
|
1821
|
+
el.insertBefore(svg, el.firstChild);
|
|
1822
|
+
// Add a small space text node if there's text content after the icon
|
|
1823
|
+
if (el.childNodes.length > 1 && !el.classList.contains('flm-button--icon')) {
|
|
1824
|
+
svg.style.marginRight = '4px';
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
el.setAttribute('data-icon-rendered', 'true');
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
return { init: init, render: render };
|
|
1832
|
+
})();
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* MessageBar component JS — auto-injects status icon and wires dismiss button.
|
|
1836
|
+
*/
|
|
1837
|
+
var FluentLMMessageBarComponent = (function () {
|
|
1838
|
+
'use strict';
|
|
1839
|
+
|
|
1840
|
+
// Map messagebar type to icon name
|
|
1841
|
+
var typeIconMap = {
|
|
1842
|
+
'info': 'Info',
|
|
1843
|
+
'success': 'Completed',
|
|
1844
|
+
'warning': 'Warning',
|
|
1845
|
+
'severeWarning': 'Warning',
|
|
1846
|
+
'error': 'ErrorBadge',
|
|
1847
|
+
'blocked': 'Blocked'
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
function getType(el) {
|
|
1851
|
+
var classes = el.className;
|
|
1852
|
+
var types = Object.keys(typeIconMap);
|
|
1853
|
+
for (var i = 0; i < types.length; i++) {
|
|
1854
|
+
if (classes.indexOf('flm-messagebar--' + types[i]) !== -1) {
|
|
1855
|
+
return types[i];
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return 'info';
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function init(root) {
|
|
1862
|
+
var els = (root || document).querySelectorAll('.flm-messagebar');
|
|
1863
|
+
for (var i = 0; i < els.length; i++) {
|
|
1864
|
+
render(els[i]);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function render(el) {
|
|
1869
|
+
if (el.getAttribute('data-messagebar-rendered')) return;
|
|
1870
|
+
|
|
1871
|
+
var type = getType(el);
|
|
1872
|
+
|
|
1873
|
+
// Auto-inject icon if none exists
|
|
1874
|
+
if (!el.querySelector('.flm-messagebar-icon')) {
|
|
1875
|
+
var iconName = typeIconMap[type];
|
|
1876
|
+
var svg = FluentIcons.getSvg(iconName);
|
|
1877
|
+
if (svg) {
|
|
1878
|
+
var iconSpan = document.createElement('span');
|
|
1879
|
+
iconSpan.className = 'flm-messagebar-icon';
|
|
1880
|
+
iconSpan.appendChild(svg);
|
|
1881
|
+
el.insertBefore(iconSpan, el.firstChild);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Wrap bare text content in .flm-messagebar-text if not already wrapped
|
|
1886
|
+
if (!el.querySelector('.flm-messagebar-text')) {
|
|
1887
|
+
var children = Array.prototype.slice.call(el.childNodes);
|
|
1888
|
+
var textWrapper = document.createElement('span');
|
|
1889
|
+
textWrapper.className = 'flm-messagebar-text';
|
|
1890
|
+
for (var i = 0; i < children.length; i++) {
|
|
1891
|
+
var child = children[i];
|
|
1892
|
+
if (!child.classList ||
|
|
1893
|
+
(!child.classList.contains('flm-messagebar-icon') &&
|
|
1894
|
+
!child.classList.contains('flm-messagebar-actions') &&
|
|
1895
|
+
!child.classList.contains('flm-messagebar-dismiss'))) {
|
|
1896
|
+
textWrapper.appendChild(child);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
// Insert text wrapper after the icon
|
|
1900
|
+
var icon = el.querySelector('.flm-messagebar-icon');
|
|
1901
|
+
if (icon) {
|
|
1902
|
+
icon.after(textWrapper);
|
|
1903
|
+
} else {
|
|
1904
|
+
el.insertBefore(textWrapper, el.firstChild);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// Wire up dismiss button
|
|
1909
|
+
var dismiss = el.querySelector('.flm-messagebar-dismiss');
|
|
1910
|
+
if (dismiss) {
|
|
1911
|
+
dismiss.addEventListener('click', function () {
|
|
1912
|
+
el.style.display = 'none';
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// ARIA
|
|
1917
|
+
el.setAttribute('role', 'status');
|
|
1918
|
+
if (type === 'error' || type === 'blocked' || type === 'severeWarning') {
|
|
1919
|
+
el.setAttribute('role', 'alert');
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
el.setAttribute('data-messagebar-rendered', 'true');
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
return { init: init };
|
|
1926
|
+
})();
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Modal component JS — open/close, Escape key, overlay dismiss.
|
|
1930
|
+
*
|
|
1931
|
+
* Usage:
|
|
1932
|
+
* FluentLMModalComponent.open('my-modal')
|
|
1933
|
+
* FluentLMModalComponent.close('my-modal')
|
|
1934
|
+
*
|
|
1935
|
+
* Trigger: <button data-modal-open="my-modal">Open</button>
|
|
1936
|
+
*/
|
|
1937
|
+
var FluentLMModalComponent = (function () {
|
|
1938
|
+
'use strict';
|
|
1939
|
+
|
|
1940
|
+
function init(root) {
|
|
1941
|
+
var doc = root || document;
|
|
1942
|
+
|
|
1943
|
+
var triggers = doc.querySelectorAll('[data-modal-open]');
|
|
1944
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
1945
|
+
wireTrigger(triggers[i]);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
var closeBtns = doc.querySelectorAll('[data-modal-close]');
|
|
1949
|
+
for (var j = 0; j < closeBtns.length; j++) {
|
|
1950
|
+
wireClose(closeBtns[j]);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
var overlays = doc.querySelectorAll('.flm-modal-overlay');
|
|
1954
|
+
for (var k = 0; k < overlays.length; k++) {
|
|
1955
|
+
wireOverlay(overlays[k]);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function wireTrigger(btn) {
|
|
1960
|
+
if (btn.getAttribute('data-modal-wired')) return;
|
|
1961
|
+
btn.addEventListener('click', function () {
|
|
1962
|
+
open(btn.getAttribute('data-modal-open'));
|
|
1963
|
+
});
|
|
1964
|
+
btn.setAttribute('data-modal-wired', 'true');
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function wireClose(btn) {
|
|
1968
|
+
if (btn.getAttribute('data-modal-wired')) return;
|
|
1969
|
+
btn.addEventListener('click', function () {
|
|
1970
|
+
var overlay = btn.closest('.flm-modal-overlay');
|
|
1971
|
+
if (overlay) closeOverlay(overlay);
|
|
1972
|
+
});
|
|
1973
|
+
btn.setAttribute('data-modal-wired', 'true');
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function wireOverlay(overlay) {
|
|
1977
|
+
if (overlay.getAttribute('data-modal-overlay-wired')) return;
|
|
1978
|
+
// Only light-dismiss if not blocking
|
|
1979
|
+
if (overlay.hasAttribute('data-light-dismiss')) {
|
|
1980
|
+
overlay.addEventListener('click', function (e) {
|
|
1981
|
+
if (e.target === overlay) closeOverlay(overlay);
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
overlay.setAttribute('data-modal-overlay-wired', 'true');
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function open(id) {
|
|
1988
|
+
var overlay = document.getElementById(id);
|
|
1989
|
+
if (!overlay) return;
|
|
1990
|
+
overlay.classList.add('flm-modal-overlay--open');
|
|
1991
|
+
document.body.style.overflow = 'hidden';
|
|
1992
|
+
|
|
1993
|
+
var escHandler = function (e) {
|
|
1994
|
+
if (e.key === 'Escape' && !overlay.hasAttribute('data-blocking')) {
|
|
1995
|
+
closeOverlay(overlay);
|
|
1996
|
+
document.removeEventListener('keydown', escHandler);
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
document.addEventListener('keydown', escHandler);
|
|
2000
|
+
overlay._escHandler = escHandler;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function close(id) {
|
|
2004
|
+
var overlay = document.getElementById(id);
|
|
2005
|
+
if (overlay) closeOverlay(overlay);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function closeOverlay(overlay) {
|
|
2009
|
+
overlay.classList.remove('flm-modal-overlay--open');
|
|
2010
|
+
document.body.style.overflow = '';
|
|
2011
|
+
if (overlay._escHandler) {
|
|
2012
|
+
document.removeEventListener('keydown', overlay._escHandler);
|
|
2013
|
+
delete overlay._escHandler;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return { init: init, open: open, close: close };
|
|
2018
|
+
})();
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Nav component JS — collapsible groups and active link tracking.
|
|
2022
|
+
*/
|
|
2023
|
+
var FluentLMNavComponent = (function () {
|
|
2024
|
+
'use strict';
|
|
2025
|
+
|
|
2026
|
+
function init(root) {
|
|
2027
|
+
var doc = root || document;
|
|
2028
|
+
|
|
2029
|
+
// Wire collapsible group headers
|
|
2030
|
+
var headers = doc.querySelectorAll('.flm-nav-group-header');
|
|
2031
|
+
for (var i = 0; i < headers.length; i++) {
|
|
2032
|
+
wireGroupHeader(headers[i]);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Wire nav link clicks
|
|
2036
|
+
var links = doc.querySelectorAll('.flm-nav-link');
|
|
2037
|
+
for (var j = 0; j < links.length; j++) {
|
|
2038
|
+
wireLink(links[j]);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function wireGroupHeader(header) {
|
|
2043
|
+
if (header.getAttribute('data-nav-wired')) return;
|
|
2044
|
+
|
|
2045
|
+
header.addEventListener('click', function () {
|
|
2046
|
+
var items = header.nextElementSibling;
|
|
2047
|
+
if (!items || !items.classList.contains('flm-nav-group-items')) return;
|
|
2048
|
+
|
|
2049
|
+
var chevron = header.querySelector('.flm-nav-chevron');
|
|
2050
|
+
var collapsed = items.classList.contains('flm-nav-group-items--collapsed');
|
|
2051
|
+
|
|
2052
|
+
if (collapsed) {
|
|
2053
|
+
items.classList.remove('flm-nav-group-items--collapsed');
|
|
2054
|
+
if (chevron) chevron.classList.add('flm-nav-chevron--expanded');
|
|
2055
|
+
} else {
|
|
2056
|
+
items.classList.add('flm-nav-group-items--collapsed');
|
|
2057
|
+
if (chevron) chevron.classList.remove('flm-nav-chevron--expanded');
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
header.setAttribute('data-nav-wired', 'true');
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function wireLink(link) {
|
|
2065
|
+
if (link.getAttribute('data-nav-wired')) return;
|
|
2066
|
+
|
|
2067
|
+
link.addEventListener('click', function () {
|
|
2068
|
+
// Remove active from all links in this nav
|
|
2069
|
+
var nav = link.closest('.flm-nav');
|
|
2070
|
+
if (nav) {
|
|
2071
|
+
var allLinks = nav.querySelectorAll('.flm-nav-link');
|
|
2072
|
+
for (var i = 0; i < allLinks.length; i++) {
|
|
2073
|
+
allLinks[i].classList.remove('flm-nav-link--active');
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
link.classList.add('flm-nav-link--active');
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
link.setAttribute('data-nav-wired', 'true');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
return { init: init };
|
|
2083
|
+
})();
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* OverflowSet component JS — responsive container that hides items into
|
|
2087
|
+
* an overflow "..." menu.
|
|
2088
|
+
*
|
|
2089
|
+
* Usage:
|
|
2090
|
+
* <div class="flm-overflowset">
|
|
2091
|
+
* <div class="flm-overflowset-items">
|
|
2092
|
+
* <button class="flm-overflowset-item" data-label="New" data-icon="Add">New</button>
|
|
2093
|
+
* <button class="flm-overflowset-item" data-label="Edit" data-icon="Edit">Edit</button>
|
|
2094
|
+
* </div>
|
|
2095
|
+
* <button class="flm-overflowset-overflow" aria-label="More items">...</button>
|
|
2096
|
+
* <div class="flm-overflowset-far">
|
|
2097
|
+
* <button class="flm-overflowset-item" data-label="Settings">Settings</button>
|
|
2098
|
+
* </div>
|
|
2099
|
+
* </div>
|
|
2100
|
+
*
|
|
2101
|
+
* Attributes:
|
|
2102
|
+
* data-overflow-order="N" on items — higher N overflows first.
|
|
2103
|
+
* data-label — label shown in overflow menu.
|
|
2104
|
+
* data-icon — icon name shown in overflow menu.
|
|
2105
|
+
*/
|
|
2106
|
+
var FluentLMOverflowSetComponent = (function () {
|
|
2107
|
+
'use strict';
|
|
2108
|
+
|
|
2109
|
+
function init(root) {
|
|
2110
|
+
var doc = root || document;
|
|
2111
|
+
var sets = doc.querySelectorAll('.flm-overflowset');
|
|
2112
|
+
for (var i = 0; i < sets.length; i++) {
|
|
2113
|
+
wireSet(sets[i]);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function wireSet(el) {
|
|
2118
|
+
if (el.getAttribute('data-overflowset-wired')) return;
|
|
2119
|
+
|
|
2120
|
+
var itemsContainer = el.querySelector('.flm-overflowset-items');
|
|
2121
|
+
var overflowBtn = el.querySelector('.flm-overflowset-overflow');
|
|
2122
|
+
|
|
2123
|
+
if (!itemsContainer) return;
|
|
2124
|
+
|
|
2125
|
+
var menuEl = null;
|
|
2126
|
+
|
|
2127
|
+
function getItems() {
|
|
2128
|
+
return itemsContainer.querySelectorAll('.flm-overflowset-item');
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function recalculate() {
|
|
2132
|
+
var items = getItems();
|
|
2133
|
+
var i;
|
|
2134
|
+
|
|
2135
|
+
// Show all items first to measure
|
|
2136
|
+
for (i = 0; i < items.length; i++) {
|
|
2137
|
+
items[i].classList.remove('flm-overflowset-item--hidden');
|
|
2138
|
+
}
|
|
2139
|
+
el.classList.remove('flm-overflowset--has-overflow');
|
|
2140
|
+
|
|
2141
|
+
// Get available width
|
|
2142
|
+
var containerWidth = itemsContainer.offsetWidth;
|
|
2143
|
+
|
|
2144
|
+
// Build array with overflow priority
|
|
2145
|
+
var itemData = [];
|
|
2146
|
+
for (i = 0; i < items.length; i++) {
|
|
2147
|
+
itemData.push({
|
|
2148
|
+
el: items[i],
|
|
2149
|
+
order: parseInt(items[i].getAttribute('data-overflow-order') || '0', 10),
|
|
2150
|
+
index: i,
|
|
2151
|
+
width: items[i].offsetWidth
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Sort by overflow order descending (higher order overflows first), then by reverse DOM order
|
|
2156
|
+
itemData.sort(function (a, b) {
|
|
2157
|
+
if (b.order !== a.order) return b.order - a.order;
|
|
2158
|
+
return b.index - a.index;
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
// Measure total width
|
|
2162
|
+
var totalWidth = 0;
|
|
2163
|
+
var gap = 4; // matches var(--spacingS2) = 4px
|
|
2164
|
+
for (i = 0; i < items.length; i++) {
|
|
2165
|
+
totalWidth += items[i].offsetWidth + (i > 0 ? gap : 0);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// Account for overflow button width if we might need it
|
|
2169
|
+
var overflowBtnWidth = overflowBtn ? 36 : 0; // 32px + gap
|
|
2170
|
+
|
|
2171
|
+
// Hide items until they fit
|
|
2172
|
+
var hiddenItems = [];
|
|
2173
|
+
var idx = 0;
|
|
2174
|
+
while (totalWidth > containerWidth && idx < itemData.length) {
|
|
2175
|
+
var item = itemData[idx];
|
|
2176
|
+
item.el.classList.add('flm-overflowset-item--hidden');
|
|
2177
|
+
totalWidth -= item.width + gap;
|
|
2178
|
+
// First time we overflow, account for the overflow button
|
|
2179
|
+
if (hiddenItems.length === 0) {
|
|
2180
|
+
totalWidth += overflowBtnWidth;
|
|
2181
|
+
}
|
|
2182
|
+
hiddenItems.push(item);
|
|
2183
|
+
idx++;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
if (hiddenItems.length > 0) {
|
|
2187
|
+
el.classList.add('flm-overflowset--has-overflow');
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function buildOverflowMenu() {
|
|
2192
|
+
// Clean up existing menu
|
|
2193
|
+
if (menuEl) {
|
|
2194
|
+
if (menuEl.classList.contains('flm-contextmenu--visible')) {
|
|
2195
|
+
hideMenu();
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
menuEl.parentNode.removeChild(menuEl);
|
|
2199
|
+
menuEl = null;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
var hiddenItems = itemsContainer.querySelectorAll('.flm-overflowset-item--hidden');
|
|
2203
|
+
if (hiddenItems.length === 0) return;
|
|
2204
|
+
|
|
2205
|
+
menuEl = document.createElement('div');
|
|
2206
|
+
menuEl.className = 'flm-contextmenu';
|
|
2207
|
+
|
|
2208
|
+
for (var i = 0; i < hiddenItems.length; i++) {
|
|
2209
|
+
var item = hiddenItems[i];
|
|
2210
|
+
var label = item.getAttribute('data-label') || item.textContent.trim();
|
|
2211
|
+
var icon = item.getAttribute('data-icon');
|
|
2212
|
+
|
|
2213
|
+
var menuItem = document.createElement('button');
|
|
2214
|
+
menuItem.className = 'flm-contextmenu-item';
|
|
2215
|
+
|
|
2216
|
+
if (icon) {
|
|
2217
|
+
var iconSpan = document.createElement('span');
|
|
2218
|
+
iconSpan.className = 'flm-contextmenu-item-icon';
|
|
2219
|
+
var svg = typeof FluentIcons !== 'undefined' ? FluentIcons.getSvg(icon) : null;
|
|
2220
|
+
if (svg) {
|
|
2221
|
+
iconSpan.appendChild(svg);
|
|
2222
|
+
}
|
|
2223
|
+
menuItem.appendChild(iconSpan);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
var textSpan = document.createElement('span');
|
|
2227
|
+
textSpan.className = 'flm-contextmenu-item-text';
|
|
2228
|
+
textSpan.textContent = label;
|
|
2229
|
+
menuItem.appendChild(textSpan);
|
|
2230
|
+
|
|
2231
|
+
// Proxy click to original item
|
|
2232
|
+
(function (originalItem) {
|
|
2233
|
+
menuItem.addEventListener('click', function () {
|
|
2234
|
+
hideMenu();
|
|
2235
|
+
originalItem.click();
|
|
2236
|
+
});
|
|
2237
|
+
})(item);
|
|
2238
|
+
|
|
2239
|
+
menuEl.appendChild(menuItem);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
document.body.appendChild(menuEl);
|
|
2243
|
+
showMenu();
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function showMenu() {
|
|
2247
|
+
if (!menuEl || !overflowBtn) return;
|
|
2248
|
+
|
|
2249
|
+
var rect = overflowBtn.getBoundingClientRect();
|
|
2250
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
2251
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
2252
|
+
|
|
2253
|
+
menuEl.style.position = 'absolute';
|
|
2254
|
+
menuEl.style.left = rect.left + scrollX + 'px';
|
|
2255
|
+
menuEl.style.top = (rect.bottom + scrollY + 2) + 'px';
|
|
2256
|
+
menuEl.classList.add('flm-contextmenu--visible');
|
|
2257
|
+
|
|
2258
|
+
// Reposition if off-screen
|
|
2259
|
+
setTimeout(function () {
|
|
2260
|
+
var menuRect = menuEl.getBoundingClientRect();
|
|
2261
|
+
if (menuRect.right > window.innerWidth) {
|
|
2262
|
+
menuEl.style.left = (rect.right + scrollX - menuRect.width) + 'px';
|
|
2263
|
+
}
|
|
2264
|
+
if (menuRect.bottom > window.innerHeight) {
|
|
2265
|
+
menuEl.style.top = (rect.top + scrollY - menuRect.height - 2) + 'px';
|
|
2266
|
+
}
|
|
2267
|
+
}, 0);
|
|
2268
|
+
|
|
2269
|
+
// Click outside to close
|
|
2270
|
+
var outsideHandler = function (e) {
|
|
2271
|
+
if (!menuEl.contains(e.target) && e.target !== overflowBtn) {
|
|
2272
|
+
hideMenu();
|
|
2273
|
+
document.removeEventListener('click', outsideHandler);
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
setTimeout(function () {
|
|
2277
|
+
document.addEventListener('click', outsideHandler);
|
|
2278
|
+
}, 0);
|
|
2279
|
+
menuEl._outsideHandler = outsideHandler;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function hideMenu() {
|
|
2283
|
+
if (!menuEl) return;
|
|
2284
|
+
menuEl.classList.remove('flm-contextmenu--visible');
|
|
2285
|
+
if (menuEl._outsideHandler) {
|
|
2286
|
+
document.removeEventListener('click', menuEl._outsideHandler);
|
|
2287
|
+
delete menuEl._outsideHandler;
|
|
2288
|
+
}
|
|
2289
|
+
// Remove from DOM after transition
|
|
2290
|
+
setTimeout(function () {
|
|
2291
|
+
if (menuEl && menuEl.parentNode) {
|
|
2292
|
+
menuEl.parentNode.removeChild(menuEl);
|
|
2293
|
+
}
|
|
2294
|
+
menuEl = null;
|
|
2295
|
+
}, 200);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// Wire overflow button click
|
|
2299
|
+
if (overflowBtn) {
|
|
2300
|
+
overflowBtn.addEventListener('click', function (e) {
|
|
2301
|
+
e.stopPropagation();
|
|
2302
|
+
buildOverflowMenu();
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// Observe size changes
|
|
2307
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2308
|
+
var observer = new ResizeObserver(function () {
|
|
2309
|
+
recalculate();
|
|
2310
|
+
});
|
|
2311
|
+
observer.observe(el);
|
|
2312
|
+
} else {
|
|
2313
|
+
window.addEventListener('resize', recalculate);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// Initial calculation
|
|
2317
|
+
recalculate();
|
|
2318
|
+
|
|
2319
|
+
el.setAttribute('data-overflowset-wired', 'true');
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
return { init: init };
|
|
2323
|
+
})();
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Panel component JS — slide-in/out, Escape key, overlay dismiss.
|
|
2327
|
+
*
|
|
2328
|
+
* Usage:
|
|
2329
|
+
* FluentLMPanelComponent.open('my-panel')
|
|
2330
|
+
* FluentLMPanelComponent.close('my-panel')
|
|
2331
|
+
*
|
|
2332
|
+
* Trigger: <button data-panel-open="my-panel">Open</button>
|
|
2333
|
+
*/
|
|
2334
|
+
var FluentLMPanelComponent = (function () {
|
|
2335
|
+
'use strict';
|
|
2336
|
+
|
|
2337
|
+
function init(root) {
|
|
2338
|
+
var doc = root || document;
|
|
2339
|
+
|
|
2340
|
+
var triggers = doc.querySelectorAll('[data-panel-open]');
|
|
2341
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
2342
|
+
wireTrigger(triggers[i]);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
var closeBtns = doc.querySelectorAll('.flm-panel-close, [data-panel-close]');
|
|
2346
|
+
for (var j = 0; j < closeBtns.length; j++) {
|
|
2347
|
+
wireClose(closeBtns[j]);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
var overlays = doc.querySelectorAll('.flm-panel-overlay');
|
|
2351
|
+
for (var k = 0; k < overlays.length; k++) {
|
|
2352
|
+
wireOverlay(overlays[k]);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function wireTrigger(btn) {
|
|
2357
|
+
if (btn.getAttribute('data-panel-wired')) return;
|
|
2358
|
+
btn.addEventListener('click', function () {
|
|
2359
|
+
open(btn.getAttribute('data-panel-open'));
|
|
2360
|
+
});
|
|
2361
|
+
btn.setAttribute('data-panel-wired', 'true');
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function wireClose(btn) {
|
|
2365
|
+
if (btn.getAttribute('data-panel-wired')) return;
|
|
2366
|
+
btn.addEventListener('click', function () {
|
|
2367
|
+
var panel = btn.closest('.flm-panel');
|
|
2368
|
+
if (panel) closePanel(panel);
|
|
2369
|
+
});
|
|
2370
|
+
btn.setAttribute('data-panel-wired', 'true');
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function wireOverlay(overlay) {
|
|
2374
|
+
if (overlay.getAttribute('data-panel-overlay-wired')) return;
|
|
2375
|
+
overlay.addEventListener('click', function (e) {
|
|
2376
|
+
if (e.target === overlay) {
|
|
2377
|
+
var panel = overlay.nextElementSibling;
|
|
2378
|
+
if (panel && panel.classList.contains('flm-panel')) {
|
|
2379
|
+
closePanel(panel);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
overlay.setAttribute('data-panel-overlay-wired', 'true');
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function open(id) {
|
|
2387
|
+
var panel = document.getElementById(id);
|
|
2388
|
+
if (!panel) return;
|
|
2389
|
+
|
|
2390
|
+
// Show overlay
|
|
2391
|
+
var overlay = panel.previousElementSibling;
|
|
2392
|
+
if (overlay && overlay.classList.contains('flm-panel-overlay')) {
|
|
2393
|
+
overlay.classList.add('flm-panel-overlay--open');
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
panel.classList.add('flm-panel--open');
|
|
2397
|
+
document.body.style.overflow = 'hidden';
|
|
2398
|
+
|
|
2399
|
+
var escHandler = function (e) {
|
|
2400
|
+
if (e.key === 'Escape') {
|
|
2401
|
+
closePanel(panel);
|
|
2402
|
+
document.removeEventListener('keydown', escHandler);
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
document.addEventListener('keydown', escHandler);
|
|
2406
|
+
panel._escHandler = escHandler;
|
|
2407
|
+
|
|
2408
|
+
setTimeout(function () {
|
|
2409
|
+
var focusable = panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
2410
|
+
if (focusable) focusable.focus();
|
|
2411
|
+
}, 50);
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
function close(id) {
|
|
2415
|
+
var panel = document.getElementById(id);
|
|
2416
|
+
if (panel) closePanel(panel);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function closePanel(panel) {
|
|
2420
|
+
panel.classList.remove('flm-panel--open');
|
|
2421
|
+
|
|
2422
|
+
var overlay = panel.previousElementSibling;
|
|
2423
|
+
if (overlay && overlay.classList.contains('flm-panel-overlay')) {
|
|
2424
|
+
overlay.classList.remove('flm-panel-overlay--open');
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
document.body.style.overflow = '';
|
|
2428
|
+
if (panel._escHandler) {
|
|
2429
|
+
document.removeEventListener('keydown', panel._escHandler);
|
|
2430
|
+
delete panel._escHandler;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
return { init: init, open: open, close: close };
|
|
2435
|
+
})();
|
|
2436
|
+
|
|
2437
|
+
/**
|
|
2438
|
+
* Pivot component JS — tab switching.
|
|
2439
|
+
* Tabs reference panels by data-panel attribute.
|
|
2440
|
+
*/
|
|
2441
|
+
var FluentLMPivotComponent = (function () {
|
|
2442
|
+
'use strict';
|
|
2443
|
+
|
|
2444
|
+
function init(root) {
|
|
2445
|
+
var doc = root || document;
|
|
2446
|
+
|
|
2447
|
+
var pivots = doc.querySelectorAll('.flm-pivot');
|
|
2448
|
+
for (var i = 0; i < pivots.length; i++) {
|
|
2449
|
+
wirePivot(pivots[i]);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function wirePivot(pivot) {
|
|
2454
|
+
if (pivot.getAttribute('data-pivot-wired')) return;
|
|
2455
|
+
|
|
2456
|
+
var tabs = pivot.querySelectorAll('.flm-pivot-tab');
|
|
2457
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
2458
|
+
wireTab(pivot, tabs[i]);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
pivot.setAttribute('data-pivot-wired', 'true');
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function wireTab(pivot, tab) {
|
|
2465
|
+
tab.addEventListener('click', function () {
|
|
2466
|
+
if (tab.classList.contains('flm-pivot-tab--disabled')) return;
|
|
2467
|
+
|
|
2468
|
+
// Deactivate all tabs
|
|
2469
|
+
var allTabs = pivot.querySelectorAll('.flm-pivot-tab');
|
|
2470
|
+
for (var i = 0; i < allTabs.length; i++) {
|
|
2471
|
+
allTabs[i].classList.remove('flm-pivot-tab--active');
|
|
2472
|
+
allTabs[i].setAttribute('aria-selected', 'false');
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// Hide all panels
|
|
2476
|
+
var allPanels = pivot.querySelectorAll('.flm-pivot-panel');
|
|
2477
|
+
for (var j = 0; j < allPanels.length; j++) {
|
|
2478
|
+
allPanels[j].classList.remove('flm-pivot-panel--active');
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// Activate clicked tab
|
|
2482
|
+
tab.classList.add('flm-pivot-tab--active');
|
|
2483
|
+
tab.setAttribute('aria-selected', 'true');
|
|
2484
|
+
|
|
2485
|
+
// Show matching panel
|
|
2486
|
+
var panelId = tab.getAttribute('data-panel');
|
|
2487
|
+
if (panelId) {
|
|
2488
|
+
var panel = document.getElementById(panelId);
|
|
2489
|
+
if (panel) panel.classList.add('flm-pivot-panel--active');
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
return { init: init };
|
|
2495
|
+
})();
|
|
2496
|
+
|
|
2497
|
+
/**
|
|
2498
|
+
* Rating component JS — stores selected rating value on the root element.
|
|
2499
|
+
*/
|
|
2500
|
+
var FluentLMRatingComponent = (function () {
|
|
2501
|
+
'use strict';
|
|
2502
|
+
|
|
2503
|
+
function init(root) {
|
|
2504
|
+
var doc = root || document;
|
|
2505
|
+
|
|
2506
|
+
var ratings = doc.querySelectorAll('.flm-rating');
|
|
2507
|
+
for (var i = 0; i < ratings.length; i++) {
|
|
2508
|
+
wireRating(ratings[i]);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function wireRating(rating) {
|
|
2513
|
+
if (rating.getAttribute('data-rating-wired')) return;
|
|
2514
|
+
|
|
2515
|
+
var inputs = rating.querySelectorAll('.flm-rating-input');
|
|
2516
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
2517
|
+
inputs[i].addEventListener('change', function () {
|
|
2518
|
+
if (this.checked) {
|
|
2519
|
+
rating.setAttribute('data-rating-value', this.value);
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Set initial value from pre-checked input
|
|
2525
|
+
var checked = rating.querySelector('.flm-rating-input:checked');
|
|
2526
|
+
if (checked) {
|
|
2527
|
+
rating.setAttribute('data-rating-value', checked.value);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
rating.setAttribute('data-rating-wired', 'true');
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
return { init: init };
|
|
2534
|
+
})();
|
|
2535
|
+
|
|
2536
|
+
/**
|
|
2537
|
+
* SearchBox component JS — injects search icon, wires clear button.
|
|
2538
|
+
*/
|
|
2539
|
+
var FluentLMSearchBoxComponent = (function () {
|
|
2540
|
+
'use strict';
|
|
2541
|
+
|
|
2542
|
+
function init(root) {
|
|
2543
|
+
var doc = root || document;
|
|
2544
|
+
|
|
2545
|
+
var boxes = doc.querySelectorAll('.flm-searchbox');
|
|
2546
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
2547
|
+
render(boxes[i]);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function render(box) {
|
|
2552
|
+
if (box.getAttribute('data-searchbox-rendered')) return;
|
|
2553
|
+
|
|
2554
|
+
// Inject search icon if not present
|
|
2555
|
+
var iconEl = box.querySelector('.flm-searchbox-icon');
|
|
2556
|
+
if (!iconEl) {
|
|
2557
|
+
iconEl = document.createElement('span');
|
|
2558
|
+
iconEl.className = 'flm-searchbox-icon';
|
|
2559
|
+
var svg = FluentIcons.getSvg('Search');
|
|
2560
|
+
if (svg) iconEl.appendChild(svg);
|
|
2561
|
+
box.insertBefore(iconEl, box.firstChild);
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// Inject clear button if not present
|
|
2565
|
+
var clearBtn = box.querySelector('.flm-searchbox-clear');
|
|
2566
|
+
if (!clearBtn) {
|
|
2567
|
+
clearBtn = document.createElement('button');
|
|
2568
|
+
clearBtn.className = 'flm-searchbox-clear';
|
|
2569
|
+
clearBtn.setAttribute('aria-label', 'Clear search');
|
|
2570
|
+
clearBtn.type = 'button';
|
|
2571
|
+
var cancelSvg = FluentIcons.getSvg('Cancel');
|
|
2572
|
+
if (cancelSvg) clearBtn.appendChild(cancelSvg);
|
|
2573
|
+
box.appendChild(clearBtn);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
var input = box.querySelector('.flm-searchbox-input');
|
|
2577
|
+
if (!input) { box.setAttribute('data-searchbox-rendered', 'true'); return; }
|
|
2578
|
+
|
|
2579
|
+
// Toggle has-value class
|
|
2580
|
+
function updateHasValue() {
|
|
2581
|
+
if (input.value) {
|
|
2582
|
+
box.classList.add('flm-searchbox--has-value');
|
|
2583
|
+
} else {
|
|
2584
|
+
box.classList.remove('flm-searchbox--has-value');
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
input.addEventListener('input', updateHasValue);
|
|
2589
|
+
updateHasValue();
|
|
2590
|
+
|
|
2591
|
+
// Clear button click
|
|
2592
|
+
clearBtn.addEventListener('click', function () {
|
|
2593
|
+
input.value = '';
|
|
2594
|
+
updateHasValue();
|
|
2595
|
+
input.focus();
|
|
2596
|
+
// Fire input event so any listeners are notified
|
|
2597
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2600
|
+
box.setAttribute('data-searchbox-rendered', 'true');
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
return { init: init };
|
|
2604
|
+
})();
|
|
2605
|
+
|
|
2606
|
+
/**
|
|
2607
|
+
* Slider component JS — updates CSS variable for track fill and value display.
|
|
2608
|
+
*/
|
|
2609
|
+
var FluentLMSliderComponent = (function () {
|
|
2610
|
+
'use strict';
|
|
2611
|
+
|
|
2612
|
+
function init(root) {
|
|
2613
|
+
var doc = root || document;
|
|
2614
|
+
|
|
2615
|
+
var sliders = doc.querySelectorAll('.flm-slider');
|
|
2616
|
+
for (var i = 0; i < sliders.length; i++) {
|
|
2617
|
+
wireSlider(sliders[i]);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function wireSlider(el) {
|
|
2622
|
+
if (el.getAttribute('data-slider-wired')) return;
|
|
2623
|
+
|
|
2624
|
+
var input = el.querySelector('.flm-slider-input');
|
|
2625
|
+
if (!input) return;
|
|
2626
|
+
|
|
2627
|
+
var valueDisplay = el.querySelector('.flm-slider-value');
|
|
2628
|
+
|
|
2629
|
+
function update() {
|
|
2630
|
+
var min = parseFloat(input.min) || 0;
|
|
2631
|
+
var max = parseFloat(input.max) || 100;
|
|
2632
|
+
var val = parseFloat(input.value) || 0;
|
|
2633
|
+
var pct = ((val - min) / (max - min)) * 100;
|
|
2634
|
+
input.style.setProperty('--flm-slider-fill', pct + '%');
|
|
2635
|
+
|
|
2636
|
+
if (valueDisplay) {
|
|
2637
|
+
valueDisplay.textContent = input.value;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
input.addEventListener('input', update);
|
|
2642
|
+
input.addEventListener('change', update);
|
|
2643
|
+
|
|
2644
|
+
// Set initial fill
|
|
2645
|
+
update();
|
|
2646
|
+
|
|
2647
|
+
el.setAttribute('data-slider-wired', 'true');
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
return { init: init };
|
|
2651
|
+
})();
|
|
2652
|
+
|
|
2653
|
+
/**
|
|
2654
|
+
* SpinButton component JS — wires increment/decrement buttons to numeric input.
|
|
2655
|
+
*/
|
|
2656
|
+
var FluentLMSpinButtonComponent = (function () {
|
|
2657
|
+
'use strict';
|
|
2658
|
+
|
|
2659
|
+
function init(root) {
|
|
2660
|
+
var doc = root || document;
|
|
2661
|
+
|
|
2662
|
+
var spinButtons = doc.querySelectorAll('.flm-spinbutton');
|
|
2663
|
+
for (var i = 0; i < spinButtons.length; i++) {
|
|
2664
|
+
wireSpinButton(spinButtons[i]);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
function wireSpinButton(el) {
|
|
2669
|
+
if (el.getAttribute('data-spinbutton-wired')) return;
|
|
2670
|
+
|
|
2671
|
+
var input = el.querySelector('.flm-spinbutton-input');
|
|
2672
|
+
if (!input) return;
|
|
2673
|
+
|
|
2674
|
+
var decBtn = el.querySelector('.flm-spinbutton-btn--decrement');
|
|
2675
|
+
var incBtn = el.querySelector('.flm-spinbutton-btn--increment');
|
|
2676
|
+
|
|
2677
|
+
// Inject icons if empty
|
|
2678
|
+
if (decBtn && !decBtn.innerHTML.trim()) {
|
|
2679
|
+
decBtn.setAttribute('data-icon', 'ChevronDown');
|
|
2680
|
+
}
|
|
2681
|
+
if (incBtn && !incBtn.innerHTML.trim()) {
|
|
2682
|
+
incBtn.setAttribute('data-icon', 'ChevronUp');
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (decBtn) {
|
|
2686
|
+
decBtn.addEventListener('click', function () {
|
|
2687
|
+
if (input.disabled) return;
|
|
2688
|
+
try { input.stepDown(); } catch (e) {
|
|
2689
|
+
input.value = (parseFloat(input.value) || 0) - (parseFloat(input.step) || 1);
|
|
2690
|
+
}
|
|
2691
|
+
fireChange(input);
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
if (incBtn) {
|
|
2696
|
+
incBtn.addEventListener('click', function () {
|
|
2697
|
+
if (input.disabled) return;
|
|
2698
|
+
try { input.stepUp(); } catch (e) {
|
|
2699
|
+
input.value = (parseFloat(input.value) || 0) + (parseFloat(input.step) || 1);
|
|
2700
|
+
}
|
|
2701
|
+
fireChange(input);
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
el.setAttribute('data-spinbutton-wired', 'true');
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
function fireChange(input) {
|
|
2709
|
+
var evt = document.createEvent('Event');
|
|
2710
|
+
evt.initEvent('change', true, true);
|
|
2711
|
+
input.dispatchEvent(evt);
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
return { init: init };
|
|
2715
|
+
})();
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* SwatchColorPicker component JS — manages selection state on color swatches.
|
|
2719
|
+
*/
|
|
2720
|
+
var FluentLMSwatchColorPickerComponent = (function () {
|
|
2721
|
+
'use strict';
|
|
2722
|
+
|
|
2723
|
+
function init(root) {
|
|
2724
|
+
var doc = root || document;
|
|
2725
|
+
|
|
2726
|
+
var pickers = doc.querySelectorAll('.flm-swatchcolorpicker');
|
|
2727
|
+
for (var i = 0; i < pickers.length; i++) {
|
|
2728
|
+
wirePicker(pickers[i]);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function wirePicker(picker) {
|
|
2733
|
+
if (picker.getAttribute('data-swatchcolorpicker-wired')) return;
|
|
2734
|
+
|
|
2735
|
+
var cells = picker.querySelectorAll('.flm-swatchcolorpicker-cell');
|
|
2736
|
+
for (var i = 0; i < cells.length; i++) {
|
|
2737
|
+
wireCell(picker, cells[i]);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
picker.setAttribute('data-swatchcolorpicker-wired', 'true');
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function wireCell(picker, cell) {
|
|
2744
|
+
cell.addEventListener('click', function () {
|
|
2745
|
+
if (cell.disabled || cell.classList.contains('flm-swatchcolorpicker-cell--disabled')) return;
|
|
2746
|
+
|
|
2747
|
+
// Clear previous selection
|
|
2748
|
+
var prev = picker.querySelector('.flm-swatchcolorpicker-cell--selected');
|
|
2749
|
+
if (prev) {
|
|
2750
|
+
prev.classList.remove('flm-swatchcolorpicker-cell--selected');
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
// Select this cell
|
|
2754
|
+
cell.classList.add('flm-swatchcolorpicker-cell--selected');
|
|
2755
|
+
|
|
2756
|
+
// Store selected color on root
|
|
2757
|
+
var color = cell.getAttribute('data-color') || cell.style.backgroundColor || '';
|
|
2758
|
+
picker.setAttribute('data-selected', 'true');
|
|
2759
|
+
picker.setAttribute('data-selected-color', color);
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
return { init: init };
|
|
2764
|
+
})();
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* TagPicker / PeoplePicker component JS — multi-select input with chip/tag UI.
|
|
2768
|
+
*
|
|
2769
|
+
* Usage:
|
|
2770
|
+
* <div class="flm-tagpicker">
|
|
2771
|
+
* <label class="flm-label">Tags</label>
|
|
2772
|
+
* <div class="flm-tagpicker-well">
|
|
2773
|
+
* <input class="flm-tagpicker-input" placeholder="Add tags…">
|
|
2774
|
+
* </div>
|
|
2775
|
+
* <div class="flm-tagpicker-listbox">
|
|
2776
|
+
* <div class="flm-tagpicker-option" data-value="a">Alpha</div>
|
|
2777
|
+
* <div class="flm-tagpicker-option" data-value="b">Beta</div>
|
|
2778
|
+
* </div>
|
|
2779
|
+
* </div>
|
|
2780
|
+
*
|
|
2781
|
+
* PeoplePicker variant: add flm-tagpicker--people on root, use
|
|
2782
|
+
* data-initials="JD" and data-secondary="Engineer" on options.
|
|
2783
|
+
*
|
|
2784
|
+
* Attributes:
|
|
2785
|
+
* data-max-tags="N" — limits number of selected tags.
|
|
2786
|
+
* data-selected-values — CSV of selected values (auto-maintained).
|
|
2787
|
+
*/
|
|
2788
|
+
var FluentLMTagPickerComponent = (function () {
|
|
2789
|
+
'use strict';
|
|
2790
|
+
|
|
2791
|
+
function init(root) {
|
|
2792
|
+
var doc = root || document;
|
|
2793
|
+
var pickers = doc.querySelectorAll('.flm-tagpicker');
|
|
2794
|
+
for (var i = 0; i < pickers.length; i++) {
|
|
2795
|
+
wirePicker(pickers[i]);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function wirePicker(el) {
|
|
2800
|
+
if (el.getAttribute('data-tagpicker-wired')) return;
|
|
2801
|
+
if (el.classList.contains('flm-tagpicker--disabled')) return;
|
|
2802
|
+
|
|
2803
|
+
var well = el.querySelector('.flm-tagpicker-well');
|
|
2804
|
+
var input = el.querySelector('.flm-tagpicker-input');
|
|
2805
|
+
var listbox = el.querySelector('.flm-tagpicker-listbox');
|
|
2806
|
+
|
|
2807
|
+
if (!well || !input || !listbox) return;
|
|
2808
|
+
|
|
2809
|
+
var isPeople = el.classList.contains('flm-tagpicker--people');
|
|
2810
|
+
var highlighted = -1;
|
|
2811
|
+
|
|
2812
|
+
function getVisibleOptions() {
|
|
2813
|
+
return listbox.querySelectorAll('.flm-tagpicker-option:not(.flm-tagpicker-option--selected):not(.flm-tagpicker-option--hidden)');
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function isOpen() {
|
|
2817
|
+
return listbox.classList.contains('flm-tagpicker-listbox--open');
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
function open() {
|
|
2821
|
+
listbox.classList.add('flm-tagpicker-listbox--open');
|
|
2822
|
+
highlighted = -1;
|
|
2823
|
+
flipIfNeeded();
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
function close() {
|
|
2827
|
+
listbox.classList.remove('flm-tagpicker-listbox--open', 'flm-tagpicker-listbox--above');
|
|
2828
|
+
highlighted = -1;
|
|
2829
|
+
clearHighlight();
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function flipIfNeeded() {
|
|
2833
|
+
setTimeout(function () {
|
|
2834
|
+
var rect = listbox.getBoundingClientRect();
|
|
2835
|
+
if (rect.bottom > window.innerHeight) {
|
|
2836
|
+
listbox.classList.add('flm-tagpicker-listbox--above');
|
|
2837
|
+
} else {
|
|
2838
|
+
listbox.classList.remove('flm-tagpicker-listbox--above');
|
|
2839
|
+
}
|
|
2840
|
+
}, 0);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
function clearHighlight() {
|
|
2844
|
+
var opts = listbox.querySelectorAll('.flm-tagpicker-option--highlighted');
|
|
2845
|
+
for (var i = 0; i < opts.length; i++) {
|
|
2846
|
+
opts[i].classList.remove('flm-tagpicker-option--highlighted');
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
function setHighlight(idx) {
|
|
2851
|
+
clearHighlight();
|
|
2852
|
+
var opts = getVisibleOptions();
|
|
2853
|
+
if (idx >= 0 && idx < opts.length) {
|
|
2854
|
+
highlighted = idx;
|
|
2855
|
+
opts[idx].classList.add('flm-tagpicker-option--highlighted');
|
|
2856
|
+
opts[idx].scrollIntoView({ block: 'nearest' });
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
function filterOptions() {
|
|
2861
|
+
var text = input.value.toLowerCase();
|
|
2862
|
+
var allOpts = listbox.querySelectorAll('.flm-tagpicker-option');
|
|
2863
|
+
for (var i = 0; i < allOpts.length; i++) {
|
|
2864
|
+
var optText = allOpts[i].textContent.toLowerCase();
|
|
2865
|
+
if (text === '' || optText.indexOf(text) !== -1) {
|
|
2866
|
+
allOpts[i].classList.remove('flm-tagpicker-option--hidden');
|
|
2867
|
+
} else {
|
|
2868
|
+
allOpts[i].classList.add('flm-tagpicker-option--hidden');
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
highlighted = -1;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function getMaxTags() {
|
|
2875
|
+
var max = el.getAttribute('data-max-tags');
|
|
2876
|
+
return max ? parseInt(max, 10) : 0;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function getChips() {
|
|
2880
|
+
return well.querySelectorAll('.flm-tagpicker-chip');
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function updateSelectedValues() {
|
|
2884
|
+
var chips = getChips();
|
|
2885
|
+
var values = [];
|
|
2886
|
+
for (var i = 0; i < chips.length; i++) {
|
|
2887
|
+
values.push(chips[i].getAttribute('data-value'));
|
|
2888
|
+
}
|
|
2889
|
+
el.setAttribute('data-selected-values', values.join(','));
|
|
2890
|
+
|
|
2891
|
+
// Fire change event
|
|
2892
|
+
var evt = document.createEvent('Event');
|
|
2893
|
+
evt.initEvent('change', true, true);
|
|
2894
|
+
el.dispatchEvent(evt);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function addChip(opt) {
|
|
2898
|
+
var max = getMaxTags();
|
|
2899
|
+
if (max > 0 && getChips().length >= max) return;
|
|
2900
|
+
|
|
2901
|
+
var value = opt.getAttribute('data-value') || opt.textContent.trim();
|
|
2902
|
+
var text = opt.getAttribute('data-value') ? opt.textContent.trim() : value;
|
|
2903
|
+
|
|
2904
|
+
// For people picker, try to get just the name
|
|
2905
|
+
var nameEl = opt.querySelector('.flm-tagpicker-option-name');
|
|
2906
|
+
if (nameEl) {
|
|
2907
|
+
text = nameEl.textContent.trim();
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
var chip = document.createElement('span');
|
|
2911
|
+
chip.className = 'flm-tagpicker-chip';
|
|
2912
|
+
chip.setAttribute('data-value', value);
|
|
2913
|
+
|
|
2914
|
+
// People variant: add small coin
|
|
2915
|
+
if (isPeople) {
|
|
2916
|
+
var initials = opt.getAttribute('data-initials') || '';
|
|
2917
|
+
if (initials) {
|
|
2918
|
+
var coin = document.createElement('span');
|
|
2919
|
+
coin.className = 'flm-tagpicker-chip-coin';
|
|
2920
|
+
coin.textContent = initials;
|
|
2921
|
+
chip.appendChild(coin);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
var textSpan = document.createElement('span');
|
|
2926
|
+
textSpan.className = 'flm-tagpicker-chip-text';
|
|
2927
|
+
textSpan.textContent = text;
|
|
2928
|
+
chip.appendChild(textSpan);
|
|
2929
|
+
|
|
2930
|
+
var removeBtn = document.createElement('button');
|
|
2931
|
+
removeBtn.className = 'flm-tagpicker-chip-remove';
|
|
2932
|
+
removeBtn.setAttribute('aria-label', 'Remove ' + text);
|
|
2933
|
+
removeBtn.type = 'button';
|
|
2934
|
+
removeBtn.innerHTML = '<svg viewBox="0 0 8 8" xmlns="http://www.w3.org/2000/svg"><path d="M1.17.46L4 3.3 6.83.46a.5.5 0 0 1 .71.71L4.7 4l2.84 2.83a.5.5 0 0 1-.71.71L4 4.7 1.17 7.54a.5.5 0 0 1-.71-.71L3.3 4 .46 1.17A.5.5 0 0 1 1.17.46z"/></svg>';
|
|
2935
|
+
chip.appendChild(removeBtn);
|
|
2936
|
+
|
|
2937
|
+
// Insert chip before input
|
|
2938
|
+
well.insertBefore(chip, input);
|
|
2939
|
+
|
|
2940
|
+
// Mark option as selected
|
|
2941
|
+
opt.classList.add('flm-tagpicker-option--selected');
|
|
2942
|
+
|
|
2943
|
+
// Wire remove button
|
|
2944
|
+
removeBtn.addEventListener('click', function (e) {
|
|
2945
|
+
e.stopPropagation();
|
|
2946
|
+
removeChip(chip, opt);
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
input.value = '';
|
|
2950
|
+
filterOptions();
|
|
2951
|
+
updateSelectedValues();
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
function removeChip(chip, opt) {
|
|
2955
|
+
chip.parentNode.removeChild(chip);
|
|
2956
|
+
opt.classList.remove('flm-tagpicker-option--selected');
|
|
2957
|
+
updateSelectedValues();
|
|
2958
|
+
input.focus();
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// Click on well focuses input
|
|
2962
|
+
well.addEventListener('click', function () {
|
|
2963
|
+
input.focus();
|
|
2964
|
+
});
|
|
2965
|
+
|
|
2966
|
+
// Input events
|
|
2967
|
+
input.addEventListener('focus', function () {
|
|
2968
|
+
if (!isOpen()) {
|
|
2969
|
+
filterOptions();
|
|
2970
|
+
open();
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
input.addEventListener('input', function () {
|
|
2975
|
+
if (!isOpen()) open();
|
|
2976
|
+
filterOptions();
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
// Keyboard navigation
|
|
2980
|
+
input.addEventListener('keydown', function (e) {
|
|
2981
|
+
var opts = getVisibleOptions();
|
|
2982
|
+
var len = opts.length;
|
|
2983
|
+
|
|
2984
|
+
if (e.key === 'ArrowDown' || e.keyCode === 40) {
|
|
2985
|
+
e.preventDefault();
|
|
2986
|
+
if (!isOpen()) { filterOptions(); open(); }
|
|
2987
|
+
setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
|
|
2988
|
+
} else if (e.key === 'ArrowUp' || e.keyCode === 38) {
|
|
2989
|
+
e.preventDefault();
|
|
2990
|
+
if (!isOpen()) { filterOptions(); open(); }
|
|
2991
|
+
setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
|
|
2992
|
+
} else if (e.key === 'Enter' || e.keyCode === 13) {
|
|
2993
|
+
e.preventDefault();
|
|
2994
|
+
if (highlighted >= 0 && highlighted < len) {
|
|
2995
|
+
addChip(opts[highlighted]);
|
|
2996
|
+
highlighted = -1;
|
|
2997
|
+
}
|
|
2998
|
+
} else if (e.key === 'Escape' || e.keyCode === 27) {
|
|
2999
|
+
close();
|
|
3000
|
+
} else if ((e.key === 'Backspace' || e.keyCode === 8) && input.value === '') {
|
|
3001
|
+
// Remove last chip on backspace with empty input
|
|
3002
|
+
var chips = getChips();
|
|
3003
|
+
if (chips.length > 0) {
|
|
3004
|
+
var lastChip = chips[chips.length - 1];
|
|
3005
|
+
var chipValue = lastChip.getAttribute('data-value');
|
|
3006
|
+
// Find corresponding option
|
|
3007
|
+
var allOpts = listbox.querySelectorAll('.flm-tagpicker-option');
|
|
3008
|
+
for (var i = 0; i < allOpts.length; i++) {
|
|
3009
|
+
var optVal = allOpts[i].getAttribute('data-value') || allOpts[i].textContent.trim();
|
|
3010
|
+
if (optVal === chipValue) {
|
|
3011
|
+
removeChip(lastChip, allOpts[i]);
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
// Option click
|
|
3020
|
+
listbox.addEventListener('click', function (e) {
|
|
3021
|
+
var opt = e.target.closest('.flm-tagpicker-option');
|
|
3022
|
+
if (opt && !opt.classList.contains('flm-tagpicker-option--selected')) {
|
|
3023
|
+
addChip(opt);
|
|
3024
|
+
input.focus();
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
|
|
3028
|
+
// Click outside
|
|
3029
|
+
document.addEventListener('click', function (e) {
|
|
3030
|
+
if (!el.contains(e.target) && isOpen()) {
|
|
3031
|
+
close();
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
el.setAttribute('data-tagpicker-wired', 'true');
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
return { init: init };
|
|
3039
|
+
})();
|
|
3040
|
+
|
|
3041
|
+
/**
|
|
3042
|
+
* TeachingBubble component JS — positioned inverted callout.
|
|
3043
|
+
*
|
|
3044
|
+
* Usage:
|
|
3045
|
+
* <button data-teachingbubble-toggle="tb1">Learn more</button>
|
|
3046
|
+
* <div class="flm-teachingbubble" id="tb1">
|
|
3047
|
+
* <div class="flm-teachingbubble-beak"></div>
|
|
3048
|
+
* <div class="flm-teachingbubble-header">
|
|
3049
|
+
* <h3 class="flm-teachingbubble-headline">Title</h3>
|
|
3050
|
+
* <button class="flm-teachingbubble-close" data-icon="Cancel" aria-label="Close"></button>
|
|
3051
|
+
* </div>
|
|
3052
|
+
* <div class="flm-teachingbubble-body">Body text</div>
|
|
3053
|
+
* <div class="flm-teachingbubble-footer">
|
|
3054
|
+
* <button class="flm-button">Got it</button>
|
|
3055
|
+
* </div>
|
|
3056
|
+
* </div>
|
|
3057
|
+
*/
|
|
3058
|
+
var FluentLMTeachingBubbleComponent = (function () {
|
|
3059
|
+
'use strict';
|
|
3060
|
+
|
|
3061
|
+
function init(root) {
|
|
3062
|
+
var doc = root || document;
|
|
3063
|
+
|
|
3064
|
+
var triggers = doc.querySelectorAll('[data-teachingbubble-toggle]');
|
|
3065
|
+
for (var i = 0; i < triggers.length; i++) {
|
|
3066
|
+
// Skip coachmarks — they wire their own click handler
|
|
3067
|
+
if (triggers[i].classList.contains('flm-coachmark')) continue;
|
|
3068
|
+
wireTrigger(triggers[i]);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// Wire close buttons
|
|
3072
|
+
var closeBtns = doc.querySelectorAll('.flm-teachingbubble-close');
|
|
3073
|
+
for (var j = 0; j < closeBtns.length; j++) {
|
|
3074
|
+
wireClose(closeBtns[j]);
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// Wire footer buttons that should dismiss
|
|
3078
|
+
var footerBtns = doc.querySelectorAll('.flm-teachingbubble-footer .flm-button');
|
|
3079
|
+
for (var k = 0; k < footerBtns.length; k++) {
|
|
3080
|
+
wireFooterBtn(footerBtns[k]);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
function wireTrigger(btn) {
|
|
3085
|
+
if (btn.getAttribute('data-teachingbubble-wired')) return;
|
|
3086
|
+
|
|
3087
|
+
btn.addEventListener('click', function (e) {
|
|
3088
|
+
var id = btn.getAttribute('data-teachingbubble-toggle');
|
|
3089
|
+
var bubble = document.getElementById(id);
|
|
3090
|
+
if (!bubble) return;
|
|
3091
|
+
|
|
3092
|
+
if (bubble.classList.contains('flm-teachingbubble--visible')) {
|
|
3093
|
+
hide(bubble);
|
|
3094
|
+
} else {
|
|
3095
|
+
show(bubble, btn);
|
|
3096
|
+
}
|
|
3097
|
+
e.stopPropagation();
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
btn.setAttribute('data-teachingbubble-wired', 'true');
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
function wireClose(btn) {
|
|
3104
|
+
if (btn.getAttribute('data-teachingbubble-close-wired')) return;
|
|
3105
|
+
|
|
3106
|
+
btn.addEventListener('click', function () {
|
|
3107
|
+
var bubble = btn.closest('.flm-teachingbubble');
|
|
3108
|
+
if (bubble) hide(bubble);
|
|
3109
|
+
});
|
|
3110
|
+
|
|
3111
|
+
btn.setAttribute('data-teachingbubble-close-wired', 'true');
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
function wireFooterBtn(btn) {
|
|
3115
|
+
if (btn.getAttribute('data-teachingbubble-footer-wired')) return;
|
|
3116
|
+
|
|
3117
|
+
btn.addEventListener('click', function () {
|
|
3118
|
+
var bubble = btn.closest('.flm-teachingbubble');
|
|
3119
|
+
if (bubble) hide(bubble);
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3122
|
+
btn.setAttribute('data-teachingbubble-footer-wired', 'true');
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
function show(bubble, target) {
|
|
3126
|
+
var rect = target.getBoundingClientRect();
|
|
3127
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
3128
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
3129
|
+
|
|
3130
|
+
// Account for positioned offset parent so absolute coords are correct
|
|
3131
|
+
var offsetX = 0;
|
|
3132
|
+
var offsetY = 0;
|
|
3133
|
+
var offsetParent = bubble.offsetParent;
|
|
3134
|
+
if (offsetParent && offsetParent !== document.body && offsetParent !== document.documentElement) {
|
|
3135
|
+
var parentRect = offsetParent.getBoundingClientRect();
|
|
3136
|
+
offsetX = parentRect.left + scrollX;
|
|
3137
|
+
offsetY = parentRect.top + scrollY;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
bubble.style.position = 'absolute';
|
|
3141
|
+
bubble.style.left = (rect.left + scrollX - offsetX) + 'px';
|
|
3142
|
+
bubble.style.top = (rect.bottom + scrollY - offsetY + 8) + 'px';
|
|
3143
|
+
|
|
3144
|
+
bubble.classList.add('flm-teachingbubble--visible');
|
|
3145
|
+
bubble.classList.remove('flm-teachingbubble--above');
|
|
3146
|
+
|
|
3147
|
+
// Position beak to point at target center
|
|
3148
|
+
setTimeout(function () {
|
|
3149
|
+
var bRect = bubble.getBoundingClientRect();
|
|
3150
|
+
var beak = bubble.querySelector('.flm-teachingbubble-beak');
|
|
3151
|
+
if (beak) {
|
|
3152
|
+
var targetCenterX = rect.left + rect.width / 2;
|
|
3153
|
+
var beakLeft = targetCenterX - bRect.left - 8; // 8 = half of 16px beak
|
|
3154
|
+
beakLeft = Math.max(8, Math.min(beakLeft, bRect.width - 24));
|
|
3155
|
+
beak.style.left = beakLeft + 'px';
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// Flip above if off-screen
|
|
3159
|
+
if (bRect.bottom > window.innerHeight) {
|
|
3160
|
+
bubble.classList.add('flm-teachingbubble--above');
|
|
3161
|
+
bubble.style.top = (rect.top + scrollY - offsetY - bRect.height - 8) + 'px';
|
|
3162
|
+
}
|
|
3163
|
+
}, 0);
|
|
3164
|
+
|
|
3165
|
+
// Click outside to dismiss
|
|
3166
|
+
var outsideHandler = function (e) {
|
|
3167
|
+
if (!bubble.contains(e.target) && !target.contains(e.target)) {
|
|
3168
|
+
hide(bubble);
|
|
3169
|
+
document.removeEventListener('click', outsideHandler);
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
setTimeout(function () {
|
|
3173
|
+
document.addEventListener('click', outsideHandler);
|
|
3174
|
+
}, 0);
|
|
3175
|
+
bubble._outsideHandler = outsideHandler;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function hide(bubble) {
|
|
3179
|
+
bubble.classList.remove('flm-teachingbubble--visible', 'flm-teachingbubble--above');
|
|
3180
|
+
if (bubble._outsideHandler) {
|
|
3181
|
+
document.removeEventListener('click', bubble._outsideHandler);
|
|
3182
|
+
delete bubble._outsideHandler;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
return { init: init, show: show, hide: hide };
|
|
3187
|
+
})();
|
|
3188
|
+
|
|
3189
|
+
/**
|
|
3190
|
+
* TimePicker component JS — scrollable time-slot dropdown with filtering.
|
|
3191
|
+
*
|
|
3192
|
+
* Usage:
|
|
3193
|
+
* <div class="flm-timepicker" data-increment="30" data-use-12h data-min-time="09:00" data-max-time="17:00">
|
|
3194
|
+
* <label class="flm-label" for="tp1">Time</label>
|
|
3195
|
+
* <div class="flm-timepicker-wrapper">
|
|
3196
|
+
* <input class="flm-timepicker-input" id="tp1" placeholder="Select a time…">
|
|
3197
|
+
* <button class="flm-timepicker-icon" data-icon="Clock" aria-label="Open time picker"></button>
|
|
3198
|
+
* </div>
|
|
3199
|
+
* </div>
|
|
3200
|
+
*
|
|
3201
|
+
* Attributes on .flm-timepicker:
|
|
3202
|
+
* data-increment="30" — minute increment (default 30)
|
|
3203
|
+
* data-use-12h — 12-hour format with AM/PM (default 24h)
|
|
3204
|
+
* data-min-time="09:00" — earliest available time (HH:MM, 24h)
|
|
3205
|
+
* data-max-time="17:00" — latest available time (HH:MM, 24h)
|
|
3206
|
+
*/
|
|
3207
|
+
var FluentLMTimePickerComponent = (function () {
|
|
3208
|
+
'use strict';
|
|
3209
|
+
|
|
3210
|
+
function init(root) {
|
|
3211
|
+
var doc = root || document;
|
|
3212
|
+
var pickers = doc.querySelectorAll('.flm-timepicker');
|
|
3213
|
+
for (var i = 0; i < pickers.length; i++) {
|
|
3214
|
+
wirePicker(pickers[i]);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
function wirePicker(el) {
|
|
3219
|
+
if (el.getAttribute('data-timepicker-wired')) return;
|
|
3220
|
+
|
|
3221
|
+
var input = el.querySelector('.flm-timepicker-input');
|
|
3222
|
+
var iconBtn = el.querySelector('.flm-timepicker-icon');
|
|
3223
|
+
|
|
3224
|
+
if (!input) return;
|
|
3225
|
+
|
|
3226
|
+
// Configuration
|
|
3227
|
+
var increment = parseInt(el.getAttribute('data-increment'), 10) || 30;
|
|
3228
|
+
var use12h = el.hasAttribute('data-use-12h');
|
|
3229
|
+
var minTime = parseTime(el.getAttribute('data-min-time'));
|
|
3230
|
+
var maxTime = parseTime(el.getAttribute('data-max-time'));
|
|
3231
|
+
|
|
3232
|
+
var highlighted = -1;
|
|
3233
|
+
|
|
3234
|
+
// Create listbox
|
|
3235
|
+
var listbox = document.createElement('div');
|
|
3236
|
+
listbox.className = 'flm-timepicker-listbox';
|
|
3237
|
+
el.appendChild(listbox);
|
|
3238
|
+
|
|
3239
|
+
// Generate options
|
|
3240
|
+
generateOptions();
|
|
3241
|
+
|
|
3242
|
+
function parseTime(str) {
|
|
3243
|
+
if (!str) return null;
|
|
3244
|
+
var parts = str.split(':');
|
|
3245
|
+
return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
function padTwo(n) {
|
|
3249
|
+
return n < 10 ? '0' + n : '' + n;
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function formatTime(totalMinutes) {
|
|
3253
|
+
var h = Math.floor(totalMinutes / 60) % 24;
|
|
3254
|
+
var m = totalMinutes % 60;
|
|
3255
|
+
if (use12h) {
|
|
3256
|
+
var period = h < 12 ? 'AM' : 'PM';
|
|
3257
|
+
var h12 = h % 12;
|
|
3258
|
+
if (h12 === 0) h12 = 12;
|
|
3259
|
+
return h12 + ':' + padTwo(m) + ' ' + period;
|
|
3260
|
+
}
|
|
3261
|
+
return padTwo(h) + ':' + padTwo(m);
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
function generateOptions() {
|
|
3265
|
+
listbox.innerHTML = '';
|
|
3266
|
+
for (var t = 0; t < 1440; t += increment) {
|
|
3267
|
+
if (minTime !== null && t < minTime) continue;
|
|
3268
|
+
if (maxTime !== null && t > maxTime) continue;
|
|
3269
|
+
var opt = document.createElement('div');
|
|
3270
|
+
opt.className = 'flm-timepicker-option';
|
|
3271
|
+
opt.setAttribute('data-value', padTwo(Math.floor(t / 60)) + ':' + padTwo(t % 60));
|
|
3272
|
+
opt.textContent = formatTime(t);
|
|
3273
|
+
listbox.appendChild(opt);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
function getOptions() {
|
|
3278
|
+
return listbox.querySelectorAll('.flm-timepicker-option:not(.flm-timepicker-option--hidden)');
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function isOpen() {
|
|
3282
|
+
return listbox.classList.contains('flm-timepicker-listbox--open');
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function open() {
|
|
3286
|
+
document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
|
|
3287
|
+
listbox.classList.add('flm-timepicker-listbox--open');
|
|
3288
|
+
highlighted = -1;
|
|
3289
|
+
flipIfNeeded();
|
|
3290
|
+
scrollToSelected();
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
function close() {
|
|
3294
|
+
listbox.classList.remove('flm-timepicker-listbox--open', 'flm-timepicker-listbox--above');
|
|
3295
|
+
highlighted = -1;
|
|
3296
|
+
clearHighlight();
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function flipIfNeeded() {
|
|
3300
|
+
setTimeout(function () {
|
|
3301
|
+
var rect = listbox.getBoundingClientRect();
|
|
3302
|
+
if (rect.bottom > window.innerHeight) {
|
|
3303
|
+
listbox.classList.add('flm-timepicker-listbox--above');
|
|
3304
|
+
} else {
|
|
3305
|
+
listbox.classList.remove('flm-timepicker-listbox--above');
|
|
3306
|
+
}
|
|
3307
|
+
}, 0);
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function scrollToSelected() {
|
|
3311
|
+
setTimeout(function () {
|
|
3312
|
+
// Scroll to selected option, or nearest-to-current-time option
|
|
3313
|
+
var selected = listbox.querySelector('.flm-timepicker-option--selected');
|
|
3314
|
+
if (selected) {
|
|
3315
|
+
selected.scrollIntoView({ block: 'nearest' });
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
// Find nearest to current time
|
|
3319
|
+
var now = new Date();
|
|
3320
|
+
var nowMinutes = now.getHours() * 60 + now.getMinutes();
|
|
3321
|
+
var opts = getOptions();
|
|
3322
|
+
var bestOpt = null;
|
|
3323
|
+
var bestDiff = Infinity;
|
|
3324
|
+
for (var i = 0; i < opts.length; i++) {
|
|
3325
|
+
var val = parseTime(opts[i].getAttribute('data-value'));
|
|
3326
|
+
var diff = Math.abs(val - nowMinutes);
|
|
3327
|
+
if (diff < bestDiff) {
|
|
3328
|
+
bestDiff = diff;
|
|
3329
|
+
bestOpt = opts[i];
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (bestOpt) {
|
|
3333
|
+
bestOpt.scrollIntoView({ block: 'nearest' });
|
|
3334
|
+
}
|
|
3335
|
+
}, 0);
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
function clearHighlight() {
|
|
3339
|
+
var opts = listbox.querySelectorAll('.flm-timepicker-option--highlighted');
|
|
3340
|
+
for (var i = 0; i < opts.length; i++) {
|
|
3341
|
+
opts[i].classList.remove('flm-timepicker-option--highlighted');
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function setHighlight(idx) {
|
|
3346
|
+
clearHighlight();
|
|
3347
|
+
var opts = getOptions();
|
|
3348
|
+
if (idx >= 0 && idx < opts.length) {
|
|
3349
|
+
highlighted = idx;
|
|
3350
|
+
opts[idx].classList.add('flm-timepicker-option--highlighted');
|
|
3351
|
+
opts[idx].scrollIntoView({ block: 'nearest' });
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function filterOptions() {
|
|
3356
|
+
var text = input.value.toLowerCase();
|
|
3357
|
+
var allOpts = listbox.querySelectorAll('.flm-timepicker-option');
|
|
3358
|
+
for (var i = 0; i < allOpts.length; i++) {
|
|
3359
|
+
var optText = allOpts[i].textContent.toLowerCase();
|
|
3360
|
+
if (text === '' || optText.indexOf(text) !== -1) {
|
|
3361
|
+
allOpts[i].classList.remove('flm-timepicker-option--hidden');
|
|
3362
|
+
} else {
|
|
3363
|
+
allOpts[i].classList.add('flm-timepicker-option--hidden');
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
highlighted = -1;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function selectOption(opt) {
|
|
3370
|
+
var value = opt.getAttribute('data-value');
|
|
3371
|
+
var text = opt.textContent;
|
|
3372
|
+
|
|
3373
|
+
// Clear previous selection
|
|
3374
|
+
var prev = listbox.querySelectorAll('.flm-timepicker-option--selected');
|
|
3375
|
+
for (var j = 0; j < prev.length; j++) {
|
|
3376
|
+
prev[j].classList.remove('flm-timepicker-option--selected');
|
|
3377
|
+
}
|
|
3378
|
+
opt.classList.add('flm-timepicker-option--selected');
|
|
3379
|
+
input.value = text;
|
|
3380
|
+
el.setAttribute('data-value', value);
|
|
3381
|
+
close();
|
|
3382
|
+
|
|
3383
|
+
// Reset filter so all options are visible next time
|
|
3384
|
+
filterOptions();
|
|
3385
|
+
|
|
3386
|
+
// Fire change event
|
|
3387
|
+
var evt = document.createEvent('Event');
|
|
3388
|
+
evt.initEvent('change', true, true);
|
|
3389
|
+
input.dispatchEvent(evt);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// Input events
|
|
3393
|
+
input.addEventListener('focus', function () {
|
|
3394
|
+
if (!isOpen()) open();
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
input.addEventListener('input', function () {
|
|
3398
|
+
if (!isOpen()) open();
|
|
3399
|
+
filterOptions();
|
|
3400
|
+
});
|
|
3401
|
+
|
|
3402
|
+
// Icon toggle
|
|
3403
|
+
if (iconBtn) {
|
|
3404
|
+
iconBtn.addEventListener('click', function (e) {
|
|
3405
|
+
e.stopPropagation();
|
|
3406
|
+
if (isOpen()) {
|
|
3407
|
+
close();
|
|
3408
|
+
} else {
|
|
3409
|
+
filterOptions();
|
|
3410
|
+
open();
|
|
3411
|
+
input.focus();
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
// Keyboard navigation
|
|
3417
|
+
input.addEventListener('keydown', function (e) {
|
|
3418
|
+
var opts = getOptions();
|
|
3419
|
+
var len = opts.length;
|
|
3420
|
+
|
|
3421
|
+
if (e.key === 'ArrowDown' || e.keyCode === 40) {
|
|
3422
|
+
e.preventDefault();
|
|
3423
|
+
if (!isOpen()) { open(); filterOptions(); }
|
|
3424
|
+
setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
|
|
3425
|
+
} else if (e.key === 'ArrowUp' || e.keyCode === 38) {
|
|
3426
|
+
e.preventDefault();
|
|
3427
|
+
if (!isOpen()) { open(); filterOptions(); }
|
|
3428
|
+
setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
|
|
3429
|
+
} else if (e.key === 'Enter' || e.keyCode === 13) {
|
|
3430
|
+
e.preventDefault();
|
|
3431
|
+
if (highlighted >= 0 && highlighted < len) {
|
|
3432
|
+
selectOption(opts[highlighted]);
|
|
3433
|
+
}
|
|
3434
|
+
} else if (e.key === 'Escape' || e.keyCode === 27) {
|
|
3435
|
+
close();
|
|
3436
|
+
}
|
|
3437
|
+
});
|
|
3438
|
+
|
|
3439
|
+
// Option click
|
|
3440
|
+
listbox.addEventListener('click', function (e) {
|
|
3441
|
+
var opt = e.target.closest('.flm-timepicker-option');
|
|
3442
|
+
if (opt) {
|
|
3443
|
+
selectOption(opt);
|
|
3444
|
+
}
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
// Click outside
|
|
3448
|
+
document.addEventListener('click', function (e) {
|
|
3449
|
+
if (!el.contains(e.target) && isOpen()) {
|
|
3450
|
+
close();
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
|
|
3454
|
+
// Close when another picker opens
|
|
3455
|
+
document.addEventListener('flm-dismiss-pickers', function (e) {
|
|
3456
|
+
if (e.detail.source !== el && isOpen()) close();
|
|
3457
|
+
});
|
|
3458
|
+
|
|
3459
|
+
el.setAttribute('data-timepicker-wired', 'true');
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
return { init: init };
|
|
3463
|
+
})();
|
|
3464
|
+
|
|
3465
|
+
/**
|
|
3466
|
+
* Toggle component JS — initializes state text from data-on / data-off.
|
|
3467
|
+
* The CSS handles display via content: attr(data-on) / attr(data-off)
|
|
3468
|
+
* using the :checked sibling selector, so this module only needs to
|
|
3469
|
+
* handle any initial ARIA setup.
|
|
3470
|
+
*/
|
|
3471
|
+
var FluentLMToggleComponent = (function () {
|
|
3472
|
+
'use strict';
|
|
3473
|
+
|
|
3474
|
+
function init(root) {
|
|
3475
|
+
var els = (root || document).querySelectorAll('.flm-toggle');
|
|
3476
|
+
for (var i = 0; i < els.length; i++) {
|
|
3477
|
+
render(els[i]);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
function render(el) {
|
|
3482
|
+
var input = el.querySelector('.flm-toggle-input');
|
|
3483
|
+
if (!input) return;
|
|
3484
|
+
|
|
3485
|
+
// Set initial ARIA
|
|
3486
|
+
input.setAttribute('role', 'switch');
|
|
3487
|
+
input.setAttribute('aria-checked', input.checked ? 'true' : 'false');
|
|
3488
|
+
|
|
3489
|
+
// Keep aria-checked in sync
|
|
3490
|
+
input.addEventListener('change', function () {
|
|
3491
|
+
input.setAttribute('aria-checked', input.checked ? 'true' : 'false');
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
return { init: init };
|
|
3496
|
+
})();
|
|
3497
|
+
|
|
3498
|
+
/**
|
|
3499
|
+
* Tooltip component JS — shows tooltip on hover/focus of host elements.
|
|
3500
|
+
*
|
|
3501
|
+
* Usage: <span class="flm-tooltip-host" data-tooltip="Help text">Hover me</span>
|
|
3502
|
+
* Or: <span class="flm-tooltip-host" data-tooltip-id="my-tooltip">Hover me</span>
|
|
3503
|
+
* <div id="my-tooltip" class="flm-tooltip">Rich tooltip content</div>
|
|
3504
|
+
*/
|
|
3505
|
+
var FluentLMTooltipComponent = (function () {
|
|
3506
|
+
'use strict';
|
|
3507
|
+
|
|
3508
|
+
var activeTooltip = null;
|
|
3509
|
+
var showDelay = 300;
|
|
3510
|
+
|
|
3511
|
+
function init(root) {
|
|
3512
|
+
var doc = root || document;
|
|
3513
|
+
|
|
3514
|
+
var hosts = doc.querySelectorAll('.flm-tooltip-host, [data-tooltip]');
|
|
3515
|
+
for (var i = 0; i < hosts.length; i++) {
|
|
3516
|
+
wireHost(hosts[i]);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
function wireHost(host) {
|
|
3521
|
+
if (host.getAttribute('data-tooltip-wired')) return;
|
|
3522
|
+
|
|
3523
|
+
var timer = null;
|
|
3524
|
+
|
|
3525
|
+
host.addEventListener('mouseenter', function () {
|
|
3526
|
+
timer = setTimeout(function () { showForHost(host); }, showDelay);
|
|
3527
|
+
});
|
|
3528
|
+
|
|
3529
|
+
host.addEventListener('mouseleave', function () {
|
|
3530
|
+
clearTimeout(timer);
|
|
3531
|
+
hideActive();
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
host.addEventListener('focus', function () {
|
|
3535
|
+
timer = setTimeout(function () { showForHost(host); }, showDelay);
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
host.addEventListener('blur', function () {
|
|
3539
|
+
clearTimeout(timer);
|
|
3540
|
+
hideActive();
|
|
3541
|
+
});
|
|
3542
|
+
|
|
3543
|
+
host.setAttribute('data-tooltip-wired', 'true');
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
function showForHost(host) {
|
|
3547
|
+
hideActive();
|
|
3548
|
+
|
|
3549
|
+
var tooltip;
|
|
3550
|
+
var tooltipId = host.getAttribute('data-tooltip-id');
|
|
3551
|
+
|
|
3552
|
+
if (tooltipId) {
|
|
3553
|
+
tooltip = document.getElementById(tooltipId);
|
|
3554
|
+
} else {
|
|
3555
|
+
// Create a dynamic tooltip from data-tooltip text
|
|
3556
|
+
var text = host.getAttribute('data-tooltip');
|
|
3557
|
+
if (!text) return;
|
|
3558
|
+
|
|
3559
|
+
tooltip = document.createElement('div');
|
|
3560
|
+
tooltip.className = 'flm-tooltip';
|
|
3561
|
+
tooltip.textContent = text;
|
|
3562
|
+
tooltip._dynamic = true;
|
|
3563
|
+
document.body.appendChild(tooltip);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
if (!tooltip) return;
|
|
3567
|
+
|
|
3568
|
+
// Position below host
|
|
3569
|
+
var rect = host.getBoundingClientRect();
|
|
3570
|
+
var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
|
3571
|
+
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
|
3572
|
+
|
|
3573
|
+
tooltip.style.position = 'absolute';
|
|
3574
|
+
tooltip.style.left = rect.left + scrollX + 'px';
|
|
3575
|
+
tooltip.style.top = (rect.bottom + scrollY + 4) + 'px';
|
|
3576
|
+
tooltip.classList.add('flm-tooltip--visible');
|
|
3577
|
+
|
|
3578
|
+
activeTooltip = tooltip;
|
|
3579
|
+
|
|
3580
|
+
// Flip if off-screen
|
|
3581
|
+
setTimeout(function () {
|
|
3582
|
+
var tRect = tooltip.getBoundingClientRect();
|
|
3583
|
+
if (tRect.bottom > window.innerHeight) {
|
|
3584
|
+
tooltip.style.top = (rect.top + scrollY - tRect.height - 4) + 'px';
|
|
3585
|
+
}
|
|
3586
|
+
if (tRect.right > window.innerWidth) {
|
|
3587
|
+
tooltip.style.left = (rect.right + scrollX - tRect.width) + 'px';
|
|
3588
|
+
}
|
|
3589
|
+
}, 0);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function hideActive() {
|
|
3593
|
+
if (!activeTooltip) return;
|
|
3594
|
+
activeTooltip.classList.remove('flm-tooltip--visible');
|
|
3595
|
+
if (activeTooltip._dynamic) {
|
|
3596
|
+
activeTooltip.parentNode.removeChild(activeTooltip);
|
|
3597
|
+
}
|
|
3598
|
+
activeTooltip = null;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
return { init: init };
|
|
3602
|
+
})();
|