Instantly share code, notes, and snippets.
Created
July 23, 2025 00:59
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save addzycullen/0f276d51869475eaf82119a3bd177992 to your computer and use it in GitHub Desktop.
ACF Menu Fields Conditional Display System - Standalone WordPress functions for conditionally showing ACF fields on menu items based on their depth level.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * ACF Menu Fields Conditional Display System | |
| * | |
| * Standalone WordPress functions for conditionally showing ACF fields | |
| * on menu items based on their depth level. | |
| * | |
| * Features: | |
| * - Single configuration array controls everything | |
| * - PHP server-side filtering | |
| * - JavaScript real-time updates | |
| * - Dynamic CSS visual indicators | |
| * - Fully DRY implementation | |
| * | |
| * Usage: Add this to your theme's functions.php or as a plugin | |
| * | |
| * @author Adam Cullen | |
| * @author Claude Code | |
| * @version 1.0.0 | |
| */ | |
| // Prevent direct access | |
| if (!defined('ABSPATH')) { | |
| exit; | |
| } | |
| /** | |
| * Get menu field visibility rules configuration | |
| * | |
| * Define which menu fields should be shown only on specific depth levels. | |
| * This is the single source of truth for the entire system. | |
| * | |
| * @return array Associative array of field names and their allowed depth levels | |
| */ | |
| function acf_get_menu_field_rules() { | |
| return [ | |
| // Example: Add more fields as needed | |
| // 'menu__main__custom_field' => [0, 1], // Show on depth 0 and 1 | |
| // 'menu__main__another_field' => [1], // Show only on depth 1 | |
| ]; | |
| } | |
| /** | |
| * Get the depth level of the current menu item | |
| * | |
| * Determines the depth level using multiple detection methods: | |
| * - Menu item classes (menu-item-depth-X) | |
| * - Parent relationship counting | |
| * - POST/GET data analysis | |
| * | |
| * @return int|null The depth level (0, 1, 2, etc.) or null if undetermined | |
| */ | |
| function acf_get_menu_item_depth() { | |
| global $post; | |
| // Get the current menu item ID from various possible sources | |
| $menu_item_id = null; | |
| // Check POST data (when saving) | |
| if (isset($_POST['menu-item-db-id'])) { | |
| $menu_item_id = intval($_POST['menu-item-db-id']); | |
| } | |
| // Check GET data (when editing) | |
| elseif (isset($_GET['menu-item'])) { | |
| $menu_item_id = intval($_GET['menu-item']); | |
| } | |
| // Check if we have a post object | |
| elseif ($post && $post->post_type === 'nav_menu_item') { | |
| $menu_item_id = $post->ID; | |
| } | |
| // If we can't determine the menu item, return null | |
| if (!$menu_item_id) { | |
| return null; | |
| } | |
| // Get the menu item and check its depth | |
| $menu_item = wp_setup_nav_menu_item(get_post($menu_item_id)); | |
| if (!$menu_item) { | |
| return null; | |
| } | |
| // Method 1: Check menu item classes for depth information | |
| $menu_classes = get_post_meta($menu_item_id, '_menu_item_classes', true); | |
| if (is_array($menu_classes)) { | |
| foreach ($menu_classes as $class) { | |
| if (preg_match('/^menu-item-depth-(\d+)$/', $class, $matches)) { | |
| return intval($matches[1]); | |
| } | |
| } | |
| } | |
| // Method 2: Count parent relationships to determine depth | |
| $depth = 0; | |
| $current_parent_id = get_post_meta($menu_item_id, '_menu_item_menu_item_parent', true); | |
| while ($current_parent_id && $depth < 10) { // Safety limit to prevent infinite loops | |
| $depth++; | |
| $current_parent_id = get_post_meta($current_parent_id, '_menu_item_menu_item_parent', true); | |
| } | |
| return $depth; | |
| } | |
| /** | |
| * Conditionally show menu field based on allowed depth levels | |
| * | |
| * Generic function that restricts the display of menu fields to only appear | |
| * on navigation menu items that are at specified depth levels. | |
| * | |
| * @param array $field The ACF field array | |
| * @param array $allowed_depths Array of allowed depth levels (0, 1, 2, etc.) | |
| * @return array Modified field array with conditional logic | |
| */ | |
| function acf_conditionally_show_menu_field($field, $allowed_depths) { | |
| // Check if we're on a nav menu item edit screen | |
| global $pagenow; | |
| // Only apply logic on nav-menus.php (menu admin page) | |
| if ($pagenow !== 'nav-menus.php') { | |
| return $field; | |
| } | |
| // Get the current menu item depth | |
| $current_depth = acf_get_menu_item_depth(); | |
| // If we can't determine depth, show the field (fail-safe) | |
| if ($current_depth === null) { | |
| return $field; | |
| } | |
| // Hide the field if current depth is not in allowed depths | |
| if (!in_array($current_depth, $allowed_depths, true)) { | |
| $field['wrapper']['style'] = 'display: none !important;'; | |
| $field['wrapper']['class'] = $field['wrapper']['class'] ?? ''; | |
| $field['wrapper']['class'] .= ' acf-hidden'; | |
| } | |
| return $field; | |
| } | |
| /** | |
| * Register conditional field filters for menu fields based on depth | |
| * | |
| * Dynamically registers ACF field filters for each configured menu field | |
| * to control their visibility based on menu item depth levels. | |
| */ | |
| function acf_register_conditional_menu_fields() { | |
| $field_rules = acf_get_menu_field_rules(); | |
| foreach ($field_rules as $field_name => $allowed_depths) { | |
| add_filter( | |
| "acf/load_field/name={$field_name}", | |
| function($field) use ($field_name, $allowed_depths) { | |
| return acf_conditionally_show_menu_field($field, $allowed_depths); | |
| } | |
| ); | |
| } | |
| } | |
| /** | |
| * Add JavaScript to conditionally show/hide menu fields based on menu depth | |
| * | |
| * Injects JavaScript into the nav-menus.php admin page to dynamically | |
| * show/hide menu fields based on the menu item's depth level. | |
| * This provides real-time conditional display as users reorganize menu items. | |
| */ | |
| function acf_add_menu_field_conditional_script() { | |
| // Get field rules from PHP and pass to JavaScript | |
| $field_rules = acf_get_menu_field_rules(); | |
| ?> | |
| <script type="text/javascript"> | |
| (function() { | |
| 'use strict'; | |
| /** | |
| * Toggle menu field visibility based on menu item depth | |
| */ | |
| function toggleMenuFields() { | |
| // Field configuration from PHP | |
| const fieldRules = <?php echo wp_json_encode($field_rules); ?>; | |
| // Find all menu items | |
| const menuItems = document.querySelectorAll('.menu-item'); | |
| menuItems.forEach(function(menuItem) { | |
| // Determine the depth of this menu item | |
| let itemDepth = 0; | |
| for (let i = 0; i <= 10; i++) { | |
| if (menuItem.classList.contains('menu-item-depth-' + i)) { | |
| itemDepth = i; | |
| break; | |
| } | |
| } | |
| // Check each configured field | |
| Object.keys(fieldRules).forEach(function(fieldName) { | |
| const field = menuItem.querySelector('[data-name="' + fieldName + '"]'); | |
| // Skip if field not found | |
| if (!field) { | |
| return; | |
| } | |
| const allowedDepths = fieldRules[fieldName]; | |
| const isAllowed = allowedDepths.includes(itemDepth); | |
| if (isAllowed) { | |
| // Show the field for allowed depths | |
| field.style.display = ''; | |
| field.classList.remove('acf-hidden'); | |
| } else { | |
| // Hide the field for non-allowed depths | |
| field.style.display = 'none'; | |
| field.classList.add('acf-hidden'); | |
| } | |
| }); | |
| }); | |
| } | |
| /** | |
| * Add event listener with compatibility check | |
| */ | |
| function addEventListenerSafely(element, event, handler) { | |
| if (element && typeof element.addEventListener === 'function') { | |
| element.addEventListener(event, handler); | |
| } | |
| } | |
| /** | |
| * Initialize when DOM is ready | |
| */ | |
| function initMenuFieldConditional() { | |
| // Initial check | |
| setTimeout(toggleMenuFields, 500); | |
| // Get menu container | |
| const menuContainer = document.getElementById('menu-to-edit'); | |
| if (menuContainer) { | |
| // Use MutationObserver for modern browsers to detect DOM changes | |
| if (window.MutationObserver) { | |
| const observer = new MutationObserver(function(mutations) { | |
| let shouldUpdate = false; | |
| mutations.forEach(function(mutation) { | |
| // Check if classes changed (depth changes) | |
| if (mutation.type === 'attributes' && mutation.attributeName === 'class') { | |
| shouldUpdate = true; | |
| } | |
| // Check if nodes were added/removed (menu structure changes) | |
| if (mutation.type === 'childList' && | |
| (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) { | |
| shouldUpdate = true; | |
| } | |
| }); | |
| if (shouldUpdate) { | |
| setTimeout(toggleMenuFields, 100); | |
| } | |
| }); | |
| // Observe the menu container for changes | |
| observer.observe(menuContainer, { | |
| childList: true, | |
| subtree: true, | |
| attributes: true, | |
| attributeFilter: ['class'] | |
| }); | |
| } | |
| // Listen for custom events that WordPress might trigger | |
| addEventListenerSafely(document, 'menu-item-added', function() { | |
| setTimeout(toggleMenuFields, 100); | |
| }); | |
| // Listen for ACF events | |
| addEventListenerSafely(document, 'acf/setup_fields', function() { | |
| setTimeout(toggleMenuFields, 100); | |
| }); | |
| } | |
| // Fallback: periodic check every 3 seconds | |
| setInterval(toggleMenuFields, 3000); | |
| } | |
| // Initialize when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initMenuFieldConditional); | |
| } else { | |
| initMenuFieldConditional(); | |
| } | |
| })(); | |
| </script> | |
| <style type="text/css"> | |
| /* Ensure hidden fields are properly hidden */ | |
| .acf-field.acf-hidden { | |
| display: none !important; | |
| } | |
| /* Dynamic visual indicators for depth-restricted fields */ | |
| <?php | |
| // Group fields by their allowed depths for efficient CSS generation | |
| $fields_by_depth = []; | |
| $depth_labels = [ | |
| 0 => 'Top Level Only', | |
| 1 => 'Level 1 Only', | |
| 2 => 'Depth 2 Only', | |
| 3 => 'Depth 3 Only' | |
| ]; | |
| $depth_colors = [ | |
| 0 => '#00a32a', // Green for top-level | |
| 1 => '#ff8c00', // Orange for level 1 | |
| 2 => '#0073aa', // Blue for depth 2 | |
| 3 => '#8b5cf6' // Purple for depth 3 | |
| ]; | |
| foreach ($field_rules as $field_name => $allowed_depths) { | |
| foreach ($allowed_depths as $depth) { | |
| if (!isset($fields_by_depth[$depth])) { | |
| $fields_by_depth[$depth] = []; | |
| } | |
| $fields_by_depth[$depth][] = $field_name; | |
| } | |
| } | |
| // Generate CSS for each depth level | |
| foreach ($fields_by_depth as $depth => $field_names) { | |
| $color = $depth_colors[$depth] ?? '#666'; | |
| $label = $depth_labels[$depth] ?? "Depth {$depth} Only"; | |
| // Generate selectors for this depth | |
| $selectors = array_map(function($field_name) use ($depth) { | |
| return ".menu-item-depth-{$depth} .acf-field[data-name=\"{$field_name}\"]"; | |
| }, $field_names); | |
| $selector_list = implode(',', $selectors); | |
| echo "\n/* Depth {$depth} fields */\n"; | |
| echo "{$selector_list} {\n"; | |
| echo " border-left: 3px solid {$color};\n"; | |
| echo " padding-left: 10px;\n"; | |
| echo "}\n\n"; | |
| // Generate ::after pseudo-elements for labels | |
| $after_selectors = array_map(function($field_name) use ($depth) { | |
| return ".menu-item-depth-{$depth} .acf-field[data-name=\"{$field_name}\"] .acf-label label::after"; | |
| }, $field_names); | |
| $after_selector_list = implode(',', $after_selectors); | |
| echo "{$after_selector_list} {\n"; | |
| echo " content: \" ({$label})\";\n"; | |
| echo " font-size: 11px;\n"; | |
| echo " color: #666;\n"; | |
| echo " font-weight: normal;\n"; | |
| echo "}\n"; | |
| } | |
| ?> | |
| </style> | |
| <?php | |
| } | |
| /** | |
| * Initialize the conditional menu fields system | |
| * | |
| * Call this function to set up the entire system. | |
| * You can call this from your theme's functions.php or plugin. | |
| */ | |
| function acf_init_conditional_menu_fields() { | |
| // Only proceed if ACF is active | |
| if (!function_exists('get_field')) { | |
| return; | |
| } | |
| // Register the conditional field filters | |
| acf_register_conditional_menu_fields(); | |
| // Add JavaScript and CSS on menu admin page | |
| add_action('admin_footer-nav-menus.php', 'acf_add_menu_field_conditional_script'); | |
| } | |
| // Auto-initialize the system | |
| add_action('init', 'acf_init_conditional_menu_fields'); | |
| /** | |
| * USAGE INSTRUCTIONS: | |
| * | |
| * 1. Copy this entire file to your WordPress theme or create a plugin | |
| * 2. Edit the acf_get_menu_field_rules() function to define your field rules | |
| * 3. Add your ACF field names and the depth levels they should appear on | |
| * | |
| * Example field rules: | |
| * | |
| * return [ | |
| * 'menu_icon' => [2], // Only show on depth 2 (sub-sub menu) | |
| * 'featured_post' => [0], // Only show on depth 0 (top level) | |
| * 'custom_field' => [0, 1], // Show on depth 0 and 1 | |
| * 'another_field' => [1, 2, 3], // Show on depth 1, 2, and 3 | |
| * ]; | |
| * | |
| * The system will automatically: | |
| * - Hide/show fields on page load (PHP) | |
| * - Update field visibility when menu items are dragged (JavaScript) | |
| * - Add color-coded visual indicators (CSS) | |
| * | |
| * Colors by depth: | |
| * - Depth 0: Green (#00a32a) | |
| * - Depth 1: Orange (#ff8c00) | |
| * - Depth 2: Blue (#0073aa) | |
| * - Depth 3: Purple (#8b5cf6) | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment