feat(phase2): shared common-form layer + CommonWidgetFields trait for all widgets

- Extract Zibll CSF_Widget::params() common-field parity into
  inc/modules/widget-common.php (required by widgets-register.php to keep
  both files <1000 lines): subtitle, title_style(simple/big), title_highlight
  +color, title_link x3, title_link_style colors, sidebar_affix, show_type
  (all/only_pc/only_sm), show_id_type(show/hide), show_ids, animation_in,
  animation_repeat, layout_pin_pc/m, layout_bg(white/dark). Adds
  normalize_link/normalize_bg helpers, affix_attrs, common_wrapper_attr,
  render_title, animation_class. Back-compat normalizes legacy tokens
  (in_affix->sidebar_affix, pc/only_pc, include/exclude->show/hide).
- Add src/AKSRT/Widgets/Traits/CommonWidgetFields.php trait
  (render_common_form_section + update_common_fields).
- Wire all 57 widgets to use the trait; migrate the 16 hand-rolled in_affix
  implementations to shared sidebar_affix (fixes 6 latent bugs where
  in_affix was never persisted). SearchWidget intentionally skips the shared
  form section (its business show_type collides) but adopts the affix helper.
- widget_display_callback parity verified via tmp assertion script.

Refs .specs/plans/zibll-widgets-1to1.md Phase 2 (US1/US5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-30 20:42:46 +08:00
parent 98aeee3cc1
commit d975ff4ce6
60 changed files with 1227 additions and 489 deletions
+5 -205
View File
@@ -7,6 +7,8 @@
defined('ABSPATH') || exit;
require_once AKSRT_DIR . '/inc/modules/widget-common.php';
/**
* Register custom widgets.
*/
@@ -307,208 +309,6 @@ if (!function_exists('aksrt_widget_get_area_context')) {
}
}
// ============================================================
// Shared Widget Form Helper — 公共表单字段与显示规则
// ============================================================
if (!function_exists('aksrt_widget_common_defaults')) {
/**
* Default values for shared widget instance fields.
*
* @return array<string, mixed>
*/
function aksrt_widget_common_defaults(): array {
return [
'subtitle' => '',
'title_link' => '',
'show_type' => 'all',
'show_id_type' => '',
'show_ids' => '',
'sidebar_affix' => false,
'animation_in' => '',
'animation_repeat'=> false,
];
}
}
if (!function_exists('aksrt_widget_normalize_common')) {
/**
* Normalize common widget instance fields, filling defaults for legacy instances.
*
* @param array<string, mixed> $instance
* @return array<string, mixed>
*/
function aksrt_widget_normalize_common(array $instance): array {
$defaults = aksrt_widget_common_defaults();
$result = array_merge($defaults, $instance);
$result['subtitle'] = sanitize_text_field($result['subtitle'] ?? '');
$result['title_link'] = esc_url_raw($result['title_link'] ?? '');
$result['show_type'] = in_array($result['show_type'] ?? '', ['all', 'pc', 'mobile'], true) ? $result['show_type'] : 'all';
$result['show_id_type'] = in_array($result['show_id_type'] ?? '', ['', 'include', 'exclude'], true) ? $result['show_id_type'] : '';
$result['show_ids'] = sanitize_text_field($result['show_ids'] ?? '');
$result['sidebar_affix'] = !empty($result['sidebar_affix']);
$result['animation_in'] = sanitize_key($result['animation_in'] ?? '');
$result['animation_repeat']= !empty($result['animation_repeat']);
return $result;
}
}
if (!function_exists('aksrt_widget_render_common_form')) {
/**
* Render shared widget admin form fields.
*
* Call this from your widget's form() method after rendering custom fields.
*
* @param array<string, mixed> $instance Current widget instance.
*/
function aksrt_widget_render_common_form(array $instance): void {
$instance = aksrt_widget_normalize_common($instance);
?>
<hr style="margin:16px 0 12px;border-color:#e0e0e0;">
<p style="font-size:12px;color:#666;margin-bottom:8px;font-weight:600;"><?php esc_html_e('显示规则', 'aksrt'); ?></p>
<p>
<label for="<?php echo esc_attr($instance['show_type_id'] ?? ''); ?>"><?php esc_html_e('显示设备', 'aksrt'); ?></label><br>
<select name="<?php echo esc_attr($instance['show_type_name'] ?? 'show_type'); ?>" id="<?php echo esc_attr($instance['show_type_id'] ?? ''); ?>" class="widefat">
<option value="all" <?php selected($instance['show_type'], 'all'); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
<option value="pc" <?php selected($instance['show_type'], 'pc'); ?>><?php esc_html_e('仅 PC', 'aksrt'); ?></option>
<option value="mobile" <?php selected($instance['show_type'], 'mobile'); ?>><?php esc_html_e('仅移动端', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($instance['show_id_type_id'] ?? ''); ?>"><?php esc_html_e('ID 显示规则', 'aksrt'); ?></label><br>
<select name="<?php echo esc_attr($instance['show_id_type_name'] ?? 'show_id_type'); ?>" id="<?php echo esc_attr($instance['show_id_type_id'] ?? ''); ?>" class="widefat">
<option value="" <?php selected($instance['show_id_type'], ''); ?>><?php esc_html_e('不限制', 'aksrt'); ?></option>
<option value="include" <?php selected($instance['show_id_type'], 'include'); ?>><?php esc_html_e('仅指定 ID 显示', 'aksrt'); ?></option>
<option value="exclude" <?php selected($instance['show_id_type'], 'exclude'); ?>><?php esc_html_e('指定 ID 隐藏', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($instance['show_ids_id'] ?? ''); ?>"><?php esc_html_e('文章/页面 ID', 'aksrt'); ?></label><br>
<input type="text" name="<?php echo esc_attr($instance['show_ids_name'] ?? 'show_ids'); ?>" id="<?php echo esc_attr($instance['show_ids_id'] ?? ''); ?>" class="widefat" value="<?php echo esc_attr($instance['show_ids']); ?>" placeholder="<?php esc_attr_e('逗号分隔,如: 1,2,3', 'aksrt'); ?>">
</p>
<p>
<label>
<input type="checkbox" name="<?php echo esc_attr($instance['sidebar_affix_name'] ?? 'sidebar_affix'); ?>" value="1" <?php checked($instance['sidebar_affix']); ?>>
<?php esc_html_e('侧栏吸顶', 'aksrt'); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr($instance['animation_in_id'] ?? ''); ?>"><?php esc_html_e('入场动画', 'aksrt'); ?></label><br>
<select name="<?php echo esc_attr($instance['animation_in_name'] ?? 'animation_in'); ?>" id="<?php echo esc_attr($instance['animation_in_id'] ?? ''); ?>" class="widefat">
<option value="" <?php selected($instance['animation_in'], ''); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
<option value="fadeIn" <?php selected($instance['animation_in'], 'fadeIn'); ?>><?php esc_html_e('淡入', 'aksrt'); ?></option>
<option value="fadeInUp" <?php selected($instance['animation_in'], 'fadeInUp'); ?>><?php esc_html_e('上移淡入', 'aksrt'); ?></option>
<option value="fadeInDown" <?php selected($instance['animation_in'], 'fadeInDown'); ?>><?php esc_html_e('下移淡入', 'aksrt'); ?></option>
<option value="fadeInLeft" <?php selected($instance['animation_in'], 'fadeInLeft'); ?>><?php esc_html_e('左移淡入', 'aksrt'); ?></option>
<option value="fadeInRight" <?php selected($instance['animation_in'], 'fadeInRight'); ?>><?php esc_html_e('右移淡入', 'aksrt'); ?></option>
<option value="zoomIn" <?php selected($instance['animation_in'], 'zoomIn'); ?>><?php esc_html_e('缩放进入', 'aksrt'); ?></option>
</select>
</p>
<p>
<label>
<input type="checkbox" name="<?php echo esc_attr($instance['animation_repeat_name'] ?? 'animation_repeat'); ?>" value="1" <?php checked($instance['animation_repeat']); ?>>
<?php esc_html_e('动画重复播放', 'aksrt'); ?>
</label>
</p>
<?php
}
}
if (!function_exists('aksrt_widget_update_common')) {
/**
* Process shared widget form fields in update().
*
* @param array<string, mixed> $new_instance
* @param array<string, mixed> $old_instance
* @return array<string, mixed>
*/
function aksrt_widget_update_common(array $new_instance, array $old_instance): array {
$merged = array_merge($old_instance, $new_instance);
return aksrt_widget_normalize_common($merged);
}
}
if (!function_exists('aksrt_widget_render_common_before')) {
/**
* Render common wrapper attributes and classes before widget content.
*
* Outputs CSS classes for animation, affix, show_type, etc.
* Call this at the start of widget() before rendering content.
*
* @param array $args Sidebar args.
* @param array $instance Widget instance.
* @return array Modified args with extra classes.
*/
function aksrt_widget_get_common_wrapper_classes(array $args, array $instance): array {
$instance = aksrt_widget_normalize_common($instance);
$classes = [];
// Show type: PC or mobile
if ($instance['show_type'] === 'pc') {
$classes[] = 'aksrt-widget--pc-only';
} elseif ($instance['show_type'] === 'mobile') {
$classes[] = 'aksrt-widget--mobile-only';
}
// Sidebar affix
if ($instance['sidebar_affix']) {
$classes[] = 'aksrt-widget--affix';
}
// Animation
if ($instance['animation_in'] !== '') {
$classes[] = 'aksrt-widget--animated';
$classes[] = 'aksrt-anim-' . $instance['animation_in'];
if ($instance['animation_repeat']) {
$classes[] = 'aksrt-anim-repeat';
}
}
return $classes;
}
}
if (!function_exists('aksrt_widget_should_display_by_ids')) {
/**
* Check if the current post/page should display the widget based on show_id_type and show_ids.
*
* @param array $instance Widget instance.
* @return bool
*/
function aksrt_widget_should_display_by_ids(array $instance): bool {
$instance = aksrt_widget_normalize_common($instance);
if ($instance['show_id_type'] === '' || $instance['show_ids'] === '') {
return true;
}
$current_id = get_the_ID();
if (!$current_id) {
return true; // Not on a singular page, allow display
}
$ids = array_filter(array_map('absint', explode(',', $instance['show_ids'])));
if ($instance['show_id_type'] === 'include') {
return in_array($current_id, $ids, true);
}
if ($instance['show_id_type'] === 'exclude') {
return !in_array($current_id, $ids, true);
}
return true;
}
}
if (!function_exists('aksrt_widget_render_location_help')) {
/**
* Render placement hint inside widget admin forms.
@@ -559,11 +359,11 @@ if (!function_exists('aksrt_filter_widget_display_by_area')) {
if (is_array($instance) && function_exists('aksrt_widget_normalize_common')) {
$normalized = aksrt_widget_normalize_common($instance);
// Show type: PC / mobile filter
if ($normalized['show_type'] === 'pc' && wp_is_mobile()) {
// Show type: PC / mobile filter (Zibll only_pc / only_sm).
if ($normalized['show_type'] === 'only_pc' && wp_is_mobile()) {
return false;
}
if ($normalized['show_type'] === 'mobile' && !wp_is_mobile()) {
if ($normalized['show_type'] === 'only_sm' && !wp_is_mobile()) {
return false;
}