@leed-ai/cli 0.0.0-stage → 6.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +57 -0
- package/README.md +125 -2
- package/dist/LICENSE.md +57 -0
- package/dist/acorn-DAgDT1om.js +5087 -0
- package/dist/angular-l0_4PK8N.js +3598 -0
- package/dist/authShared-Dray38Ls.js +1161 -0
- package/dist/babel-CJeRQd5u.js +10038 -0
- package/dist/buildResults-tiq1nugP.js +21180 -0
- package/dist/chunk-1VJ9lAw7.js +42 -0
- package/dist/cliAuth-38JjD6hw.js +15 -0
- package/dist/env-B0bHiXJJ.js +29 -0
- package/dist/errors-CqKamGCu.js +481 -0
- package/dist/estree-E8WG8UHj.js +6930 -0
- package/dist/flow-6xaRIbPQ.js +52255 -0
- package/dist/glimmer-aJPjsfbT.js +7272 -0
- package/dist/graphql-mcgtPEiZ.js +1901 -0
- package/dist/html--eeea3Kl.js +6553 -0
- package/dist/leed.eleventy.mjs +6525 -0
- package/dist/leed.mjs +88952 -0
- package/dist/leed.sources.json +798 -0
- package/dist/leedLogger-Zkada5aO.js +78 -0
- package/dist/markdown-D73jNKwx.js +6836 -0
- package/dist/meriyah-CMp1nDDW.js +8932 -0
- package/dist/package.json +101 -0
- package/dist/postcss-h4fBr2Fn.js +6760 -0
- package/dist/prettier-CwFSKxfN.js +16821 -0
- package/dist/static/customer-site-worker/package.json +9 -0
- package/dist/static/customer-site-worker/src/assetsHydration.ts +55 -0
- package/dist/static/customer-site-worker/src/constants.ts +20 -0
- package/dist/static/customer-site-worker/src/cors.ts +120 -0
- package/dist/static/customer-site-worker/src/index.ts +242 -0
- package/dist/static/customer-site-worker/src/mcp-proxy.ts +98 -0
- package/dist/static/customer-site-worker/src/proxy.ts +92 -0
- package/dist/static/customer-site-worker/src/proxyHeaders.ts +47 -0
- package/dist/static/customer-site-worker/src/tier.ts +40 -0
- package/dist/static/customer-site-worker/src/types.ts +82 -0
- package/dist/static/customer-site-worker/test/functions-mock.worker.js +50 -0
- package/dist/static/customer-site-worker/tsconfig.json +19 -0
- package/dist/static/customer-site-worker/vitest.workers.config.ts +61 -0
- package/dist/static/install/husky/colors.mjs +5 -0
- package/dist/static/install/husky/hooks/commit-msg +1 -0
- package/dist/static/install/husky/hooks/pre-commit +1 -0
- package/dist/static/install/husky/hooks/pre-push +1 -0
- package/dist/static/install/husky/valid-command.mjs +29 -0
- package/dist/static/install/husky/valid-commit-msg.mjs +20 -0
- package/dist/static/install/package.json +10 -0
- package/dist/static/install/packages.txt +4 -0
- package/dist/static/install/tasks.json +111 -0
- package/dist/static/javascript/detectIncognito.js +195 -0
- package/dist/static/javascript/documentation.js +2781 -0
- package/dist/static/javascript/fp/fp.esm.js +2896 -0
- package/dist/static/javascript/fp/fp.js +3067 -0
- package/dist/static/javascript/live-search.js +986 -0
- package/dist/static/javascript/lunr-search.js +843 -0
- package/dist/static/javascript/timeme.js +451 -0
- package/dist/static/javascript/tracker/webmcp.js +67 -0
- package/dist/static/javascript/tracker/whisper.js +1 -0
- package/dist/static/javascript/utilities.js +819 -0
- package/dist/static/javascript/versions.json +74 -0
- package/dist/static/javascript/zoomable.js +488 -0
- package/dist/static/site/_data/eleventyComputed.js +228 -0
- package/dist/static/site/_data/leed-form-data.json +253 -0
- package/dist/static/site/_includes/leed/api/bits/array-type.hbs +24 -0
- package/dist/static/site/_includes/leed/api/bits/badges.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/description.hbs +9 -0
- package/dist/static/site/_includes/leed/api/bits/examples.hbs +42 -0
- package/dist/static/site/_includes/leed/api/bits/scopes.hbs +28 -0
- package/dist/static/site/_includes/leed/api/object.hbs +110 -0
- package/dist/static/site/_includes/leed/api/parameters.hbs +11 -0
- package/dist/static/site/_includes/leed/api/property.hbs +153 -0
- package/dist/static/site/_includes/leed/api/request-body.hbs +51 -0
- package/dist/static/site/_includes/leed/api/responses.hbs +89 -0
- package/dist/static/site/_includes/leed/api/security-constraints.hbs +59 -0
- package/dist/static/site/_includes/leed/api/security.hbs +75 -0
- package/dist/static/site/_includes/leed/components/pagination.hbs +45 -0
- package/dist/static/site/_includes/leed/components/scroll-status-indicator-horizontal.hbs +22 -0
- package/dist/static/site/_includes/leed/docs/alpha/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/alpha/base.hbs +26 -0
- package/dist/static/site/_includes/leed/docs/alpha/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/api-article.hbs +75 -0
- package/dist/static/site/_includes/leed/docs/bravo/api.hbs +5 -0
- package/dist/static/site/_includes/leed/docs/bravo/base.hbs +31 -0
- package/dist/static/site/_includes/leed/docs/bravo/documentation.hbs +7 -0
- package/dist/static/site/_includes/leed/docs/charlie/api.hbs +6 -0
- package/dist/static/site/_includes/leed/docs/charlie/base.hbs +27 -0
- package/dist/static/site/_includes/leed/docs/charlie/documentation.hbs +10 -0
- package/dist/static/site/_includes/leed/docs/doc-article.hbs +40 -0
- package/dist/static/site/_includes/leed/docs/docs-footer.hbs +35 -0
- package/dist/static/site/_includes/leed/docs/docs-header.hbs +59 -0
- package/dist/static/site/_includes/leed/docs/document-search-modal.hbs +36 -0
- package/dist/static/site/_includes/leed/docs/document-search.hbs +23 -0
- package/dist/static/site/_includes/leed/docs/footer.hbs +12 -0
- package/dist/static/site/_includes/leed/docs/header/brand.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/chrome.hbs +33 -0
- package/dist/static/site/_includes/leed/docs/header/compact.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/header/cta.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/header/nav.hbs +18 -0
- package/dist/static/site/_includes/leed/docs/header/top-search.hbs +15 -0
- package/dist/static/site/_includes/leed/docs/mini-bar.hbs +53 -0
- package/dist/static/site/_includes/leed/docs/pagination.hbs +19 -0
- package/dist/static/site/_includes/leed/docs/shell.hbs +56 -0
- package/dist/static/site/_includes/leed/docs/sidebar.hbs +52 -0
- package/dist/static/site/_includes/leed/docs/social-icons.hbs +81 -0
- package/dist/static/site/_includes/leed/docs/style.hbs +17 -0
- package/dist/static/site/_includes/leed/docs/toc.hbs +7 -0
- package/dist/static/site/_includes/leed/form.hbs +266 -0
- package/dist/static/site/_includes/leed/head.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/builder.hbs +35 -0
- package/dist/static/site/_includes/leed/menu/item.hbs +67 -0
- package/dist/static/site/_includes/leed/menu/link.hbs +59 -0
- package/dist/static/site/_includes/leed/metadata/ogCard.hbs +40 -0
- package/dist/static/site/_includes/leed/metadata/siteMetadata.hbs +24 -0
- package/dist/static/site/_includes/leed/metadata/twitterCard.hbs +34 -0
- package/dist/static/site/_includes/leed/popup-video.hbs +21 -0
- package/dist/static/site/_includes/leed/stub-template.hbs +17 -0
- package/dist/static/site/_includes/leed/unsubscribe-form.hbs +30 -0
- package/dist/static/site/_layouts/leed-documentation.hbs +12 -0
- package/dist/static/site/leed-feed-atom.hbs +42 -0
- package/dist/static/site/leed-feed-json.hbs +56 -0
- package/dist/static/site/leed-feed-rss.hbs +39 -0
- package/dist/static/site/leed-headers.hbs +38 -0
- package/dist/static/site/leed-redirects.hbs +10 -0
- package/dist/static/site/leed-sitemap-pageType.hbs +28 -0
- package/dist/static/site/leed-sitemap.hbs +19 -0
- package/dist/static/site/leed-unsubscribe.hbs +44 -0
- package/dist/static/site/leed-unsubscribed.hbs +40 -0
- package/dist/static/tailwind/components/alerts.css +132 -0
- package/dist/static/tailwind/components/api-methods.css +39 -0
- package/dist/static/tailwind/components/breadcrumbs.css +31 -0
- package/dist/static/tailwind/components/code.css +88 -0
- package/dist/static/tailwind/components/collapsible.css +38 -0
- package/dist/static/tailwind/components/document-search.css +222 -0
- package/dist/static/tailwind/components/form.css +185 -0
- package/dist/static/tailwind/components/kbd.css +7 -0
- package/dist/static/tailwind/components/pagination.css +81 -0
- package/dist/static/tailwind/components/scroll-status-indicator.css +5 -0
- package/dist/static/tailwind/components/tab-groups.css +117 -0
- package/dist/static/tailwind/components/tables.css +0 -0
- package/dist/static/tailwind/components/toc.css +60 -0
- package/dist/static/tailwind/components.css +248 -0
- package/dist/static/tailwind/defaults.css +260 -0
- package/dist/static/tailwind/docs/api.css +701 -0
- package/dist/static/tailwind/docs/components/blockquote.css +5 -0
- package/dist/static/tailwind/docs/components/button.css +11 -0
- package/dist/static/tailwind/docs/components/docs-pagination.css +71 -0
- package/dist/static/tailwind/docs/components/mobile-bar.css +83 -0
- package/dist/static/tailwind/docs/components/navigation.css +279 -0
- package/dist/static/tailwind/docs/components/table.css +54 -0
- package/dist/static/tailwind/docs/components.css +6 -0
- package/dist/static/tailwind/docs/docs.css +427 -0
- package/dist/static/tailwind/docs/layouts/alpha.css +108 -0
- package/dist/static/tailwind/docs/layouts/bravo.css +218 -0
- package/dist/static/tailwind/docs/layouts/charlie.css +105 -0
- package/dist/static/tailwind/docs/layouts.css +3 -0
- package/dist/static/tailwind/docs/themes/amber.css +113 -0
- package/dist/static/tailwind/docs/themes/blue.css +94 -0
- package/dist/static/tailwind/docs/themes/cyan.css +111 -0
- package/dist/static/tailwind/docs/themes/emerald.css +111 -0
- package/dist/static/tailwind/docs/themes/fuchsia.css +111 -0
- package/dist/static/tailwind/docs/themes/green.css +95 -0
- package/dist/static/tailwind/docs/themes/indigo.css +111 -0
- package/dist/static/tailwind/docs/themes/lime.css +111 -0
- package/dist/static/tailwind/docs/themes/orange.css +111 -0
- package/dist/static/tailwind/docs/themes/pink.css +111 -0
- package/dist/static/tailwind/docs/themes/purple.css +111 -0
- package/dist/static/tailwind/docs/themes/red.css +111 -0
- package/dist/static/tailwind/docs/themes/rose.css +111 -0
- package/dist/static/tailwind/docs/themes/sky.css +111 -0
- package/dist/static/tailwind/docs/themes/teal.css +111 -0
- package/dist/static/tailwind/docs/themes/violet.css +111 -0
- package/dist/static/tailwind/docs/themes/yellow.css +111 -0
- package/dist/static/tailwind/docs/themes.css +84 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fontawesome.css +8997 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/fonts.css +45 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1/icons.json +2608 -0
- package/dist/static/tailwind/font-awesome/free-7.3.1.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/brands.css +2215 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fontawesome.css +19281 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0/fonts.css +445 -0
- package/dist/static/tailwind/font-awesome/pro-7.0.0.css +3 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/brands.css +2455 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fontawesome.css +20365 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1/fonts.css +589 -0
- package/dist/static/tailwind/font-awesome/pro-7.3.1.css +3 -0
- package/dist/static/tailwind/fonts/all.css +6 -0
- package/dist/static/tailwind/fonts/config.jsonc +31 -0
- package/dist/static/tailwind/fonts/geist-mono.css +142 -0
- package/dist/static/tailwind/fonts/geist.css +142 -0
- package/dist/static/tailwind/fonts/inter.css +215 -0
- package/dist/static/tailwind/fonts/jetbrains-mono.css +126 -0
- package/dist/static/tailwind/fonts/noto-sans.css +134 -0
- package/dist/static/tailwind/fonts/open-sans.css +89 -0
- package/dist/static/tailwind/highlighters.css +14 -0
- package/dist/static/tailwind/hljs/README.md +53 -0
- package/dist/static/tailwind/hljs/a11y.css +96 -0
- package/dist/static/tailwind/hljs/atom-fruit.css +90 -0
- package/dist/static/tailwind/hljs/atom-one.css +164 -0
- package/dist/static/tailwind/hljs/default.css +164 -0
- package/dist/static/tailwind/hljs/github.css +101 -0
- package/dist/static/tailwind/hljs/google-code.css +112 -0
- package/dist/static/tailwind/hljs/grayscale.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-hard.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-medium.css +115 -0
- package/dist/static/tailwind/hljs/gruvbox-soft.css +115 -0
- package/dist/static/tailwind/hljs/papercolor.css +120 -0
- package/dist/static/tailwind/hljs/qt-creator.css +91 -0
- package/dist/static/tailwind/hljs/summerfruit.css +115 -0
- package/dist/static/tailwind/hljs/tomorrow.css +115 -0
- package/dist/static/tailwind/leed.css +170 -0
- package/dist/static/tailwind/site-tokens.reference.css +105 -0
- package/dist/static/tailwind/utils/icons.css +102 -0
- package/dist/static/tailwind/utils/utilities.css +105 -0
- package/dist/static/templates/pages/PAGE.hbs +27 -0
- package/dist/static/templates/pages/PAGE_LAYOUT.hbs +19 -0
- package/dist/static/templates/pages/PAGE_TYPE_INDEX.hbs +89 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_ITEM.hbs +52 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_LIST.hbs +39 -0
- package/dist/static/templates/pages/PAGINATED_PAGE_TYPE_PAGINATION.hbs +11 -0
- package/dist/static/templates/raw-content/.ci/build.sh +58 -0
- package/dist/static/templates/raw-content/README.md +179 -0
- package/dist/static/templates/raw-content/_gitignore +50 -0
- package/dist/static/templates/raw-content/src/404.hbs +21 -0
- package/dist/static/templates/raw-content/src/_data/autolink.json +1 -0
- package/dist/static/templates/raw-content/src/_data/ctaList.json +1 -0
- package/dist/static/templates/raw-content/src/_data/leedForms.json +1 -0
- package/dist/static/templates/raw-content/src/_data/menu.json +1 -0
- package/dist/static/templates/raw-content/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/raw-content/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/raw-content/src/_includes/header.hbs +5 -0
- package/dist/static/templates/raw-content/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribe-DISABLED.hbs +17 -0
- package/dist/static/templates/raw-content/src/_includes/unsubscribed-DISABLED.hbs +13 -0
- package/dist/static/templates/raw-content/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/raw-content/src/index.hbs +21 -0
- package/dist/static/templates/raw-content/src/robots.hbs +9 -0
- package/dist/static/templates/raw-content/tailwind/site/base.css +44 -0
- package/dist/static/templates/raw-content/tailwind/site.config.css +3 -0
- package/dist/static/templates/sandbox/_gitignore +50 -0
- package/dist/static/templates/sandbox/identity.json +1 -0
- package/dist/static/templates/sandbox/src/404.hbs +21 -0
- package/dist/static/templates/sandbox/src/_data/autolink.json +1 -0
- package/dist/static/templates/sandbox/src/_data/ctaList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/labelList.json +9 -0
- package/dist/static/templates/sandbox/src/_data/leedForms.json +20 -0
- package/dist/static/templates/sandbox/src/_data/menu.json +19 -0
- package/dist/static/templates/sandbox/src/_data/pageTypeList.json +14 -0
- package/dist/static/templates/sandbox/src/_data/searchIndexList.json +1 -0
- package/dist/static/templates/sandbox/src/_data/userList.json +11 -0
- package/dist/static/templates/sandbox/src/_includes/footer.hbs +18 -0
- package/dist/static/templates/sandbox/src/_includes/header-includes.hbs +25 -0
- package/dist/static/templates/sandbox/src/_includes/header.hbs +5 -0
- package/dist/static/templates/sandbox/src/_includes/site-template.hbs +23 -0
- package/dist/static/templates/sandbox/src/_layouts/empty.hbs +2 -0
- package/dist/static/templates/sandbox/src/index.hbs +20 -0
- package/dist/static/templates/sandbox/src/robots.hbs +9 -0
- package/dist/static/templates/sandbox/src/src.11tydata.json +39 -0
- package/dist/static/templates/sandbox/tailwind/site/base.css +44 -0
- package/dist/static/templates/sandbox/tailwind/site.config.css +3 -0
- package/dist/thrown-sukVkp_1.js +113 -0
- package/dist/typescript-D4T0V_Hn.js +23004 -0
- package/dist/yaml-9wR8GxF_.js +5236 -0
- package/package.json +157 -4
|
@@ -0,0 +1,2781 @@
|
|
|
1
|
+
// Configuration and constants
|
|
2
|
+
const CONFIG = {
|
|
3
|
+
NOTIFICATION_TIMEOUT: 2000,
|
|
4
|
+
DEBOUNCE_DELAYS: {
|
|
5
|
+
FAST: 30,
|
|
6
|
+
MEDIUM: 50,
|
|
7
|
+
SLOW: 100,
|
|
8
|
+
// Announcements only: a live region has to wait for a pause in typing, well
|
|
9
|
+
// past the point where the thing it describes has finished moving.
|
|
10
|
+
ANNOUNCE: 500,
|
|
11
|
+
},
|
|
12
|
+
BREAKPOINTS: {
|
|
13
|
+
SMALLEST: 640,
|
|
14
|
+
MOBILE: 768,
|
|
15
|
+
DESKTOP: 1024,
|
|
16
|
+
},
|
|
17
|
+
SELECTORS: {
|
|
18
|
+
CODE_BLOCKS: "pre code:not(.math)",
|
|
19
|
+
TABS: '[role="tab"]',
|
|
20
|
+
MENU_ITEMS: ".leed-menu__item, .leed-submenu__item",
|
|
21
|
+
SIDEBAR: ".docs-sidebar",
|
|
22
|
+
TOC_LINKS: ".leed-nav-toc a",
|
|
23
|
+
MERMAID_DIAGRAMS: "pre.mermaid",
|
|
24
|
+
MATH_ELEMENTS: "code.math",
|
|
25
|
+
},
|
|
26
|
+
ARIA: {
|
|
27
|
+
EXPANDED: "aria-expanded",
|
|
28
|
+
HIDDEN: "aria-hidden",
|
|
29
|
+
CURRENT: "aria-current",
|
|
30
|
+
CONTROLS: "aria-controls",
|
|
31
|
+
HASPOPUP: "aria-haspopup",
|
|
32
|
+
},
|
|
33
|
+
CLASSES: {
|
|
34
|
+
NOT_PROSE: "not-prose",
|
|
35
|
+
COPIED: "copied",
|
|
36
|
+
JUST_COPIED: "just-copied",
|
|
37
|
+
HIDDEN: "hidden",
|
|
38
|
+
BLOCK: "block",
|
|
39
|
+
WITH_ANCHOR: "docs-with-anchor",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Utility functions
|
|
44
|
+
const Utils = {
|
|
45
|
+
debounce(func, wait) {
|
|
46
|
+
let timeout;
|
|
47
|
+
return (...args) => {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
timeout = setTimeout(() => func(...args), wait);
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
isSmallest() {
|
|
54
|
+
return window.innerWidth < CONFIG.BREAKPOINTS.SMALLEST;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
isMobile() {
|
|
58
|
+
return window.innerWidth < CONFIG.BREAKPOINTS.MOBILE;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
isSmallerThanDesktop() {
|
|
62
|
+
return window.innerWidth < CONFIG.BREAKPOINTS.DESKTOP;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
isDesktop() {
|
|
66
|
+
return window.innerWidth >= CONFIG.BREAKPOINTS.DESKTOP;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Notification system
|
|
71
|
+
const Notification = {
|
|
72
|
+
show(element, message, className = "notification", timeout = CONFIG.NOTIFICATION_TIMEOUT) {
|
|
73
|
+
// Remove any existing notifications of the same class
|
|
74
|
+
const existingNotification = element.querySelector(`.${className}`);
|
|
75
|
+
if (existingNotification) {
|
|
76
|
+
existingNotification.remove();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const notification = document.createElement("span");
|
|
80
|
+
notification.className = className;
|
|
81
|
+
notification.textContent = message;
|
|
82
|
+
element.appendChild(notification);
|
|
83
|
+
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (notification.parentNode) {
|
|
86
|
+
notification.parentNode.removeChild(notification);
|
|
87
|
+
}
|
|
88
|
+
}, timeout);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ARIA state management
|
|
93
|
+
const AriaManager = {
|
|
94
|
+
setExpanded(element, expanded) {
|
|
95
|
+
element.setAttribute(CONFIG.ARIA.EXPANDED, expanded);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
setHidden(element, hidden) {
|
|
99
|
+
element.setAttribute(CONFIG.ARIA.HIDDEN, hidden);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
setCurrent(element, current) {
|
|
103
|
+
element.setAttribute(CONFIG.ARIA.CURRENT, current);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
removeCurrent(element) {
|
|
107
|
+
element.removeAttribute(CONFIG.ARIA.CURRENT);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// DOM cache for performance
|
|
112
|
+
const DOMCache = {
|
|
113
|
+
elements: {},
|
|
114
|
+
|
|
115
|
+
get(selector) {
|
|
116
|
+
if (!this.elements[selector]) {
|
|
117
|
+
this.elements[selector] = document.querySelector(selector);
|
|
118
|
+
}
|
|
119
|
+
return this.elements[selector];
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
getAll(selector) {
|
|
123
|
+
if (!this.elements[selector]) {
|
|
124
|
+
this.elements[selector] = document.querySelectorAll(selector);
|
|
125
|
+
}
|
|
126
|
+
return this.elements[selector];
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
clear() {
|
|
130
|
+
this.elements = {};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Mermaid diagram manager
|
|
135
|
+
const MermaidManager = {
|
|
136
|
+
init() {
|
|
137
|
+
if (typeof mermaid === "undefined") return;
|
|
138
|
+
|
|
139
|
+
this.setupTheme();
|
|
140
|
+
this.ensureThemeRenders().then(() => this.renderDiagrams());
|
|
141
|
+
this.setupThemeListener();
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A site can put its own palette on diagrams by shipping
|
|
146
|
+
* `static/js/mermaid.theme.json`, which the build publishes as
|
|
147
|
+
* `window.leedMermaidTheme` — one Mermaid config per scheme.
|
|
148
|
+
*
|
|
149
|
+
* It is applied here rather than where it is published because this is the
|
|
150
|
+
* `initialize()` that wins: it runs after the bootstrap, and again on every
|
|
151
|
+
* scheme change. That is also what makes a themed site switch live — the
|
|
152
|
+
* diagrams were already being redrawn, so they are redrawn in the other
|
|
153
|
+
* palette rather than waiting for a reload.
|
|
154
|
+
*
|
|
155
|
+
* `theme` comes first so a site's own `theme` (usually `"base"`, which is
|
|
156
|
+
* what makes `themeVariables` take effect at all) replaces our default.
|
|
157
|
+
* `startOnLoad` comes last and cannot be overridden — we render below, and a
|
|
158
|
+
* site turning it back on would get every diagram drawn twice.
|
|
159
|
+
*/
|
|
160
|
+
setupTheme() {
|
|
161
|
+
const prefersDarkMode = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
162
|
+
const theme = prefersDarkMode ? "dark" : "default";
|
|
163
|
+
const site = window.leedMermaidTheme?.[prefersDarkMode ? "dark" : "light"];
|
|
164
|
+
|
|
165
|
+
mermaid.initialize({
|
|
166
|
+
theme,
|
|
167
|
+
...site,
|
|
168
|
+
startOnLoad: false,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Prove the site's palette can render anything at all, before the real
|
|
174
|
+
* diagrams are drawn.
|
|
175
|
+
*
|
|
176
|
+
* A palette may name a color Mermaid cannot parse — `var(--token)` is the
|
|
177
|
+
* useful case, and Mermaid accepts it in the variables it uses verbatim but
|
|
178
|
+
* throws on the handful it derives other colors from. Which are which is
|
|
179
|
+
* internal to Mermaid and can move in a minor version, so a palette that
|
|
180
|
+
* worked can start throwing on an upgrade, and the throw would otherwise
|
|
181
|
+
* take every diagram on the page down to raw source.
|
|
182
|
+
*
|
|
183
|
+
* A probe rather than a reaction, deliberately. Reacting to the first render
|
|
184
|
+
* failure cannot tell a bad palette from one badly written diagram, and
|
|
185
|
+
* guessing "palette" there means a single typo'd diagram silently strips the
|
|
186
|
+
* colors from every other one on the page. This asks the question directly,
|
|
187
|
+
* on a diagram known to be valid, and costs one render of two nodes.
|
|
188
|
+
*/
|
|
189
|
+
async ensureThemeRenders() {
|
|
190
|
+
if (!window.leedMermaidTheme) return;
|
|
191
|
+
try {
|
|
192
|
+
await mermaid.render("leed-mermaid-theme-probe", "flowchart LR\n a --> b");
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const prefersDarkMode = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
|
195
|
+
console.error(
|
|
196
|
+
"Mermaid rejected this site's palette from static/js/mermaid.theme.json; rendering in Mermaid's own colors instead.",
|
|
197
|
+
error,
|
|
198
|
+
);
|
|
199
|
+
mermaid.initialize({ theme: prefersDarkMode ? "dark" : "default", startOnLoad: false });
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
renderDiagrams() {
|
|
204
|
+
const diagrams = document.querySelectorAll(CONFIG.SELECTORS.MERMAID_DIAGRAMS);
|
|
205
|
+
let counter = 0;
|
|
206
|
+
|
|
207
|
+
diagrams.forEach((diagram) => {
|
|
208
|
+
const id = `mermaid-${counter++}`;
|
|
209
|
+
|
|
210
|
+
if (diagram.originalCode === undefined) {
|
|
211
|
+
diagram.originalCode = diagram.textContent.trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
mermaid
|
|
215
|
+
.render(id, diagram.originalCode)
|
|
216
|
+
.then(({ svg }) => {
|
|
217
|
+
diagram.innerHTML = svg;
|
|
218
|
+
})
|
|
219
|
+
.catch((err) => {
|
|
220
|
+
// This diagram's own problem. The palette was already proven in
|
|
221
|
+
// ensureThemeRenders, so one bad diagram never costs the others
|
|
222
|
+
// their colors.
|
|
223
|
+
console.error("Mermaid rendering failed:", err);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
setupThemeListener() {
|
|
229
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
230
|
+
this.setupTheme();
|
|
231
|
+
this.ensureThemeRenders().then(() => this.renderDiagrams());
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Code block manager
|
|
237
|
+
const CodeBlockManager = {
|
|
238
|
+
init() {
|
|
239
|
+
this.setupCodeHighlighting();
|
|
240
|
+
this.setupCopyButtons();
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
setupCodeHighlighting() {
|
|
244
|
+
document.querySelectorAll("pre > code").forEach((element) => {
|
|
245
|
+
element.classList.add(CONFIG.CLASSES.NOT_PROSE);
|
|
246
|
+
element.parentElement.classList.add(CONFIG.CLASSES.NOT_PROSE);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (typeof hljs !== "undefined") {
|
|
250
|
+
document.querySelectorAll("pre > code").forEach((element) => {
|
|
251
|
+
if (!element.classList.contains("math")) {
|
|
252
|
+
hljs.highlightElement(element);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeof katex !== "undefined") {
|
|
258
|
+
const mathElements = document.querySelectorAll(CONFIG.SELECTORS.MATH_ELEMENTS);
|
|
259
|
+
mathElements.forEach((element) => {
|
|
260
|
+
katex.render(element.textContent, element, { strict: false });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
setupCopyButtons() {
|
|
266
|
+
const codeBlocks = document.querySelectorAll(CONFIG.SELECTORS.CODE_BLOCKS);
|
|
267
|
+
|
|
268
|
+
codeBlocks.forEach((codeBlock) => {
|
|
269
|
+
const pre = codeBlock.parentElement;
|
|
270
|
+
const wrapper = document.createElement("div");
|
|
271
|
+
wrapper.className = "code-block-wrapper";
|
|
272
|
+
|
|
273
|
+
pre.parentNode.insertBefore(wrapper, pre);
|
|
274
|
+
wrapper.appendChild(pre);
|
|
275
|
+
|
|
276
|
+
const copyButton = document.createElement("button");
|
|
277
|
+
copyButton.className = "copy-code-button";
|
|
278
|
+
copyButton.setAttribute("aria-label", "Copy code to clipboard");
|
|
279
|
+
copyButton.innerHTML = '<i class="copy"></i>';
|
|
280
|
+
|
|
281
|
+
wrapper.appendChild(copyButton);
|
|
282
|
+
|
|
283
|
+
copyButton.addEventListener("click", async () => {
|
|
284
|
+
try {
|
|
285
|
+
await navigator.clipboard.writeText(codeBlock.textContent);
|
|
286
|
+
|
|
287
|
+
copyButton.classList.add(CONFIG.CLASSES.COPIED);
|
|
288
|
+
wrapper.classList.add(CONFIG.CLASSES.JUST_COPIED);
|
|
289
|
+
|
|
290
|
+
Notification.show(copyButton, "Copied!", "code-copied-notification");
|
|
291
|
+
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
copyButton.classList.remove(CONFIG.CLASSES.COPIED);
|
|
294
|
+
wrapper.classList.remove(CONFIG.CLASSES.JUST_COPIED);
|
|
295
|
+
}, CONFIG.NOTIFICATION_TIMEOUT);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error("Failed to copy: ", err);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Tab manager
|
|
305
|
+
const TabManager = {
|
|
306
|
+
init() {
|
|
307
|
+
document.querySelectorAll(".leed-tabs__container").forEach((container) => {
|
|
308
|
+
this.initializeTabs(container);
|
|
309
|
+
});
|
|
310
|
+
this.restoreTabStates();
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
initializeTabs(container) {
|
|
314
|
+
const tabs = Array.from(container.querySelectorAll(CONFIG.SELECTORS.TABS));
|
|
315
|
+
const tabList = container.querySelector('[role="tablist"]');
|
|
316
|
+
tabList.classList.add(CONFIG.CLASSES.NOT_PROSE);
|
|
317
|
+
|
|
318
|
+
tabs.forEach((tab, index) => {
|
|
319
|
+
tab.setAttribute("data-tab-index", index);
|
|
320
|
+
tab.addEventListener("click", () => this.activateTabGroup(tab));
|
|
321
|
+
tab.addEventListener("keydown", this.handleTabKeydown);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (tabList) {
|
|
325
|
+
tabList.addEventListener("keydown", this.handleTablistKeydown);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
|
|
329
|
+
if (!activeTab && tabs.length > 0) {
|
|
330
|
+
this.activateTab(tabs[0]);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
activateTabGroup(clickedTab) {
|
|
335
|
+
const clickedTabIndex = parseInt(clickedTab.getAttribute("data-tab-index"), 10);
|
|
336
|
+
const tabContainer = clickedTab.closest(".leed-tabs__container");
|
|
337
|
+
const groupId = tabContainer.getAttribute("data-tabs-groupId");
|
|
338
|
+
|
|
339
|
+
if (!groupId) {
|
|
340
|
+
this.activateTab(clickedTab);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.saveTabStateByIndex(groupId, clickedTabIndex);
|
|
345
|
+
|
|
346
|
+
const containersInGroup = document.querySelectorAll(`.leed-tabs__container[data-tabs-groupId="${groupId}"]`);
|
|
347
|
+
|
|
348
|
+
containersInGroup.forEach((container) => {
|
|
349
|
+
const tabsInContainer = Array.from(container.querySelectorAll(CONFIG.SELECTORS.TABS));
|
|
350
|
+
if (clickedTabIndex < tabsInContainer.length) {
|
|
351
|
+
this.activateTab(tabsInContainer[clickedTabIndex]);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
handleTabKeydown(event) {
|
|
357
|
+
if (event.key === " " || event.key === "Enter") {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
TabManager.activateTabGroup(event.currentTarget);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
handleTablistKeydown(event) {
|
|
364
|
+
const tabs = Array.from(event.currentTarget.querySelectorAll(CONFIG.SELECTORS.TABS));
|
|
365
|
+
const currentTab = document.activeElement;
|
|
366
|
+
|
|
367
|
+
if (!tabs.includes(currentTab)) return;
|
|
368
|
+
|
|
369
|
+
const currentIndex = tabs.indexOf(currentTab);
|
|
370
|
+
let targetTab;
|
|
371
|
+
|
|
372
|
+
switch (event.key) {
|
|
373
|
+
case "ArrowRight":
|
|
374
|
+
case "ArrowDown":
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
targetTab = tabs[(currentIndex + 1) % tabs.length];
|
|
377
|
+
targetTab.focus();
|
|
378
|
+
break;
|
|
379
|
+
case "ArrowLeft":
|
|
380
|
+
case "ArrowUp":
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
targetTab = tabs[(currentIndex - 1 + tabs.length) % tabs.length];
|
|
383
|
+
targetTab.focus();
|
|
384
|
+
break;
|
|
385
|
+
case "Home":
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
tabs[0].focus();
|
|
388
|
+
break;
|
|
389
|
+
case "End":
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
tabs[tabs.length - 1].focus();
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
activateTab(tab) {
|
|
397
|
+
const tabContainer = tab.closest(".leed-tabs__container");
|
|
398
|
+
const tabs = Array.from(tab.closest('[role="tablist"]').querySelectorAll(CONFIG.SELECTORS.TABS));
|
|
399
|
+
const panelId = tab.getAttribute(CONFIG.ARIA.CONTROLS);
|
|
400
|
+
const panel = document.getElementById(panelId);
|
|
401
|
+
|
|
402
|
+
tabs.forEach((tab) => {
|
|
403
|
+
tab.setAttribute("aria-selected", "false");
|
|
404
|
+
tab.setAttribute("tabindex", "-1");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
tabContainer.querySelectorAll('[role="tabpanel"]').forEach((tabPanel) => {
|
|
408
|
+
tabPanel.classList.add(CONFIG.CLASSES.HIDDEN);
|
|
409
|
+
tabPanel.classList.remove(CONFIG.CLASSES.BLOCK);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
tab.setAttribute("aria-selected", "true");
|
|
413
|
+
tab.setAttribute("tabindex", "0");
|
|
414
|
+
|
|
415
|
+
if (panel) {
|
|
416
|
+
panel.classList.remove(CONFIG.CLASSES.HIDDEN);
|
|
417
|
+
panel.classList.add(CONFIG.CLASSES.BLOCK);
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
saveTabStateByIndex(groupId, tabIndex) {
|
|
422
|
+
if (!groupId) return false;
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const storageKey = `leed-tabs-${groupId}`;
|
|
426
|
+
localStorage.setItem(storageKey, tabIndex.toString());
|
|
427
|
+
return true;
|
|
428
|
+
} catch (e) {
|
|
429
|
+
console.warn("Unable to save tab state:", e);
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
restoreTabStates() {
|
|
435
|
+
const groupIds = new Set();
|
|
436
|
+
document.querySelectorAll(".leed-tabs__container[data-tabs-groupId]").forEach((container) => {
|
|
437
|
+
const groupId = container.getAttribute("data-tabs-groupId");
|
|
438
|
+
if (groupId) {
|
|
439
|
+
groupIds.add(groupId);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
groupIds.forEach((groupId) => {
|
|
444
|
+
try {
|
|
445
|
+
const storageKey = `leed-tabs-${groupId}`;
|
|
446
|
+
const savedTabIndex = localStorage.getItem(storageKey);
|
|
447
|
+
|
|
448
|
+
if (savedTabIndex !== null) {
|
|
449
|
+
const tabIndex = parseInt(savedTabIndex, 10);
|
|
450
|
+
|
|
451
|
+
document.querySelectorAll(`.leed-tabs__container[data-tabs-groupId="${groupId}"]`).forEach((container) => {
|
|
452
|
+
const tabs = container.querySelectorAll(CONFIG.SELECTORS.TABS);
|
|
453
|
+
if (tabIndex < tabs.length) {
|
|
454
|
+
this.activateTab(tabs[tabIndex]);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
} catch (e) {
|
|
459
|
+
console.warn(`Unable to restore tab state for group ${groupId}:`, e);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Menu manager
|
|
466
|
+
const MenuManager = {
|
|
467
|
+
createComponent() {
|
|
468
|
+
return {
|
|
469
|
+
openDropdowns: {},
|
|
470
|
+
|
|
471
|
+
init() {
|
|
472
|
+
this.setupMenuStructure();
|
|
473
|
+
this.markActivePageAndExpandMenus();
|
|
474
|
+
this.setupMenuToggle();
|
|
475
|
+
this.setupKeyboardNavigation();
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
isTopLevelMenuItem(item) {
|
|
479
|
+
// Check if this is a top-level menu item (direct child of .leed-menu)
|
|
480
|
+
return item.closest(".leed-menu") === item.parentElement;
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
shouldItemBeExpanded(item) {
|
|
484
|
+
// Check if the item itself has data-expanded="true"
|
|
485
|
+
if (item.getAttribute("data-expanded") === "true") {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check if the parent .leed-menu has data-expanded="true"
|
|
490
|
+
const parentMenu = item.closest(".leed-menu");
|
|
491
|
+
if (parentMenu && parentMenu.getAttribute("data-expanded") === "true") {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return false;
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
setupMenuStructure() {
|
|
499
|
+
this.$el.querySelectorAll(".leed-menu__item--toggle").forEach((toggle) => {
|
|
500
|
+
toggle.setAttribute("aria-hidden", "false");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
this.$el.querySelectorAll(".leed-menu").forEach((menu) => {
|
|
504
|
+
menu.setAttribute("role", "menu");
|
|
505
|
+
menu.setAttribute("aria-orientation", "vertical");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
this.$el.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS).forEach((item) => {
|
|
509
|
+
item.setAttribute("role", "menuitem");
|
|
510
|
+
|
|
511
|
+
const submenu = item.querySelector(".leed-submenu");
|
|
512
|
+
if (submenu) {
|
|
513
|
+
// Check if this is a top-level menu item (direct child of .leed-menu)
|
|
514
|
+
const isTopLevel = this.isTopLevelMenuItem(item);
|
|
515
|
+
|
|
516
|
+
if (isTopLevel) {
|
|
517
|
+
// Top-level menus are always expanded and not collapsible
|
|
518
|
+
item.setAttribute(CONFIG.ARIA.EXPANDED, "true");
|
|
519
|
+
item.setAttribute("aria-disabled", "true"); // Prevent interaction
|
|
520
|
+
|
|
521
|
+
const submenuId = submenu.id || `submenu-${item.dataset.submenuId}`;
|
|
522
|
+
submenu.id = submenuId;
|
|
523
|
+
item.setAttribute(CONFIG.ARIA.CONTROLS, submenuId);
|
|
524
|
+
|
|
525
|
+
submenu.setAttribute(CONFIG.ARIA.HIDDEN, "false");
|
|
526
|
+
submenu.setAttribute("role", "menu");
|
|
527
|
+
|
|
528
|
+
// Store the submenu ID as always open
|
|
529
|
+
const id = item.dataset.submenuId;
|
|
530
|
+
if (id) {
|
|
531
|
+
this.openDropdowns[id] = true;
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
// Nested menus are collapsible, but preserve items if they should be expanded
|
|
535
|
+
const shouldBeExpanded = this.shouldItemBeExpanded(item);
|
|
536
|
+
|
|
537
|
+
// Only set to collapsed if item should not be expanded
|
|
538
|
+
if (!shouldBeExpanded) {
|
|
539
|
+
item.setAttribute(CONFIG.ARIA.EXPANDED, "false");
|
|
540
|
+
// Remove focus from any focused elements within the submenu before hiding
|
|
541
|
+
const focusedElement = submenu.querySelector(":focus");
|
|
542
|
+
if (focusedElement) {
|
|
543
|
+
focusedElement.blur();
|
|
544
|
+
}
|
|
545
|
+
submenu.setAttribute(CONFIG.ARIA.HIDDEN, "true");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const submenuId = submenu.id || `submenu-${item.dataset.submenuId}`;
|
|
549
|
+
submenu.id = submenuId;
|
|
550
|
+
item.setAttribute(CONFIG.ARIA.CONTROLS, submenuId);
|
|
551
|
+
|
|
552
|
+
submenu.setAttribute("role", "menu");
|
|
553
|
+
|
|
554
|
+
// Store the submenu ID as open if it should be expanded
|
|
555
|
+
if (shouldBeExpanded) {
|
|
556
|
+
const id = item.dataset.submenuId;
|
|
557
|
+
if (id) {
|
|
558
|
+
this.openDropdowns[id] = true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Prevent sidebar from closing when clicking on menu links
|
|
566
|
+
this.$el.querySelectorAll("a").forEach((link) => {
|
|
567
|
+
link.addEventListener("click", (e) => {
|
|
568
|
+
// Stop the click event from bubbling up to the sidebar's @click.away
|
|
569
|
+
e.stopPropagation();
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
// Center the active item in the sidebar's own scroller. scrollIntoView
|
|
575
|
+
// would scroll every scrollable ancestor up to and including the
|
|
576
|
+
// viewport, so an item near the bottom of the menu — past what the
|
|
577
|
+
// sidebar can scroll on its own — leaves the remainder to the document
|
|
578
|
+
// and the page loads scrolled down. The sidebar is sticky, so that
|
|
579
|
+
// scroll does not even move the item.
|
|
580
|
+
centerInSidebar(item) {
|
|
581
|
+
const scroller = item.closest(CONFIG.SELECTORS.SIDEBAR);
|
|
582
|
+
if (!scroller || scroller.scrollHeight <= scroller.clientHeight) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const offset = item.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
587
|
+
// The browser clamps scrollTop to the scrollable range.
|
|
588
|
+
scroller.scrollTop += offset - (scroller.clientHeight - item.offsetHeight) / 2;
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
markActivePageAndExpandMenus() {
|
|
592
|
+
const currentPath = window.location.pathname;
|
|
593
|
+
let foundActive = false;
|
|
594
|
+
|
|
595
|
+
this.$el.querySelectorAll("a").forEach((link) => {
|
|
596
|
+
const href = link.getAttribute("href");
|
|
597
|
+
|
|
598
|
+
if (href && (href === currentPath || currentPath.endsWith(href))) {
|
|
599
|
+
foundActive = true;
|
|
600
|
+
|
|
601
|
+
const activeItem = link.closest(CONFIG.SELECTORS.MENU_ITEMS);
|
|
602
|
+
if (activeItem) {
|
|
603
|
+
AriaManager.setCurrent(activeItem, "page");
|
|
604
|
+
this.expandParentMenus(activeItem);
|
|
605
|
+
// After Alpine has applied the expansion above, so the item is
|
|
606
|
+
// measured at its final position.
|
|
607
|
+
this.$nextTick(() => this.centerInSidebar(activeItem));
|
|
608
|
+
|
|
609
|
+
if (activeItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
|
|
610
|
+
const id = activeItem.dataset.submenuId;
|
|
611
|
+
if (id) {
|
|
612
|
+
this.openDropdowns[id] = true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
AriaManager.setExpanded(activeItem, true);
|
|
616
|
+
const submenuId = activeItem.getAttribute(CONFIG.ARIA.CONTROLS);
|
|
617
|
+
if (submenuId) {
|
|
618
|
+
const submenu = document.getElementById(submenuId);
|
|
619
|
+
if (submenu) {
|
|
620
|
+
AriaManager.setHidden(submenu, false);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Make current page link not clickable
|
|
626
|
+
link.addEventListener("click", (e) => {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Add visual indication that this is the current page
|
|
631
|
+
link.classList.add("cursor-default");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
expandParentMenus(item) {
|
|
638
|
+
let parent = item.parentElement;
|
|
639
|
+
|
|
640
|
+
while (parent) {
|
|
641
|
+
if (parent.classList.contains("leed-submenu")) {
|
|
642
|
+
const parentMenuItem = parent.parentElement;
|
|
643
|
+
|
|
644
|
+
if (parentMenuItem?.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
|
|
645
|
+
const id = parentMenuItem.dataset.submenuId;
|
|
646
|
+
if (id) {
|
|
647
|
+
this.openDropdowns[id] = true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
AriaManager.setExpanded(parentMenuItem, true);
|
|
651
|
+
AriaManager.setHidden(parent, false);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
parent = parent.parentElement;
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
setupMenuToggle() {
|
|
660
|
+
this.$el.querySelectorAll(`[${CONFIG.ARIA.HASPOPUP}='menu']`).forEach((item) => {
|
|
661
|
+
// Skip top-level menu items - they should not be collapsible
|
|
662
|
+
const isTopLevel = this.isTopLevelMenuItem(item);
|
|
663
|
+
if (isTopLevel) {
|
|
664
|
+
return; // Skip setting up toggle functionality for top-level items
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const submenuId = item.getAttribute(CONFIG.ARIA.CONTROLS);
|
|
668
|
+
const submenu = document.getElementById(submenuId);
|
|
669
|
+
const id = item.dataset.submenuId;
|
|
670
|
+
|
|
671
|
+
if (submenu && id) {
|
|
672
|
+
Alpine.bind(item, {
|
|
673
|
+
"x-data"() {
|
|
674
|
+
return { id };
|
|
675
|
+
},
|
|
676
|
+
"@click.stop"() {
|
|
677
|
+
const isExpanded = item.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
|
|
678
|
+
const newState = !isExpanded;
|
|
679
|
+
|
|
680
|
+
this.openDropdowns[id] = newState;
|
|
681
|
+
AriaManager.setExpanded(item, newState);
|
|
682
|
+
// Remove focus from any focused elements within the submenu before hiding
|
|
683
|
+
if (!newState) {
|
|
684
|
+
const focusedElement = submenu.querySelector(":focus");
|
|
685
|
+
if (focusedElement) {
|
|
686
|
+
focusedElement.blur();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
AriaManager.setHidden(submenu, !newState);
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
setupKeyboardNavigation() {
|
|
697
|
+
this.$el.addEventListener("keydown", (e) => {
|
|
698
|
+
const activeElement = document.activeElement;
|
|
699
|
+
const menuItem = activeElement.closest("[role='menuitem']");
|
|
700
|
+
|
|
701
|
+
if (!menuItem) return;
|
|
702
|
+
|
|
703
|
+
// Skip keyboard navigation for top-level menu items
|
|
704
|
+
const isTopLevel = this.isTopLevelMenuItem(menuItem);
|
|
705
|
+
if (isTopLevel && menuItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
|
|
706
|
+
return; // Skip keyboard toggle for top-level items
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
switch (e.key) {
|
|
710
|
+
case "Enter":
|
|
711
|
+
case " ":
|
|
712
|
+
if (menuItem.hasAttribute(CONFIG.ARIA.HASPOPUP)) {
|
|
713
|
+
e.preventDefault();
|
|
714
|
+
const isExpanded = menuItem.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
|
|
715
|
+
const newState = !isExpanded;
|
|
716
|
+
|
|
717
|
+
AriaManager.setExpanded(menuItem, newState);
|
|
718
|
+
|
|
719
|
+
const submenuId = menuItem.getAttribute(CONFIG.ARIA.CONTROLS);
|
|
720
|
+
const submenu = document.getElementById(submenuId);
|
|
721
|
+
if (submenu) {
|
|
722
|
+
// Remove focus from any focused elements within the submenu before hiding
|
|
723
|
+
if (!newState) {
|
|
724
|
+
const focusedElement = submenu.querySelector(":focus");
|
|
725
|
+
if (focusedElement) {
|
|
726
|
+
focusedElement.blur();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
AriaManager.setHidden(submenu, !newState);
|
|
730
|
+
|
|
731
|
+
const id = menuItem.dataset.submenuId;
|
|
732
|
+
if (id) this.openDropdowns[id] = newState;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
break;
|
|
736
|
+
|
|
737
|
+
case "Escape": {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
const parentMenu = menuItem.closest(".leed-submenu");
|
|
740
|
+
if (parentMenu) {
|
|
741
|
+
const parentItem = parentMenu.closest(`[${CONFIG.ARIA.HASPOPUP}='menu']`);
|
|
742
|
+
if (parentItem) {
|
|
743
|
+
// Don't collapse if it's a top-level menu
|
|
744
|
+
const isParentTopLevel = this.isTopLevelMenuItem(parentItem);
|
|
745
|
+
if (!isParentTopLevel) {
|
|
746
|
+
AriaManager.setExpanded(parentItem, false);
|
|
747
|
+
// Remove focus from any focused elements within the submenu before hiding
|
|
748
|
+
const focusedElement = parentMenu.querySelector(":focus");
|
|
749
|
+
if (focusedElement) {
|
|
750
|
+
focusedElement.blur();
|
|
751
|
+
}
|
|
752
|
+
AriaManager.setHidden(parentMenu, true);
|
|
753
|
+
|
|
754
|
+
const id = parentItem.dataset.submenuId;
|
|
755
|
+
if (id) this.openDropdowns[id] = false;
|
|
756
|
+
|
|
757
|
+
parentItem.focus();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// Left navigation filter. Deliberately a plain manager rather than an Alpine
|
|
771
|
+
// component: it runs on its own attribute channel — `data-filtering` on
|
|
772
|
+
// #docs-navigation, `data-filter-hidden` on the items — and never writes
|
|
773
|
+
// aria-expanded or MenuManager's openDropdowns, which stay Alpine's exclusively.
|
|
774
|
+
//
|
|
775
|
+
// The one exception is aria-hidden on a submenu the filter reveals. Revealing a
|
|
776
|
+
// collapsed section with CSS alone would paint tabbable links that are absent
|
|
777
|
+
// from the accessibility tree (WCAG 4.1.2), so the reveal is done in aria and
|
|
778
|
+
// marked with data-filter-revealed. Nothing is stashed: aria-hidden on a submenu
|
|
779
|
+
// is derivable, always the negation of its controlling <li>'s aria-expanded, so
|
|
780
|
+
// clearing recomputes it. A section the reader toggles mid-filter therefore
|
|
781
|
+
// keeps the reader's choice, and the two systems cannot end up disagreeing.
|
|
782
|
+
//
|
|
783
|
+
// The two systems never run at the same time either: a click on a section
|
|
784
|
+
// header exits filter mode in the capture phase, so MenuManager's toggle only
|
|
785
|
+
// ever reads a menu the filter has already handed back (see bindEvents).
|
|
786
|
+
const NavFilterManager = {
|
|
787
|
+
items: [],
|
|
788
|
+
pending: "",
|
|
789
|
+
query: "",
|
|
790
|
+
// The debounced writer for the live region, built in bindEvents.
|
|
791
|
+
setStatus: null,
|
|
792
|
+
|
|
793
|
+
// Case- and diacritic-insensitive, with the before an external-link
|
|
794
|
+
// icon folded into ordinary whitespace. A slug's word separators fold here
|
|
795
|
+
// rather than in slugOf, so the reader's query goes through the same fold the
|
|
796
|
+
// slug haystack does: fold one side only and typing the slug the way the URL
|
|
797
|
+
// spells it — "plans_and-pricing" — is the one query the fallback cannot
|
|
798
|
+
// answer, which is the fallback's own headline case.
|
|
799
|
+
normalize(value) {
|
|
800
|
+
return value
|
|
801
|
+
.normalize("NFD")
|
|
802
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
803
|
+
.toLowerCase()
|
|
804
|
+
.replace(/[-_]/g, " ")
|
|
805
|
+
.replace(/\s+/g, " ")
|
|
806
|
+
.trim();
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
// The text the slug fallback matches on: the last non-empty path segment of
|
|
810
|
+
// an href, and nothing else. A docs site can be served under any prefix — a
|
|
811
|
+
// whole site under /docs/ would otherwise match every item on "docs", and the
|
|
812
|
+
// prefix is the customer's, so it cannot be known here and subtracted. The
|
|
813
|
+
// query and the fragment are dropped first (neither is part of the path), then
|
|
814
|
+
// the scheme and authority, so the host of an absolute URL is never matched
|
|
815
|
+
// either. An href with no path segments — an external link to a bare origin,
|
|
816
|
+
// or a parent with no href at all — yields "" and is simply never matched.
|
|
817
|
+
slugOf(href) {
|
|
818
|
+
const raw = (href || "").split(/[?#]/)[0];
|
|
819
|
+
|
|
820
|
+
// An opaque scheme — mailto:, tel: — has no path at all, so there is
|
|
821
|
+
// nothing to take a slug from. Slugging the whole href instead would answer
|
|
822
|
+
// a query with an address the menu never shows, announced as a link match.
|
|
823
|
+
if (/^[a-z][a-z0-9+.-]*:(?!\/\/)/i.test(raw)) {
|
|
824
|
+
return "";
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Scheme and authority come off together, and the scheme is optional so a
|
|
828
|
+
// protocol-relative "//host/…" loses its host the same way an absolute URL
|
|
829
|
+
// does — it is an authority, not a path segment.
|
|
830
|
+
const path = raw.replace(/^([a-z][a-z0-9+.-]*:)?\/\/[^/]*/i, "");
|
|
831
|
+
const segments = path.split("/").filter(Boolean);
|
|
832
|
+
const segment = segments[segments.length - 1];
|
|
833
|
+
|
|
834
|
+
if (!segment) {
|
|
835
|
+
return "";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// The same normalization a name gets — including the `-`/`_` fold, which
|
|
839
|
+
// lives there so the reader's query is folded the same way.
|
|
840
|
+
return this.normalize(this.decodeSegment(segment));
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
// decodeURIComponent throws a URIError on a malformed escape (`%E0%A4%A`).
|
|
844
|
+
// One bad href in a menu must not cost the reader the whole filter, so the
|
|
845
|
+
// raw segment stands in — still matchable, just not decoded.
|
|
846
|
+
decodeSegment(segment) {
|
|
847
|
+
try {
|
|
848
|
+
return decodeURIComponent(segment);
|
|
849
|
+
} catch {
|
|
850
|
+
return segment;
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
|
|
854
|
+
init() {
|
|
855
|
+
this.nav = DOMCache.get("#docs-navigation");
|
|
856
|
+
this.container = DOMCache.get("#docs-nav-filter");
|
|
857
|
+
this.input = DOMCache.get("#docs-nav-filter-input");
|
|
858
|
+
this.clearButton = DOMCache.get("#docs-nav-filter-clear");
|
|
859
|
+
this.empty = DOMCache.get("#docs-nav-filter-empty");
|
|
860
|
+
this.status = DOMCache.get("#docs-nav-filter-status");
|
|
861
|
+
|
|
862
|
+
if (!this.nav || !this.container || !this.input || !this.clearButton || !this.empty || !this.status) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// A left menu that does not resolve renders no list at all, which would
|
|
867
|
+
// leave the box an orphan control. Remove it rather than hide it: any
|
|
868
|
+
// `display` on #docs-nav-filter outranks the user agent's [hidden] rule.
|
|
869
|
+
if (!this.nav.querySelector(".leed-menu")) {
|
|
870
|
+
this.container.remove();
|
|
871
|
+
this.empty.remove();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
this.cacheItems();
|
|
876
|
+
this.bindEvents();
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
// Names are read once. Scoped to the item's own wrapper so a parent never
|
|
880
|
+
// picks up a child's label, and to the name span so the API method badge —
|
|
881
|
+
// a CSS ::before on a sibling icon — contributes nothing. The subtree and the
|
|
882
|
+
// controlled submenu are resolved here too: querying them per match inside the
|
|
883
|
+
// match loop is O(items × matches) of DOM work on every keystroke.
|
|
884
|
+
//
|
|
885
|
+
// Slugs are cached in the same pass, for the same reason: the fallback runs on
|
|
886
|
+
// a keystroke like any other pass, and re-deriving a slug there would put a
|
|
887
|
+
// getAttribute, a split and a decode per item behind every character.
|
|
888
|
+
cacheItems() {
|
|
889
|
+
this.items = Array.from(this.nav.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS)).map((li) => {
|
|
890
|
+
const name = li.querySelector(":scope > .leed-menu__item--wrapper .leed-menu__item--name");
|
|
891
|
+
// The wrapper is an <a> only when the href resolved; a no-link parent
|
|
892
|
+
// renders a <div>, which has no href and so has no slug.
|
|
893
|
+
const wrapper = li.querySelector(":scope > .leed-menu__item--wrapper");
|
|
894
|
+
return {
|
|
895
|
+
li,
|
|
896
|
+
name: this.normalize(name ? name.textContent : ""),
|
|
897
|
+
slug: this.slugOf(wrapper?.getAttribute("href")),
|
|
898
|
+
descendants: Array.from(li.querySelectorAll(CONFIG.SELECTORS.MENU_ITEMS)),
|
|
899
|
+
submenu: li.querySelector(":scope > .leed-submenu"),
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
},
|
|
903
|
+
|
|
904
|
+
bindEvents() {
|
|
905
|
+
const runFilter = Utils.debounce(() => this.applyFilter(this.pending), CONFIG.DEBOUNCE_DELAYS.FAST);
|
|
906
|
+
// A debounce of its own, an order of magnitude longer than the filter's: the
|
|
907
|
+
// tree has to keep up with the keystroke, but a polite live region that
|
|
908
|
+
// speaks on every one queues an announcement per character — about eleven
|
|
909
|
+
// for "page editor". Clearing routes through the same debounced setter, so a
|
|
910
|
+
// queued announcement can never outlive the query that produced it.
|
|
911
|
+
this.setStatus = Utils.debounce((message) => {
|
|
912
|
+
this.status.textContent = message;
|
|
913
|
+
}, CONFIG.DEBOUNCE_DELAYS.ANNOUNCE);
|
|
914
|
+
// Read from the event: some engines clear a type="search" input themselves,
|
|
915
|
+
// so the element's value cannot be trusted once the handler has yielded.
|
|
916
|
+
const onQuery = (e) => {
|
|
917
|
+
this.pending = e.target.value;
|
|
918
|
+
runFilter();
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
this.input.addEventListener("input", onQuery);
|
|
922
|
+
// Safari fires `search` on Escape and Enter as well as `input`. Not for its
|
|
923
|
+
// native clear affordance — navigation.css removes that with
|
|
924
|
+
// `::-webkit-search-cancel-button { appearance: none }` so the explicit
|
|
925
|
+
// clear button is the only one on screen.
|
|
926
|
+
this.input.addEventListener("search", onQuery);
|
|
927
|
+
|
|
928
|
+
this.input.addEventListener("keydown", (e) => {
|
|
929
|
+
if (e.key !== "Escape") {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
// No preventDefault — a native search-input clear reaches the same end
|
|
933
|
+
// state. stopPropagation keeps Escape away from the shell's window-level
|
|
934
|
+
// handler; MenuManager's own handler already early-returns because the
|
|
935
|
+
// input has no [role="menuitem"] ancestor, so no section can collapse.
|
|
936
|
+
e.stopPropagation();
|
|
937
|
+
this.reset();
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
this.clearButton.addEventListener("click", () => {
|
|
941
|
+
this.reset();
|
|
942
|
+
this.input.focus();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// A section header is a live control while filtering, not a dead one.
|
|
946
|
+
// MenuManager's toggle derives its next state from the <li>'s aria-expanded,
|
|
947
|
+
// which the reveal deliberately leaves at "false" — so a click on a revealed
|
|
948
|
+
// section would compute "expand" on a section already on screen, move
|
|
949
|
+
// nothing, and leak that flipped state out when the query is cleared.
|
|
950
|
+
// Capture phase runs ahead of MenuManager's @click.stop handler: the filter
|
|
951
|
+
// exits first, the menu is back at its pre-filter state, and the toggle then
|
|
952
|
+
// reads an aria-expanded that matches what the reader is looking at.
|
|
953
|
+
//
|
|
954
|
+
// Only for a click that would actually reach that toggle. aria-haspopup
|
|
955
|
+
// sits on the section <li>, which wraps the whole submenu, so asking for
|
|
956
|
+
// that ancestor alone would reset on every result in the section too — and
|
|
957
|
+
// a target="_blank" or ctrl-clicked result leaves the reader here with
|
|
958
|
+
// their list gone, having bought nothing.
|
|
959
|
+
this.nav.addEventListener(
|
|
960
|
+
"click",
|
|
961
|
+
(e) => {
|
|
962
|
+
if ((this.query || this.pending) && !e.target.closest("a") && this.sectionToggleOf(e.target)) {
|
|
963
|
+
this.reset();
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
true,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// The keyboard reaches the same toggle by another route, and nothing on the
|
|
970
|
+
// click path guards it: setupKeyboardNavigation listens on this same node
|
|
971
|
+
// and acts on the focused item, so Enter on a revealed header hits exactly
|
|
972
|
+
// the dead-control case above, and Escape on a filtered result collapses
|
|
973
|
+
// the section out from under the reader while data-filtering is still set
|
|
974
|
+
// and the live region still says the result matched. Capture here lands
|
|
975
|
+
// ahead of that handler's bubble-phase listener, same as the click.
|
|
976
|
+
this.nav.addEventListener(
|
|
977
|
+
"keydown",
|
|
978
|
+
(e) => {
|
|
979
|
+
if ((this.query || this.pending) && this.keyTogglesSection(e.target, e.key)) {
|
|
980
|
+
this.reset();
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
true,
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
// The box can be hidden three ways — a site's own media query, the drawer's
|
|
987
|
+
// inline display:none, and .docs-sidebar[aria-hidden="true"] — and only the
|
|
988
|
+
// first is a window resize, so watch the element itself as well.
|
|
989
|
+
if (typeof ResizeObserver === "function") {
|
|
990
|
+
new ResizeObserver(() => this.clearWhenHidden()).observe(this.container);
|
|
991
|
+
}
|
|
992
|
+
ResizeManager.addListener(() => this.clearWhenHidden());
|
|
993
|
+
},
|
|
994
|
+
|
|
995
|
+
// isTopLevelMenuItem's own test, on the component that owns the toggles: a
|
|
996
|
+
// direct child of the root .leed-menu. MenuManager binds no toggle there at
|
|
997
|
+
// all — not on click and not on a key — so there is nothing to get out of the
|
|
998
|
+
// way of, and resetting would only cost the reader the query.
|
|
999
|
+
isTopLevelItem(item) {
|
|
1000
|
+
return item.closest(".leed-menu") === item.parentElement;
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
// The section <li> whose toggle an event on this target reaches, or null.
|
|
1004
|
+
sectionToggleOf(target) {
|
|
1005
|
+
const item = target.closest(`[${CONFIG.ARIA.HASPOPUP}='menu']`);
|
|
1006
|
+
return item && !this.isTopLevelItem(item) ? item : null;
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
// Which keydowns setupKeyboardNavigation actually acts on, mirrored rather
|
|
1010
|
+
// than approximated: a key it ignores must not cost the reader the query
|
|
1011
|
+
// either — Space on a focused result scrolls the page and moves no section.
|
|
1012
|
+
// Enter and Space toggle the focused section; Escape collapses the section
|
|
1013
|
+
// the focused item sits inside, which is why an ordinary result is an Escape
|
|
1014
|
+
// target while it is not a click target.
|
|
1015
|
+
keyTogglesSection(target, key) {
|
|
1016
|
+
const item = target.closest("[role='menuitem']");
|
|
1017
|
+
|
|
1018
|
+
if (!item || (item.hasAttribute(CONFIG.ARIA.HASPOPUP) && this.isTopLevelItem(item))) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (key === "Enter" || key === " ") {
|
|
1023
|
+
return item.hasAttribute(CONFIG.ARIA.HASPOPUP);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return key === "Escape" && Boolean(this.sectionToggleOf(item.parentElement));
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
// Not offsetParent === null, the usual idiom in this file: that reports a
|
|
1030
|
+
// visible element as hidden through the fixed drawer panel.
|
|
1031
|
+
isHidden() {
|
|
1032
|
+
return this.container.getClientRects().length === 0;
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
// `pending` as well as `query`: a keystroke inside the debounce window has not
|
|
1036
|
+
// reached applyFilter yet, so gating on the applied query alone would let the
|
|
1037
|
+
// queued run land on a box that is already gone.
|
|
1038
|
+
clearWhenHidden() {
|
|
1039
|
+
if ((this.query || this.pending) && this.isHidden()) {
|
|
1040
|
+
this.reset();
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
reset() {
|
|
1045
|
+
this.input.value = "";
|
|
1046
|
+
this.pending = "";
|
|
1047
|
+
this.applyFilter("");
|
|
1048
|
+
},
|
|
1049
|
+
|
|
1050
|
+
applyFilter(value) {
|
|
1051
|
+
const tokens = this.normalize(value).split(" ").filter(Boolean);
|
|
1052
|
+
|
|
1053
|
+
if (tokens.length === 0) {
|
|
1054
|
+
this.clearFilter();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Names own the normal case. The slug fallback runs only where the reader
|
|
1059
|
+
// would otherwise be looking at "No matches", so a query that finds a title
|
|
1060
|
+
// never consults a URL and the ordinary result cannot regress — while a page
|
|
1061
|
+
// retitled away from its slug stays reachable by the slug.
|
|
1062
|
+
let { visible, matches } = this.collectMatches(tokens, "name");
|
|
1063
|
+
const byLink = matches === 0;
|
|
1064
|
+
|
|
1065
|
+
if (byLink) {
|
|
1066
|
+
({ visible, matches } = this.collectMatches(tokens, "slug"));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
for (const item of this.items) {
|
|
1070
|
+
if (visible.has(item.li)) {
|
|
1071
|
+
item.li.removeAttribute("data-filter-hidden");
|
|
1072
|
+
this.revealSubmenu(item);
|
|
1073
|
+
} else {
|
|
1074
|
+
item.li.setAttribute("data-filter-hidden", "");
|
|
1075
|
+
this.restoreSubmenu(item);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
this.query = tokens.join(" ");
|
|
1080
|
+
this.nav.setAttribute("data-filtering", "true");
|
|
1081
|
+
this.clearButton.hidden = false;
|
|
1082
|
+
this.empty.hidden = matches > 0;
|
|
1083
|
+
this.announce(matches, byLink);
|
|
1084
|
+
},
|
|
1085
|
+
|
|
1086
|
+
// One pass, parameterized only by which cached text it reads — `name` or
|
|
1087
|
+
// `slug`. Ancestor and subtree visibility live here rather than in the caller
|
|
1088
|
+
// precisely so the fallback cannot grow rules of its own: the two passes are
|
|
1089
|
+
// the same code over a different field, and an item with no text in that field
|
|
1090
|
+
// (a no-link parent's slug, say) is skipped rather than matched by everything.
|
|
1091
|
+
collectMatches(tokens, field) {
|
|
1092
|
+
const visible = new Set();
|
|
1093
|
+
let matches = 0;
|
|
1094
|
+
|
|
1095
|
+
for (const item of this.items) {
|
|
1096
|
+
const haystack = item[field];
|
|
1097
|
+
if (!haystack || !tokens.every((token) => haystack.includes(token))) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
matches += 1;
|
|
1102
|
+
visible.add(item.li);
|
|
1103
|
+
|
|
1104
|
+
// Ancestors keep the match reachable — including ancestors that could
|
|
1105
|
+
// never match themselves, which is how a no-link parent stays on screen.
|
|
1106
|
+
let parent = item.li.parentElement;
|
|
1107
|
+
while (parent && parent !== this.nav) {
|
|
1108
|
+
if (parent.matches(CONFIG.SELECTORS.MENU_ITEMS)) {
|
|
1109
|
+
visible.add(parent);
|
|
1110
|
+
}
|
|
1111
|
+
parent = parent.parentElement;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ...and a matching parent keeps its whole subtree.
|
|
1115
|
+
for (const descendant of item.descendants) {
|
|
1116
|
+
visible.add(descendant);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return { visible, matches };
|
|
1121
|
+
},
|
|
1122
|
+
|
|
1123
|
+
// A section kept visible has to be reachable, not merely painted: a submenu
|
|
1124
|
+
// left at aria-hidden="true" holds focusable links that assistive technology
|
|
1125
|
+
// cannot see. The marker records only that the filter did this, never what the
|
|
1126
|
+
// value was — restoreSubmenu derives that again.
|
|
1127
|
+
revealSubmenu(item) {
|
|
1128
|
+
if (item.submenu?.getAttribute(CONFIG.ARIA.HIDDEN) !== "true") {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
item.submenu.setAttribute("data-filter-revealed", "");
|
|
1133
|
+
item.submenu.setAttribute(CONFIG.ARIA.HIDDEN, "false");
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
// Recomputed from the controlling <li>, not restored from a stashed value, so
|
|
1137
|
+
// a section the reader collapsed while filtering stays collapsed afterwards.
|
|
1138
|
+
restoreSubmenu(item) {
|
|
1139
|
+
if (!item.submenu?.hasAttribute("data-filter-revealed")) {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const expanded = item.li.getAttribute(CONFIG.ARIA.EXPANDED) === "true";
|
|
1144
|
+
item.submenu.removeAttribute("data-filter-revealed");
|
|
1145
|
+
item.submenu.setAttribute(CONFIG.ARIA.HIDDEN, expanded ? "false" : "true");
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
// Counting self-matches rather than the ancestors and subtrees kept for
|
|
1149
|
+
// context is the honest number. textContent only — the reader's query is
|
|
1150
|
+
// never used to build markup.
|
|
1151
|
+
//
|
|
1152
|
+
// A fallback result says so. Every item on screen then matched on its link and
|
|
1153
|
+
// not on any word the reader can see, and a bare "2 items match" would leave a
|
|
1154
|
+
// screen reader user hunting a highlight that is not there.
|
|
1155
|
+
announce(matches, byLink) {
|
|
1156
|
+
if (matches === 0) {
|
|
1157
|
+
this.setStatus("No items match");
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (byLink) {
|
|
1161
|
+
this.setStatus(`No title matches — ${matches} ${matches === 1 ? "page" : "pages"} matched by link`);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
this.setStatus(matches === 1 ? "1 item matches" : `${matches} items match`);
|
|
1165
|
+
},
|
|
1166
|
+
|
|
1167
|
+
clearFilter() {
|
|
1168
|
+
this.nav.removeAttribute("data-filtering");
|
|
1169
|
+
for (const item of this.items) {
|
|
1170
|
+
item.li.removeAttribute("data-filter-hidden");
|
|
1171
|
+
this.restoreSubmenu(item);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// `pending` too, not only the applied query: a whitespace-only query filters
|
|
1175
|
+
// nothing and lands here with a populated field and a hidden clear button,
|
|
1176
|
+
// and a pending query left standing would make the next click on a section
|
|
1177
|
+
// header reset — silently emptying a field the reader is still typing in.
|
|
1178
|
+
this.query = "";
|
|
1179
|
+
this.pending = "";
|
|
1180
|
+
this.clearButton.hidden = true;
|
|
1181
|
+
this.empty.hidden = true;
|
|
1182
|
+
this.setStatus("");
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// Centralized resize manager to avoid multiple event listeners
|
|
1187
|
+
const ResizeManager = {
|
|
1188
|
+
listeners: [],
|
|
1189
|
+
|
|
1190
|
+
addListener(callback, delay = CONFIG.DEBOUNCE_DELAYS.MEDIUM) {
|
|
1191
|
+
const debouncedCallback = Utils.debounce(callback, delay);
|
|
1192
|
+
this.listeners.push(debouncedCallback);
|
|
1193
|
+
return debouncedCallback;
|
|
1194
|
+
},
|
|
1195
|
+
|
|
1196
|
+
init() {
|
|
1197
|
+
const debouncedResize = Utils.debounce(() => {
|
|
1198
|
+
this.listeners.forEach((listener) => {
|
|
1199
|
+
listener();
|
|
1200
|
+
});
|
|
1201
|
+
}, CONFIG.DEBOUNCE_DELAYS.MEDIUM);
|
|
1202
|
+
|
|
1203
|
+
window.addEventListener("resize", debouncedResize);
|
|
1204
|
+
window.addEventListener("load", debouncedResize);
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
// TOC manager
|
|
1209
|
+
const TOCManager = {
|
|
1210
|
+
init() {
|
|
1211
|
+
const tocSidebar = DOMCache.get("#leed-toc");
|
|
1212
|
+
if (!tocSidebar) return;
|
|
1213
|
+
|
|
1214
|
+
this.tocSidebar = tocSidebar;
|
|
1215
|
+
this.desktopTocContainer = DOMCache.get("#leed-toc");
|
|
1216
|
+
this.mobileTocContainer = DOMCache.get("#leed-mobile-toc-container");
|
|
1217
|
+
this.miniTocButton = DOMCache.get("#leed-mini-toc-button-text");
|
|
1218
|
+
this.miniTocCurrent = DOMCache.get("#leed-mini-toc-current");
|
|
1219
|
+
this.miniTocDropdown = DOMCache.get("#docs-mini-toc");
|
|
1220
|
+
|
|
1221
|
+
this.setupTOC();
|
|
1222
|
+
this.setupResponsiveBehavior();
|
|
1223
|
+
this.setupScrollTracking();
|
|
1224
|
+
this.setupMobileTocWatcher();
|
|
1225
|
+
},
|
|
1226
|
+
|
|
1227
|
+
setupTOC() {
|
|
1228
|
+
const tocHeading = this.tocSidebar.querySelector("#leed-toc-heading");
|
|
1229
|
+
const headingText = tocHeading ? tocHeading.textContent : "On this page";
|
|
1230
|
+
|
|
1231
|
+
if (this.miniTocButton && this.miniTocButton.textContent === "") {
|
|
1232
|
+
this.miniTocButton.textContent = headingText;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
this.originalTocParent = null;
|
|
1236
|
+
this.navTocElement = null;
|
|
1237
|
+
this.initializeIcons();
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
initializeIcons() {
|
|
1241
|
+
const tocListItems = document.querySelectorAll(".leed-nav-toc li");
|
|
1242
|
+
|
|
1243
|
+
tocListItems.forEach((li) => {
|
|
1244
|
+
if (!li.querySelector(".current-item")) {
|
|
1245
|
+
const icon = document.createElement("i");
|
|
1246
|
+
icon.className = "current-item";
|
|
1247
|
+
|
|
1248
|
+
const anchor = li.querySelector("a");
|
|
1249
|
+
if (anchor) {
|
|
1250
|
+
li.insertBefore(icon, anchor);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
},
|
|
1255
|
+
|
|
1256
|
+
setupResponsiveBehavior() {
|
|
1257
|
+
ResizeManager.addListener(() => {
|
|
1258
|
+
this.handleTocPosition();
|
|
1259
|
+
if (Utils.isDesktop() && window.Alpine?.store("sidebar")) {
|
|
1260
|
+
Alpine.store("sidebar").open = false;
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
this.handleTocPosition();
|
|
1264
|
+
},
|
|
1265
|
+
|
|
1266
|
+
handleTocPosition() {
|
|
1267
|
+
if (!this.mobileTocContainer) return;
|
|
1268
|
+
|
|
1269
|
+
if (!this.navTocElement) {
|
|
1270
|
+
this.navTocElement = this.tocSidebar.querySelector(".leed-nav-toc");
|
|
1271
|
+
if (!this.navTocElement) return;
|
|
1272
|
+
this.originalTocParent = this.navTocElement.parentNode;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (Utils.isMobile() || Utils.isSmallerThanDesktop()) {
|
|
1276
|
+
if (this.navTocElement.parentNode !== this.mobileTocContainer) {
|
|
1277
|
+
this.mobileTocContainer.appendChild(this.navTocElement);
|
|
1278
|
+
}
|
|
1279
|
+
} else {
|
|
1280
|
+
if (this.navTocElement.parentNode !== this.originalTocParent) {
|
|
1281
|
+
this.originalTocParent.appendChild(this.navTocElement);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
this.updateCurrentSection();
|
|
1286
|
+
},
|
|
1287
|
+
|
|
1288
|
+
setupScrollTracking() {
|
|
1289
|
+
const debouncedSetActiveLink = Utils.debounce(() => {
|
|
1290
|
+
this.setActiveLink();
|
|
1291
|
+
}, CONFIG.DEBOUNCE_DELAYS.FAST);
|
|
1292
|
+
|
|
1293
|
+
window.addEventListener("scroll", debouncedSetActiveLink);
|
|
1294
|
+
|
|
1295
|
+
const scrollableDiv = document.querySelector(".fixed-content-layout");
|
|
1296
|
+
if (scrollableDiv) {
|
|
1297
|
+
scrollableDiv.addEventListener("scroll", debouncedSetActiveLink);
|
|
1298
|
+
}
|
|
1299
|
+
this.setActiveLink();
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
setActiveLink() {
|
|
1303
|
+
const tocLinks = document.querySelectorAll(CONFIG.SELECTORS.TOC_LINKS);
|
|
1304
|
+
const sections = Array.from(tocLinks)
|
|
1305
|
+
.map((link) => {
|
|
1306
|
+
const id = link.getAttribute("href")?.substring(1);
|
|
1307
|
+
return id ? document.getElementById(id) : null;
|
|
1308
|
+
})
|
|
1309
|
+
.filter(Boolean);
|
|
1310
|
+
|
|
1311
|
+
let currentSectionIndex = 0;
|
|
1312
|
+
|
|
1313
|
+
sections.forEach((section, index) => {
|
|
1314
|
+
if (!section) return;
|
|
1315
|
+
|
|
1316
|
+
const rect = section.getBoundingClientRect();
|
|
1317
|
+
|
|
1318
|
+
// Get header and TOC heights from HeaderManager
|
|
1319
|
+
const triggerOffset = HeaderManager.getHeaderHeight() + HeaderManager.getTocHeight();
|
|
1320
|
+
|
|
1321
|
+
if (rect.top <= triggerOffset) {
|
|
1322
|
+
currentSectionIndex = index;
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
tocLinks.forEach((link) => {
|
|
1327
|
+
AriaManager.removeCurrent(link);
|
|
1328
|
+
|
|
1329
|
+
const parentLi = link.closest("li");
|
|
1330
|
+
const icon = parentLi?.querySelector(".current-item");
|
|
1331
|
+
if (icon) {
|
|
1332
|
+
AriaManager.removeCurrent(icon);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
if (tocLinks[currentSectionIndex]) {
|
|
1337
|
+
AriaManager.setCurrent(tocLinks[currentSectionIndex], "location");
|
|
1338
|
+
|
|
1339
|
+
const activeLi = tocLinks[currentSectionIndex].closest("li");
|
|
1340
|
+
const activeIcon = activeLi?.querySelector(".current-item");
|
|
1341
|
+
if (activeIcon) {
|
|
1342
|
+
AriaManager.setCurrent(activeIcon, "location");
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (this.navTocElement && this.navTocElement.parentNode === this.desktopTocContainer) {
|
|
1346
|
+
this.scrollTocItemIntoView(activeLi, this.desktopTocContainer, "center");
|
|
1347
|
+
} else if (this.navTocElement && this.navTocElement.parentNode === this.mobileTocContainer) {
|
|
1348
|
+
// Check if mobile TOC is visible/open
|
|
1349
|
+
const isMobileTocVisible =
|
|
1350
|
+
this.mobileTocContainer.offsetParent !== null && this.mobileTocContainer.scrollHeight > 0;
|
|
1351
|
+
|
|
1352
|
+
if (isMobileTocVisible) {
|
|
1353
|
+
this.scrollTocItemIntoView(activeLi, this.mobileTocContainer, "start");
|
|
1354
|
+
} else {
|
|
1355
|
+
// Store the active item for when TOC is opened
|
|
1356
|
+
this.pendingMobileScrollItem = activeLi;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
this.updateCurrentSection();
|
|
1362
|
+
},
|
|
1363
|
+
|
|
1364
|
+
updateCurrentSection() {
|
|
1365
|
+
if (!this.miniTocCurrent) return;
|
|
1366
|
+
|
|
1367
|
+
const activeLink = document.querySelector(`${CONFIG.SELECTORS.TOC_LINKS}[${CONFIG.ARIA.CURRENT}="location"]`);
|
|
1368
|
+
if (activeLink) {
|
|
1369
|
+
this.miniTocCurrent.textContent = activeLink.textContent;
|
|
1370
|
+
} else {
|
|
1371
|
+
this.miniTocCurrent.textContent = "Overview";
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
|
|
1375
|
+
scrollTocItemIntoView(activeLi, container, block) {
|
|
1376
|
+
if (!activeLi || !container) return;
|
|
1377
|
+
|
|
1378
|
+
// Find the actual scrollable element within the container
|
|
1379
|
+
let scrollableElement = container;
|
|
1380
|
+
|
|
1381
|
+
// Check if the container itself is scrollable
|
|
1382
|
+
if (container.scrollHeight <= container.clientHeight || container.scrollHeight === 0) {
|
|
1383
|
+
// Look for a scrollable child element
|
|
1384
|
+
const scrollableChildren = Array.from(container.querySelectorAll("*")).filter(
|
|
1385
|
+
(el) => el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== "visible",
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
if (scrollableChildren.length > 0) {
|
|
1389
|
+
scrollableElement = scrollableChildren[0];
|
|
1390
|
+
} else {
|
|
1391
|
+
// For mobile, look for the docs-mini-toc element which is the actual scrollable container
|
|
1392
|
+
const docsMiniToc = document.getElementById("docs-mini-toc");
|
|
1393
|
+
|
|
1394
|
+
if (docsMiniToc && docsMiniToc.scrollHeight > docsMiniToc.clientHeight) {
|
|
1395
|
+
scrollableElement = docsMiniToc;
|
|
1396
|
+
} else {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Calculate the current scroll position within the scrollable element
|
|
1403
|
+
const currentScrollTop = scrollableElement.scrollTop;
|
|
1404
|
+
|
|
1405
|
+
// Calculate the item's position relative to the scrollable element
|
|
1406
|
+
const itemRect = activeLi.getBoundingClientRect();
|
|
1407
|
+
const containerRect = scrollableElement.getBoundingClientRect();
|
|
1408
|
+
const itemTop = itemRect.top - containerRect.top + scrollableElement.scrollTop;
|
|
1409
|
+
const itemHeight = activeLi.offsetHeight;
|
|
1410
|
+
const containerHeight = scrollableElement.clientHeight;
|
|
1411
|
+
|
|
1412
|
+
let targetScrollTop;
|
|
1413
|
+
|
|
1414
|
+
if (block === "center") {
|
|
1415
|
+
// Center the item in the container
|
|
1416
|
+
targetScrollTop = itemTop - containerHeight / 2 + itemHeight / 2;
|
|
1417
|
+
} else if (block === "start") {
|
|
1418
|
+
// Align item to the top of the container
|
|
1419
|
+
targetScrollTop = itemTop;
|
|
1420
|
+
} else {
|
|
1421
|
+
// Default to start
|
|
1422
|
+
targetScrollTop = itemTop;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Ensure we don't scroll beyond the container bounds
|
|
1426
|
+
const maxScrollTop = scrollableElement.scrollHeight - containerHeight;
|
|
1427
|
+
targetScrollTop = Math.max(0, Math.min(targetScrollTop, maxScrollTop));
|
|
1428
|
+
|
|
1429
|
+
// Only scroll if the item is not already visible and we need to scroll
|
|
1430
|
+
if (Math.abs(currentScrollTop - targetScrollTop) > 1) {
|
|
1431
|
+
scrollableElement.scrollTop = targetScrollTop;
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
|
|
1435
|
+
// Method to handle mobile TOC opening - call this when mobile TOC becomes visible
|
|
1436
|
+
handleMobileTocOpen() {
|
|
1437
|
+
if (this.pendingMobileScrollItem && this.mobileTocContainer) {
|
|
1438
|
+
// Small delay to ensure the TOC is fully rendered
|
|
1439
|
+
setTimeout(() => {
|
|
1440
|
+
this.scrollTocItemIntoView(this.pendingMobileScrollItem, this.mobileTocContainer, "start");
|
|
1441
|
+
this.pendingMobileScrollItem = null;
|
|
1442
|
+
}, 100);
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
|
|
1446
|
+
setupMobileTocWatcher() {
|
|
1447
|
+
// Watch for Alpine.js isOpen changes on the mobile TOC
|
|
1448
|
+
if (window.Alpine && this.mobileTocContainer) {
|
|
1449
|
+
// Find the Alpine component that controls the mobile TOC
|
|
1450
|
+
const alpineComponent = Alpine.$data(this.mobileTocContainer.closest("[x-data]"));
|
|
1451
|
+
|
|
1452
|
+
if (alpineComponent && alpineComponent.isOpen !== undefined) {
|
|
1453
|
+
// Watch for changes to isOpen
|
|
1454
|
+
Alpine.effect(() => {
|
|
1455
|
+
if (alpineComponent.isOpen) {
|
|
1456
|
+
this.handleMobileTocOpen();
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// Header manager - handles header state, responsive behavior, and height calculations
|
|
1465
|
+
const HeaderManager = {
|
|
1466
|
+
init(params = {}) {
|
|
1467
|
+
this.hasScrollBar = params.scrollBarId?.length > 1 || false;
|
|
1468
|
+
this.topMenuOpen = false;
|
|
1469
|
+
this.headerHeight = 0;
|
|
1470
|
+
this.tocHeight = 0;
|
|
1471
|
+
this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
|
|
1472
|
+
this.isSmallest = Utils.isSmallest();
|
|
1473
|
+
this.headerSearchShow = false;
|
|
1474
|
+
|
|
1475
|
+
this.setupHeaderComponent();
|
|
1476
|
+
},
|
|
1477
|
+
|
|
1478
|
+
setupHeaderComponent() {
|
|
1479
|
+
// Calculate initial heights
|
|
1480
|
+
this.calculateHeaderHeight();
|
|
1481
|
+
this.calculateTocHeight();
|
|
1482
|
+
|
|
1483
|
+
// Setup responsive behavior
|
|
1484
|
+
ResizeManager.addListener(() => {
|
|
1485
|
+
this.handleResize();
|
|
1486
|
+
});
|
|
1487
|
+
},
|
|
1488
|
+
|
|
1489
|
+
calculateTocHeight() {
|
|
1490
|
+
const docsMiniBar = document.getElementById("docs-mini-bar");
|
|
1491
|
+
if (!docsMiniBar) {
|
|
1492
|
+
this.tocHeight = 0;
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
this.tocHeight = docsMiniBar.offsetHeight;
|
|
1497
|
+
document.documentElement.style.setProperty("--height-toc", `${this.tocHeight}px`);
|
|
1498
|
+
},
|
|
1499
|
+
|
|
1500
|
+
calculateHeaderHeight() {
|
|
1501
|
+
const header = document.getElementById("docs-header-wrapper");
|
|
1502
|
+
if (!header) {
|
|
1503
|
+
this.headerHeight = 0;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// --height-header is an OUTPUT of this function and never an input to it.
|
|
1508
|
+
//
|
|
1509
|
+
// Two different jobs had been collapsed onto that one name:
|
|
1510
|
+
// SEED the pre-JS estimate a site declares, e.g.
|
|
1511
|
+
// `@theme { --height-header: 6rem; }`. Tailwind v4 also mints a
|
|
1512
|
+
// `.h-header` utility (`height: var(--height-header)`) from it.
|
|
1513
|
+
// MEASURED what this function publishes after looking at the rendered bar,
|
|
1514
|
+
// and what every offset consumes: the docs shell's
|
|
1515
|
+
// `mt-(--height-header)`, `scroll-padding-top` (defaults.css),
|
|
1516
|
+
// the TOC's `top`/`max-h` (components/toc.css), the sidebar.
|
|
1517
|
+
//
|
|
1518
|
+
// A site that puts `h-header` on its fixed header wrapper — leed.ai did —
|
|
1519
|
+
// therefore SIZES that wrapper from the token. The wrapper is out of flow, so
|
|
1520
|
+
// the fallback below measured its children, i.e. the very box the token had
|
|
1521
|
+
// just sized: seed sizes box, box is measured, measurement rewrites seed.
|
|
1522
|
+
// And because that wrapper also carries a `transition: all 200ms`, its height
|
|
1523
|
+
// ANIMATES towards each new value, so the read lands mid-flight; whatever is
|
|
1524
|
+
// sampled becomes the next target, the transition retargets from where it
|
|
1525
|
+
// already is, and the pair sticks at an arbitrary fixed point somewhere
|
|
1526
|
+
// between the seed and the truth. Measured on leed.ai with a 96px seed and a
|
|
1527
|
+
// 73px header: 89px published on /docs/, 73px on a content page, 57px and
|
|
1528
|
+
// 96px in other sessions. The reader sees the whole reading column sitting
|
|
1529
|
+
// up to 39px too low, by a different amount on different loads.
|
|
1530
|
+
//
|
|
1531
|
+
// Clearing the token for the duration of the measurement breaks that loop for
|
|
1532
|
+
// ANY markup: while it is unset, `.h-header` — and anything else derived from
|
|
1533
|
+
// it — computes to `auto`, so every box reports its own content height and
|
|
1534
|
+
// the stale value cannot feed itself back in. The clear and the restore
|
|
1535
|
+
// happen in one synchronous task, so the browser never paints the in-between
|
|
1536
|
+
// state; `height: auto` is not an interpolable endpoint either, so the
|
|
1537
|
+
// transition jumps rather than animating through it.
|
|
1538
|
+
//
|
|
1539
|
+
// Do NOT reorder this to measure first and clear afterwards, and do not merge
|
|
1540
|
+
// the seed and the measured value back under a single name.
|
|
1541
|
+
const root = document.documentElement;
|
|
1542
|
+
const publishedValue = root.style.getPropertyValue("--height-header");
|
|
1543
|
+
root.style.setProperty("--height-header", "auto");
|
|
1544
|
+
|
|
1545
|
+
let height;
|
|
1546
|
+
try {
|
|
1547
|
+
// The wrapper is what the layouts position and what the page body offsets
|
|
1548
|
+
// against via --height-header. A site-supplied header (docs-header.hbs) may
|
|
1549
|
+
// position itself fixed or absolute, which takes it out of the wrapper's
|
|
1550
|
+
// flow and collapses offsetHeight to 0 — measure the children in that case,
|
|
1551
|
+
// or the whole page slides up underneath the header.
|
|
1552
|
+
height = header.offsetHeight;
|
|
1553
|
+
if (!height) {
|
|
1554
|
+
for (const child of header.children) {
|
|
1555
|
+
height = Math.max(height, child.getBoundingClientRect().height);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
} finally {
|
|
1559
|
+
// Put the page back exactly as it was before anything can paint, whatever
|
|
1560
|
+
// happened above. Restoring "" would leave an empty inline declaration
|
|
1561
|
+
// rather than falling through to the site's seed, hence removeProperty.
|
|
1562
|
+
if (publishedValue) {
|
|
1563
|
+
root.style.setProperty("--height-header", publishedValue);
|
|
1564
|
+
} else {
|
|
1565
|
+
root.style.removeProperty("--height-header");
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
this.headerHeight = height;
|
|
1570
|
+
document.documentElement.style.setProperty("--height-header", `${this.headerHeight}px`);
|
|
1571
|
+
},
|
|
1572
|
+
|
|
1573
|
+
handleResize() {
|
|
1574
|
+
this.calculateTocHeight();
|
|
1575
|
+
this.calculateHeaderHeight();
|
|
1576
|
+
this.topMenuOpen = false;
|
|
1577
|
+
this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
|
|
1578
|
+
this.isSmallest = Utils.isSmallest();
|
|
1579
|
+
},
|
|
1580
|
+
|
|
1581
|
+
// Public methods for external access
|
|
1582
|
+
setTopMenuOpen(open) {
|
|
1583
|
+
this.topMenuOpen = open;
|
|
1584
|
+
},
|
|
1585
|
+
|
|
1586
|
+
setHeaderSearchShow(show) {
|
|
1587
|
+
this.headerSearchShow = show;
|
|
1588
|
+
},
|
|
1589
|
+
|
|
1590
|
+
getHeaderHeight() {
|
|
1591
|
+
return this.headerHeight;
|
|
1592
|
+
},
|
|
1593
|
+
|
|
1594
|
+
getTocHeight() {
|
|
1595
|
+
return this.tocHeight;
|
|
1596
|
+
},
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
// Anchor link manager
|
|
1600
|
+
const AnchorLinkManager = {
|
|
1601
|
+
init() {
|
|
1602
|
+
const contentArea = DOMCache.get("#leed-docs-copy");
|
|
1603
|
+
if (!contentArea) return;
|
|
1604
|
+
|
|
1605
|
+
const headings = contentArea.querySelectorAll("h2, h3, h4");
|
|
1606
|
+
|
|
1607
|
+
headings.forEach((heading) => {
|
|
1608
|
+
if (heading.classList.contains("no-anchor") || !heading.id) return;
|
|
1609
|
+
|
|
1610
|
+
this.createAnchorLink(heading);
|
|
1611
|
+
});
|
|
1612
|
+
},
|
|
1613
|
+
|
|
1614
|
+
createAnchorLink(heading) {
|
|
1615
|
+
// Nothing should run twice over the same heading: a second pass would nest
|
|
1616
|
+
// another wrapper and append a second anchor.
|
|
1617
|
+
if (heading.classList.contains(CONFIG.CLASSES.WITH_ANCHOR)) return;
|
|
1618
|
+
|
|
1619
|
+
const anchorLink = document.createElement("a");
|
|
1620
|
+
anchorLink.href = `#${heading.id}`;
|
|
1621
|
+
anchorLink.classList.add("docs-heading-anchor");
|
|
1622
|
+
anchorLink.setAttribute("aria-label", "Copy link to section");
|
|
1623
|
+
|
|
1624
|
+
const linkIcon = document.createElement("i");
|
|
1625
|
+
anchorLink.appendChild(linkIcon);
|
|
1626
|
+
|
|
1627
|
+
anchorLink.addEventListener("click", (e) => {
|
|
1628
|
+
e.preventDefault();
|
|
1629
|
+
e.stopPropagation();
|
|
1630
|
+
|
|
1631
|
+
const newUrl = `${window.location.pathname}${window.location.search}#${heading.id}`;
|
|
1632
|
+
window.history.pushState(null, "", newUrl);
|
|
1633
|
+
|
|
1634
|
+
navigator.clipboard
|
|
1635
|
+
.writeText(window.location.href)
|
|
1636
|
+
.then(() => {
|
|
1637
|
+
anchorLink.classList.add(CONFIG.CLASSES.COPIED);
|
|
1638
|
+
heading.classList.add(CONFIG.CLASSES.JUST_COPIED);
|
|
1639
|
+
|
|
1640
|
+
Notification.show(anchorLink, "Link copied!", "anchor-copied-notification");
|
|
1641
|
+
|
|
1642
|
+
setTimeout(() => {
|
|
1643
|
+
anchorLink.classList.remove(CONFIG.CLASSES.COPIED);
|
|
1644
|
+
heading.classList.remove(CONFIG.CLASSES.JUST_COPIED);
|
|
1645
|
+
}, CONFIG.NOTIFICATION_TIMEOUT);
|
|
1646
|
+
})
|
|
1647
|
+
.catch((err) => {
|
|
1648
|
+
console.error("Failed to copy link:", err);
|
|
1649
|
+
alert("Failed to copy link. Please try again.");
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Collect the heading's own content into a single element before the
|
|
1655
|
+
* heading becomes a flex row.
|
|
1656
|
+
*
|
|
1657
|
+
* `.docs-with-anchor` is `flex items-baseline gap-2`, and its rule grows
|
|
1658
|
+
* `> :not(.docs-heading-anchor)` so the text takes the width and wraps
|
|
1659
|
+
* normally. That assumes the heading has exactly one child, which is true
|
|
1660
|
+
* only of a heading made of plain text. Write `## Where the \`@font-face\`
|
|
1661
|
+
* rules come from` and the h2 holds three nodes — text, `<code>`, text.
|
|
1662
|
+
* Flex then lays out each as its own item: the `<code>` matches the
|
|
1663
|
+
* `:not()` and stretches, the two bare text runs do not, and an 8px gap
|
|
1664
|
+
* opens on either side of it. The sentence comes apart across the line.
|
|
1665
|
+
*
|
|
1666
|
+
* Wrapping first means there is one thing to grow no matter what the
|
|
1667
|
+
* heading contains, and the inline markup inside it is laid out as inline
|
|
1668
|
+
* content again rather than as flex children.
|
|
1669
|
+
*/
|
|
1670
|
+
const headingText = document.createElement("span");
|
|
1671
|
+
headingText.className = "docs-heading-text";
|
|
1672
|
+
while (heading.firstChild) {
|
|
1673
|
+
headingText.appendChild(heading.firstChild);
|
|
1674
|
+
}
|
|
1675
|
+
heading.appendChild(headingText);
|
|
1676
|
+
|
|
1677
|
+
heading.classList.add(CONFIG.CLASSES.WITH_ANCHOR);
|
|
1678
|
+
heading.appendChild(anchorLink);
|
|
1679
|
+
},
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
// Select manager for API documentation
|
|
1683
|
+
const SelectManager = {
|
|
1684
|
+
createResponseSelector() {
|
|
1685
|
+
return {
|
|
1686
|
+
selectedStatus: "",
|
|
1687
|
+
selectedContentType: "",
|
|
1688
|
+
availableContentTypes: [],
|
|
1689
|
+
statusOpen: false,
|
|
1690
|
+
contentTypeOpen: false,
|
|
1691
|
+
statusOptionsCount: 0,
|
|
1692
|
+
contentTypeOptionsCount: 0,
|
|
1693
|
+
|
|
1694
|
+
init() {
|
|
1695
|
+
this.$nextTick(() => {
|
|
1696
|
+
// Get status options from the custom dropdown options
|
|
1697
|
+
const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
|
|
1698
|
+
const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
|
|
1699
|
+
this.statusOptionsCount = availableStatuses.length;
|
|
1700
|
+
|
|
1701
|
+
const storageKey = "leed-tabs-responses";
|
|
1702
|
+
const savedTabIndex = localStorage.getItem(storageKey);
|
|
1703
|
+
|
|
1704
|
+
let initialStatus = "";
|
|
1705
|
+
if (savedTabIndex !== null && savedTabIndex !== undefined) {
|
|
1706
|
+
const arrayIndex = parseInt(savedTabIndex, 10);
|
|
1707
|
+
if (arrayIndex >= 0 && arrayIndex < availableStatuses.length) {
|
|
1708
|
+
initialStatus = availableStatuses[arrayIndex];
|
|
1709
|
+
} else if (availableStatuses.length > 0) {
|
|
1710
|
+
initialStatus = availableStatuses[0];
|
|
1711
|
+
}
|
|
1712
|
+
} else if (availableStatuses.length > 0) {
|
|
1713
|
+
initialStatus = availableStatuses[0];
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (initialStatus && availableStatuses.includes(initialStatus)) {
|
|
1717
|
+
this.selectedStatus = initialStatus;
|
|
1718
|
+
this.updateContentTypes();
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
this.setupTabClickListener();
|
|
1723
|
+
this.$watch("selectedStatus", (value, oldValue) => {
|
|
1724
|
+
if (value && value !== oldValue) {
|
|
1725
|
+
this.updateContentTypes();
|
|
1726
|
+
this.handleStatusChange();
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
},
|
|
1730
|
+
|
|
1731
|
+
updateContentTypes() {
|
|
1732
|
+
// Get all content type options and filter by selected status
|
|
1733
|
+
const allOptions = this.$el.querySelectorAll(".api-item[data-status-code]");
|
|
1734
|
+
this.availableContentTypes = Array.from(allOptions)
|
|
1735
|
+
.filter((option) => option.getAttribute("data-status-code") === this.selectedStatus)
|
|
1736
|
+
.map((option) => option.getAttribute("data-value"));
|
|
1737
|
+
|
|
1738
|
+
this.contentTypeOptionsCount = this.availableContentTypes.length;
|
|
1739
|
+
this.selectedContentType = this.availableContentTypes.length > 0 ? this.availableContentTypes[0] : "";
|
|
1740
|
+
},
|
|
1741
|
+
|
|
1742
|
+
setupTabClickListener() {
|
|
1743
|
+
setTimeout(() => {
|
|
1744
|
+
const tabs = document.querySelectorAll('[data-tabs-groupId="responses"] li[role="tab"]');
|
|
1745
|
+
tabs.forEach((tab) => {
|
|
1746
|
+
tab.addEventListener("click", (event) => {
|
|
1747
|
+
const tabIndex = tab.getAttribute("data-tab-index");
|
|
1748
|
+
if (tabIndex !== null) {
|
|
1749
|
+
const arrayIndex = parseInt(tabIndex, 10);
|
|
1750
|
+
const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
|
|
1751
|
+
const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
|
|
1752
|
+
if (arrayIndex >= 0 && arrayIndex < availableStatuses.length) {
|
|
1753
|
+
const targetStatus = availableStatuses[arrayIndex];
|
|
1754
|
+
this.selectedStatus = targetStatus;
|
|
1755
|
+
this.updateContentTypes();
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
}, CONFIG.DEBOUNCE_DELAYS.SLOW);
|
|
1761
|
+
},
|
|
1762
|
+
|
|
1763
|
+
handleStatusChange() {
|
|
1764
|
+
const statusOptions = this.$el.querySelectorAll(".api-item[data-value]");
|
|
1765
|
+
const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
|
|
1766
|
+
const statusIndex = availableStatuses.indexOf(this.selectedStatus);
|
|
1767
|
+
if (statusIndex !== -1) {
|
|
1768
|
+
setTimeout(() => {
|
|
1769
|
+
const tabs = document.querySelectorAll('[data-tabs-groupId="responses"] li[role="tab"]');
|
|
1770
|
+
const targetTab = Array.from(tabs).find((tab) => {
|
|
1771
|
+
const tabIndexAttr = tab.getAttribute("data-tab-index");
|
|
1772
|
+
return tabIndexAttr && parseInt(tabIndexAttr, 10) === statusIndex;
|
|
1773
|
+
});
|
|
1774
|
+
if (targetTab) {
|
|
1775
|
+
if (typeof activateTabGroup === "function") {
|
|
1776
|
+
activateTabGroup(targetTab);
|
|
1777
|
+
} else {
|
|
1778
|
+
targetTab.click();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}, 50);
|
|
1782
|
+
}
|
|
1783
|
+
},
|
|
1784
|
+
};
|
|
1785
|
+
},
|
|
1786
|
+
|
|
1787
|
+
createRequestBodySelector() {
|
|
1788
|
+
return {
|
|
1789
|
+
selectedContentType: "",
|
|
1790
|
+
contentTypes: [],
|
|
1791
|
+
contentTypeOpen: false,
|
|
1792
|
+
contentTypeOptionsCount: 0,
|
|
1793
|
+
|
|
1794
|
+
init() {
|
|
1795
|
+
// Get content types from the custom dropdown options
|
|
1796
|
+
const options = this.$el.querySelectorAll(".api-item[data-value]");
|
|
1797
|
+
this.contentTypes = Array.from(options).map((option) => option.getAttribute("data-value"));
|
|
1798
|
+
this.contentTypeOptionsCount = this.contentTypes.length;
|
|
1799
|
+
this.selectedContentType = this.contentTypes.length > 0 ? this.contentTypes[0] : "";
|
|
1800
|
+
},
|
|
1801
|
+
};
|
|
1802
|
+
},
|
|
1803
|
+
|
|
1804
|
+
createSecuritySelector() {
|
|
1805
|
+
return {
|
|
1806
|
+
selectedSecurity: "",
|
|
1807
|
+
securityOptions: [],
|
|
1808
|
+
securityOpen: false,
|
|
1809
|
+
securityOptionsCount: 0,
|
|
1810
|
+
|
|
1811
|
+
init() {
|
|
1812
|
+
this.$nextTick(() => {
|
|
1813
|
+
// Get security options from the custom dropdown options
|
|
1814
|
+
const options = this.$el.querySelectorAll(".api-item[data-value]");
|
|
1815
|
+
this.securityOptions = Array.from(options).map((option) => option.getAttribute("data-value"));
|
|
1816
|
+
this.securityOptionsCount = this.securityOptions.length;
|
|
1817
|
+
const savedSecurity = localStorage.getItem("api-security-selection");
|
|
1818
|
+
|
|
1819
|
+
if (savedSecurity && this.securityOptions.includes(savedSecurity)) {
|
|
1820
|
+
this.selectedSecurity = savedSecurity;
|
|
1821
|
+
} else if (this.securityOptions.length > 0) {
|
|
1822
|
+
this.selectedSecurity = this.securityOptions[0];
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if (this.selectedSecurity) {
|
|
1826
|
+
localStorage.setItem("api-security-selection", this.selectedSecurity);
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
},
|
|
1830
|
+
|
|
1831
|
+
getSelectedSecurityName() {
|
|
1832
|
+
return this.selectedSecurity;
|
|
1833
|
+
},
|
|
1834
|
+
|
|
1835
|
+
showSecurity() {
|
|
1836
|
+
if (this.selectedSecurity) {
|
|
1837
|
+
localStorage.setItem("api-security-selection", this.selectedSecurity);
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
};
|
|
1841
|
+
},
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
// Sidebar manager
|
|
1845
|
+
const SidebarManager = {
|
|
1846
|
+
init() {
|
|
1847
|
+
Alpine.store("sidebar", {
|
|
1848
|
+
open: false,
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
Alpine.data("sidebarNav", () => ({
|
|
1852
|
+
open: false,
|
|
1853
|
+
isSmallerThanDesktop: Utils.isSmallerThanDesktop(),
|
|
1854
|
+
// Tracked separately from `isSmallerThanDesktop`: the undebounced listener
|
|
1855
|
+
// in init() has already updated that by the time the debounced template
|
|
1856
|
+
// handler runs, so it cannot be compared against to detect a crossing.
|
|
1857
|
+
wasSmallerThanDesktop: Utils.isSmallerThanDesktop(),
|
|
1858
|
+
|
|
1859
|
+
init() {
|
|
1860
|
+
this.$watch("$store.sidebar.open", (value) => {
|
|
1861
|
+
this.open = value;
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
window.addEventListener("resize", () => {
|
|
1865
|
+
// Deliberately does NOT touch wasSmallerThanDesktop: this listener is
|
|
1866
|
+
// undebounced, so syncing here would make the debounced handleResize()
|
|
1867
|
+
// always compare equal and never detect a crossing.
|
|
1868
|
+
this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
|
|
1869
|
+
});
|
|
1870
|
+
window.addEventListener("load", () => {
|
|
1871
|
+
this.isSmallerThanDesktop = Utils.isSmallerThanDesktop();
|
|
1872
|
+
// No resize fires for `load`, so handleResize() never runs to resync.
|
|
1873
|
+
// Without this the tracker keeps the pre-load side and the next real
|
|
1874
|
+
// crossing compares equal, skipping the close().
|
|
1875
|
+
this.wasSmallerThanDesktop = this.isSmallerThanDesktop;
|
|
1876
|
+
});
|
|
1877
|
+
},
|
|
1878
|
+
|
|
1879
|
+
toggle() {
|
|
1880
|
+
this.open = !this.open;
|
|
1881
|
+
this.$store.sidebar.open = this.open;
|
|
1882
|
+
},
|
|
1883
|
+
|
|
1884
|
+
close() {
|
|
1885
|
+
this.open = false;
|
|
1886
|
+
this.$store.sidebar.open = false;
|
|
1887
|
+
},
|
|
1888
|
+
|
|
1889
|
+
// Closing on every window resize also closed the drawer when a mobile
|
|
1890
|
+
// soft keyboard opened, which made the nav filter unusable there. Only an
|
|
1891
|
+
// actual breakpoint crossing — the handler's real intent — closes it.
|
|
1892
|
+
handleResize() {
|
|
1893
|
+
const isSmaller = Utils.isSmallerThanDesktop();
|
|
1894
|
+
this.isSmallerThanDesktop = isSmaller;
|
|
1895
|
+
|
|
1896
|
+
if (isSmaller === this.wasSmallerThanDesktop) {
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
this.wasSmallerThanDesktop = isSmaller;
|
|
1901
|
+
this.close();
|
|
1902
|
+
},
|
|
1903
|
+
|
|
1904
|
+
isVisible() {
|
|
1905
|
+
return this.open || !this.isSmallerThanDesktop;
|
|
1906
|
+
},
|
|
1907
|
+
}));
|
|
1908
|
+
},
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
// Alpine.js components initialization
|
|
1912
|
+
document.addEventListener("alpine:init", () => {
|
|
1913
|
+
Alpine.data("leedDocsNavigation", () => MenuManager.createComponent());
|
|
1914
|
+
Alpine.data("responseSelector", () => SelectManager.createResponseSelector());
|
|
1915
|
+
Alpine.data("requestBodySelector", () => SelectManager.createRequestBodySelector());
|
|
1916
|
+
Alpine.data("securitySelector", () => SelectManager.createSecuritySelector());
|
|
1917
|
+
|
|
1918
|
+
// Register the header component immediately when Alpine is available
|
|
1919
|
+
Alpine.data("documentationHeaderComponent", (params = {}) => ({
|
|
1920
|
+
hasScrollBar: params.scrollBarId?.length > 1 || false,
|
|
1921
|
+
topMenuOpen: false,
|
|
1922
|
+
headerHeight: 0,
|
|
1923
|
+
tocHeight: 0,
|
|
1924
|
+
isSmallerThanDesktop: Utils.isSmallerThanDesktop(),
|
|
1925
|
+
isSmallest: Utils.isSmallest(),
|
|
1926
|
+
headerSearchShow: false,
|
|
1927
|
+
|
|
1928
|
+
calculateTOCHeight() {
|
|
1929
|
+
HeaderManager.calculateTocHeight();
|
|
1930
|
+
this.tocHeight = HeaderManager.getTocHeight();
|
|
1931
|
+
},
|
|
1932
|
+
|
|
1933
|
+
calculateHeaderHeight() {
|
|
1934
|
+
HeaderManager.calculateHeaderHeight();
|
|
1935
|
+
this.headerHeight = HeaderManager.getHeaderHeight();
|
|
1936
|
+
},
|
|
1937
|
+
|
|
1938
|
+
handleResize() {
|
|
1939
|
+
HeaderManager.handleResize();
|
|
1940
|
+
this.topMenuOpen = HeaderManager.topMenuOpen;
|
|
1941
|
+
this.isSmallerThanDesktop = HeaderManager.isSmallerThanDesktop;
|
|
1942
|
+
this.isSmallest = HeaderManager.isSmallest;
|
|
1943
|
+
this.headerHeight = HeaderManager.getHeaderHeight();
|
|
1944
|
+
this.tocHeight = HeaderManager.getTocHeight();
|
|
1945
|
+
},
|
|
1946
|
+
|
|
1947
|
+
init() {
|
|
1948
|
+
this.handleResize();
|
|
1949
|
+
},
|
|
1950
|
+
}));
|
|
1951
|
+
|
|
1952
|
+
SidebarManager.init();
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
// API Tab Dropdown Manager - converts tablists in .api-sidebar-content to dropdowns
|
|
1956
|
+
const ApiTabDropdownManager = {
|
|
1957
|
+
dropdowns: [],
|
|
1958
|
+
|
|
1959
|
+
init() {
|
|
1960
|
+
const apiSidebarContent = document.querySelector(".api-sidebar-content");
|
|
1961
|
+
if (!apiSidebarContent) return;
|
|
1962
|
+
|
|
1963
|
+
// Find all tab containers
|
|
1964
|
+
const tabContainers = apiSidebarContent.querySelectorAll(".leed-tabs__container");
|
|
1965
|
+
|
|
1966
|
+
tabContainers.forEach((container, index) => {
|
|
1967
|
+
const tablist = container.querySelector('ul[role="tablist"]');
|
|
1968
|
+
if (!tablist) return;
|
|
1969
|
+
|
|
1970
|
+
// First tablist always converts, others check width
|
|
1971
|
+
if (index === 0) {
|
|
1972
|
+
this.createDropdown(tablist, container);
|
|
1973
|
+
} else {
|
|
1974
|
+
// Check if tablist overflows (exceeds 80% of container width)
|
|
1975
|
+
if (this.shouldConvertToDropdown(tablist, container)) {
|
|
1976
|
+
this.createDropdown(tablist, container);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
// Close dropdowns when clicking outside
|
|
1982
|
+
document.addEventListener("click", (e) => {
|
|
1983
|
+
if (!e.target.closest(".api-tab-dropdown")) {
|
|
1984
|
+
this.closeAllDropdowns();
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
// Listen for tab changes to update dropdown display
|
|
1989
|
+
this.observeTabChanges();
|
|
1990
|
+
|
|
1991
|
+
// Recheck on resize for non-first tablists
|
|
1992
|
+
ResizeManager.addListener(() => {
|
|
1993
|
+
this.handleResize();
|
|
1994
|
+
});
|
|
1995
|
+
},
|
|
1996
|
+
|
|
1997
|
+
shouldConvertToDropdown(tablist, container) {
|
|
1998
|
+
// Temporarily show tablist if it's hidden to get accurate measurements
|
|
1999
|
+
const wasHidden = tablist.style.display === "none";
|
|
2000
|
+
const originalStyles = {
|
|
2001
|
+
display: tablist.style.display,
|
|
2002
|
+
visibility: tablist.style.visibility,
|
|
2003
|
+
position: tablist.style.position,
|
|
2004
|
+
overflow: tablist.style.overflow,
|
|
2005
|
+
width: tablist.style.width,
|
|
2006
|
+
maxWidth: tablist.style.maxWidth,
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
if (wasHidden) {
|
|
2010
|
+
// Make it invisible but still in layout for measurement
|
|
2011
|
+
tablist.style.display = "";
|
|
2012
|
+
tablist.style.visibility = "hidden";
|
|
2013
|
+
tablist.style.position = "absolute";
|
|
2014
|
+
tablist.style.overflow = "visible";
|
|
2015
|
+
tablist.style.width = "auto";
|
|
2016
|
+
tablist.style.maxWidth = "none";
|
|
2017
|
+
} else {
|
|
2018
|
+
// Temporarily remove any width constraints to get natural width
|
|
2019
|
+
tablist.style.width = "auto";
|
|
2020
|
+
tablist.style.maxWidth = "none";
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// Force a reflow to ensure accurate measurements
|
|
2024
|
+
void tablist.offsetWidth;
|
|
2025
|
+
|
|
2026
|
+
// Get the natural width of the tablist (all tabs laid out without constraints)
|
|
2027
|
+
// Use scrollWidth which gives the full content width
|
|
2028
|
+
const tablistNaturalWidth = tablist.scrollWidth;
|
|
2029
|
+
|
|
2030
|
+
// Check if there's a wrapper (like .leed-tabs__container) that constrains the width
|
|
2031
|
+
const wrapper = tablist.closest(".leed-tabs__container");
|
|
2032
|
+
let availableWidth;
|
|
2033
|
+
|
|
2034
|
+
if (wrapper) {
|
|
2035
|
+
// Use wrapper's available width if it exists
|
|
2036
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
2037
|
+
availableWidth = wrapperRect.width;
|
|
2038
|
+
} else {
|
|
2039
|
+
// Use container's available width
|
|
2040
|
+
const containerRect = container.getBoundingClientRect();
|
|
2041
|
+
availableWidth = containerRect.width;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const threshold = (80 / 100) * availableWidth;
|
|
2045
|
+
|
|
2046
|
+
// Restore original styles
|
|
2047
|
+
Object.keys(originalStyles).forEach((key) => {
|
|
2048
|
+
tablist.style[key] = originalStyles[key] || "";
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
// Check if tablist natural width exceeds 80% of the available width
|
|
2052
|
+
const shouldConvert = tablistNaturalWidth > threshold;
|
|
2053
|
+
|
|
2054
|
+
return shouldConvert;
|
|
2055
|
+
},
|
|
2056
|
+
|
|
2057
|
+
createDropdown(tablist, container) {
|
|
2058
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
2059
|
+
if (tabs.length === 0) return;
|
|
2060
|
+
|
|
2061
|
+
// Find the active tab
|
|
2062
|
+
const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
|
|
2063
|
+
|
|
2064
|
+
// Create dropdown wrapper
|
|
2065
|
+
const dropdown = document.createElement("div");
|
|
2066
|
+
dropdown.className = "api-tab-dropdown";
|
|
2067
|
+
|
|
2068
|
+
// Create button
|
|
2069
|
+
const button = document.createElement("button");
|
|
2070
|
+
button.className = "api-tab-dropdown-button";
|
|
2071
|
+
button.setAttribute("type", "button");
|
|
2072
|
+
button.setAttribute("aria-expanded", "false");
|
|
2073
|
+
button.setAttribute("aria-haspopup", "true");
|
|
2074
|
+
button.setAttribute("aria-controls", `api-tab-dropdown-menu-${this.dropdowns.length}`);
|
|
2075
|
+
|
|
2076
|
+
// Get button content from active tab
|
|
2077
|
+
const buttonContent = this.getTabContent(activeTab);
|
|
2078
|
+
button.innerHTML = buttonContent;
|
|
2079
|
+
|
|
2080
|
+
// Create menu
|
|
2081
|
+
const menu = document.createElement("div");
|
|
2082
|
+
menu.className = "api-tab-dropdown-menu";
|
|
2083
|
+
menu.id = `api-tab-dropdown-menu-${this.dropdowns.length}`;
|
|
2084
|
+
menu.setAttribute("role", "menu");
|
|
2085
|
+
menu.setAttribute("aria-hidden", "true");
|
|
2086
|
+
|
|
2087
|
+
// Create menu items from tabs
|
|
2088
|
+
tabs.forEach((tab) => {
|
|
2089
|
+
const menuItem = document.createElement("div");
|
|
2090
|
+
menuItem.className = "api-tab-dropdown-item";
|
|
2091
|
+
menuItem.setAttribute("role", "menuitem");
|
|
2092
|
+
menuItem.setAttribute("tabindex", "-1");
|
|
2093
|
+
menuItem.setAttribute("data-tab-id", tab.id);
|
|
2094
|
+
menuItem.setAttribute("aria-selected", tab.getAttribute("aria-selected") === "true" ? "true" : "false");
|
|
2095
|
+
|
|
2096
|
+
const tabContent = this.getTabContent(tab);
|
|
2097
|
+
menuItem.innerHTML = tabContent;
|
|
2098
|
+
|
|
2099
|
+
menuItem.addEventListener("click", () => {
|
|
2100
|
+
this.selectTab(tab, button, menu);
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
menu.appendChild(menuItem);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// Button click handler
|
|
2107
|
+
button.addEventListener("click", (e) => {
|
|
2108
|
+
e.stopPropagation();
|
|
2109
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
2110
|
+
this.toggleDropdown(button, menu, !isExpanded);
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
// Keyboard navigation
|
|
2114
|
+
button.addEventListener("keydown", (e) => {
|
|
2115
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2116
|
+
e.preventDefault();
|
|
2117
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
2118
|
+
this.toggleDropdown(button, menu, !isExpanded);
|
|
2119
|
+
} else if (e.key === "Escape") {
|
|
2120
|
+
this.toggleDropdown(button, menu, false);
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
menu.addEventListener("keydown", (e) => {
|
|
2125
|
+
const items = Array.from(menu.querySelectorAll(".api-tab-dropdown-item"));
|
|
2126
|
+
const currentIndex = items.indexOf(e.target);
|
|
2127
|
+
|
|
2128
|
+
if (e.key === "ArrowDown") {
|
|
2129
|
+
e.preventDefault();
|
|
2130
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
2131
|
+
items[nextIndex].focus();
|
|
2132
|
+
} else if (e.key === "ArrowUp") {
|
|
2133
|
+
e.preventDefault();
|
|
2134
|
+
const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
|
|
2135
|
+
items[prevIndex].focus();
|
|
2136
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
2137
|
+
e.preventDefault();
|
|
2138
|
+
const tabId = e.target.getAttribute("data-tab-id");
|
|
2139
|
+
const tab = document.getElementById(tabId);
|
|
2140
|
+
if (tab) {
|
|
2141
|
+
this.selectTab(tab, button, menu);
|
|
2142
|
+
}
|
|
2143
|
+
} else if (e.key === "Escape") {
|
|
2144
|
+
this.toggleDropdown(button, menu, false);
|
|
2145
|
+
button.focus();
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
|
|
2149
|
+
dropdown.appendChild(button);
|
|
2150
|
+
dropdown.appendChild(menu);
|
|
2151
|
+
|
|
2152
|
+
// Insert dropdown before the tablist
|
|
2153
|
+
tablist.parentNode.insertBefore(dropdown, tablist);
|
|
2154
|
+
|
|
2155
|
+
// Hide the original tablist by adding a class and setting display
|
|
2156
|
+
tablist.classList.add("converted-to-dropdown");
|
|
2157
|
+
tablist.style.display = "none";
|
|
2158
|
+
|
|
2159
|
+
// Store reference
|
|
2160
|
+
this.dropdowns.push({ dropdown, button, menu, tablist, tabs, container });
|
|
2161
|
+
},
|
|
2162
|
+
|
|
2163
|
+
getTabContent(tab) {
|
|
2164
|
+
// Clone the tab content (including icons, text, and any nested elements)
|
|
2165
|
+
const clone = tab.cloneNode(true);
|
|
2166
|
+
// Remove any classes that might interfere with styling
|
|
2167
|
+
clone.className = "";
|
|
2168
|
+
// Remove any attributes that might interfere
|
|
2169
|
+
clone.removeAttribute("role");
|
|
2170
|
+
clone.removeAttribute("aria-selected");
|
|
2171
|
+
clone.removeAttribute("aria-controls");
|
|
2172
|
+
clone.removeAttribute("tabindex");
|
|
2173
|
+
clone.removeAttribute("id");
|
|
2174
|
+
clone.removeAttribute("data-tab-index");
|
|
2175
|
+
// Return the innerHTML which includes all content dynamically
|
|
2176
|
+
return clone.innerHTML;
|
|
2177
|
+
},
|
|
2178
|
+
|
|
2179
|
+
toggleDropdown(button, menu, isOpen) {
|
|
2180
|
+
button.setAttribute("aria-expanded", isOpen.toString());
|
|
2181
|
+
menu.setAttribute("aria-hidden", (!isOpen).toString());
|
|
2182
|
+
|
|
2183
|
+
if (isOpen) {
|
|
2184
|
+
// Focus first item when opening
|
|
2185
|
+
const firstItem = menu.querySelector(".api-tab-dropdown-item");
|
|
2186
|
+
if (firstItem) {
|
|
2187
|
+
firstItem.setAttribute("tabindex", "0");
|
|
2188
|
+
firstItem.focus();
|
|
2189
|
+
// Set other items to -1
|
|
2190
|
+
menu.querySelectorAll(".api-tab-dropdown-item").forEach((item, index) => {
|
|
2191
|
+
if (item !== firstItem) {
|
|
2192
|
+
item.setAttribute("tabindex", "-1");
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
} else {
|
|
2197
|
+
// Reset tabindex when closing
|
|
2198
|
+
menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
|
|
2199
|
+
item.removeAttribute("tabindex");
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
},
|
|
2203
|
+
|
|
2204
|
+
selectTab(tab, button, menu) {
|
|
2205
|
+
// Activate the tab using TabManager
|
|
2206
|
+
if (TabManager?.activateTabGroup) {
|
|
2207
|
+
TabManager.activateTabGroup(tab);
|
|
2208
|
+
} else {
|
|
2209
|
+
// Fallback: click the tab directly
|
|
2210
|
+
tab.click();
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Update button content
|
|
2214
|
+
const buttonContent = this.getTabContent(tab);
|
|
2215
|
+
button.innerHTML = buttonContent;
|
|
2216
|
+
|
|
2217
|
+
// Update menu items
|
|
2218
|
+
menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
|
|
2219
|
+
const tabId = item.getAttribute("data-tab-id");
|
|
2220
|
+
if (tabId === tab.id) {
|
|
2221
|
+
item.setAttribute("aria-selected", "true");
|
|
2222
|
+
} else {
|
|
2223
|
+
item.setAttribute("aria-selected", "false");
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
// Sync with response selector if this is a responses tab
|
|
2228
|
+
this.syncWithResponseSelector(tab);
|
|
2229
|
+
|
|
2230
|
+
// Close dropdown
|
|
2231
|
+
this.toggleDropdown(button, menu, false);
|
|
2232
|
+
},
|
|
2233
|
+
|
|
2234
|
+
syncWithResponseSelector(tab) {
|
|
2235
|
+
// Check if this tab is part of the responses group
|
|
2236
|
+
const tabContainer = tab.closest(".leed-tabs__container");
|
|
2237
|
+
const groupId = tabContainer?.getAttribute("data-tabs-groupId");
|
|
2238
|
+
|
|
2239
|
+
if (groupId === "responses") {
|
|
2240
|
+
// Find the response selector component - try multiple selectors
|
|
2241
|
+
let responseSection = document.querySelector('.api-section[x-data="responseSelector()"]');
|
|
2242
|
+
if (!responseSection) {
|
|
2243
|
+
responseSection = document.querySelector('.api-section[x-data*="responseSelector"]');
|
|
2244
|
+
}
|
|
2245
|
+
if (!responseSection || !window.Alpine) return;
|
|
2246
|
+
|
|
2247
|
+
// Get the Alpine component instance
|
|
2248
|
+
const responseSelector = Alpine.$data(responseSection);
|
|
2249
|
+
if (!responseSelector) return;
|
|
2250
|
+
|
|
2251
|
+
// Get the tab index
|
|
2252
|
+
const tabIndex = tab.getAttribute("data-tab-index");
|
|
2253
|
+
if (tabIndex === null) return;
|
|
2254
|
+
|
|
2255
|
+
const arrayIndex = parseInt(tabIndex, 10);
|
|
2256
|
+
|
|
2257
|
+
// Get available statuses from the response selector
|
|
2258
|
+
// Look for status options (not content type options which have data-status-code)
|
|
2259
|
+
const statusOptions = responseSection.querySelectorAll(".api-item[data-value]:not([data-status-code])");
|
|
2260
|
+
const availableStatuses = Array.from(statusOptions).map((option) => option.getAttribute("data-value"));
|
|
2261
|
+
|
|
2262
|
+
// Update selectedStatus if index is valid
|
|
2263
|
+
if (
|
|
2264
|
+
arrayIndex >= 0 &&
|
|
2265
|
+
arrayIndex < availableStatuses.length &&
|
|
2266
|
+
responseSelector.selectedStatus !== availableStatuses[arrayIndex]
|
|
2267
|
+
) {
|
|
2268
|
+
responseSelector.selectedStatus = availableStatuses[arrayIndex];
|
|
2269
|
+
// updateContentTypes will be called automatically via the $watch
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
},
|
|
2273
|
+
|
|
2274
|
+
closeAllDropdowns() {
|
|
2275
|
+
this.dropdowns.forEach(({ button, menu }) => {
|
|
2276
|
+
this.toggleDropdown(button, menu, false);
|
|
2277
|
+
});
|
|
2278
|
+
},
|
|
2279
|
+
|
|
2280
|
+
observeTabChanges() {
|
|
2281
|
+
// Use MutationObserver to watch for tab changes
|
|
2282
|
+
this.dropdowns.forEach(({ tablist, button, menu }) => {
|
|
2283
|
+
const observer = new MutationObserver(() => {
|
|
2284
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
2285
|
+
const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
|
|
2286
|
+
|
|
2287
|
+
if (activeTab) {
|
|
2288
|
+
// Update button content
|
|
2289
|
+
const buttonContent = this.getTabContent(activeTab);
|
|
2290
|
+
button.innerHTML = buttonContent;
|
|
2291
|
+
|
|
2292
|
+
// Update menu items
|
|
2293
|
+
menu.querySelectorAll(".api-tab-dropdown-item").forEach((item) => {
|
|
2294
|
+
const tabId = item.getAttribute("data-tab-id");
|
|
2295
|
+
if (tabId === activeTab.id) {
|
|
2296
|
+
item.setAttribute("aria-selected", "true");
|
|
2297
|
+
} else {
|
|
2298
|
+
item.setAttribute("aria-selected", "false");
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
// Sync with response selector if this is a responses tab
|
|
2303
|
+
this.syncWithResponseSelector(activeTab);
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
observer.observe(tablist, {
|
|
2308
|
+
attributes: true,
|
|
2309
|
+
attributeFilter: ["aria-selected"],
|
|
2310
|
+
subtree: true,
|
|
2311
|
+
});
|
|
2312
|
+
});
|
|
2313
|
+
},
|
|
2314
|
+
|
|
2315
|
+
handleResize() {
|
|
2316
|
+
// Recheck all containers and convert/remove dropdowns as needed
|
|
2317
|
+
const apiSidebarContent = document.querySelector(".api-sidebar-content");
|
|
2318
|
+
if (!apiSidebarContent) return;
|
|
2319
|
+
|
|
2320
|
+
const tabContainers = apiSidebarContent.querySelectorAll(".leed-tabs__container");
|
|
2321
|
+
|
|
2322
|
+
tabContainers.forEach((container, index) => {
|
|
2323
|
+
const tablist = container.querySelector('ul[role="tablist"]');
|
|
2324
|
+
if (!tablist) return;
|
|
2325
|
+
|
|
2326
|
+
// Check if we already have a dropdown for this tablist
|
|
2327
|
+
const existingDropdown = this.dropdowns.find((d) => d.tablist === tablist && d.container === container);
|
|
2328
|
+
|
|
2329
|
+
// First tablist always converts, others check width
|
|
2330
|
+
let shouldConvert = false;
|
|
2331
|
+
if (index === 0) {
|
|
2332
|
+
shouldConvert = true;
|
|
2333
|
+
} else {
|
|
2334
|
+
shouldConvert = this.shouldConvertToDropdown(tablist, container);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (shouldConvert && !existingDropdown) {
|
|
2338
|
+
// Need to create dropdown
|
|
2339
|
+
this.createDropdown(tablist, container);
|
|
2340
|
+
} else if (!shouldConvert && existingDropdown && index !== 0) {
|
|
2341
|
+
// Need to remove dropdown and show tablist (but not for first tablist)
|
|
2342
|
+
if (existingDropdown.tablist) {
|
|
2343
|
+
existingDropdown.tablist.classList.remove("converted-to-dropdown");
|
|
2344
|
+
existingDropdown.tablist.style.display = "";
|
|
2345
|
+
}
|
|
2346
|
+
if (existingDropdown.dropdown?.parentNode) {
|
|
2347
|
+
existingDropdown.dropdown.remove();
|
|
2348
|
+
}
|
|
2349
|
+
this.dropdowns = this.dropdowns.filter((d) => d !== existingDropdown);
|
|
2350
|
+
}
|
|
2351
|
+
});
|
|
2352
|
+
},
|
|
2353
|
+
};
|
|
2354
|
+
|
|
2355
|
+
// Docs Tab Dropdown Manager - converts overflowing tablists in #leed-docs-copy to dropdowns
|
|
2356
|
+
const DocsTabDropdownManager = {
|
|
2357
|
+
dropdowns: [],
|
|
2358
|
+
|
|
2359
|
+
init() {
|
|
2360
|
+
const docsContent = document.getElementById("leed-docs-copy");
|
|
2361
|
+
if (!docsContent) return;
|
|
2362
|
+
|
|
2363
|
+
// Find all tab containers under #leed-docs-copy
|
|
2364
|
+
const tabContainers = docsContent.querySelectorAll(".leed-tabs__container");
|
|
2365
|
+
|
|
2366
|
+
tabContainers.forEach((container) => {
|
|
2367
|
+
// Skip containers that are inside .api-sidebar-content (handled by ApiTabDropdownManager)
|
|
2368
|
+
if (container.closest(".api-sidebar-content")) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const tablist = container.querySelector('ul[role="tablist"]');
|
|
2373
|
+
if (!tablist) return;
|
|
2374
|
+
|
|
2375
|
+
// Check if tablist overflows
|
|
2376
|
+
if (this.shouldConvertToDropdown(tablist, container)) {
|
|
2377
|
+
this.createDropdown(tablist, container);
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// Close dropdowns when clicking outside
|
|
2382
|
+
document.addEventListener("click", (e) => {
|
|
2383
|
+
if (!e.target.closest(".docs-tab-dropdown")) {
|
|
2384
|
+
this.closeAllDropdowns();
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
|
|
2388
|
+
// Listen for tab changes to update dropdown display
|
|
2389
|
+
this.observeTabChanges();
|
|
2390
|
+
|
|
2391
|
+
// Delay resize listener registration to avoid immediate resize event from ResizeManager.init()
|
|
2392
|
+
// which fires on window.load
|
|
2393
|
+
requestAnimationFrame(() => {
|
|
2394
|
+
requestAnimationFrame(() => {
|
|
2395
|
+
// Recheck on resize
|
|
2396
|
+
ResizeManager.addListener(() => {
|
|
2397
|
+
this.handleResize();
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
2400
|
+
});
|
|
2401
|
+
},
|
|
2402
|
+
|
|
2403
|
+
shouldConvertToDropdown(tablist, container) {
|
|
2404
|
+
// Temporarily show tablist if it's hidden to get accurate measurements
|
|
2405
|
+
const wasHidden = tablist.style.display === "none" || tablist.classList.contains("converted-to-dropdown");
|
|
2406
|
+
const hadClass = tablist.classList.contains("converted-to-dropdown");
|
|
2407
|
+
const originalStyles = {
|
|
2408
|
+
display: tablist.style.display,
|
|
2409
|
+
visibility: tablist.style.visibility,
|
|
2410
|
+
position: tablist.style.position,
|
|
2411
|
+
overflow: tablist.style.overflow,
|
|
2412
|
+
width: tablist.style.width,
|
|
2413
|
+
maxWidth: tablist.style.maxWidth,
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
if (wasHidden) {
|
|
2417
|
+
// Temporarily remove the class if present
|
|
2418
|
+
if (hadClass) {
|
|
2419
|
+
tablist.classList.remove("converted-to-dropdown");
|
|
2420
|
+
}
|
|
2421
|
+
// Make it invisible but still in layout for measurement
|
|
2422
|
+
tablist.style.display = "";
|
|
2423
|
+
tablist.style.visibility = "hidden";
|
|
2424
|
+
tablist.style.position = "absolute";
|
|
2425
|
+
tablist.style.overflow = "visible";
|
|
2426
|
+
tablist.style.width = "auto";
|
|
2427
|
+
tablist.style.maxWidth = "none";
|
|
2428
|
+
} else {
|
|
2429
|
+
// Temporarily remove any width constraints to get natural width
|
|
2430
|
+
tablist.style.width = "auto";
|
|
2431
|
+
tablist.style.maxWidth = "none";
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// Force a reflow to ensure accurate measurements
|
|
2435
|
+
void tablist.offsetWidth;
|
|
2436
|
+
|
|
2437
|
+
// Get the natural width of the tablist (all tabs laid out without constraints)
|
|
2438
|
+
// Use scrollWidth which gives the full content width
|
|
2439
|
+
const tablistNaturalWidth = tablist.scrollWidth;
|
|
2440
|
+
|
|
2441
|
+
// Check if there's a wrapper (like .leed-tabs__container) that constrains the width
|
|
2442
|
+
const wrapper = tablist.closest(".leed-tabs__container");
|
|
2443
|
+
let availableWidth;
|
|
2444
|
+
|
|
2445
|
+
if (wrapper) {
|
|
2446
|
+
// Use wrapper's available width if it exists
|
|
2447
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
2448
|
+
availableWidth = wrapperRect.width;
|
|
2449
|
+
} else {
|
|
2450
|
+
// Use container's available width
|
|
2451
|
+
const containerRect = container.getBoundingClientRect();
|
|
2452
|
+
availableWidth = containerRect.width;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const threshold = (80 / 100) * availableWidth;
|
|
2456
|
+
|
|
2457
|
+
// Restore original styles
|
|
2458
|
+
Object.keys(originalStyles).forEach((key) => {
|
|
2459
|
+
tablist.style[key] = originalStyles[key] || "";
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
// Restore the class if it was present
|
|
2463
|
+
if (hadClass) {
|
|
2464
|
+
tablist.classList.add("converted-to-dropdown");
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// Check if tablist natural width exceeds 80% of the available width
|
|
2468
|
+
const shouldConvert = tablistNaturalWidth > threshold;
|
|
2469
|
+
|
|
2470
|
+
return shouldConvert;
|
|
2471
|
+
},
|
|
2472
|
+
|
|
2473
|
+
createDropdown(tablist, container) {
|
|
2474
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
2475
|
+
if (tabs.length === 0) return;
|
|
2476
|
+
|
|
2477
|
+
// Find the active tab
|
|
2478
|
+
const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true") || tabs[0];
|
|
2479
|
+
|
|
2480
|
+
// Create dropdown wrapper
|
|
2481
|
+
const dropdown = document.createElement("div");
|
|
2482
|
+
dropdown.className = "docs-tab-dropdown";
|
|
2483
|
+
|
|
2484
|
+
// Create button
|
|
2485
|
+
const button = document.createElement("button");
|
|
2486
|
+
button.className = "docs-tab-dropdown-button";
|
|
2487
|
+
button.setAttribute("type", "button");
|
|
2488
|
+
button.setAttribute("aria-expanded", "false");
|
|
2489
|
+
button.setAttribute("aria-haspopup", "true");
|
|
2490
|
+
button.setAttribute("aria-controls", `docs-tab-dropdown-menu-${this.dropdowns.length}`);
|
|
2491
|
+
|
|
2492
|
+
// Get button content from active tab
|
|
2493
|
+
const buttonContent = this.getTabContent(activeTab);
|
|
2494
|
+
button.innerHTML = buttonContent;
|
|
2495
|
+
|
|
2496
|
+
// Create menu
|
|
2497
|
+
const menu = document.createElement("div");
|
|
2498
|
+
menu.className = "docs-tab-dropdown-menu";
|
|
2499
|
+
menu.id = `docs-tab-dropdown-menu-${this.dropdowns.length}`;
|
|
2500
|
+
menu.setAttribute("role", "menu");
|
|
2501
|
+
menu.setAttribute("aria-hidden", "true");
|
|
2502
|
+
|
|
2503
|
+
// Create menu items from tabs
|
|
2504
|
+
tabs.forEach((tab) => {
|
|
2505
|
+
const menuItem = document.createElement("div");
|
|
2506
|
+
menuItem.className = "docs-tab-dropdown-item";
|
|
2507
|
+
menuItem.setAttribute("role", "menuitem");
|
|
2508
|
+
menuItem.setAttribute("tabindex", "-1");
|
|
2509
|
+
menuItem.setAttribute("data-tab-id", tab.id);
|
|
2510
|
+
menuItem.setAttribute("aria-selected", tab.getAttribute("aria-selected") === "true" ? "true" : "false");
|
|
2511
|
+
|
|
2512
|
+
const tabContent = this.getTabContent(tab);
|
|
2513
|
+
menuItem.innerHTML = tabContent;
|
|
2514
|
+
|
|
2515
|
+
menuItem.addEventListener("click", () => {
|
|
2516
|
+
this.selectTab(tab, button, menu);
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
menu.appendChild(menuItem);
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
// Button click handler
|
|
2523
|
+
button.addEventListener("click", (e) => {
|
|
2524
|
+
e.stopPropagation();
|
|
2525
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
2526
|
+
this.toggleDropdown(button, menu, !isExpanded);
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
// Keyboard navigation
|
|
2530
|
+
button.addEventListener("keydown", (e) => {
|
|
2531
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2532
|
+
e.preventDefault();
|
|
2533
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
|
2534
|
+
this.toggleDropdown(button, menu, !isExpanded);
|
|
2535
|
+
} else if (e.key === "Escape") {
|
|
2536
|
+
this.toggleDropdown(button, menu, false);
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
menu.addEventListener("keydown", (e) => {
|
|
2541
|
+
const items = Array.from(menu.querySelectorAll(".docs-tab-dropdown-item"));
|
|
2542
|
+
const currentIndex = items.indexOf(e.target);
|
|
2543
|
+
|
|
2544
|
+
if (e.key === "ArrowDown") {
|
|
2545
|
+
e.preventDefault();
|
|
2546
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
2547
|
+
items[nextIndex].focus();
|
|
2548
|
+
} else if (e.key === "ArrowUp") {
|
|
2549
|
+
e.preventDefault();
|
|
2550
|
+
const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
|
|
2551
|
+
items[prevIndex].focus();
|
|
2552
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
2553
|
+
e.preventDefault();
|
|
2554
|
+
const tabId = e.target.getAttribute("data-tab-id");
|
|
2555
|
+
const tab = document.getElementById(tabId);
|
|
2556
|
+
if (tab) {
|
|
2557
|
+
this.selectTab(tab, button, menu);
|
|
2558
|
+
}
|
|
2559
|
+
} else if (e.key === "Escape") {
|
|
2560
|
+
this.toggleDropdown(button, menu, false);
|
|
2561
|
+
button.focus();
|
|
2562
|
+
}
|
|
2563
|
+
});
|
|
2564
|
+
|
|
2565
|
+
dropdown.appendChild(button);
|
|
2566
|
+
dropdown.appendChild(menu);
|
|
2567
|
+
|
|
2568
|
+
// Insert dropdown before the tablist
|
|
2569
|
+
tablist.parentNode.insertBefore(dropdown, tablist);
|
|
2570
|
+
|
|
2571
|
+
// Hide the original tablist by adding a class and setting display
|
|
2572
|
+
tablist.classList.add("converted-to-dropdown");
|
|
2573
|
+
tablist.style.display = "none";
|
|
2574
|
+
|
|
2575
|
+
// Store reference
|
|
2576
|
+
this.dropdowns.push({ dropdown, button, menu, tablist, tabs, container });
|
|
2577
|
+
},
|
|
2578
|
+
|
|
2579
|
+
getTabContent(tab) {
|
|
2580
|
+
// Clone the tab content (including icons, text, and any nested elements)
|
|
2581
|
+
const clone = tab.cloneNode(true);
|
|
2582
|
+
// Remove any classes that might interfere with styling
|
|
2583
|
+
clone.className = "";
|
|
2584
|
+
// Remove any attributes that might interfere
|
|
2585
|
+
clone.removeAttribute("role");
|
|
2586
|
+
clone.removeAttribute("aria-selected");
|
|
2587
|
+
clone.removeAttribute("aria-controls");
|
|
2588
|
+
clone.removeAttribute("tabindex");
|
|
2589
|
+
clone.removeAttribute("id");
|
|
2590
|
+
clone.removeAttribute("data-tab-index");
|
|
2591
|
+
// Return the innerHTML which includes all content dynamically
|
|
2592
|
+
return clone.innerHTML;
|
|
2593
|
+
},
|
|
2594
|
+
|
|
2595
|
+
toggleDropdown(button, menu, isOpen) {
|
|
2596
|
+
button.setAttribute("aria-expanded", isOpen.toString());
|
|
2597
|
+
menu.setAttribute("aria-hidden", (!isOpen).toString());
|
|
2598
|
+
|
|
2599
|
+
if (isOpen) {
|
|
2600
|
+
// Focus first item when opening
|
|
2601
|
+
const firstItem = menu.querySelector(".docs-tab-dropdown-item");
|
|
2602
|
+
if (firstItem) {
|
|
2603
|
+
firstItem.setAttribute("tabindex", "0");
|
|
2604
|
+
firstItem.focus();
|
|
2605
|
+
// Set other items to -1
|
|
2606
|
+
menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
|
|
2607
|
+
if (item !== firstItem) {
|
|
2608
|
+
item.setAttribute("tabindex", "-1");
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
} else {
|
|
2613
|
+
// Reset tabindex when closing
|
|
2614
|
+
menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
|
|
2615
|
+
item.removeAttribute("tabindex");
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
},
|
|
2619
|
+
|
|
2620
|
+
selectTab(tab, button, menu) {
|
|
2621
|
+
// Activate the tab using TabManager
|
|
2622
|
+
if (TabManager?.activateTabGroup) {
|
|
2623
|
+
TabManager.activateTabGroup(tab);
|
|
2624
|
+
} else {
|
|
2625
|
+
// Fallback: click the tab directly
|
|
2626
|
+
tab.click();
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// Update button content
|
|
2630
|
+
const buttonContent = this.getTabContent(tab);
|
|
2631
|
+
button.innerHTML = buttonContent;
|
|
2632
|
+
|
|
2633
|
+
// Update menu items
|
|
2634
|
+
menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
|
|
2635
|
+
const tabId = item.getAttribute("data-tab-id");
|
|
2636
|
+
if (tabId === tab.id) {
|
|
2637
|
+
item.setAttribute("aria-selected", "true");
|
|
2638
|
+
} else {
|
|
2639
|
+
item.setAttribute("aria-selected", "false");
|
|
2640
|
+
}
|
|
2641
|
+
});
|
|
2642
|
+
|
|
2643
|
+
// Close dropdown
|
|
2644
|
+
this.toggleDropdown(button, menu, false);
|
|
2645
|
+
},
|
|
2646
|
+
|
|
2647
|
+
closeAllDropdowns() {
|
|
2648
|
+
this.dropdowns.forEach(({ button, menu }) => {
|
|
2649
|
+
this.toggleDropdown(button, menu, false);
|
|
2650
|
+
});
|
|
2651
|
+
},
|
|
2652
|
+
|
|
2653
|
+
observeTabChanges() {
|
|
2654
|
+
// Use MutationObserver to watch for tab changes
|
|
2655
|
+
this.dropdowns.forEach(({ tablist, button, menu }) => {
|
|
2656
|
+
const observer = new MutationObserver(() => {
|
|
2657
|
+
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
|
2658
|
+
const activeTab = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
|
|
2659
|
+
|
|
2660
|
+
if (activeTab) {
|
|
2661
|
+
// Update button content
|
|
2662
|
+
const buttonContent = this.getTabContent(activeTab);
|
|
2663
|
+
button.innerHTML = buttonContent;
|
|
2664
|
+
|
|
2665
|
+
// Update menu items
|
|
2666
|
+
menu.querySelectorAll(".docs-tab-dropdown-item").forEach((item) => {
|
|
2667
|
+
const tabId = item.getAttribute("data-tab-id");
|
|
2668
|
+
if (tabId === activeTab.id) {
|
|
2669
|
+
item.setAttribute("aria-selected", "true");
|
|
2670
|
+
} else {
|
|
2671
|
+
item.setAttribute("aria-selected", "false");
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
observer.observe(tablist, {
|
|
2678
|
+
attributes: true,
|
|
2679
|
+
attributeFilter: ["aria-selected"],
|
|
2680
|
+
subtree: true,
|
|
2681
|
+
});
|
|
2682
|
+
});
|
|
2683
|
+
},
|
|
2684
|
+
|
|
2685
|
+
handleResize() {
|
|
2686
|
+
// Recheck all containers and convert/remove dropdowns as needed
|
|
2687
|
+
const docsContent = document.getElementById("leed-docs-copy");
|
|
2688
|
+
if (!docsContent) return;
|
|
2689
|
+
|
|
2690
|
+
const tabContainers = docsContent.querySelectorAll(".leed-tabs__container");
|
|
2691
|
+
|
|
2692
|
+
tabContainers.forEach((container) => {
|
|
2693
|
+
// Skip containers that are inside .api-sidebar-content (handled by ApiTabDropdownManager)
|
|
2694
|
+
if (container.closest(".api-sidebar-content")) {
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
const tablist = container.querySelector('ul[role="tablist"]');
|
|
2699
|
+
if (!tablist) return;
|
|
2700
|
+
|
|
2701
|
+
// Validate container has a valid width before making decisions
|
|
2702
|
+
const containerRect = container.getBoundingClientRect();
|
|
2703
|
+
if (containerRect.width === 0) {
|
|
2704
|
+
// Container not yet laid out, skip this check
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Check if we already have a dropdown for this tablist
|
|
2709
|
+
const existingDropdown = this.dropdowns.find((d) => d.tablist === tablist && d.container === container);
|
|
2710
|
+
|
|
2711
|
+
const shouldConvert = this.shouldConvertToDropdown(tablist, container);
|
|
2712
|
+
|
|
2713
|
+
if (shouldConvert && !existingDropdown) {
|
|
2714
|
+
// Need to create dropdown
|
|
2715
|
+
this.createDropdown(tablist, container);
|
|
2716
|
+
} else if (!shouldConvert && existingDropdown) {
|
|
2717
|
+
// Only remove dropdown if container has valid dimensions
|
|
2718
|
+
// This prevents premature removal during initial layout
|
|
2719
|
+
if (containerRect.width > 0 && containerRect.height > 0) {
|
|
2720
|
+
// Need to remove dropdown and show tablist
|
|
2721
|
+
if (existingDropdown.tablist) {
|
|
2722
|
+
existingDropdown.tablist.classList.remove("converted-to-dropdown");
|
|
2723
|
+
existingDropdown.tablist.style.display = "";
|
|
2724
|
+
}
|
|
2725
|
+
if (existingDropdown.dropdown?.parentNode) {
|
|
2726
|
+
existingDropdown.dropdown.remove();
|
|
2727
|
+
}
|
|
2728
|
+
this.dropdowns = this.dropdowns.filter((d) => d !== existingDropdown);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
},
|
|
2733
|
+
};
|
|
2734
|
+
|
|
2735
|
+
/**
|
|
2736
|
+
* Tables scroll rather than crush.
|
|
2737
|
+
*
|
|
2738
|
+
* A reference table has a width below which it stops being readable, and a
|
|
2739
|
+
* narrow column takes it straight through: the flag column wrapping `--env`
|
|
2740
|
+
* over two lines is a column of noise where the whole point is a value you can
|
|
2741
|
+
* scan.
|
|
2742
|
+
*
|
|
2743
|
+
* CSS alone cannot fix it. `overflow-x` on a `<table>` only applies with
|
|
2744
|
+
* `display: block`, and that makes the inner table box shrink to the block's
|
|
2745
|
+
* width, so the cells crush and nothing ever overflows. The element that
|
|
2746
|
+
* scrolls has to be a real wrapper, and markdown gives the renderer none — so
|
|
2747
|
+
* one is added here, the same way `setupCopyButtons` wraps a `<pre>`.
|
|
2748
|
+
*
|
|
2749
|
+
* Scoped to `#leed-docs-copy`, the article body that doc-article.hbs and
|
|
2750
|
+
* api-article.hbs both emit, so a table used for layout elsewhere in the
|
|
2751
|
+
* chrome is left alone.
|
|
2752
|
+
*
|
|
2753
|
+
* With JavaScript off the table is exactly what it was before: readable, just
|
|
2754
|
+
* narrow. Nothing is hidden by this, which is the right way to fail.
|
|
2755
|
+
*/
|
|
2756
|
+
const TableManager = {
|
|
2757
|
+
init() {
|
|
2758
|
+
document.querySelectorAll("#leed-docs-copy table").forEach((table) => {
|
|
2759
|
+
if (table.parentElement?.classList.contains("table-scroll")) return;
|
|
2760
|
+
const wrapper = document.createElement("div");
|
|
2761
|
+
wrapper.className = "table-scroll";
|
|
2762
|
+
table.parentNode.insertBefore(wrapper, table);
|
|
2763
|
+
wrapper.appendChild(table);
|
|
2764
|
+
});
|
|
2765
|
+
},
|
|
2766
|
+
};
|
|
2767
|
+
|
|
2768
|
+
// Main initialization
|
|
2769
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
2770
|
+
ResizeManager.init();
|
|
2771
|
+
MermaidManager.init();
|
|
2772
|
+
CodeBlockManager.init();
|
|
2773
|
+
TableManager.init();
|
|
2774
|
+
TabManager.init();
|
|
2775
|
+
HeaderManager.init();
|
|
2776
|
+
TOCManager.init();
|
|
2777
|
+
NavFilterManager.init();
|
|
2778
|
+
AnchorLinkManager.init();
|
|
2779
|
+
ApiTabDropdownManager.init();
|
|
2780
|
+
DocsTabDropdownManager.init();
|
|
2781
|
+
});
|