feat: rewrite schema-driven widget system
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user