- 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)
214 lines
7.7 KiB
PHP
214 lines
7.7 KiB
PHP
<?php
|
||
|
||
namespace AKSRT\WidgetSystem;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Field schema defaults and recursive value normalization.
|
||
*/
|
||
final class FieldSchema {
|
||
private const DISPLAY_ONLY = ['content', 'subheading', 'submessage'];
|
||
|
||
public static function supported_types(): array {
|
||
return [
|
||
'text', 'textarea', 'number', 'spinner', 'slider', 'switcher', 'radio',
|
||
'button_set', 'select', 'checkbox', 'color', 'palette', 'image_select', 'icon', 'upload',
|
||
'link', 'spacing', 'background', 'group', 'fieldset', 'accordion', 'terms',
|
||
'content', 'subheading', 'submessage',
|
||
];
|
||
}
|
||
|
||
public static function defaults(array $fields): array {
|
||
$defaults = [];
|
||
foreach ($fields as $field) {
|
||
$id = (string) ($field['id'] ?? '');
|
||
$type = (string) ($field['type'] ?? 'text');
|
||
if ($id === '' || in_array($type, self::DISPLAY_ONLY, true)) {
|
||
continue;
|
||
}
|
||
$defaults[$id] = self::normalize_value($field, $field['default'] ?? self::empty_value($type));
|
||
}
|
||
return $defaults;
|
||
}
|
||
|
||
public static function normalize(array $fields, array $input): array {
|
||
$output = [];
|
||
foreach ($fields as $field) {
|
||
$id = (string) ($field['id'] ?? '');
|
||
$type = (string) ($field['type'] ?? 'text');
|
||
if ($id === '' || in_array($type, self::DISPLAY_ONLY, true)) {
|
||
continue;
|
||
}
|
||
$value = array_key_exists($id, $input) ? $input[$id] : ($field['default'] ?? self::empty_value($type));
|
||
$output[$id] = self::normalize_value($field, $value);
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
public static function normalize_value(array $field, $value) {
|
||
$type = (string) ($field['type'] ?? 'text');
|
||
switch ($type) {
|
||
case 'switcher':
|
||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||
case 'number':
|
||
case 'spinner':
|
||
case 'slider':
|
||
return self::number($field, $value);
|
||
case 'radio':
|
||
case 'button_set':
|
||
case 'select':
|
||
case 'palette':
|
||
case 'image_select':
|
||
return self::choice($field, $value);
|
||
case 'checkbox':
|
||
return self::checkbox($field, $value);
|
||
case 'terms':
|
||
return self::terms($value);
|
||
case 'link':
|
||
return self::link($value);
|
||
case 'spacing':
|
||
return self::spacing($value);
|
||
case 'background':
|
||
return self::background($value);
|
||
case 'group':
|
||
return self::group($field, $value);
|
||
case 'fieldset':
|
||
case 'accordion':
|
||
return self::nested($field, $value);
|
||
case 'upload':
|
||
return esc_url_raw((string) $value);
|
||
case 'color':
|
||
return self::color((string) $value);
|
||
case 'icon':
|
||
return sanitize_text_field((string) $value);
|
||
case 'textarea':
|
||
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_textarea_field((string) $value);
|
||
case 'text':
|
||
default:
|
||
if (($field['sanitize'] ?? '') === 'url') {
|
||
return esc_url_raw((string) $value);
|
||
}
|
||
if (($field['sanitize'] ?? '') === 'ids') {
|
||
return implode(',', array_values(array_unique(array_filter(array_map('absint', preg_split('/[,,\s]+/', (string) $value))))));
|
||
}
|
||
if (($field['sanitize'] ?? '') === 'reference_ids') {
|
||
$tokens = preg_split('/[,,\s]+/', trim((string) $value), -1, PREG_SPLIT_NO_EMPTY);
|
||
$tokens = array_map(static fn($token): string => preg_match('/^-?\d+$/', (string) $token) ? (string) (int) $token : '', $tokens ?: []);
|
||
return implode(',', array_values(array_unique(array_filter($tokens, static fn($token): bool => $token !== ''))));
|
||
}
|
||
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_text_field((string) $value);
|
||
}
|
||
}
|
||
|
||
private static function empty_value(string $type) {
|
||
if ($type === 'switcher') {
|
||
return false;
|
||
}
|
||
if (in_array($type, ['group', 'checkbox', 'terms'], true)) {
|
||
return [];
|
||
}
|
||
if (in_array($type, ['link', 'spacing', 'background', 'fieldset', 'accordion'], true)) {
|
||
return [];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
private static function number(array $field, $value) {
|
||
$number = is_numeric($value) ? (float) $value : (float) ($field['default'] ?? 0);
|
||
if (isset($field['min'])) {
|
||
$number = max((float) $field['min'], $number);
|
||
}
|
||
if (isset($field['max'])) {
|
||
$number = min((float) $field['max'], $number);
|
||
}
|
||
$step = (float) ($field['step'] ?? 1);
|
||
// Whole values stay int so JSON round trips keep normalize identity
|
||
// even for fractional-step fields (e.g. partners speed 0.1–10s).
|
||
return $step >= 1 || fmod($number, 1.0) === 0.0 ? (int) round($number) : $number;
|
||
}
|
||
|
||
private static function choice(array $field, $value) {
|
||
$options = (array) ($field['options'] ?? []);
|
||
if (!empty($field['multiple'])) {
|
||
$values = is_array($value) ? $value : [];
|
||
return array_values(array_filter(array_map('strval', $values), static fn($item) => array_key_exists($item, $options)));
|
||
}
|
||
$key = (string) $value;
|
||
return array_key_exists($key, $options) ? $key : (string) ($field['default'] ?? array_key_first($options) ?? '');
|
||
}
|
||
|
||
private static function checkbox(array $field, $value) {
|
||
$options = (array) ($field['options'] ?? []);
|
||
if (!$options) {
|
||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||
}
|
||
$values = is_array($value) ? $value : [];
|
||
return array_values(array_filter(array_map('strval', $values), static fn($item) => array_key_exists($item, $options)));
|
||
}
|
||
|
||
private static function terms($value): array {
|
||
$values = is_array($value) ? $value : (preg_split('/[,,\s]+/', (string) $value) ?: []);
|
||
return array_values(array_unique(array_filter(array_map('absint', $values))));
|
||
}
|
||
|
||
private static function link($value): array {
|
||
$value = is_array($value) ? $value : [];
|
||
return [
|
||
'url' => esc_url_raw((string) ($value['url'] ?? '')),
|
||
'text' => sanitize_text_field((string) ($value['text'] ?? '')),
|
||
'target' => ($value['target'] ?? '') === '_blank' ? '_blank' : '',
|
||
];
|
||
}
|
||
|
||
private static function spacing($value): array {
|
||
$value = is_array($value) ? $value : [];
|
||
$result = [];
|
||
foreach (['top', 'right', 'bottom', 'left'] as $edge) {
|
||
$result[$edge] = isset($value[$edge]) && is_numeric($value[$edge]) ? (int) $value[$edge] : '';
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
private static function background($value): array {
|
||
$value = is_array($value) ? $value : [];
|
||
return [
|
||
'color' => self::color((string) ($value['color'] ?? '')),
|
||
'image' => esc_url_raw((string) ($value['image'] ?? '')),
|
||
'position' => self::allow((string) ($value['position'] ?? 'center center'), ['center center', 'top center', 'bottom center', 'center left', 'center right'], 'center center'),
|
||
'repeat' => self::allow((string) ($value['repeat'] ?? 'no-repeat'), ['no-repeat', 'repeat', 'repeat-x', 'repeat-y'], 'no-repeat'),
|
||
'attachment' => self::allow((string) ($value['attachment'] ?? 'scroll'), ['scroll', 'fixed'], 'scroll'),
|
||
'size' => self::allow((string) ($value['size'] ?? 'cover'), ['cover', 'contain', 'auto'], 'cover'),
|
||
];
|
||
}
|
||
|
||
private static function group(array $field, $value): array {
|
||
$rows = is_array($value) ? array_values($value) : [];
|
||
$max = max(1, (int) ($field['max_items'] ?? 50));
|
||
$children = (array) ($field['fields'] ?? []);
|
||
return array_map(static fn($row) => self::normalize($children, is_array($row) ? $row : []), array_slice($rows, 0, $max));
|
||
}
|
||
|
||
private static function nested(array $field, $value): array {
|
||
$children = (array) ($field['fields'] ?? []);
|
||
return self::normalize($children, is_array($value) ? $value : []);
|
||
}
|
||
|
||
private static function color(string $value): string {
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
$hex = sanitize_hex_color($value);
|
||
if ($hex) {
|
||
return $hex;
|
||
}
|
||
return preg_match('/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/', $value) ? $value : '';
|
||
}
|
||
|
||
private static function allow(string $value, array $allowed, string $fallback): string {
|
||
return in_array($value, $allowed, true) ? $value : $fallback;
|
||
}
|
||
}
|
||
|