Skip to content

Commit

Permalink
Prevent Prism highlighter from slowing down tab switches (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeSCahill authored Jan 21, 2025
1 parent c03b761 commit 2b4f682
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 42 deletions.
191 changes: 158 additions & 33 deletions src/js/13-open-nested-tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
});
});
})();
1 change: 0 additions & 1 deletion src/partials/footer-scripts.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<!-- Site-specific scripts -->
<script defer src="{{{uiRootPath}}}/js/vendor/tabs.js" data-sync-storage-key="preferred-tab"></script>
<script defer id="site-script" src="{{{uiRootPath}}}/js/site.js" data-ui-root-path="{{{uiRootPath}}}"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/sorttable.js"></script>
<!--<script async src="{{{uiRootPath}}}/js/vendor/highlight.js"></script>-->
Expand Down
17 changes: 9 additions & 8 deletions src/partials/head-scripts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
<script>
var uiRootPath="{{{uiRootPath}}}"
</script>
{{#if (ne page.attributes.role 'bloblang-playground')}}
<link rel="stylesheet" href="{{{uiRootPath}}}/css/vendor/prism/prism.min.css">
<!-- Prism scripts for code blocks -->
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-core.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-line-numbers-plugin.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-line-highlight-plugin.js"></script>
{{/if}}
{{#if (or (eq page.attributes.role 'bloblang-playground')(eq page.attributes.role 'bloblang-snippets'))}}
<script defer src="{{{uiRootPath}}}/js/vendor/ace/ace.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/ace/theme-github.js"></script>
Expand All @@ -45,7 +38,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
<script defer src="{{{uiRootPath}}}/js/vendor/ace/worker-json.js"></script>
<script src="{{{uiRootPath}}}/js/vendor/wasm_exec.js"></script>
{{/if}}
<script async
<script defer
src="https://widget.kapa.ai/kapa-widget.bundle.js"
data-website-id="fe00d579-9e9a-4f83-8f9f-0ca7672262a6"
data-project-name="Redpanda"
Expand All @@ -59,4 +52,12 @@ data-user-analytics-fingerprint-enabled="true"
data-user-analytics-store-ip="true"
data-modal-disclaimer="This is a custom LLM for Redpanda with access to all developer docs, API specs, support FAQs, YouTube tutorials, and resolved GitHub discussions."
></script>
{{#if (ne page.attributes.role 'bloblang-playground')}}
<link rel="stylesheet" href="{{{uiRootPath}}}/css/vendor/prism/prism.min.css">
<!-- Prism scripts for code blocks -->
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-core.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-line-numbers-plugin.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/prism/prism-line-highlight-plugin.js"></script>
<script defer src="{{{uiRootPath}}}/js/vendor/tabs.js" data-sync-storage-key="preferred-tab"></script>
{{/if}}

0 comments on commit 2b4f682

Please sign in to comment.