display_builder-1.0.x-dev/components/shoelace/tabs/tabs.js
components/shoelace/tabs/tabs.js
/**
* @file
* Specific behaviors for the display builder tabs component.
*/
((Drupal, once) => {
/**
* Synchronizes the visibility of tab panes based on active tab state.
*
* @param {NodeList} tabs - Collection of tab elements
*/
function syncPanes(tabs) {
Array.from(tabs).forEach((tab) => {
const target = tab.getAttribute('data-target');
const pane = document.querySelector(target);
if (!pane) {
return;
}
pane.style.display = tab.classList.contains('db-tabs__tab--active')
? 'block'
: 'none';
});
}
/**
* Switches the active tab and updates tab pane visibility.
*
* @param {string} builderId - The builder id.
* @param {NodeList} tabs - Collection of tab elements
* @param {HTMLElement} activeTab - The tab to make active
* @param {string} tabId - The tab wrapper id.
* @param {bool} saveState - Save the tab selected.
*/
function switchTab(builderId, tabs, activeTab, tabId, saveState = true) {
tabs.forEach((tab) => {
tab.classList.remove('db-tabs__tab--active');
tab.removeAttribute('active');
Drupal.displayBuilder.LocalStorageManager.remove(
builderId,
`tabActive.${tabId}`,
);
});
activeTab.classList.add('db-tabs__tab--active');
activeTab.setAttribute('active', true);
if (saveState) {
Drupal.displayBuilder.LocalStorageManager.set(
builderId,
`tabActive.${tabId}`,
activeTab.dataset.target,
);
}
syncPanes(tabs);
}
/**
* Adds click and keyboard event handlers for tab switching.
*
* @param {string} builderId - The builder id.
* @param {HTMLElement} tabsComponent - The tabs container element
* @listens event:click
* @listens event:keydown
*/
function addSwitchingMechanism(builderId, tabsComponent) {
const tabs = tabsComponent.querySelectorAll('.db-tabs__tab');
syncPanes(tabs);
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
switchTab(builderId, tabs, tab, tabsComponent.id);
});
tab.addEventListener('keydown', (event) => {
if (event.keyCode === 13) {
switchTab(builderId, tabs, tab, tabsComponent.id);
}
});
});
}
/**
* Checks if a tab pane is empty.
*
* @param {HTMLElement} pane - The tab pane element to check
* @return {boolean} True if the pane is empty
*/
function isPaneEmpty(pane) {
return (
pane.textContent.trim().length === 0 &&
!pane.querySelector('input:not([type="hidden"])')
);
}
/**
* Hides empty tabs and activates the first visible tab.
*
* @param {string} builderId - The builder id.
* @param {HTMLElement} tabsComponent - The tabs container element
*/
function hideEmptyTabs(builderId, tabsComponent) {
const tabs = tabsComponent.querySelectorAll('.db-tabs__tab');
Array.from(tabs).forEach((tab) => {
const target = tab.getAttribute('data-target');
const pane = document.querySelector(target);
tab.classList.remove('db-tabs__tab--hidden');
if (isPaneEmpty(pane)) {
tab.classList.add('db-tabs__tab--hidden');
}
});
const firstVisibleTab = tabsComponent.querySelector(
'.db-tabs__tab:not(.db-tabs__tab--hidden)',
);
if (firstVisibleTab) {
switchTab(builderId, tabs, firstVisibleTab, null, false);
}
}
/**
* Restore tabs state from local storage.
*
* @param {string} builderId - The builder id.
* @param {NodeList} tabs - Collection of tab elements
*/
function restoreTabsState(builderId, tabs) {
const tabOpen = Drupal.displayBuilder.LocalStorageManager.get(
builderId,
`tabActive.${tabs.id}`,
);
if (!tabOpen) {
return;
}
const tab = document.querySelector(
`.db-tabs__tab[data-target="${tabOpen}"]`,
);
if (!tab) {
return;
}
const tabsList = tabs.querySelectorAll('.db-tabs__tab');
if (!tabsList) {
return;
}
switchTab(builderId, tabsList, tab, null, false);
}
/**
* Drupal behavior for display builder tabs.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behaviors for display builder functionality.
*/
Drupal.behaviors.displayBuilderTabs = {
attach(context) {
once('dbTabs', '.db-display-builder .db-tabs', context).forEach(
(tabsComponent) => {
const builderId = tabsComponent.closest('.db-display-builder').id;
addSwitchingMechanism(builderId, tabsComponent);
// Restore tabs state from local storage
restoreTabsState(builderId, tabsComponent);
},
);
// Island instance form is dynamically loaded, once not persist on htmx
// calls, need to act when it's loaded.
// @todo pass to once
if (
context.classList &&
context.classList.contains('db-island-instance_form')
) {
const tabsComponents = document.querySelectorAll(
'.db-display-builder .db-tabs--contextual',
);
Array.from(tabsComponents).forEach((tabsComponent) => {
const builderId = tabsComponent.closest('.db-display-builder').id;
hideEmptyTabs(builderId, tabsComponent);
restoreTabsState(builderId, tabsComponent);
});
}
},
};
})(Drupal, once);
