Files
wordpress-theme-aksrt/src/Admin/NavMenuOptions.php
T
kate522 009f9874c0 feat(widgets): render icon fields with the universal icon picker
The widget FormRenderer icon case and the nav-menu item icon field now
emit the shared picker (preview + choose/clear + editable text input,
delegated events so lazy forms and repeater rows work). Icon values are
sanitized through aksrt_sanitize_icon_value on both save paths so inline
customsvg- markup survives; the widget admin screen enqueues the picker.
2026-09-06 12:59:12 +08:00

275 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Native WP nav-menu item options using legacy's aksrt_menu_options meta contract.
*
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class NavMenuOptions {
public static function init(): void {
add_action('wp_nav_menu_item_custom_fields', [self::class, 'render_fields'], 10, 2);
add_action('wp_update_nav_menu_item', [self::class, 'save'], 10, 2);
add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin']);
}
public static function enqueue_admin(string $hook): void {
if ($hook !== 'nav-menus.php') {
return;
}
wp_enqueue_media();
aksrt_icon_picker_assets();
$path = get_template_directory() . '/assets/js/admin-nav-menu-senior.js';
if (file_exists($path)) {
wp_enqueue_script(
'aksrt-admin-nav-menu-senior',
get_template_directory_uri() . '/assets/js/admin-nav-menu-senior.js',
['jquery'],
(string) filemtime($path),
true
);
}
}
/**
* @param int $item_id
* @param \WP_Post $item
*/
public static function render_fields($item_id, $item): void {
$item_id = (int) $item_id;
$opts = get_post_meta($item_id, 'aksrt_menu_options', true);
$opts = is_array($opts) ? $opts : [];
$icon = (string) ($opts['icon'] ?? '');
$badge = (string) ($opts['badge'] ?? '');
$badge_class = (string) ($opts['badge_class'] ?? '');
$submenu_s = !empty($opts['submenu_s']);
$submenu_type = (string) ($opts['submenu_type'] ?? '');
$graphic_json = !empty($opts['graphic_card_opts'])
? wp_json_encode($opts['graphic_card_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: self::sample_graphic_json();
$multi_json = !empty($opts['multi_column_links_opts'])
? wp_json_encode($opts['multi_column_links_opts'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: self::sample_multi_json();
?>
<p class="field-aksrt-icon description description-wide">
<label for="edit-menu-item-aksrt-icon-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('图标', 'aksrt'); ?>
</label>
<?php
echo aksrt_icon_picker_field(
sprintf('aksrt_menu_options[%d][icon]', $item_id),
sprintf('edit-menu-item-aksrt-icon-%d', $item_id),
$icon
);
?>
</p>
<p class="field-aksrt-badge description description-thin">
<label for="edit-menu-item-aksrt-badge-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('徽章文字', 'aksrt'); ?><br>
<input type="text" id="edit-menu-item-aksrt-badge-<?php echo esc_attr((string) $item_id); ?>"
class="widefat" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][badge]"
value="<?php echo esc_attr($badge); ?>">
</label>
</p>
<p class="field-aksrt-badge-class description description-thin">
<label for="edit-menu-item-aksrt-badge-class-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('徽章 class', 'aksrt'); ?><br>
<input type="text" id="edit-menu-item-aksrt-badge-class-<?php echo esc_attr((string) $item_id); ?>"
class="widefat" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][badge_class]"
value="<?php echo esc_attr($badge_class); ?>">
</label>
</p>
<p class="field-aksrt-submenu description description-wide">
<label>
<input type="checkbox" name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][submenu_s]" value="1" <?php checked($submenu_s); ?>>
<?php esc_html_e('启用高级子菜单(仅一级菜单)', 'aksrt'); ?>
</label>
</p>
<p class="field-aksrt-submenu-type description description-wide">
<label for="edit-menu-item-aksrt-submenu-type-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('高级子菜单类型', 'aksrt'); ?><br>
<select id="edit-menu-item-aksrt-submenu-type-<?php echo esc_attr((string) $item_id); ?>"
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][submenu_type]">
<option value="" <?php selected($submenu_type, ''); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
<option value="graphic_card" <?php selected($submenu_type, 'graphic_card'); ?>><?php esc_html_e('图文卡片', 'aksrt'); ?></option>
<option value="multi_column_links" <?php selected($submenu_type, 'multi_column_links'); ?>><?php esc_html_e('多列链接', 'aksrt'); ?></option>
</select>
</label>
</p>
<p class="field-aksrt-graphic description description-wide">
<label for="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('图文卡片 JSON(type=graphic_card)', 'aksrt'); ?><br>
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-graphic-<?php echo esc_attr((string) $item_id); ?>"
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][graphic_card_opts_json]"><?php echo esc_textarea((string) $graphic_json); ?></textarea>
</label>
</p>
<p class="field-aksrt-multi description description-wide">
<label for="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>">
<?php esc_html_e('多列链接 JSON(type=multi_column_links)', 'aksrt'); ?><br>
<textarea rows="8" class="widefat code" id="edit-menu-item-aksrt-multi-<?php echo esc_attr((string) $item_id); ?>"
name="aksrt_menu_options[<?php echo esc_attr((string) $item_id); ?>][multi_column_links_opts_json]"><?php echo esc_textarea((string) $multi_json); ?></textarea>
</label>
</p>
<?php
}
/**
* @param int $menu_id
* @param int $menu_item_db_id
*/
public static function save($menu_id, $menu_item_db_id): void {
$menu_item_db_id = (int) $menu_item_db_id;
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if (empty($_POST['aksrt_menu_options'][ $menu_item_db_id ]) || !is_array($_POST['aksrt_menu_options'][ $menu_item_db_id ])) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$raw = wp_unslash($_POST['aksrt_menu_options'][ $menu_item_db_id ]);
$clean = [
'icon' => aksrt_sanitize_icon_value((string) ($raw['icon'] ?? '')),
'badge' => sanitize_text_field((string) ($raw['badge'] ?? '')),
'badge_class' => sanitize_html_class((string) ($raw['badge_class'] ?? '')),
'submenu_s' => !empty($raw['submenu_s']),
'submenu_type' => sanitize_key((string) ($raw['submenu_type'] ?? '')),
];
$graphic = self::decode_json_field((string) ($raw['graphic_card_opts_json'] ?? ''));
if (is_array($graphic)) {
$clean['graphic_card_opts'] = self::sanitize_graphic_opts($graphic);
}
$multi = self::decode_json_field((string) ($raw['multi_column_links_opts_json'] ?? ''));
if (is_array($multi)) {
$clean['multi_column_links_opts'] = self::sanitize_multi_opts($multi);
}
update_post_meta($menu_item_db_id, 'aksrt_menu_options', $clean);
}
/**
* @return array<string,mixed>|null
*/
private static function decode_json_field(string $json): ?array {
$json = trim($json);
if ($json === '') {
return null;
}
$data = json_decode($json, true);
return is_array($data) ? $data : null;
}
/**
* @param array<string,mixed> $opts
* @return array<string,mixed>
*/
private static function sanitize_graphic_opts(array $opts): array {
$items_in = is_array($opts['items'] ?? null) ? $opts['items'] : [];
$items = [];
foreach ($items_in as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'img' => esc_url_raw((string) ($item['img'] ?? '')),
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
'badge' => sanitize_text_field((string) ($item['badge'] ?? '')),
'badge_class' => sanitize_html_class((string) ($item['badge_class'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
],
];
}
return [
'arrangement' => sanitize_key((string) ($opts['arrangement'] ?? '')),
'align' => sanitize_key((string) ($opts['align'] ?? '')),
'size' => sanitize_key((string) ($opts['size'] ?? '')),
'img_scale' => max(0, min(200, (int) ($opts['img_scale'] ?? 100))),
'items' => $items,
];
}
/**
* @param array<string,mixed> $opts
* @return array<string,mixed>
*/
private static function sanitize_multi_opts(array $opts): array {
$columns_in = is_array($opts['columns'] ?? null) ? $opts['columns'] : [];
$columns = [];
foreach ($columns_in as $col) {
if (!is_array($col)) {
continue;
}
$items = [];
if (!empty($col['items']) && is_array($col['items'])) {
foreach ($col['items'] as $item) {
if (!is_array($item)) {
continue;
}
$items[] = [
'title' => sanitize_text_field((string) ($item['title'] ?? '')),
'icon' => sanitize_text_field((string) ($item['icon'] ?? '')),
'img_icon' => esc_url_raw((string) ($item['img_icon'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($item['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($item['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($item['link']['target'] ?? '')),
],
];
}
}
$columns[] = [
'title' => sanitize_text_field((string) ($col['title'] ?? '')),
'icon' => sanitize_text_field((string) ($col['icon'] ?? '')),
'img_icon' => esc_url_raw((string) ($col['img_icon'] ?? '')),
'link' => [
'url' => esc_url_raw((string) ($col['link']['url'] ?? '')),
'text' => sanitize_text_field((string) ($col['link']['text'] ?? '')),
'target' => sanitize_text_field((string) ($col['link']['target'] ?? '')),
],
'items' => $items,
];
}
return ['columns' => $columns];
}
private static function sample_graphic_json(): string {
$sample = [
'arrangement' => '',
'size' => 'md',
'img_scale' => 100,
'items' => [
[
'img' => '',
'title' => '卡片标题',
'badge' => '',
'badge_class' => 'jb-red',
'link' => ['url' => home_url('/'), 'text' => '', 'target' => ''],
],
],
];
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
private static function sample_multi_json(): string {
$sample = [
'columns' => [
[
'title' => '栏目一',
'icon' => 'fa fas fa-folder',
'link' => ['url' => '', 'text' => '', 'target' => ''],
'items' => [
['title' => '链接 A', 'icon' => '', 'link' => ['url' => home_url('/'), 'text' => '', 'target' => '']],
],
],
],
];
return (string) wp_json_encode($sample, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}
}