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
+638
View File
@@ -0,0 +1,638 @@
<?php
/**
* Shared widget common-form layer — Zibll CSF_Widget::params() parity.
*
* Extracted from widgets-register.php to keep each file under the 1000-line
* hard limit. Provides:
* - common field defaults / normalization (subtitle, title_style, highlight,
* 3 title links, sidebar_affix, show_type, show_id_type, animation, layout)
* - shared admin form renderer + update processor
* - wrapper class/attr builders (affix, animation, show-type, layout bg)
* - Zibll-aligned title block renderer (simple / big styles)
* - display-by-id gate consumed by widget_display_callback
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
// Shared Widget Form Helper — 公共表单字段与显示规则
// ============================================================
if (!function_exists('aksrt_widget_common_defaults')) {
/**
* Default values for shared widget instance fields.
*
* Zibll CSF_Widget::params() common-field parity:
* subtitle, title_style, title_highlight, title_highlight_color,
* title_link (×3), 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).
*
* @return array<string, mixed>
*/
function aksrt_widget_common_defaults(): array {
return [
'subtitle' => '',
'title_style' => '',
'title_highlight' => '',
'title_highlight_color'=> 'focus-color',
'title_link' => ['url' => '', 'text' => '', 'target' => ''],
'title_link2' => ['url' => '', 'text' => '', 'target' => ''],
'title_link3' => ['url' => '', 'text' => '', 'target' => ''],
'title_link_style' => ['color_1' => 'jb-blue', 'color_2' => 'jb-yellow', 'color_3' => 'jb-green'],
'sidebar_affix' => false,
'show_type' => 'all',
'show_id_type' => '',
'show_ids' => '',
'animation_in' => '',
'animation_repeat' => false,
'layout_pin_pc' => ['top' => '', 'bottom' => ''],
'layout_pin_m' => ['top' => '', 'bottom' => ''],
'layout_bg' => ['img_white' => [], 'img_dark' => []],
];
}
}
if (!function_exists('aksrt_widget_normalize_link')) {
/**
* Normalize a title_linkN array {url,text,target}.
*
* @param mixed $raw
* @return array{url:string,text:string,target:string}
*/
function aksrt_widget_normalize_link($raw): array {
$raw = is_array($raw) ? $raw : [];
return [
'url' => esc_url_raw(trim((string) ($raw['url'] ?? ''))),
'text' => sanitize_text_field((string) ($raw['text'] ?? '')),
'target' => ($raw['target'] ?? '') === '_blank' ? '_blank' : '',
];
}
}
if (!function_exists('aksrt_widget_normalize_common')) {
/**
* Normalize common widget instance fields, filling defaults for legacy instances.
*
* Accepts both the new structured shape (title_link as array) and legacy
* flat shapes (title_link as URL string, in_affix bool) emitted by older
* widget versions, normalizing them onto the canonical shared schema.
*
* @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_style'] = in_array($result['title_style'] ?? '', ['', 'big'], true) ? $result['title_style'] : '';
$result['title_highlight'] = sanitize_text_field($result['title_highlight'] ?? '');
$result['title_highlight_color'] = sanitize_html_class((string) ($result['title_highlight_color'] ?? 'focus-color'));
if ($result['title_highlight_color'] === '') {
$result['title_highlight_color'] = 'focus-color';
}
// Legacy flat title_link (URL string) → structured first link.
if (is_string($result['title_link'] ?? null)) {
$result['title_link'] = ['url' => esc_url_raw(trim($result['title_link'])), 'text' => '', 'target' => ''];
}
$result['title_link'] = aksrt_widget_normalize_link($result['title_link'] ?? []);
$result['title_link2'] = aksrt_widget_normalize_link($result['title_link2'] ?? []);
$result['title_link3'] = aksrt_widget_normalize_link($result['title_link3'] ?? []);
$tl_style = is_array($result['title_link_style'] ?? null) ? $result['title_link_style'] : [];
$result['title_link_style'] = [
'color_1' => sanitize_html_class((string) ($tl_style['color_1'] ?? 'jb-blue')) ?: 'jb-blue',
'color_2' => sanitize_html_class((string) ($tl_style['color_2'] ?? 'jb-yellow')) ?: 'jb-yellow',
'color_3' => sanitize_html_class((string) ($tl_style['color_3'] ?? 'jb-green')) ?: 'jb-green',
];
// show_type: accept both AKSRT legacy (pc/mobile) and Zibll (only_pc/only_sm) tokens.
$show_type = (string) ($result['show_type'] ?? 'all');
if ($show_type === 'only_pc' || $show_type === 'pc') {
$show_type = 'only_pc';
} elseif ($show_type === 'only_sm' || $show_type === 'mobile') {
$show_type = 'only_sm';
} else {
$show_type = 'all';
}
$result['show_type'] = $show_type;
// show_id_type: Zibll (show/hide) ↔ AKSRT legacy (include/exclude).
$id_type = (string) ($result['show_id_type'] ?? '');
if ($id_type === 'include') {
$id_type = 'show';
} elseif ($id_type === 'exclude') {
$id_type = 'hide';
} elseif (!in_array($id_type, ['', 'show', 'hide'], true)) {
$id_type = '';
}
$result['show_id_type'] = $id_type;
$result['show_ids'] = sanitize_text_field($result['show_ids'] ?? '');
$result['sidebar_affix'] = !empty($result['sidebar_affix']) || !empty($result['in_affix'] ?? false);
$result['animation_in'] = sanitize_key($result['animation_in'] ?? '');
$result['animation_repeat'] = !empty($result['animation_repeat']);
foreach (['layout_pin_pc', 'layout_pin_m'] as $pin_key) {
$pin = is_array($result[$pin_key] ?? null) ? $result[$pin_key] : [];
$result[$pin_key] = [
'top' => preg_match('/^-?\d+$/', (string) ($pin['top'] ?? '')) ? (string) (int) $pin['top'] : '',
'bottom' => preg_match('/^-?\d+$/', (string) ($pin['bottom'] ?? '')) ? (string) (int) $pin['bottom'] : '',
];
}
$bg = is_array($result['layout_bg'] ?? null) ? $result['layout_bg'] : [];
$result['layout_bg'] = [
'img_white' => aksrt_widget_normalize_bg($bg['img_white'] ?? []),
'img_dark' => aksrt_widget_normalize_bg($bg['img_dark'] ?? []),
];
return $result;
}
}
if (!function_exists('aksrt_widget_normalize_bg')) {
/**
* Normalize a layout_bg sub-block (background CSS props).
*
* @param mixed $raw
* @return array<string,string>
*/
function aksrt_widget_normalize_bg($raw): array {
$raw = is_array($raw) ? $raw : [];
$out = [];
foreach (['background-color', 'background-image', 'background-position', 'background-repeat', 'background-attachment', 'background-size', 'background-origin'] as $k) {
$v = trim((string) ($raw[$k] ?? ''));
if ($k === 'background-image' && $v !== '') {
$v = esc_url_raw($v);
} elseif ($k === 'background-color' && $v !== '') {
// Allow hex / rgb / rgba / named; strip tags defensively.
$v = sanitize_text_field($v);
} else {
$v = sanitize_text_field($v);
}
if ($v !== '') {
$out[$k] = $v;
}
}
return $out;
}
}
if (!function_exists('aksrt_widget_animation_options')) {
/**
* Zibll-aligned module entrance animation options.
*
* @return array<string,string>
*/
function aksrt_widget_animation_options(): array {
return [
'' => __('无动画', 'aksrt'),
'fade' => __('淡入', 'aksrt'),
'slideup' => __('从下往上滑出', 'aksrt'),
'slidedown' => __('从上往下滑出', 'aksrt'),
'slideright' => __('从左往右滑出', 'aksrt'),
'slideleft' => __('从右往左滑出', 'aksrt'),
'zoomin' => __('由小变大', 'aksrt'),
'zoomout' => __('由大变小', 'aksrt'),
];
}
}
if (!function_exists('aksrt_widget_highlight_color_options')) {
/**
* Highlight color palette (Zibll zib_palette parity, theme classes).
*
* @return array<string,string>
*/
function aksrt_widget_highlight_color_options(): array {
return [
'focus-color' => __('主题色', 'aksrt'),
'jb-blue' => __('渐变蓝', 'aksrt'),
'jb-green' => __('渐变绿', 'aksrt'),
'jb-yellow' => __('渐变黄', 'aksrt'),
'jb-orange' => __('渐变橙', 'aksrt'),
'jb-red' => __('渐变红', 'aksrt'),
'jb-purple' => __('渐变紫', 'aksrt'),
'jb-pink' => __('渐变粉', 'aksrt'),
'jb-cyan' => __('渐变青', 'aksrt'),
'c-blue' => __('蓝色', 'aksrt'),
'c-green' => __('绿色', 'aksrt'),
'c-red' => __('红色', 'aksrt'),
'c-yellow' => __('黄色', 'aksrt'),
'c-purple' => __('紫色', 'aksrt'),
];
}
}
if (!function_exists('aksrt_widget_render_link_field')) {
/**
* Render a single title-link sub-field (url/text/target) for the admin form.
*
* @param string $base Field base name, e.g. 'title_link'.
* @param array $link Normalized link {url,text,target}.
* @param string $label Field label.
*/
function aksrt_widget_render_link_field(string $base, array $link, string $label): void {
?>
<p>
<label><?php echo esc_html($label); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($base); ?>[text]" value="<?php echo esc_attr($link['text']); ?>" placeholder="<?php esc_attr_e('按钮文字', 'aksrt'); ?>">
</label>
<input class="widefat" type="url" name="<?php echo esc_attr($base); ?>[url]" value="<?php echo esc_attr($link['url']); ?>" placeholder="<?php esc_attr_e('链接地址', 'aksrt'); ?>">
<label><input type="checkbox" name="<?php echo esc_attr($base); ?>[target]" value="_blank" <?php checked($link['target'] === '_blank'); ?>> <?php esc_html_e('新窗口打开', 'aksrt'); ?></label>
</p>
<?php
}
}
if (!function_exists('aksrt_widget_render_common_form')) {
/**
* Render shared widget admin form fields.
*
* Call this from your widget's form() method. Renders the full Zibll
* common-field set: title block (subtitle / style / highlight / 3 links),
* sidebar affix, display rules, entrance animation, layout padding + bg.
*
* @param array<string, mixed> $instance Current widget instance.
*/
function aksrt_widget_render_common_form(array $instance): void {
$instance = aksrt_widget_normalize_common($instance);
$pin_pc = $instance['layout_pin_pc'];
$pin_m = $instance['layout_pin_m'];
$bg_w = $instance['layout_bg']['img_white'];
$bg_d = $instance['layout_bg']['img_dark'];
?>
<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><?php esc_html_e('副标题', 'aksrt'); ?>
<input class="widefat" type="text" name="subtitle" value="<?php echo esc_attr($instance['subtitle']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('标题样式', 'aksrt'); ?></label>
<label><input type="radio" name="title_style" value="" <?php checked($instance['title_style'], ''); ?>> <?php esc_html_e('简约', 'aksrt'); ?></label>
<label><input type="radio" name="title_style" value="big" <?php checked($instance['title_style'], 'big'); ?>> <?php esc_html_e('居中大标题', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('标题选字高亮(标题中要被高亮的部分)', 'aksrt'); ?>
<input class="widefat" type="text" name="title_highlight" value="<?php echo esc_attr($instance['title_highlight']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('高亮颜色', 'aksrt'); ?>
<select class="widefat" name="title_highlight_color">
<?php foreach (aksrt_widget_highlight_color_options() as $val => $lbl) : ?>
<option value="<?php echo esc_attr($val); ?>" <?php selected($instance['title_highlight_color'], $val); ?>><?php echo esc_html($lbl); ?></option>
<?php endforeach; ?>
</select>
</label>
</p>
<?php aksrt_widget_render_link_field('title_link', $instance['title_link'], __('标题链接', 'aksrt')); ?>
<?php if ($instance['title_style'] !== 'big') : ?>
<?php aksrt_widget_render_link_field('title_link2', $instance['title_link2'], __('标题链接 2', 'aksrt')); ?>
<?php aksrt_widget_render_link_field('title_link3', $instance['title_link3'], __('标题链接 3', 'aksrt')); ?>
<?php endif; ?>
<?php if ($instance['title_style'] === 'big') : ?>
<p>
<label><?php esc_html_e('链接样式(颜色1/2/3)', 'aksrt'); ?></label>
<select name="title_link_style[color_1]">
<?php foreach (aksrt_widget_highlight_color_options() as $val => $lbl) : ?>
<option value="<?php echo esc_attr($val); ?>" <?php selected($instance['title_link_style']['color_1'], $val); ?>><?php echo esc_html($lbl); ?></option>
<?php endforeach; ?>
</select>
<select name="title_link_style[color_2]">
<?php foreach (aksrt_widget_highlight_color_options() as $val => $lbl) : ?>
<option value="<?php echo esc_attr($val); ?>" <?php selected($instance['title_link_style']['color_2'], $val); ?>><?php echo esc_html($lbl); ?></option>
<?php endforeach; ?>
</select>
<select name="title_link_style[color_3]">
<?php foreach (aksrt_widget_highlight_color_options() as $val => $lbl) : ?>
<option value="<?php echo esc_attr($val); ?>" <?php selected($instance['title_link_style']['color_3'], $val); ?>><?php echo esc_html($lbl); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php endif; ?>
<p style="font-size:12px;color:#666;margin:10px 0 8px;font-weight:600;"><?php esc_html_e('显示规则', 'aksrt'); ?></p>
<p>
<label><?php esc_html_e('显示设备', 'aksrt'); ?></label>
<label><input type="radio" name="show_type" value="all" <?php checked($instance['show_type'], 'all'); ?>> <?php esc_html_e('全部', 'aksrt'); ?></label>
<label><input type="radio" name="show_type" value="only_pc" <?php checked($instance['show_type'], 'only_pc'); ?>> <?php esc_html_e('仅 PC', 'aksrt'); ?></label>
<label><input type="radio" name="show_type" value="only_sm" <?php checked($instance['show_type'], 'only_sm'); ?>> <?php esc_html_e('仅移动端', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('按 ID 显示或隐藏', 'aksrt'); ?></label>
<label><input type="radio" name="show_id_type" value="" <?php checked($instance['show_id_type'], ''); ?>> <?php esc_html_e('不限制', 'aksrt'); ?></label>
<label><input type="radio" name="show_id_type" value="show" <?php checked($instance['show_id_type'], 'show'); ?>> <?php esc_html_e('指定 ID 显示', 'aksrt'); ?></label>
<label><input type="radio" name="show_id_type" value="hide" <?php checked($instance['show_id_type'], 'hide'); ?>> <?php esc_html_e('指定 ID 隐藏', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('文章/页面 ID(逗号分隔)', 'aksrt'); ?>
<input class="widefat" type="text" name="show_ids" value="<?php echo esc_attr($instance['show_ids']); ?>" placeholder="1,2,3">
</label>
</p>
<p>
<label><input type="checkbox" name="sidebar_affix" value="1" <?php checked($instance['sidebar_affix']); ?>> <?php esc_html_e('侧栏吸顶(仅侧栏有效)', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('入场动画', 'aksrt'); ?>
<select class="widefat" name="animation_in">
<?php foreach (aksrt_widget_animation_options() as $val => $lbl) : ?>
<option value="<?php echo esc_attr($val); ?>" <?php selected($instance['animation_in'], $val); ?>><?php echo esc_html($lbl); ?></option>
<?php endforeach; ?>
</select>
</label>
</p>
<p>
<label><input type="checkbox" name="animation_repeat" value="1" <?php checked($instance['animation_repeat']); ?>> <?php esc_html_e('动画重复播放', 'aksrt'); ?></label>
</p>
<p style="font-size:12px;color:#666;margin:10px 0 8px;font-weight:600;"><?php esc_html_e('布局:间距与背景', 'aksrt'); ?></p>
<p>
<label><?php esc_html_e('PC 端额外上/下间距 (px)', 'aksrt'); ?>
<input class="tiny-text" type="number" name="layout_pin_pc[top]" value="<?php echo esc_attr($pin_pc['top']); ?>" placeholder="0">
<input class="tiny-text" type="number" name="layout_pin_pc[bottom]" value="<?php echo esc_attr($pin_pc['bottom']); ?>" placeholder="0">
</label>
</p>
<p>
<label><?php esc_html_e('移动端额外上/下间距 (px)', 'aksrt'); ?>
<input class="tiny-text" type="number" name="layout_pin_m[top]" value="<?php echo esc_attr($pin_m['top']); ?>" placeholder="0">
<input class="tiny-text" type="number" name="layout_pin_m[bottom]" value="<?php echo esc_attr($pin_m['bottom']); ?>" placeholder="0">
</label>
</p>
<p>
<label><?php esc_html_e('日间背景色', 'aksrt'); ?>
<input class="widefat" type="text" name="layout_bg[img_white][background-color]" value="<?php echo esc_attr($bg_w['background-color'] ?? ''); ?>">
</label>
<label><?php esc_html_e('日间背景图', 'aksrt'); ?>
<input class="widefat" type="url" name="layout_bg[img_white][background-image]" value="<?php echo esc_attr($bg_w['background-image'] ?? ''); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('夜间背景色', 'aksrt'); ?>
<input class="widefat" type="text" name="layout_bg[img_dark][background-color]" value="<?php echo esc_attr($bg_d['background-color'] ?? ''); ?>">
</label>
<label><?php esc_html_e('夜间背景图', 'aksrt'); ?>
<input class="widefat" type="url" name="layout_bg[img_dark][background-image]" value="<?php echo esc_attr($bg_d['background-image'] ?? ''); ?>">
</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')) {
/**
* Build common wrapper classes/attrs for a widget instance.
*
* Zibll CSF_Widget::wrap_attributes() parity: show-type class, sidebar
* affix data-affix, entrance animation class, layout padding/bg CSS vars.
* Call this at the start of widget() before rendering content.
*
* @param array $args Sidebar args.
* @param array $instance Widget instance.
* @return array<int,string> CSS class names (Zibll show_class style).
*/
function aksrt_widget_get_common_wrapper_classes(array $args, array $instance): array {
$instance = aksrt_widget_normalize_common($instance);
$classes = [];
// Show type: PC or mobile (Zibll hidden-xs / visible-xs-block).
if ($instance['show_type'] === 'only_pc') {
$classes[] = 'hidden-xs';
} elseif ($instance['show_type'] === 'only_sm') {
$classes[] = 'visible-xs-block';
}
// Sidebar affix
if ($instance['sidebar_affix']) {
$classes[] = 'is-affix';
}
// Animation (Zibll obs-animate ani-*).
if ($instance['animation_in'] !== '') {
$classes[] = 'obs-animate';
$classes[] = 'ani-' . $instance['animation_in'];
if ($instance['animation_repeat']) {
$classes[] = 'obs-animate-repeat';
}
}
return $classes;
}
}
if (!function_exists('aksrt_widget_common_wrapper_attr')) {
/**
* Build the full wrapper attribute string (class + style + data-affix) for
* the Zibll .zib-widget-wrap parity layer.
*
* Combine with a widget-specific class via a leading space, or use as-is.
*
* @param array<string,mixed> $instance Widget instance.
* @param string $extra_class Additional class (widget-specific).
* @return string Escaped attribute string beginning with a leading space, e.g. ' class="…" style="…" data-affix="true"'.
*/
function aksrt_widget_common_wrapper_attr(array $instance, string $extra_class = ''): string {
$instance = aksrt_widget_normalize_common($instance);
$classes = ['zib-widget-wrap'];
foreach (aksrt_widget_get_common_wrapper_classes([], $instance) as $c) {
$classes[] = $c;
}
$extra_class = trim($extra_class);
if ($extra_class !== '') {
$classes[] = $extra_class;
}
$styles = [];
if ($instance['layout_pin_pc']['top'] !== '') {
$styles[] = '--pt-pc:' . (int) $instance['layout_pin_pc']['top'] . 'px;';
}
if ($instance['layout_pin_pc']['bottom'] !== '') {
$styles[] = '--pb-pc:' . (int) $instance['layout_pin_pc']['bottom'] . 'px;';
}
if ($instance['layout_pin_m']['top'] !== '') {
$styles[] = '--pt-m:' . (int) $instance['layout_pin_m']['top'] . 'px;';
}
if ($instance['layout_pin_m']['bottom'] !== '') {
$styles[] = '--pb-m:' . (int) $instance['layout_pin_m']['bottom'] . 'px;';
}
foreach (['white' => 'img_white', 'dark' => 'img_dark'] as $mode => $bg_key) {
$bg = $instance['layout_bg'][$bg_key] ?? [];
if (!empty($bg['background-color'])) {
$styles[] = '--bg-color-' . $mode . ':' . $bg['background-color'] . ';';
}
if (!empty($bg['background-image'])) {
$styles[] = '--bg-img-' . $mode . ':url("' . $bg['background-image'] . '");';
}
}
$attr = ' class="' . esc_attr(implode(' ', $classes)) . '"';
if ($styles) {
$attr .= ' style="' . esc_attr(implode('', $styles)) . '"';
}
if ($instance['sidebar_affix']) {
$attr .= ' data-affix="true"';
}
return $attr;
}
}
if (!function_exists('aksrt_widget_affix_attrs')) {
/**
* Compact affix class+attribute helper for widgets that apply affix onto an
* inner wrapper (the two-family pattern from the legacy in_affix widgets).
*
* @param array<string,mixed> $instance Widget instance.
* @return array{class:string,attr:string} class includes leading space when active; attr includes leading space.
*/
function aksrt_widget_affix_attrs(array $instance): array {
$instance = aksrt_widget_normalize_common($instance);
if (empty($instance['sidebar_affix'])) {
return ['class' => '', 'attr' => ''];
}
return ['class' => ' is-affix', 'attr' => ' data-affix="true"'];
}
}
if (!function_exists('aksrt_widget_render_title')) {
/**
* Render the Zibll-aligned widget title block (CSF_Widget::show_title parity).
*
* Two styles:
* - '' (简约): .box-body notop > .title-theme with optional subtitle + more link
* - 'big' (居中大标题): .big-title-wrap with up to 3 button links
*
* Highlight wraps the matched substring in a colored span.
*
* @param array<string,mixed> $instance Widget instance.
* @return string Title HTML (empty when no title).
*/
function aksrt_widget_render_title(array $instance): string {
$instance = aksrt_widget_normalize_common($instance);
$title = trim((string) ($instance['title'] ?? ''));
if ($title === '') {
return '';
}
$highlight = trim((string) $instance['title_highlight']);
$subtitle = (string) $instance['subtitle'];
$anim = function_exists('aksrt_widget_animation_class')
? aksrt_widget_animation_class($instance)
: '';
$title = esc_html($title);
if ($highlight !== '' && $highlight !== $title) {
$color = $instance['title_highlight_color'];
$hl_esc = esc_html($highlight);
$title = str_replace($hl_esc, '<span class="title-highlight ' . esc_attr($color) . '">' . $hl_esc . '</span>', $title);
}
if ($instance['title_style'] === 'big') {
$style = $instance['title_link_style'];
$links = '';
foreach (['title_link' => 'color_1', 'title_link2' => 'color_2', 'title_link3' => 'color_3'] as $key => $color_key) {
$link = $instance[$key];
if ($link['url'] !== '' && $link['text'] !== '') {
$blank = $link['target'] === '_blank' ? ' target="_blank"' : '';
$links .= '<a' . $blank . ' href="' . esc_url($link['url']) . '" class="but ' . esc_attr($style[$color_key]) . '">' . esc_html($link['text']) . '</a>';
}
}
$links = $links !== '' ? '<div class="flex jc big-title-buts' . $anim . '">' . $links . '</div>' : '';
$sub = $subtitle !== '' ? '<p class="big-title-sub muted-2-color' . $anim . '">' . esc_html($subtitle) . '</p>' : '';
return '<div class="big-title-wrap"><h2 class="big-title' . $anim . '">' . $title . '</h2>' . $sub . $links . '</div>';
}
// Simple style.
$link = $instance['title_link'];
$sub = $subtitle !== '' ? '<small class="ml10">' . esc_html($subtitle) . '</small>' : '';
$more = '';
if ($link['url'] !== '') {
$text = $link['text'] !== '' ? $link['text'] : '<i class="fa fa-angle-right fa-fw"></i>' . __('更多', 'aksrt');
$blank = $link['target'] === '_blank' ? ' target="_blank"' : '';
$more = '<div class="pull-right em09 mt3"><a' . $blank . ' href="' . esc_url($link['url']) . '" class="muted-2-color">' . wp_kses_post($text) . '</a></div>';
}
return '<div class="box-body notop' . $anim . '"><div class="title-theme">' . $title . $sub . $more . '</div></div>';
}
}
if (!function_exists('aksrt_widget_animation_class')) {
/**
* Zibll CSF_Widget::animation_class parity: obs-animate ani-* [+repeat].
*
* @param array<string,mixed> $instance Normalized widget instance.
* @param bool $is_title Reserved for title-specific animation.
* @return string
*/
function aksrt_widget_animation_class(array $instance, bool $is_title = false): string {
$animation = (string) ($instance['animation_in'] ?? '');
if ($animation === '') {
return '';
}
$class = ' obs-animate ani-' . $animation;
if (!empty($instance['animation_repeat'])) {
$class .= ' obs-animate-repeat';
}
return $class;
}
}
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.
*
* show_id_type tokens: '' (no limit) | 'show' (whitelist) | 'hide' (blacklist).
* Accepts legacy 'include'/'exclude' via normalize_common.
*
* @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
}
// Zibll splits on comma/Chinese-comma/whitespace/newline.
$ids = array_filter(array_map('absint', preg_split('/[,,\s]+/', $instance['show_ids']) ?: []));
if ($instance['show_id_type'] === 'show') {
return in_array($current_id, $ids, true);
}
if ($instance['show_id_type'] === 'hide') {
return !in_array($current_id, $ids, true);
}
return true;
}
}