Skip to content

Instantly share code, notes, and snippets.

@addzycullen
Created July 23, 2025 00:59
Show Gist options
  • Select an option

  • Save addzycullen/0f276d51869475eaf82119a3bd177992 to your computer and use it in GitHub Desktop.

Select an option

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.
<?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