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}}