From 548541b33b0e920fdab802c4820e21132eae558e Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 22 Aug 2025 12:52:51 +0200 Subject: [PATCH 01/16] Add deeplink anchors --- docs/syntax/dropdowns.md | 50 +++++++++++++++++++ docs/testing/nested/index.md | 7 +++ src/Elastic.Documentation.Site/Assets/main.ts | 4 +- .../Assets/open-details-with-anchor.ts | 26 ++++++---- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/docs/syntax/dropdowns.md b/docs/syntax/dropdowns.md index 0c0301cd5..9a74e4010 100644 --- a/docs/syntax/dropdowns.md +++ b/docs/syntax/dropdowns.md @@ -10,6 +10,7 @@ Dropdowns allow you to hide and reveal content on user interaction. By default, ::::{tab-item} Output :::{dropdown} Dropdown Title +:name: basic-dropdown Dropdown content ::: @@ -18,6 +19,7 @@ Dropdown content ::::{tab-item} Markdown ```markdown :::{dropdown} Dropdown Title +:name: basic-dropdown Dropdown content ::: ``` @@ -35,6 +37,7 @@ You can specify that the dropdown content should be visible by default. Do this :::{dropdown} Dropdown Title :open: +:name: open-dropdown Dropdown content ::: @@ -44,9 +47,56 @@ Dropdown content ```markdown :::{dropdown} Dropdown Title :open: +:name: open-dropdown Dropdown content ::: ``` :::: ::::: + +## Deeplinking + +Dropdowns support deeplinking via anchor links. When you navigate to a URL with a hash that points to a dropdown or content within a dropdown, the dropdown will automatically open. + +:::::{tab-set} + +::::{tab-item} Output + +:::{dropdown} Deeplink Example +:name: deeplink-example + +This dropdown can be opened by navigating to `#deeplink-example`. + +#### Nested Content [#nested-content] + +You can also link directly to content within dropdowns. This content has the anchor `#nested-content`. + +::: + +Test links: +- [Link to dropdown](#deeplink-example) +- [Link to nested content](#nested-content) + +:::: + +::::{tab-item} Markdown +```markdown +:::{dropdown} Deeplink Example +:name: deeplink-example + +This dropdown can be opened by navigating to `#deeplink-example`. + +#### Nested Content [#nested-content] + +You can also link directly to content within dropdowns. This content has the anchor `#nested-content`. + +::: + +Test links: +- [Link to dropdown](#deeplink-example) +- [Link to nested content](#nested-content) +``` +:::: + +::::: diff --git a/docs/testing/nested/index.md b/docs/testing/nested/index.md index c65382bcd..dda6d6db9 100644 --- a/docs/testing/nested/index.md +++ b/docs/testing/nested/index.md @@ -11,3 +11,10 @@ The files in this directory are used for testing purposes. Do not edit these fil ## Injecting a {{x}} is supported in headers. This should show up in the file's table of contents too. + +:::{dropdown} Dropdown Title +:name: dropdown-title + +Text. + +::: \ No newline at end of file diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index 1765ed596..92228563f 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -2,7 +2,7 @@ import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' import './markdown/applies-to' -import { openDetailsWithAnchor } from './open-details-with-anchor' +import { initOpenDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' import { initTabs } from './tabs' @@ -31,7 +31,7 @@ document.addEventListener('htmx:load', function (event) { initNav() } initSmoothScroll() - openDetailsWithAnchor() + initOpenDetailsWithAnchor() initImageCarousel() const urlParams = new URLSearchParams(window.location.search) diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index bec3af533..d9e12b018 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -1,23 +1,29 @@ -import { UAParser } from 'ua-parser-js' - -const { getBrowser } = new UAParser() - -// This is a fix for anchors in details elements in non-Chrome browsers. +// Opens details elements (dropdowns) when navigating to an anchor link within them +// This enables deeplinking to collapsed dropdown content export function openDetailsWithAnchor() { if (window.location.hash) { const target = document.querySelector(window.location.hash) if (target) { const closestDetails = target.closest('details') if (closestDetails) { - const browser = getBrowser() - if (browser.name !== 'Chrome') { - closestDetails.open = true + closestDetails.open = true + // Small delay to ensure the details element is open before scrolling + setTimeout(() => { target.scrollIntoView({ - behavior: 'instant', + behavior: 'smooth', block: 'start', }) - } + }, 50) } } } } + +// Initialize the anchor handling functionality +export function initOpenDetailsWithAnchor() { + // Handle initial page load + openDetailsWithAnchor() + + // Handle hash changes within the same page (e.g., clicking anchor links) + window.addEventListener('hashchange', openDetailsWithAnchor) +} From 126e90fb03d365d9caeb48ed60961ba3edb9f608 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 22 Aug 2025 13:22:27 +0200 Subject: [PATCH 02/16] Add anchors --- docs/syntax/dropdowns.md | 42 ++++++++++++++----- .../Assets/open-details-with-anchor.ts | 30 +++++++++++++ .../Directives/AdmonitionTests.cs | 24 +++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/docs/syntax/dropdowns.md b/docs/syntax/dropdowns.md index 9a74e4010..12816eb8d 100644 --- a/docs/syntax/dropdowns.md +++ b/docs/syntax/dropdowns.md @@ -10,7 +10,6 @@ Dropdowns allow you to hide and reveal content on user interaction. By default, ::::{tab-item} Output :::{dropdown} Dropdown Title -:name: basic-dropdown Dropdown content ::: @@ -19,7 +18,6 @@ Dropdown content ::::{tab-item} Markdown ```markdown :::{dropdown} Dropdown Title -:name: basic-dropdown Dropdown content ::: ``` @@ -37,7 +35,6 @@ You can specify that the dropdown content should be visible by default. Do this :::{dropdown} Dropdown Title :open: -:name: open-dropdown Dropdown content ::: @@ -47,7 +44,6 @@ Dropdown content ```markdown :::{dropdown} Dropdown Title :open: -:name: open-dropdown Dropdown content ::: ``` @@ -57,7 +53,14 @@ Dropdown content ## Deeplinking -Dropdowns support deeplinking via anchor links. When you navigate to a URL with a hash that points to a dropdown or content within a dropdown, the dropdown will automatically open. +Dropdowns support deeplinking via anchor links. When you navigate to a URL with a hash that points to a dropdown or content within a dropdown, the dropdown will automatically open. When you manually open a dropdown that has a name/anchor, the URL will automatically update to reflect the current state. + +### Features + +- **Automatic opening**: Navigate to `#dropdown-name` and the dropdown opens automatically +- **URL updates**: Open a dropdown manually and the URL updates to show the anchor +- **Nested content**: Link directly to headings or content within dropdowns +- **Browser navigation**: Proper back/forward button support :::::{tab-set} @@ -68,15 +71,18 @@ Dropdowns support deeplinking via anchor links. When you navigate to a URL with This dropdown can be opened by navigating to `#deeplink-example`. +When you open this dropdown manually by clicking the title, the URL will automatically update to show `#deeplink-example`. + #### Nested Content [#nested-content] You can also link directly to content within dropdowns. This content has the anchor `#nested-content`. ::: -Test links: -- [Link to dropdown](#deeplink-example) -- [Link to nested content](#nested-content) +**Test the features:** +- [Link to dropdown](#deeplink-example) - Opens the dropdown and updates URL +- [Link to nested content](#nested-content) - Opens dropdown and scrolls to nested content +- Try opening/closing the dropdown manually and watch the URL change :::: @@ -87,16 +93,30 @@ Test links: This dropdown can be opened by navigating to `#deeplink-example`. +When you open this dropdown manually by clicking the title, the URL will automatically update to show `#deeplink-example`. + #### Nested Content [#nested-content] You can also link directly to content within dropdowns. This content has the anchor `#nested-content`. ::: -Test links: -- [Link to dropdown](#deeplink-example) -- [Link to nested content](#nested-content) +**Test the features:** +- [Link to dropdown](#deeplink-example) - Opens the dropdown and updates URL +- [Link to nested content](#nested-content) - Opens dropdown and scrolls to nested content +- Try opening/closing the dropdown manually and watch the URL change ``` :::: ::::: + +### Use Cases + +Deeplinking is particularly useful for: + +- **FAQ sections**: Allow users to share links to specific questions +- **Documentation**: Link directly to explanations that might be collapsed by default +- **Troubleshooting guides**: Share direct links to specific solutions +- **API documentation**: Link to specific endpoint details within collapsed sections + +The URL behaves just like clicking on a heading with an anchor - it updates automatically when you interact with the content. diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index d9e12b018..cbd185d04 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -19,6 +19,19 @@ export function openDetailsWithAnchor() { } } +// Updates the URL when a dropdown is manually opened/closed +function updateUrlForDropdown(details: HTMLDetailsElement, isOpening: boolean) { + const dropdownId = details.id + if (!dropdownId) return + + if (isOpening) { + // Update URL to show the dropdown anchor (like clicking a heading link) + window.history.pushState(null, '', `#${dropdownId}`) + } + // Note: We don't remove the hash when closing, just like headings don't + // This keeps the URL consistent with how headings behave +} + // Initialize the anchor handling functionality export function initOpenDetailsWithAnchor() { // Handle initial page load @@ -26,4 +39,21 @@ export function initOpenDetailsWithAnchor() { // Handle hash changes within the same page (e.g., clicking anchor links) window.addEventListener('hashchange', openDetailsWithAnchor) + + // Handle manual dropdown toggling to update URL + // Use event delegation to catch all toggle events + document.addEventListener('toggle', (event) => { + const target = event.target as HTMLElement + + // Check if the target is a details element with dropdown class + if (target.tagName === 'DETAILS' && target.classList.contains('dropdown')) { + const details = target as HTMLDetailsElement + const isOpening = details.open + + // Use setTimeout to ensure the toggle state has been processed + setTimeout(() => { + updateUrlForDropdown(details, isOpening) + }, 0) + } + }, true) // Use capture phase to ensure we catch the event } diff --git a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs index 75f67eb04..791541cd2 100644 --- a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs @@ -99,3 +99,27 @@ A regular paragraph. [Fact] public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue(); } + +public class DropdownWithNameTests(ITestOutputHelper output) : DirectiveTest(output, +""" +:::{dropdown} Dropdown with name +:name: test-dropdown +:open: +This is a dropdown with a name +::: +A regular paragraph. +""" +) +{ + [Fact] + public void SetsCorrectAdmonitionType() => Block!.Admonition.Should().Be("dropdown"); + + [Fact] + public void SetsCustomTitle() => Block!.Title.Should().Be("Dropdown with name"); + + [Fact] + public void SetsDropdownOpen() => Block!.DropdownOpen.Should().BeTrue(); + + [Fact] + public void SetsCrossReferenceName() => Block!.CrossReferenceName.Should().Be("test-dropdown"); +} From 9a92523155ac60ab92a7349a748836edd6979c15 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 22 Aug 2025 13:24:18 +0200 Subject: [PATCH 03/16] Edit docs --- docs/syntax/dropdowns.md | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/docs/syntax/dropdowns.md b/docs/syntax/dropdowns.md index 12816eb8d..f21535aad 100644 --- a/docs/syntax/dropdowns.md +++ b/docs/syntax/dropdowns.md @@ -53,14 +53,7 @@ Dropdown content ## Deeplinking -Dropdowns support deeplinking via anchor links. When you navigate to a URL with a hash that points to a dropdown or content within a dropdown, the dropdown will automatically open. When you manually open a dropdown that has a name/anchor, the URL will automatically update to reflect the current state. - -### Features - -- **Automatic opening**: Navigate to `#dropdown-name` and the dropdown opens automatically -- **URL updates**: Open a dropdown manually and the URL updates to show the anchor -- **Nested content**: Link directly to headings or content within dropdowns -- **Browser navigation**: Proper back/forward button support +Dropdowns support deeplinking through anchor links. When you navigate to a URL with a hash that points to a dropdown or content within a dropdown, the dropdown will automatically open. When you manually open a dropdown that has a name/anchor, the URL will automatically update to reflect the current state. :::::{tab-set} @@ -79,11 +72,6 @@ You can also link directly to content within dropdowns. This content has the anc ::: -**Test the features:** -- [Link to dropdown](#deeplink-example) - Opens the dropdown and updates URL -- [Link to nested content](#nested-content) - Opens dropdown and scrolls to nested content -- Try opening/closing the dropdown manually and watch the URL change - :::: ::::{tab-item} Markdown @@ -100,23 +88,6 @@ When you open this dropdown manually by clicking the title, the URL will automat You can also link directly to content within dropdowns. This content has the anchor `#nested-content`. ::: - -**Test the features:** -- [Link to dropdown](#deeplink-example) - Opens the dropdown and updates URL -- [Link to nested content](#nested-content) - Opens dropdown and scrolls to nested content -- Try opening/closing the dropdown manually and watch the URL change -``` :::: ::::: - -### Use Cases - -Deeplinking is particularly useful for: - -- **FAQ sections**: Allow users to share links to specific questions -- **Documentation**: Link directly to explanations that might be collapsed by default -- **Troubleshooting guides**: Share direct links to specific solutions -- **API documentation**: Link to specific endpoint details within collapsed sections - -The URL behaves just like clicking on a heading with an anchor - it updates automatically when you interact with the content. From fa2ea6b736990d2cda876f3da1f52c0282d4eeb2 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 22 Aug 2025 13:33:51 +0200 Subject: [PATCH 04/16] Format file --- .../Assets/open-details-with-anchor.ts | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index cbd185d04..447c30312 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -23,7 +23,7 @@ export function openDetailsWithAnchor() { function updateUrlForDropdown(details: HTMLDetailsElement, isOpening: boolean) { const dropdownId = details.id if (!dropdownId) return - + if (isOpening) { // Update URL to show the dropdown anchor (like clicking a heading link) window.history.pushState(null, '', `#${dropdownId}`) @@ -36,24 +36,31 @@ function updateUrlForDropdown(details: HTMLDetailsElement, isOpening: boolean) { export function initOpenDetailsWithAnchor() { // Handle initial page load openDetailsWithAnchor() - + // Handle hash changes within the same page (e.g., clicking anchor links) window.addEventListener('hashchange', openDetailsWithAnchor) - + // Handle manual dropdown toggling to update URL // Use event delegation to catch all toggle events - document.addEventListener('toggle', (event) => { - const target = event.target as HTMLElement - - // Check if the target is a details element with dropdown class - if (target.tagName === 'DETAILS' && target.classList.contains('dropdown')) { - const details = target as HTMLDetailsElement - const isOpening = details.open - - // Use setTimeout to ensure the toggle state has been processed - setTimeout(() => { - updateUrlForDropdown(details, isOpening) - }, 0) - } - }, true) // Use capture phase to ensure we catch the event + document.addEventListener( + 'toggle', + (event) => { + const target = event.target as HTMLElement + + // Check if the target is a details element with dropdown class + if ( + target.tagName === 'DETAILS' && + target.classList.contains('dropdown') + ) { + const details = target as HTMLDetailsElement + const isOpening = details.open + + // Use setTimeout to ensure the toggle state has been processed + setTimeout(() => { + updateUrlForDropdown(details, isOpening) + }, 0) + } + }, + true + ) // Use capture phase to ensure we catch the event } From bda669a9d9c5270fac6461c2b8ded3acc39c5d9d Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Wed, 27 Aug 2025 15:47:06 +0200 Subject: [PATCH 05/16] Instant and autogenerate anchors --- docs/syntax/dropdowns.md | 6 +-- .../Assets/open-details-with-anchor.ts | 43 ++++++++++++++----- .../Directives/Admonition/AdmonitionBlock.cs | 7 +++ .../Directives/Dropdown/DropdownView.cshtml | 2 +- src/Elastic.Markdown/Myst/ParserContext.cs | 16 +++++++ test-dropdown-names.md | 0 6 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 test-dropdown-names.md diff --git a/docs/syntax/dropdowns.md b/docs/syntax/dropdowns.md index f21535aad..47d454ecd 100644 --- a/docs/syntax/dropdowns.md +++ b/docs/syntax/dropdowns.md @@ -9,7 +9,7 @@ Dropdowns allow you to hide and reveal content on user interaction. By default, ::::{tab-item} Output -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 1 Dropdown content ::: @@ -17,7 +17,7 @@ Dropdown content ::::{tab-item} Markdown ```markdown -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 1 Dropdown content ::: ``` @@ -33,7 +33,7 @@ You can specify that the dropdown content should be visible by default. Do this ::::{tab-item} Output -:::{dropdown} Dropdown Title +:::{dropdown} Dropdown Title 2 :open: Dropdown content ::: diff --git a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts index 447c30312..5400abe4e 100644 --- a/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts +++ b/src/Elastic.Documentation.Site/Assets/open-details-with-anchor.ts @@ -5,16 +5,22 @@ export function openDetailsWithAnchor() { const target = document.querySelector(window.location.hash) if (target) { const closestDetails = target.closest('details') - if (closestDetails) { + if (closestDetails && !closestDetails.open) { + // Mark that we're doing a programmatic open + isProgrammaticOpen = true closestDetails.open = true - // Small delay to ensure the details element is open before scrolling + + // Reset the flag after the toggle event has fired setTimeout(() => { - target.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }) - }, 50) + isProgrammaticOpen = false + }, 10) } + + // Chrome automatically ensures parent content is visible, scroll immediately + target.scrollIntoView({ + behavior: 'instant', + block: 'start', + }) } } } @@ -32,6 +38,9 @@ function updateUrlForDropdown(details: HTMLDetailsElement, isOpening: boolean) { // This keeps the URL consistent with how headings behave } +// Track if we're currently in a programmatic open operation +let isProgrammaticOpen = false + // Initialize the anchor handling functionality export function initOpenDetailsWithAnchor() { // Handle initial page load @@ -40,6 +49,15 @@ export function initOpenDetailsWithAnchor() { // Handle hash changes within the same page (e.g., clicking anchor links) window.addEventListener('hashchange', openDetailsWithAnchor) + // Remove data-skip-url-update on first click to enable URL updates + document.addEventListener('click', (event) => { + const target = event.target as HTMLElement + const dropdown = target.closest('details.dropdown') as HTMLDetailsElement + if (dropdown && dropdown.dataset.skipUrlUpdate === "true") { + delete dropdown.dataset.skipUrlUpdate + } + }, true) + // Handle manual dropdown toggling to update URL // Use event delegation to catch all toggle events document.addEventListener( @@ -55,10 +73,13 @@ export function initOpenDetailsWithAnchor() { const details = target as HTMLDetailsElement const isOpening = details.open - // Use setTimeout to ensure the toggle state has been processed - setTimeout(() => { - updateUrlForDropdown(details, isOpening) - }, 0) + // Only update URL if NOT skipping and NOT a programmatic open + if (!details.dataset.skipUrlUpdate && !isProgrammaticOpen) { + // Use setTimeout to ensure the toggle state has been processed + setTimeout(() => { + updateUrlForDropdown(details, isOpening) + }, 0) + } } }, true diff --git a/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs b/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs index ba676590f..845e2861c 100644 --- a/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Admonition/AdmonitionBlock.cs @@ -43,5 +43,12 @@ public override void FinalizeAndValidate(ParserContext context) else if (!string.IsNullOrEmpty(Arguments)) Title += $" {Arguments}"; Title = Title.ReplaceSubstitutions(context); + + // Auto-generate CrossReferenceName for dropdowns without explicit name + if (string.IsNullOrEmpty(CrossReferenceName) && (Admonition == "dropdown" || Classes == "dropdown")) + { + var baseSlug = Title.Slugify(); + CrossReferenceName = context.GetUniqueSlug($"dropdown-{baseSlug}"); + } } } diff --git a/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml b/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml index bdf9f6dc8..2c65ffa4c 100644 --- a/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Dropdown/DropdownView.cshtml @@ -1,5 +1,5 @@ @inherits RazorSlice -