feat: rewrite schema-driven widget system

This commit is contained in:
2026-08-17 17:29:54 +08:00
parent 6c6c462df8
commit ad99ab53ce
28 changed files with 3489 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
<?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', 'icon', 'upload',
'link', 'spacing', 'background', 'group', 'fieldset', 'accordion',
'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':
return self::choice($field, $value);
case 'checkbox':
return self::checkbox($field, $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'] ?? '') === 'ids') {
return implode(',', array_values(array_unique(array_filter(array_map('absint', preg_split('/[,,\s]+/', (string) $value))))));
}
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'], 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);
return $step >= 1 ? (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 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;
}
}