Files

235 lines
6.7 KiB
PHP
Raw Permalink Normal View History

2026-09-24 18:06:11 +02:00
<?php
/**
* Eight fields on the menu item screen.
*
* WordPress core has offered `wp_nav_menu_item_custom_fields` since 5.4, so
* this needs no plugin and no custom admin page — the fields appear inside the
* item WordPress already draws, and save with it.
*
* Eight against UberMenu's 149. That is a real reduction in control and it is
* deliberate: what went away is per-item colour, per-item background image and
* per-item height, which is exactly the set that made the old menu a picture
* instead of a design. Colour comes from tokens, the ground comes from the
* parchment border-image, and height comes from the content.
*
* @package TLIG
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* The field definitions, in the order they are drawn.
*
* @return array<string,array<string,mixed>>
*/
function tlig_nav_fields(): array {
return [
'panel' => [
'type' => 'checkbox',
'label' => __( 'Opens a mega panel', 'tlig' ),
'help' => __( 'Top-level items only. The panel is as tall as its content.', 'tlig' ),
],
'heading' => [
'type' => 'checkbox',
'label' => __( 'Heading, not a link', 'tlig' ),
],
'span' => [
'type' => 'select',
'label' => __( 'Column width', 'tlig' ),
'options' => [
'' => __( 'Automatic', 'tlig' ),
'3' => __( 'Quarter', 'tlig' ),
'4' => __( 'Third', 'tlig' ),
'6' => __( 'Half', 'tlig' ),
'8' => __( 'Two thirds', 'tlig' ),
'12' => __( 'Full width', 'tlig' ),
],
],
'layout' => [
'type' => 'select',
'label' => __( 'Image position', 'tlig' ),
'options' => [
'' => __( 'No image', 'tlig' ),
'image_left' => __( 'Beside the label', 'tlig' ),
'image_below' => __( 'Above the label', 'tlig' ),
],
],
'image' => [
'type' => 'number',
'label' => __( 'Image (attachment ID)', 'tlig' ),
],
'icon' => [
'type' => 'select',
'label' => __( 'Icon', 'tlig' ),
'options' => [
'' => __( 'None', 'tlig' ),
'search' => __( 'Search', 'tlig' ),
'book' => __( 'Book', 'tlig' ),
'headphones' => __( 'Headphones', 'tlig' ),
],
],
'feed' => [
'type' => 'number',
'label' => __( 'List children of page ID', 'tlig' ),
'help' => __( 'Generated at render time, so new pages appear by themselves.', 'tlig' ),
],
'block' => [
'type' => 'textarea',
'label' => __( 'HTML block', 'tlig' ),
'help' => __( 'An escape hatch. Filtered through wp_kses_post, so it cannot carry a stylesheet.', 'tlig' ),
],
];
}
/**
* Draws the fields inside the menu item.
*
* @param int $item_id Menu item post ID.
*/
function tlig_nav_item_fields( $item_id ): void {
$nav = tlig_nav_meta( (int) $item_id );
wp_nonce_field( 'tlig_nav_' . $item_id, 'tlig_nav_nonce_' . $item_id );
echo '<fieldset class="field-tlig-nav description-wide" style="margin:10px 0;padding:8px 10px;border:1px solid #dcdcde;">';
echo '<legend style="font-weight:600;padding:0 4px;">' . esc_html__( 'TLIG navigation', 'tlig' ) . '</legend>';
foreach ( tlig_nav_fields() as $key => $field ) {
$name = sprintf( 'tlig_nav[%d][%s]', $item_id, $key );
$id = sprintf( 'tlig-nav-%d-%s', $item_id, $key );
$value = $nav[ $key ] ?? '';
// `feed` is stored as an array so the walker can grow it later; the
// screen only ever asks for the parent, which is the part an editor
// would change.
if ( 'feed' === $key && is_array( $value ) ) {
$value = $value['parent'] ?? '';
}
echo '<p class="description description-wide" style="margin:6px 0;">';
switch ( $field['type'] ) {
case 'checkbox':
printf(
'<label for="%1$s"><input type="checkbox" id="%1$s" name="%2$s" value="1"%3$s> %4$s</label>',
esc_attr( $id ),
esc_attr( $name ),
checked( ! empty( $value ), true, false ),
esc_html( $field['label'] )
);
break;
case 'select':
printf( '<label for="%s">%s<br>', esc_attr( $id ), esc_html( $field['label'] ) );
printf( '<select id="%s" name="%s" class="widefat">', esc_attr( $id ), esc_attr( $name ) );
foreach ( $field['options'] as $opt => $text ) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr( $opt ),
selected( (string) $value, (string) $opt, false ),
esc_html( $text )
);
}
echo '</select></label>';
break;
case 'textarea':
printf(
'<label for="%1$s">%2$s<br><textarea id="%1$s" name="%3$s" rows="3" class="widefat">%4$s</textarea></label>',
esc_attr( $id ),
esc_html( $field['label'] ),
esc_attr( $name ),
esc_textarea( (string) $value )
);
break;
default:
printf(
'<label for="%1$s">%2$s<br><input type="number" id="%1$s" name="%3$s" value="%4$s" class="widefat"></label>',
esc_attr( $id ),
esc_html( $field['label'] ),
esc_attr( $name ),
esc_attr( (string) $value )
);
}
if ( ! empty( $field['help'] ) ) {
printf( '<span class="description" style="display:block;color:#646970;">%s</span>', esc_html( $field['help'] ) );
}
echo '</p>';
}
echo '</fieldset>';
}
add_action( 'wp_nav_menu_item_custom_fields', 'tlig_nav_item_fields', 10, 1 );
/**
* Saves them.
*
* Stored as one array under `_tlig_nav` rather than eight meta rows: the whole
* point is that an item's configuration is small enough to read at a glance.
* An empty configuration deletes the row instead of writing an empty array, so
* the 214 items that need no settings carry no meta at all.
*
* @param int $menu_id Menu term ID.
* @param int $item_id Menu item post ID.
*/
function tlig_nav_item_save( $menu_id, $item_id ): void {
$nonce = $_POST[ 'tlig_nav_nonce_' . $item_id ] ?? '';
if ( ! wp_verify_nonce( sanitize_key( $nonce ), 'tlig_nav_' . $item_id ) ) {
return;
}
$raw = $_POST['tlig_nav'][ $item_id ] ?? [];
if ( ! is_array( $raw ) ) {
$raw = [];
}
$nav = [];
foreach ( tlig_nav_fields() as $key => $field ) {
$value = $raw[ $key ] ?? '';
switch ( $field['type'] ) {
case 'checkbox':
if ( ! empty( $value ) ) {
$nav[ $key ] = 1;
}
break;
case 'select':
$value = (string) $value;
if ( '' !== $value && isset( $field['options'][ $value ] ) ) {
$nav[ $key ] = 'span' === $key ? (int) $value : $value;
}
break;
case 'textarea':
$value = wp_kses_post( (string) $value );
if ( '' !== trim( $value ) ) {
$nav[ $key ] = $value;
}
break;
default:
$value = (int) $value;
if ( $value > 0 ) {
$nav[ $key ] = 'feed' === $key
? [ 'parent' => $value, 'count' => -1, 'orderby' => 'title' ]
: $value;
}
}
}
if ( $nav ) {
update_post_meta( $item_id, '_tlig_nav', $nav );
} else {
delete_post_meta( $item_id, '_tlig_nav' );
}
}
add_action( 'wp_update_nav_menu_item', 'tlig_nav_item_save', 10, 2 );