feat(nav): complete remaining zibll parity (search, i18n, senior menus, CSS)
- Remove obsolete dual-path nav CSS; add utilities + language dropdown styles - Wire header/mobile translate switcher from i18n config - Layout-2 inline search hot keywords panel; nav_folding on resize - NavMenuOptions JSON editors for graphic_card / multi_column_links - Section-navs zibll posts-nav-lists; page-navs enqueue; mobile article TOC box - SPA/PHP user-card publish type; rebuild admin assets
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Native WP nav-menu item options (aksrt_menu_options).
|
||||
* Native WP nav-menu item options (aksrt_menu_options) with senior submenu JSON.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -29,6 +29,12 @@ class NavMenuOptions {
|
||||
$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); ?>">
|
||||
@@ -57,7 +63,7 @@ class NavMenuOptions {
|
||||
<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'); ?>
|
||||
<?php esc_html_e('启用高级子菜单(仅一级菜单)', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field-aksrt-submenu-type description description-wide">
|
||||
@@ -71,8 +77,19 @@ class NavMenuOptions {
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p class="description description-wide">
|
||||
<?php esc_html_e('高级子菜单内容(graphic_card_opts / multi_column_links_opts)可在后续可视化编辑;也可通过 meta 写入 JSON。', 'aksrt'); ?>
|
||||
<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
|
||||
}
|
||||
@@ -83,15 +100,12 @@ class NavMenuOptions {
|
||||
*/
|
||||
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 -- WP core nav-menu form nonce.
|
||||
// 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 ])) {
|
||||
// If fields absent, leave existing meta (WP may not resubmit all fields in bulk edits).
|
||||
return;
|
||||
}
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$raw = wp_unslash($_POST['aksrt_menu_options'][ $menu_item_db_id ]);
|
||||
$existing = get_post_meta($menu_item_db_id, 'aksrt_menu_options', true);
|
||||
$existing = is_array($existing) ? $existing : [];
|
||||
|
||||
$clean = [
|
||||
'icon' => sanitize_text_field((string) ($raw['icon'] ?? '')),
|
||||
@@ -100,13 +114,138 @@ class NavMenuOptions {
|
||||
'submenu_s' => !empty($raw['submenu_s']),
|
||||
'submenu_type' => sanitize_key((string) ($raw['submenu_type'] ?? '')),
|
||||
];
|
||||
// Preserve complex opts not edited by this form.
|
||||
if (!empty($existing['graphic_card_opts'])) {
|
||||
$clean['graphic_card_opts'] = $existing['graphic_card_opts'];
|
||||
|
||||
$graphic = self::decode_json_field((string) ($raw['graphic_card_opts_json'] ?? ''));
|
||||
if (is_array($graphic)) {
|
||||
$clean['graphic_card_opts'] = self::sanitize_graphic_opts($graphic);
|
||||
}
|
||||
if (!empty($existing['multi_column_links_opts'])) {
|
||||
$clean['multi_column_links_opts'] = $existing['multi_column_links_opts'];
|
||||
$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' => '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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user