evaluator = $evaluator; } /** * Registers hooks. * * @return void */ public function register(): void { add_filter( 'wp_nav_menu_objects', array( $this, 'filter' ) ); } /** * Removes hidden menu items and everything below them. * * - A hidden parent hides all of its sub-items. Walker::walk() would * otherwise show orphans at the top level (or drop them, with a depth). * - A visible parent whose sub-items are all hidden loses its * `menu-item-has-children` class, which wp_nav_menu() adds before this * filter runs, so themes don't draw a dropdown arrow for nothing. * * @param mixed $items Sorted menu items, keyed by position. Usually an array of * objects, but earlier callbacks of this filter may change it. * @return mixed The visible items, or $items unchanged if it isn't an array. */ public function filter( $items ) { if ( ! is_array( $items ) || empty( $items ) ) { return $items; } $parents = array(); foreach ( $items as $item ) { $parents[ (int) $item->ID ] = (int) $item->menu_item_parent; } $hidden = array(); foreach ( $items as $item ) { $rule = get_post_meta( (int) $item->ID, self::META_KEY, true ); if ( is_array( $rule ) && ! $this->evaluator->is_visible( $rule ) ) { $hidden[ (int) $item->ID ] = true; } } if ( empty( $hidden ) ) { return $items; } $visible = array(); foreach ( $items as $key => $item ) { if ( ! $this->is_hidden_or_below_hidden( (int) $item->ID, $parents, $hidden ) ) { $visible[ $key ] = $item; } } return $this->drop_empty_parent_classes( $visible ); } /** * Whether an item is hidden, or any of its ancestors is. * * @param int $id Menu item ID. * @param array $parents Parent ID of each item. * @param array $hidden Hidden item IDs. * @return bool */ private function is_hidden_or_below_hidden( int $id, array $parents, array $hidden ): bool { $seen = array(); while ( $id && ! isset( $seen[ $id ] ) ) { if ( isset( $hidden[ $id ] ) ) { return true; } $seen[ $id ] = true; $id = $parents[ $id ] ?? 0; } return false; } /** * Removes `menu-item-has-children` from items that have no visible sub-items left. * * @param array $items Visible menu items. * @return array */ private function drop_empty_parent_classes( array $items ): array { $has_children = array(); foreach ( $items as $item ) { $has_children[ (int) $item->menu_item_parent ] = true; } foreach ( $items as $item ) { if ( ! isset( $has_children[ (int) $item->ID ] ) && isset( $item->classes ) && is_array( $item->classes ) ) { $item->classes = array_values( array_diff( $item->classes, array( 'menu-item-has-children' ) ) ); } } return $items; } }