isProductDetailsBlock = (name) => { return ["woocommerce/product-details"].includes(name); }; const defaultDisplayTabs = ["description", "additional_information", "reviews"]; const TAB_OPTIONS = { description: wp.i18n.__("Description", "brandy"), additional_information: wp.i18n.__("Additional Information", "brandy"), reviews: wp.i18n.__("Reviews", "brandy"), }; function getTabIdByLabel(tabLabel) { const label = typeof tabLabel === "string" ? tabLabel : tabLabel.value; return Object.entries(TAB_OPTIONS).find(([, value]) => value === label)?.[0]; } function addProductDetailsAttributes(settings) { if (typeof settings.attributes === "undefined") { return settings; } if (!isProductDetailsBlock(settings.name)) { return settings; } settings.attributes = Object.assign(settings.attributes, { displayTabs: { type: "array", default: defaultDisplayTabs, }, }); return settings; } wp.hooks.addFilter( "blocks.registerBlockType", "brandy/product-details-attributes", addProductDetailsAttributes ); /** Display controls */ const ProductDetailsDisplayTabsControls = wp.compose.createHigherOrderComponent( (BlockEdit) => { return (props) => { const { Fragment, useEffect } = wp.element; const { __experimentalToolsPanel: ToolsPanel, __experimentalToolsPanelItem: ToolPanelItem, __experimentalToggleGroupControl: ToggleGroupControl, __experimentalToggleGroupControlOption: ToggleGroupControlOption, FormTokenField, } = wp.components; const { InspectorControls } = wp.blockEditor; const { isSelected, attributes, setAttributes, name, clientId } = props; const displayTabs = attributes.displayTabs || []; const canAddSettings = isProductDetailsBlock(name); // Update editor preview when displayTabs changes useEffect(() => { if (!canAddSettings) { return; } const blockElement = document.querySelector( `[data-block="${clientId}"]` ); if (!blockElement) { return; } const productDetailsBlock = blockElement.querySelector( ".wp-block-woocommerce-product-details" ); if (!productDetailsBlock) { return; } // Update data attributes productDetailsBlock.setAttribute( "data-has-description", displayTabs.includes("description") ? "true" : "false" ); productDetailsBlock.setAttribute( "data-has-additional-information", displayTabs.includes("additional_information") ? "true" : "false" ); productDetailsBlock.setAttribute( "data-has-reviews", displayTabs.includes("reviews") ? "true" : "false" ); }, [displayTabs, clientId, canAddSettings]); return React.createElement( Fragment, null, React.createElement(BlockEdit, props), isSelected && canAddSettings && React.createElement( InspectorControls, {}, React.createElement( ToolsPanel, { label: wp.i18n.__("Tabs settings", "brandy"), resetAll: () => setAttributes({ displayTabs: defaultDisplayTabs, }), }, React.createElement( ToolPanelItem, { label: wp.i18n.__("Tabs", "brandy"), isShownByDefault: true, hasValue: () => attributes.displayTabs != null && attributes.displayTabs.length > 0, onDeselect: () => setAttributes({ displayTabs: defaultDisplayTabs }), __nextHasNoMarginBottom: true, }, React.createElement(FormTokenField, { label: wp.i18n.__("Tabs", "brandy"), suggestions: Object.values(TAB_OPTIONS), validateInput: (value) => { return Object.values(TAB_OPTIONS).includes(value); }, value: displayTabs.map((tab) => TAB_OPTIONS[tab]) || [], onChange: (value) => { const updatedTabs = value .map(getTabIdByLabel) .filter(Boolean); setAttributes({ displayTabs: updatedTabs, }); }, __experimentalExpandOnFocus: true, __experimentalShowHowTo: false, }) ) ) ) ); }; }, "ProductDetailsDisplayTabsControls" ); wp.hooks.addFilter( "editor.BlockEdit", "brandy/product-details-display-tabs-controls", ProductDetailsDisplayTabsControls ); const addProductDetailsDisplayTabsStyleToBlock = wp.compose.createHigherOrderComponent((BlockListBlock) => { return (props) => { const { attributes, name } = props; const extraWrapperProps = props.wrapperProps ?? {}; if (isProductDetailsBlock(name) && attributes.displayTabs != null) { const displayTabs = attributes.displayTabs || []; extraWrapperProps["data-has-description"] = displayTabs.includes( "description" ) ? "true" : "false"; extraWrapperProps["data-has-additional-information"] = displayTabs.includes("additional_information") ? "true" : "false"; extraWrapperProps["data-has-reviews"] = displayTabs.includes("reviews") ? "true" : "false"; } return React.createElement(BlockListBlock, props, extraWrapperProps); }; }, "addProductDetailsDisplayTabsStyleToBlock"); wp.hooks.addFilter( "editor.BlockListBlock", "brandy/product-details-display-tabs-style", addProductDetailsDisplayTabsStyleToBlock, 1 ); /** * Save function */ function addProductDetailsDisplayTabsProps(props, blockType, attributes) { if (!isProductDetailsBlock(blockType.name ?? "")) { return props; } if (attributes.displayTabs != null) { const displayTabs = attributes.displayTabs || []; Object.assign(props, { "data-has-description": displayTabs.includes("description") ? "true" : "false", "data-has-additional-information": displayTabs.includes( "additional_information" ) ? "true" : "false", "data-has-reviews": displayTabs.includes("reviews") ? "true" : "false", }); } return props; } wp.hooks.addFilter( "blocks.getSaveContent.extraProps", "brandy-blocks/product-details-display-tabs-props", addProductDetailsDisplayTabsProps, 1 ); /** * Frontend: Add data attributes to product details blocks on page load * This is a fallback in case PHP filter doesn't work */ (function () { if (typeof window === "undefined") { return; } function addDataAttributesToBlocks() { const blocks = document.querySelectorAll( ".wp-block-woocommerce-product-details[data-display-tabs]" ); blocks.forEach((block) => { // Skip if attributes already added if ( block.hasAttribute("data-has-description") && block.hasAttribute("data-has-additional-information") && block.hasAttribute("data-has-reviews") ) { return; } try { // Get displayTabs from data-display-tabs attribute const displayTabsAttr = block.getAttribute("data-display-tabs"); if (!displayTabsAttr) { return; } // Parse the JSON array const displayTabs = JSON.parse(displayTabsAttr); // Add individual data attributes block.setAttribute( "data-has-description", displayTabs.includes("description") ? "true" : "false" ); block.setAttribute( "data-has-additional-information", displayTabs.includes("additional_information") ? "true" : "false" ); block.setAttribute( "data-has-reviews", displayTabs.includes("reviews") ? "true" : "false" ); } catch (e) { console.warn("Error parsing displayTabs:", e); } }); } // Run on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", addDataAttributesToBlocks); } else { addDataAttributesToBlocks(); } // Also run after a short delay to catch dynamically loaded content setTimeout(addDataAttributesToBlocks, 100); /** * Frontend: Fix active tab when first tab is hidden * Activates the first visible tab if the currently active tab is hidden */ function fixActiveTab() { const productDetailsBlocks = document.querySelectorAll( ".wp-block-woocommerce-product-details" ); productDetailsBlocks.forEach((block) => { // Only process blocks that have our data attributes (tabs have been customized) if ( !block.hasAttribute("data-has-description") && !block.hasAttribute("data-has-additional-information") && !block.hasAttribute("data-has-reviews") ) { return; } // Check if all tabs are hidden - if so, CSS will hide the wrapper, no need to process const hasDescription = block.getAttribute("data-has-description") === "true"; const hasAdditionalInfo = block.getAttribute("data-has-additional-information") === "true"; const hasReviews = block.getAttribute("data-has-reviews") === "true"; // If all tabs are hidden, skip processing (CSS will hide the wrapper) if (!hasDescription && !hasAdditionalInfo && !hasReviews) { return; } // Find the tabs wrapper within this block const tabsWrapper = block.querySelector( ".wc-tabs-wrapper, .woocommerce-tabs" ); if (!tabsWrapper) { return; } // Get all tab links const tabs = tabsWrapper.querySelectorAll( ".wc-tabs li a, ul.tabs li a, .nav-tabs li a" ); if (!tabs || tabs.length === 0) { return; } // Helper function to check if a tab is visible function isTabVisible(tabLink) { const tabLi = tabLink.closest("li"); if (!tabLi) { return false; } const computedStyle = window.getComputedStyle(tabLi); return computedStyle.display !== "none"; } // Find the currently active tab const activeTab = tabsWrapper.querySelector( ".wc-tabs li.active a, ul.tabs li.active a, .nav-tabs li.active a, .wc-tabs li:first-child a, ul.tabs li:first-child a" ); // Check if active tab is hidden or if no active tab exists let needsActivation = false; if (!activeTab) { needsActivation = true; } else if (!isTabVisible(activeTab)) { needsActivation = true; } // If we need to activate a tab, find and activate the first visible one if (needsActivation) { for (let i = 0; i < tabs.length; i++) { if (isTabVisible(tabs[i])) { // Activate this tab // Use jQuery if available (for WooCommerce compatibility) if (typeof jQuery !== "undefined") { jQuery(tabs[i]).trigger("click"); } else { // Fallback to native click tabs[i].click(); } break; } } } }); } // Run fix after WooCommerce tabs are initialized function initTabFix() { // Wait for WooCommerce to initialize tabs setTimeout(() => { fixActiveTab(); }, 200); // Also listen for WooCommerce tab initialization events if (typeof jQuery !== "undefined") { jQuery(document).on( "init", ".wc-tabs-wrapper, .woocommerce-tabs", function () { setTimeout(fixActiveTab, 100); } ); } } // Run on DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initTabFix); } else { initTabFix(); } })();