Files
wordpress-theme-aksrt/src/WidgetSystem/FormRenderer.php
T
kate522 b639d57c5b feat(widgets): replicate 12 reference widgets for full Zibll parity
- rewrite schemas for posts, tab_posts, oneline_posts, hot_posts,
  term_cards, term_aggregation, media_cards, combo_info, partners,
  recent_comments, posts_toc and slider_media_combo to mirror the
  modern reference modules field by field (options, defaults,
  dependencies, min/max/step, subheadings)
- port reference renderers: hot ranking with TOP badges, category
  aggregation card, graphic cover cards, partners CSS marquee,
  combo info cards with auto band palette, recent comment list
- rename 分类与标签聚合卡片 to 分类聚合卡片 and restrict it to
  categories (no tags/topics branch in this project)
- drop topics filter fields and the topics query branch; align
  orderby value domain with supported post meta
- add searchable sortable terms admin field type and spinner unit
  suffix rendering
- unify widget pagination and lazy loading on the REST client with
  the runtime-owned ias reveal trigger; remove the legacy
  post_ajax_trigger dual path
- align section-navs TOC with the reference contract (wznav ids,
  3-heading minimum, zib-widget box)
2026-09-05 11:28:00 +08:00

280 lines
15 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
namespace AKSRT\WidgetSystem;
defined('ABSPATH') || exit;
/**
* Server-rendered, schema-driven widget form.
*/
final class FormRenderer {
public static function render(SchemaWidget $widget, array $fields, array $instance): void {
echo '<div class="aksrt-widget-form" data-aksrt-widget-form="1">';
foreach ($fields as $field) {
self::field($widget, $field, $instance[(string) ($field['id'] ?? '')] ?? ($field['default'] ?? ''), '');
}
echo '</div>';
}
private static function field(SchemaWidget $widget, array $field, $value, string $parent): void {
$type = (string) ($field['type'] ?? 'text');
$id = (string) ($field['id'] ?? '');
if (in_array($type, ['content', 'subheading', 'submessage'], true)) {
self::display_field($field, $type);
return;
}
if ($id === '') {
return;
}
$key = $parent === '' ? $id : $parent . '[' . $id . ']';
$field_id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
$field_name = $widget->get_field_name($key);
$dependency = !empty($field['dependency']) ? wp_json_encode($field['dependency']) : '';
echo '<div class="aksrt-widget-field aksrt-widget-field--' . esc_attr($type) . '" data-field-id="' . esc_attr($id) . '"';
if ($dependency !== '') {
echo ' data-dependency="' . esc_attr($dependency) . '"';
}
echo '>';
if (!empty($field['label'])) {
echo '<label class="aksrt-widget-field__label" for="' . esc_attr($field_id) . '">' . esc_html((string) $field['label']) . '</label>';
}
if (!empty($field['description'])) {
echo '<p class="description">' . wp_kses_post((string) $field['description']) . '</p>';
}
switch ($type) {
case 'switcher':
self::switcher($field_id, $field_name, (bool) $value);
break;
case 'radio':
case 'button_set':
case 'palette':
case 'image_select':
self::radios($field_id, $field_name, (array) ($field['options'] ?? []), (string) $value, $type);
break;
case 'select':
self::select($field_id, $field_name, $field, $value);
break;
case 'checkbox':
self::checkboxes($field_id, $field_name, $field, $value);
break;
case 'terms':
self::terms($field_id, $field_name, $field, $value);
break;
case 'number':
case 'spinner':
case 'slider':
self::number($field_id, $field_name, $field, $value, $type);
break;
case 'textarea':
$rows = max(1, (int) ($field['attributes']['rows'] ?? 4));
echo '<textarea class="widefat" rows="' . $rows . '" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '">' . esc_textarea((string) $value) . '</textarea>';
break;
case 'upload':
self::upload($field_id, $field_name, (string) $value, (string) ($field['library'] ?? ''));
break;
case 'color':
echo '<input class="widefat aksrt-widget-color" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '">';
break;
case 'icon':
echo '<div class="aksrt-widget-icon"><i class="' . esc_attr((string) $value) . '" aria-hidden="true"></i><input class="widefat" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '" placeholder="fa fas fa-star"></div>';
break;
case 'link':
self::link($widget, $key, is_array($value) ? $value : []);
break;
case 'spacing':
self::spacing($widget, $key, is_array($value) ? $value : []);
break;
case 'background':
self::background($widget, $key, is_array($value) ? $value : []);
break;
case 'group':
self::group($widget, $field, is_array($value) ? $value : [], $key);
break;
case 'fieldset':
case 'accordion':
self::nested($widget, $field, is_array($value) ? $value : [], $key);
break;
case 'text':
default:
echo '<input class="widefat" type="text" id="' . esc_attr($field_id) . '" name="' . esc_attr($field_name) . '" value="' . esc_attr((string) $value) . '">';
}
echo '</div>';
}
private static function display_field(array $field, string $type): void {
$content = (string) ($field['content'] ?? $field['label'] ?? '');
echo '<div class="aksrt-widget-field aksrt-widget-field--' . esc_attr($type) . '">' . wp_kses_post($content) . '</div>';
}
private static function switcher(string $id, string $name, bool $value): void {
echo '<input type="hidden" name="' . esc_attr($name) . '" value="0">';
echo '<label class="aksrt-widget-switch"><input type="checkbox" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="1" ' . checked($value, true, false) . '><span aria-hidden="true"></span></label>';
}
private static function radios(string $id, string $name, array $options, string $value, string $type): void {
echo '<div class="aksrt-widget-choices aksrt-widget-choices--' . esc_attr($type) . '">';
foreach ($options as $key => $label) {
$option_id = $id . '-' . sanitize_html_class((string) $key);
$image = is_array($label) ? (string) ($label['image'] ?? $label['url'] ?? '') : (string) $label;
$text = is_array($label) ? (string) ($label['label'] ?? $label['title'] ?? $key) : (string) $label;
if ($type === 'image_select' && $text === $image) $text = (string) $key;
$body = $type === 'image_select' && filter_var($image, FILTER_VALIDATE_URL)
? '<img src="' . esc_url($image) . '" alt="' . esc_attr($text) . '"><small>' . esc_html($text) . '</small>'
: esc_html($text);
echo '<label for="' . esc_attr($option_id) . '"><input type="radio" id="' . esc_attr($option_id) . '" name="' . esc_attr($name) . '" value="' . esc_attr((string) $key) . '" ' . checked($value, (string) $key, false) . '><span>' . $body . '</span></label>';
}
echo '</div>';
}
private static function select(string $id, string $name, array $field, $value): void {
$multiple = !empty($field['multiple']);
echo '<select class="widefat" id="' . esc_attr($id) . '" name="' . esc_attr($name) . ($multiple ? '[]' : '') . '"' . ($multiple ? ' multiple' : '') . '>';
$values = $multiple ? array_map('strval', (array) $value) : [(string) $value];
foreach ((array) ($field['options'] ?? []) as $key => $label) {
echo '<option value="' . esc_attr((string) $key) . '" ' . selected(in_array((string) $key, $values, true), true, false) . '>' . esc_html((string) $label) . '</option>';
}
echo '</select>';
}
private static function checkboxes(string $id, string $name, array $field, $value): void {
$options = (array) ($field['options'] ?? []);
if (!$options) {
self::switcher($id, $name, (bool) $value);
return;
}
$values = array_map('strval', (array) $value);
foreach ($options as $key => $label) {
echo '<label><input type="checkbox" name="' . esc_attr($name) . '[]" value="' . esc_attr((string) $key) . '" ' . checked(in_array((string) $key, $values, true), true, false) . '> ' . esc_html((string) $label) . '</label> ';
}
}
private static function number(string $id, string $name, array $field, $value, string $type): void {
$min = isset($field['min']) ? ' min="' . esc_attr((string) $field['min']) . '"' : '';
$max = isset($field['max']) ? ' max="' . esc_attr((string) $field['max']) . '"' : '';
$step = ' step="' . esc_attr((string) ($field['step'] ?? 1)) . '"';
$input_type = $type === 'slider' ? 'range' : 'number';
$unit = trim((string) ($field['unit'] ?? ''));
echo '<div class="aksrt-widget-number"><input class="widefat" type="' . esc_attr($input_type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . $min . $max . $step . '><output>' . esc_html((string) $value) . '</output>' . ($unit !== '' ? '<span class="aksrt-widget-number__unit">' . esc_html($unit) . '</span>' : '') . '</div>';
}
private static function terms(string $id, string $name, array $field, $value): void {
$taxonomy = (string) ($field['taxonomy'] ?? 'category');
$terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => 500, 'orderby' => 'name']);
if (is_wp_error($terms)) {
$terms = [];
}
$selected = array_values(array_filter(array_map('absint', (array) $value)));
$by_id = [];
foreach ($terms as $term) {
$by_id[(int) $term->term_id] = $term;
}
echo '<div class="aksrt-widget-terms" data-aksrt-terms="' . esc_attr($taxonomy) . '">';
echo '<input type="hidden" class="aksrt-widget-terms__value" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr(implode(',', $selected)) . '">';
echo '<div class="aksrt-widget-terms__selected">';
foreach ($selected as $term_id) {
if (!isset($by_id[$term_id])) {
continue;
}
echo '<span class="aksrt-widget-terms__chip" data-term-id="' . esc_attr((string) $term_id) . '"><span class="dashicons dashicons-move" aria-hidden="true"></span>' . esc_html((string) $by_id[$term_id]->name) . '<button type="button" class="button-link-delete aksrt-widget-terms__remove" aria-label="' . esc_attr__('移除', 'aksrt') . '">×</button></span>';
}
echo '</div>';
echo '<input type="search" class="widefat aksrt-widget-terms__search" placeholder="' . esc_attr__('输入关键词筛选', 'aksrt') . '" aria-label="' . esc_attr__('筛选可选分类', 'aksrt') . '">';
echo '<div class="aksrt-widget-terms__options">';
foreach ($terms as $term) {
$term_id = (int) $term->term_id;
echo '<label class="aksrt-widget-terms__option" data-term-name="' . esc_attr((string) $term->name) . '"><input type="checkbox" value="' . esc_attr((string) $term_id) . '" ' . checked(in_array($term_id, $selected, true), true, false) . '> ' . esc_html((string) $term->name) . '</label>';
}
echo '</div></div>';
}
private static function upload(string $id, string $name, string $value, string $library = ''): void {
$library = in_array($library, ['image', 'video'], true) ? $library : '';
echo '<div class="aksrt-widget-upload" data-media-library="' . esc_attr($library) . '">';
if ($value !== '') {
echo '<img src="' . esc_url($value) . '" alt="">';
}
echo '<input class="widefat" type="url" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '">';
echo '<button type="button" class="button aksrt-widget-upload__choose">' . esc_html($library === 'video' ? __('选择视频', 'aksrt') : __('选择图片', 'aksrt')) . '</button> <button type="button" class="button-link-delete aksrt-widget-upload__remove">' . esc_html__('移除', 'aksrt') . '</button></div>';
}
private static function link(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-link">';
self::plain_input($widget, $key . '[url]', (string) ($value['url'] ?? ''), 'url', 'URL');
self::plain_input($widget, $key . '[text]', (string) ($value['text'] ?? ''), 'text', '链接文字');
$name = $widget->get_field_name($key . '[target]');
echo '<label><input type="checkbox" name="' . esc_attr($name) . '" value="_blank" ' . checked(($value['target'] ?? '') === '_blank', true, false) . '> ' . esc_html__('新窗口打开', 'aksrt') . '</label></div>';
}
private static function spacing(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-spacing">';
foreach (['top' => '上', 'right' => '右', 'bottom' => '下', 'left' => '左'] as $edge => $label) {
echo '<label><span>' . esc_html($label) . '</span>';
self::plain_input($widget, $key . '[' . $edge . ']', (string) ($value[$edge] ?? ''), 'number', 'px');
echo '</label>';
}
echo '</div>';
}
private static function background(SchemaWidget $widget, string $key, array $value): void {
echo '<div class="aksrt-widget-background">';
self::plain_input($widget, $key . '[color]', (string) ($value['color'] ?? ''), 'text', '背景颜色');
self::plain_input($widget, $key . '[image]', (string) ($value['image'] ?? ''), 'url', '背景图片 URL');
foreach ([
'position' => ['center center' => '居中', 'top center' => '顶部', 'bottom center' => '底部', 'center left' => '左侧', 'center right' => '右侧'],
'repeat' => ['no-repeat' => '不重复', 'repeat' => '平铺', 'repeat-x' => '水平平铺', 'repeat-y' => '垂直平铺'],
'attachment' => ['scroll' => '滚动', 'fixed' => '固定'],
'size' => ['cover' => '覆盖', 'contain' => '包含', 'auto' => '原始尺寸'],
] as $part => $options) {
$name = $widget->get_field_name($key . '[' . $part . ']');
echo '<select name="' . esc_attr($name) . '">';
foreach ($options as $option => $label) {
echo '<option value="' . esc_attr($option) . '" ' . selected((string) ($value[$part] ?? ''), $option, false) . '>' . esc_html($label) . '</option>';
}
echo '</select>';
}
echo '</div>';
}
private static function group(SchemaWidget $widget, array $field, array $rows, string $key): void {
$token = '__index_' . substr(md5($key), 0, 8) . '__';
echo '<div class="aksrt-widget-repeater" data-index-token="' . esc_attr($token) . '" data-max-items="' . esc_attr((string) ($field['max_items'] ?? 50)) . '"><div class="aksrt-widget-repeater__rows">';
foreach ($rows as $index => $row) {
self::group_row($widget, $field, is_array($row) ? $row : [], $key, (string) $index);
}
echo '</div><template class="aksrt-widget-repeater__template">';
self::group_row($widget, $field, [], $key, $token);
echo '</template><button type="button" class="button aksrt-widget-repeater__add">' . esc_html__('添加项目', 'aksrt') . '</button></div>';
}
private static function group_row(SchemaWidget $widget, array $field, array $row, string $key, string $index): void {
$prefix = (string) ($field['item_label'] ?? $field['label'] ?? __('项目', 'aksrt'));
$number = ctype_digit($index) ? ((int) $index + 1) : 0;
$title = $prefix . ($number ? ' ' . $number : '');
if (!empty($row['title'])) $title .= ':' . strip_tags((string) $row['title']);
echo '<div class="aksrt-widget-repeater__row" data-row-index="' . esc_attr(ctype_digit($index) ? $index : '0') . '"><div class="aksrt-widget-repeater__bar"><span class="dashicons dashicons-move" aria-hidden="true"></span><strong>' . esc_html($title) . '</strong><button type="button" class="button-link-delete aksrt-widget-repeater__remove">' . esc_html__('删除', 'aksrt') . '</button></div><div class="aksrt-widget-repeater__fields">';
foreach ((array) ($field['fields'] ?? []) as $child) {
$child_id = (string) ($child['id'] ?? '');
self::field($widget, $child, $row[$child_id] ?? ($child['default'] ?? ''), $key . '[' . $index . ']');
}
echo '</div></div>';
}
private static function nested(SchemaWidget $widget, array $field, array $value, string $key): void {
echo '<details class="aksrt-widget-nested"><summary>' . esc_html((string) ($field['label'] ?? '更多设置')) . '</summary><div class="aksrt-widget-nested__body">';
foreach ((array) ($field['fields'] ?? []) as $child) {
$child_id = (string) ($child['id'] ?? '');
self::field($widget, $child, $value[$child_id] ?? ($child['default'] ?? ''), $key);
}
echo '</div></details>';
}
private static function plain_input(SchemaWidget $widget, string $key, string $value, string $type, string $placeholder): void {
$name = $widget->get_field_name($key);
$id = $widget->get_field_id(str_replace(['[', ']'], ['-', ''], $key));
echo '<input class="widefat" type="' . esc_attr($type) . '" id="' . esc_attr($id) . '" name="' . esc_attr($name) . '" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '">';
}
}