diff --git a/src/js/13-open-nested-tabs.js b/src/js/13-open-nested-tabs.js index 5d8a3161..3a1e4a8d 100644 --- a/src/js/13-open-nested-tabs.js +++ b/src/js/13-open-nested-tabs.js @@ -17,6 +17,12 @@ (function () { 'use strict'; + /** + * Debounce utility function to limit the rate at which a function can fire. + * @param {Function} func - The function to debounce. + * @param {number} wait - The debounce delay in milliseconds. + * @returns {Function} - The debounced function. + */ function debounce(func, wait) { let timeout; return function(...args) { @@ -25,53 +31,172 @@ }; } - const debouncedHighlightAll = debounce(async () => { - const elementsToHighlight = document.querySelectorAll('.tabs.is-loaded pre.highlight'); - elementsToHighlight && await Prism.highlightAll(); - elementsToHighlight.forEach(async (element) => { - await Prism.plugins.lineNumbers.resize(element); + /** + * Debounced function to highlight all code blocks using Prism.js. + * This ensures that highlighting does not occur excessively during rapid tab switches. + */ + const debouncedHighlightAll = debounce(() => { + if (typeof Prism !== 'undefined' && Prism.highlightAll) { + Prism.highlightAll(); + // If using line numbers plugin, resize them accordingly + const lineNumberElements = document.querySelectorAll('.line-numbers'); + lineNumberElements.forEach(element => { + if (Prism.plugins.lineNumbers) { + Prism.plugins.lineNumbers.resize(element); + } + }); + } + }, 200); + + /** + * Retrieves the closest ancestor element with a 'data-sync-group-id' attribute. + * @param {HTMLElement} element - The element to start searching from. + * @returns {HTMLElement|null} - The closest ancestor with 'data-sync-group-id', or null if not found. + */ + function getClosestSyncGroupElement(element) { + if (element) { + return element.closest('[data-sync-group-id]'); + } + return null; + } + + /** + * Activates a tab by its ID across all synchronization groups it belongs to. + * @param {string} tabId - The ID of the tab to activate. + */ + function activateTab(tabId) { + + const targetTab = document.getElementById(tabId); + if (!targetTab) { + return; + } + + const syncGroupElement = getClosestSyncGroupElement(targetTab); + if (!syncGroupElement) { + return; + } + + const syncGroupIds = syncGroupElement.dataset.syncGroupId.split('|').map(id => id.trim()); + + syncGroupIds.forEach(syncGroupId => { + // Select all tab groups with this synchronization group ID + const tabGroups = document.querySelectorAll(`[data-sync-group-id="${syncGroupId}"]`); + tabGroups.forEach(group => { + // Deactivate all tabs in this group + const tabs = group.querySelectorAll('li.tab'); + tabs.forEach(tab => { + tab.classList.remove('active'); + tab.setAttribute('aria-selected', 'false'); + tab.setAttribute('tabindex', '-1'); + }); + + // Hide all tab panels in this group + const panels = group.querySelectorAll('.tabpanel'); + panels.forEach(panel => { + panel.classList.add('is-hidden'); + panel.setAttribute('hidden', ''); + }); + + // Activate the target tab in this group + const currentTab = group.querySelector(`#${tabId}`); + if (currentTab) { + currentTab.classList.add('active'); + currentTab.setAttribute('aria-selected', 'true'); + currentTab.setAttribute('tabindex', '0'); + + // Show the corresponding tab panel + const panelId = `${tabId}--panel`; + const targetPanel = group.querySelector(`#${panelId}`); + if (targetPanel) { + targetPanel.classList.remove('is-hidden'); + targetPanel.removeAttribute('hidden'); + + // Optionally, scroll to the activated tab panel + targetPanel.scrollIntoView({ behavior: 'smooth' }); + } + } + }); }); - }, 300); - window.addEventListener('DOMContentLoaded', function () { + // Update the URL with the active tab ID const url = new URL(window.location.href); - const tabParam = url.searchParams.get('tab'); + url.searchParams.set('tab', tabId); + window.history.pushState(null, '', url); - function getClosestSyncGroupId(element) { - if (element) { - return element.closest('[data-sync-group-id]'); + // Trigger Prism.js highlighting + debouncedHighlightAll(); + } + + /** + * Removes the 'tab' query parameter from the URL if an anchor is present. + * Ensures that the hash fragment takes precedence over the 'tab' parameter. + */ + function stripTabParamIfHashPresent(url) { + if (url.hash && url.searchParams.has('tab')) { + url.searchParams.delete('tab'); + window.history.replaceState(null, '', url); + } + } + + /** + * Handles deep linking by activating the appropriate tab based on the URL's query parameter or hash fragment. + */ + function handleDeepLinking() { + const url = new URL(window.location.href); + stripTabParamIfHashPresent(url); + + // Re-parse the URL after potential modification + const updatedUrl = new URL(window.location.href); + const updatedTabParam = updatedUrl.searchParams.get('tab'); + const hash = updatedUrl.hash.substring(1); // Remove the '#' character + + // Prioritize hash fragment over query parameter + if (hash) { + const targetTab = document.getElementById(hash); + if (targetTab) { + activateTab(hash); + return; } - return null; } - // Handle deep linking when tabParam exists - if (tabParam) { - const targetTab = document.getElementById(tabParam); + if (updatedTabParam) { + const targetTab = document.getElementById(updatedTabParam); if (targetTab) { - const syncGroup = getClosestSyncGroupId(targetTab); - if (!syncGroup.classList.contains('is-loaded')) { - targetTab.click(); - } - setTimeout(() => { - targetTab.scrollIntoView({ behavior: 'smooth' }); - }, 0); + activateTab(updatedTabParam); + return; } } + } + /** + * Sets up event listeners for tab clicks to handle activation and synchronization. + */ + function setupTabListeners() { const tabs = document.querySelectorAll('li.tab'); + tabs.forEach(function (tab) { tab.addEventListener('click', function (event) { - const currentTab = event.target.closest('li.tab'); - const tabId = currentTab.getAttribute('id'); + event.preventDefault(); + const clickedTab = event.currentTarget; + const tabId = clickedTab.id; + activateTab(tabId); + }); + }); + } - // Update the URL query parameter for the active tab - const url = new URL(window.location.href); - url.searchParams.set('tab', tabId); - window.history.pushState(null, null, url); + /** + * Initializes the tab synchronization and deep linking on page load. + */ + function initializeTabs() { + setupTabListeners(); + handleDeepLinking(); + } + + window.addEventListener('DOMContentLoaded', initializeTabs); + + /** + * Handle browser navigation (back/forward buttons) to maintain tab state. + */ + window.addEventListener('popstate', handleDeepLinking); - // Perform syntax highlighting and re-add pencil spans - debouncedHighlightAll(); - }, true); - }); - }); })(); diff --git a/src/partials/footer-scripts.hbs b/src/partials/footer-scripts.hbs index 6629aaf6..eadc1f5b 100644 --- a/src/partials/footer-scripts.hbs +++ b/src/partials/footer-scripts.hbs @@ -1,5 +1,4 @@ - diff --git a/src/partials/head-scripts.hbs b/src/partials/head-scripts.hbs index d03ab1f7..2e1eae52 100644 --- a/src/partials/head-scripts.hbs +++ b/src/partials/head-scripts.hbs @@ -30,13 +30,6 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= -{{#if (ne page.attributes.role 'bloblang-playground')}} - - - - - -{{/if}} {{#if (or (eq page.attributes.role 'bloblang-playground')(eq page.attributes.role 'bloblang-snippets'))}} @@ -45,7 +38,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= {{/if}} - +{{#if (ne page.attributes.role 'bloblang-playground')}} + + + + + + +{{/if}}