PluginProbe
WPJAM Basic / trunk
WPJAM Basic vtrunk
wpjam-basic / includes / class-wpjam-field.php

class-wpjam-field.php in WPJAM Basic trunk, at includes/class-wpjam-field.php

1,299 lines 47.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 class WPJAM_Attr extends WPJAM_Args{
3 public function __toString(){
4 return (string)$this->render();
5 }
6
7 public function jsonSerialize(){
8 return $this->render();
9 }
10
11 public function attr($key, ...$args){
12 return is_array($key)
13 ? wpjam_reduce($key, fn($c, $v, $k)=> $c->attr($k, $v), $this)
14 : [$this, $args ? (is_closure($args[0]) ? 'process_arg' : 'update_arg') : 'get_arg']($key, ...$args);
15 }
16
17 public function val(...$args){
18 return $this->attr('value', ...$args);
19 }
20
21 public function data($key='', ...$args){
22 return is_array($key)
23 ? $this->attr(wpjam_array($key, fn($k)=> 'data-'.$k))
24 : ($key
25 ? ($this->attr('data-'.$key, ...$args) ?? (wpjam_array($this->data)[$key] ?? null))
26 : wpjam_array($this, fn($k)=> try_prefix($k, '-', 'data-') ? $k : null)+wpjam_array($this->data)
27 );
28 }
29
30 public function remove_attr($key){
31 return $this->delete_arg($key);
32 }
33
34 public function remove_data($key){
35 $keys = wp_parse_list($key);
36
37 return $this->attr('data', wpjam_except(wpjam_array($this->data), $keys))->remove_attr(array_map(fn($k)=> 'data-'.$k), $keys);
38 }
39
40 public function class(){
41 return wp_parse_list($this->class ?: []);
42 }
43
44 public function has_class($name){
45 return in_array($name, $this->class());
46 }
47
48 public function add_class($name){
49 return $this->attr('class', array_merge($this->class(), wp_parse_list($name ?: [])));
50 }
51
52 public function remove_class(...$args){
53 return $this->attr('class', $args ? array_diff($this->class(), wp_parse_list($args[0] ?: [])) : []);
54 }
55
56 public function style(...$args){
57 $style = wpjam_array($this->style);
58
59 return $args
60 ? $this->attr('style', array_merge($style, (array)(count($args) == 1 || is_array($args[0]) ? $args[0] : implode(':', $args))))
61 : wpjam_array($style, fn($k, $v)=> is_blank($v) ? null : [null, rtrim(is_numeric($k) ? $v : $k.':'.$v, ';').';']);
62 }
63
64 public function render(){
65 $r = '';
66 $data = $this->pull('__data');
67
68 foreach($data ? [] : self::parse($this->add_class($this->pick(['readonly', 'disabled']))) as $k => $v){
69 if($k == 'data'
70 || array_any(['_callback', '_column'], fn($e)=> str_ends_with($k, $e))
71 || array_any(['_', 'column_', 'data-'], fn($s)=> str_starts_with($k, $s))
72 || ($k == 'value' ? is_null($v) : is_blank($v))
73 ){
74 continue;
75 }
76
77 if(in_array($k, ['style', 'class'])){
78 $v = implode(' ', array_unique($this->$k()));
79 }elseif(!is_scalar($v)){
80 trigger_error($k.' '.var_export($v, true));
81 }
82
83 $r .= ' '.$k.'="'.esc_attr($v).'"';
84 }
85
86 foreach($data ? $this : $this->data() as $k => $v){
87 $v = ($k == 'show_if' ? wpjam_parse_show_if($v) : $v) ?? false;
88 $r .= $v === false ? '' : ' data-'.$k.'=\''.(is_scalar($v) ? esc_attr($v) : ($k == 'data' ? http_build_query($v) : wpjam_json_encode($v))).'\'';
89 }
90
91 return $r;
92 }
93
94 public static function is_bool($key){
95 return in_array($key, ['allowfullscreen', 'allowpaymentrequest', 'allowusermedia', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'defer', 'disabled', 'download', 'formnovalidate', 'hidden', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'playsinline', 'readonly', 'required', 'reversed', 'selected', 'typemustmatch']);
96 }
97
98 public static function parse($attr){
99 return wpjam_array($attr, fn($k, $v)=> ($k = strtolower(trim($k))) && !is_numeric($k)
100 ? (self::is_bool($k) ? ($v ? [$k, $k] : null) : [$k, $v])
101 : (($v = strtolower(trim($v))) && self::is_bool($v) ? [$v, $v] : null));
102 }
103
104 public static function create($attr, $type=''){
105 return new static(($attr && is_string($attr) ? shortcode_parse_atts($attr) : wpjam_array($attr))+($type ? ['__'.$type=>true] : []));
106 }
107 }
108
109 class WPJAM_Tag extends WPJAM_Attr{
110 public function __construct($tag='', $attr=[], $text=''){
111 $this->init($tag, $attr, $text);
112 }
113
114 public function __call($method, $args){
115 if(in_array($method, ['text', 'tag', 'before', 'after', 'prepend', 'append'])){
116 $key = '_'.$method;
117
118 if(!$args){
119 return $this->$key;
120 }
121
122 if($key == '_tag'){
123 return $this->update_arg($key, $args[0]);
124 }
125
126 $value = count($args) > 1 ? new self(...(is_array($args[1]) ? $args : [$args[1], ($args[2] ?? []), $args[0]])) : $args[0];
127
128 if(is_array($value)){
129 wpjam_map($value, fn($v)=> $this->$method(...(is_array($v) ? $v : [$v])));
130 }elseif($key == '_text'){
131 $this->$key = (string)$value;
132 }elseif($value){
133 $this->$key = in_array($key, ['_before', '_prepend']) ? [$value, ...$this->$key] : [...$this->$key, $value];
134 }
135 }elseif(in_array($method, ['insert_before', 'insert_after', 'append_to', 'prepend_to'])){
136 [$args[0], str_replace(['insert_', '_to'], '', $method)]($this);
137 }else{
138 trigger_error($method);
139 }
140
141 return $this;
142 }
143
144 public function is($tag){
145 return array_intersect([$this->_tag, $this->_tag === 'input' ? ':'.$this->type : null], wp_parse_list($tag));
146 }
147
148 public function init($tag, $attr, $text){
149 $this->args = ['_tag'=>$tag]+array_fill_keys(['_before', '_after', '_prepend', '_append'], []);
150 $this->args += wpjam_is_assoc_array($attr) ? $attr : array_filter(['class'=>$attr]);
151
152 return $this->text(...($text && is_array($text) ? $text : [is_blank($text) ? '' : $text]));
153 }
154
155 public function render(){
156 $tag = $this->update_args(['a'=>['href'=>'javascript:;'], 'img'=>['title'=>$this->alt]][$this->_tag] ?? [], false)->_tag;
157 $arr = $this->is_single($tag) ? [] : [...$this->_prepend, (string)$this->_text, ...$this->_append];
158 $arr = $tag ? ['<'.$tag.parent::render(), ...($arr ? ['>', ...$arr, '</'.$tag.'>'] : [' />'])] : $arr;
159
160 return implode([...$this->_before, ...$arr, ...$this->_after]);
161 }
162
163 public function wrap($tag, ...$args){
164 $wrap = $tag && str_contains($tag, '></');
165 $tag = $wrap ? (preg_match('/<(\w+)([^>]+)>/', ($args ? sprintf($tag, ...$args) : $tag), $matches) ? $matches[1] : '') : $tag;
166
167 return $tag ? $this->init($tag, $wrap ? shortcode_parse_atts($matches[2]) : ($args[0] ?? []), clone($this)) : $this;
168 }
169
170 public static function is_single($tag){
171 return $tag && in_array($tag, ['area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
172 }
173 }
174
175 class WPJAM_Field extends WPJAM_Attr{
176 protected function __construct($args){
177 $this->args = $args;
178 $this->options = maybe_callback($this->options);
179 $this->_title ??= $this->init($this->pull('prepend_name'))->title.''.$this->key.'';
180
181 if($type = $this->data_type){
182 $args = wp_parse_args($this->query_args ?: []);
183
184 if($this->$type){
185 $args[$type] = $this->$type;
186 }elseif(!empty($args[$type])){
187 $this->$type = $args[$type];
188 }
189
190 if($type == 'model'){
191 $args['label_field'] ??= wpjam_pull($args, 'label_key') ?: 'title';
192 $args['id_field'] ??= wpjam_pull($args, 'id_key') ?: wpjam_call($args[$type].'::get_primary_key');
193 }
194
195 if($this->_data_type = wpjam_get_data_type($type, $args)){
196 $this->query_args = $args ?: new StdClass;
197 $this->query_nonce = wp_create_nonce($this->_data_type->nonce_action($args));
198 }
199 }
200
201 if($this->is('mu')){
202 if($this->is('mu-fields')){
203 $attr = ['type'=>'fieldset', 'fieldset'=>'object']+($this->tag_label ? ['group'=>true] : []);
204 }else{
205 $attr = ['type'=>$this->is('mu-text') ? ($this->item_type ?? 'text') : wpjam_prefix($this->type, '-', 'mu-')];
206 $attr += $attr['type'] == 'text' && $this->direction == 'row' ? ['class'=>$this->class ?? 'medium-text'] : [];
207 }
208
209 $this->_item = self::create($attr+['_mu'=>$this]+wpjam_except($this->get_args(), ['required', 'filterable', 'multiple']));
210 }elseif($this->is('fieldset')){
211 $this->_fields = WPJAM_Fields::create($this->fields, $this->_fields_args ?: [], $this);
212 $this->fields = $this->_fields->fields;
213 }else{
214 $this->attr(wpjam_pattern($this->pattern) ?: []);
215 }
216 }
217
218 public function __call($method, $args){
219 [$action, $type] = explode_last('_by', $method)+['', ''];
220
221 if($type == '_data_type' && in_array($action, ['render', 'parse', 'validate'])){
222 if(!$this->$type){
223 return $args[0];
224 }
225
226 if($this->multiple && is_array($args[0])){
227 return array_map([$this, $method], $args[0]);
228 }
229
230 $args = [$action, ...$args, $this];
231 $action = 'with_field';
232 }
233
234 return $this->$type ? wpjam_try([$this->$type, $action], ...$args) : null;
235 }
236
237 public function init($pn){
238 return $this->attr('_names', [...wpjam_names($pn), ...wpjam_names($this->name)])->attr('name', wpjam_names($this->_names));
239 }
240
241 public function is($type, $strict=false){
242 $type = wp_parse_list($type);
243
244 return (in_array('mu', $type) && str_starts_with($this->type, 'mu-'))
245 || in_array($this->type, $strict ? $type : array_merge($type, wpjam_pick(['fieldset'=>'fields', 'view'=>'hr'], $type)));
246 }
247
248 protected function el($name, $attr=[]){
249 $tag = wpjam_tag($name, $this->get_args())->attr($attr)->remove_attr($name == 'input' ? [] : ['type', 'value']);
250 $data = $tag->pull(['key', 'data_type', 'query_args', 'query_nonce', 'filter_key', 'custom_validity', 'show_option_all']);
251
252 $this->class = $tag->class ??= in_array($tag->type, ['text', 'url', 'email']) ? 'regular-text' : ($name == 'textarea' ? 'wide-text' : '');
253
254 return $tag->data($data)->add_class($name == 'fieldset' ? '' : 'field-key-'.$this->key)->remove_attr(['default', 'options', 'multiple', 'title', 'label', 'render', 'before', 'after', 'description', 'wrap_class', 'wrap_tag', 'show_option_none', 'option_all_value', 'option_none_value', 'direction', 'group', 'buttons', 'size', 'post_type', 'taxonomy', 'sep', 'fields', 'parse_required', 'show_if', 'show_in_rest', 'column', 'custom_input', 'max_items', 'min_items', 'unique_items', 'filterable', 'summarization']);
255 }
256
257 public function schema(...$args){
258 if($args){
259 $value = $args[0];
260
261 foreach(wpjam_pull($value, ['enum', 'items', 'properties']) as $k => $v){
262 if($k == 'enum'){
263 $value[$k] = array_map(fn($i)=> $this->sanitize($i, $value), $v);
264 }elseif($value['type'] == ($k == 'items' ? 'array' : 'object')){
265 $value[$k] = $k == 'items' ? $this->schema($v) : array_map([$this, 'schema'], $v);
266 }
267 }
268
269 return $value;
270 }
271
272 if(isset($this->_schema)){
273 return $this->_schema;
274 }
275
276 $value = array_filter(['type'=>$this->get_arg('show_in_rest.type')])+($this->get_arg_by_data_type('schema') ?: []);
277
278 if($this->is('mu')){
279 $value = ['type'=>'array', 'items'=>($value+$this->schema_by_item())];
280 }elseif($this->is('fieldset')){
281 $value += ['type'=>'object', 'properties'=>array_filter($this->schema_by_fields())];
282 }elseif($this->is('email')){
283 $value += ['format'=>'email'];
284 }elseif($this->is('color')){
285 $value += $this->data('alpha-enabled') ? [] : ['format'=>'hex-color'];
286 }elseif($this->is('url, image, file, img')){
287 $value += ($this->is('img') && $this->item_type != 'url') ? ['type'=>'integer'] : ['format'=>'uri'];
288 }elseif($this->is('number, range')){
289 $step = $this->step ?: '';
290 $value += ['type'=>($step == 'any' || strpos($step, '.')) ? 'number' : 'integer'];
291 $value += $value['type'] == 'integer' && $step > 1 ? ['multipleOf'=>$step] : [];
292 }elseif($this->is('toggle')){
293 $value += ['type'=>'boolean'];
294 }elseif($this->is('radio, select, checkbox')){
295 $value += ['type'=>'string']+($this->custom_input ? [] : ['enum'=>array_keys($this->options())]);
296 $value = $this->multiple ? ['type'=>'array', 'items'=>$value] : $value;
297 }
298
299 $value += ['type'=>'string'];
300 $value += wpjam_array((array_fill_keys(['integer', 'number'], ['minimum'=>'min', 'maximum'=>'max'])+[
301 'array' => ['maxItems'=>'max_items', 'minItems'=>'min_items', 'uniqueItems'=>'unique_items'],
302 'string' => ['minLength'=>'minlength', 'maxLength'=>'maxlength', 'pattern'=>'pattern'],
303 ])[$value['type']] ?? [], fn($k, $v)=> is_blank($this->$v) ? null : [$k, $this->$v]);
304
305 $value += $value['type'] === 'array' && $this->required ? ['minItems'=>1] : [];
306
307 return $this->_schema = $this->schema($value);
308 }
309
310 public function button_text(){
311 if($text = $this->pull('button_text')){
312 return $text;
313 }
314
315 if($this->is('uploader')){
316 return __('Select Files', 'wpjam');
317 }elseif($this->is('img, image, file, mu-img, mu-image, mu-file')){
318 return __('Select '.($this->is('file, mu-file') ? 'file' : 'image'), 'wpjam').($this->is('mu') ? '[多选]' : '');
319 }elseif($this->is('mu-text, mu-fields')){
320 return '添加'.(wpjam_compare(mb_strwidth($this->title ?: ''), 'between', 4, 8) ? $this->title : '选项');
321 }
322 }
323
324 public function item_type(){
325 return $this->pull('item_type') ?? ($this->is('img, mu-img') ? 'id' : ($this->is('image, mu-image') ? 'image' : ''));
326 }
327
328 public function show_if(...$args){
329 if($args = wpjam_parse_show_if($args ? $args[0] : $this->show_if)){
330 return $args+['value'=>true]+($this->_prop ? ['current'=>$this->key] : []);
331 }
332 }
333
334 public function options($flat=true, ...$args){
335 $res = [];
336 $options = $args[0] ?? array_replace($this->is('select') && !$this->multiple ? array_reduce(['all', 'none'], fn($c, $k)=> $c+array_filter([($this->{'option_'.$k.'_value'} ?? '') => $this->{'show_option_'.$k}]), []) : [], $this->options);
337
338 foreach(wpjam_array($options) as $key => $item){
339 if(is_array($item)){
340 $label = array_first(wpjam_pull($item, ['label', 'title']));
341 $subs = isset($item['options']) ? $this->options($flat, wpjam_pull($item, 'options')) : null;
342
343 if($flat){
344 $res = array_replace($res, $subs ?? array_fill_keys([$key, ...wp_parse_list(wpjam_pull($item, 'alias', []))], $label));
345 }else{
346 $opt = ['label'=>$label]+(isset($subs) ? ['options'=>$subs] : ['value'=>(string)$key]);
347
348 foreach($item as $k => $v){
349 if(is_numeric($k)){
350 self::is_bool($v) && ($opt[$v] = $v);
351 }elseif(self::is_bool($k)){
352 $v && ($opt[$k] = $k);
353 }elseif(in_array($k, ['class', 'image', 'description'])){
354 $opt[$k] = $v;
355 }elseif($k == 'alias'){
356 $opt[$k] = wp_parse_list($v);
357 }elseif($k != 'fields'){
358 $opt['data'][$k] = $k == 'show_if' ? $this->show_if($v) : $v;
359 }
360 }
361
362 $res[] = $opt;
363 }
364 }else{
365 if($flat){
366 $res[$key] = $item;
367 }else{
368 $res[] = ['value'=>(string)$key, 'label'=>$item];
369 }
370 }
371 }
372
373 return $res;
374 }
375
376 public function custom_input(){
377 if($input = $this->custom_input){
378 $cv = '__custom';
379 $by = (is_array($input) ? $input : [])+['key'=>$this->key.$cv, 'type'=>'text', 'required'=>true];
380 $title = ($by['title'] ?? '') ?: (is_string($input) ? $input : __('Other', 'wpjam'));
381
382 return $by+['by'=>$cv, 'title'=>$title, 'placeholder'=>'请输�
383 �'.$title.'选项'];
384 }
385 }
386
387 public function validate($value, $type=''){
388 $mu = $this->is('mu');
389
390 if(!$mu && ($cb = $this->validate_callback) && wpjam_try($cb, $value) === false){
391 wpjam_throw('invalid_'.($type ?: 'value'), [$this->key]);
392 }
393
394 if($this->is('fieldset')){
395 $value = $this->validate_by_fields($value, $type);
396
397 goto st;
398 }
399
400 if($type == 'parameter'){
401 if(is_null($value ??= $this->default) && $this->required){
402 wpjam_throw('missing_parameter', sprintf(__('Missing parameter: %s.', 'wpjam'), $this->key));
403 }
404
405 if($mu && !is_array($value)){
406 $value = wpjam_trap('wpjam_json_decode', $value, []);
407 }
408 }
409
410 if($mu){
411 $value = array_values(wpjam_filter((array)$value, fn($v)=> !is_blank($v), true));
412 $value = $type == 'if_value' ? $value : array_map(fn($v)=> $this->validate_by_item($v, $type), $value);
413 }else{
414 $custom = $this->is('radio, select, checkbox') ? $this->custom_input() : [];
415 $value = $this->multiple ? wpjam_diff((array)$value, $custom ? [$custom['by']] : []) : $value;
416
417 if($custom && ($diff = array_diff((array)$value, array_map('strval', array_keys($this->options()))))){
418 (self::create($custom+['_title'=>$this->_title.'的「'.$custom['title'].'', '_schema'=>[]]))->validate(array_first($diff));
419 }
420
421 if($value){
422 $value = $this->validate_by_data_type($value);
423 }
424 }
425
426 if($type == 'parameter'){
427 if(isset($value)){
428 $value = $this->sanitize($value);
429 }
430 }else{
431 if($this->required && is_blank($value)){ // 空值只需 required 验证
432 wpjam_throw(($type ?: 'value').'_required', [$this->_title]);
433 }
434
435 if($schema = $this->schema()){
436 $value = [$this, $this->_prop ? 'sanitize' : 'prepare']($value, $schema);
437
438 if(is_array($value) || !is_blank($value)){
439 wpjam_try('rest_validate_value_from_schema', $value, $schema, $this->_title);
440 }
441 }
442 }
443
444 st:
445
446 if(!$mu && ($cb = $this->sanitize_callback)){
447 return wpjam_try($cb, $value ?? '');
448 }
449
450 return $value;
451 }
452
453 public function value_callback($args=[]){
454 if(!$args || ($this->is('view') && $this->value)){
455 return $this->value;
456 }
457
458 $cb = $this->value_callback;
459
460 return wpjam_value_callback(
461 $cb ? ['value_callback'=>$cb]+wpjam_pick($args, ['id']) : $args,
462 $cb ? array_last($this->_names) : $this->_names
463 ) ?? $this->value;
464 }
465
466 public function prepare(...$args){
467 if(is_array($args[1] ?? '')){
468 $rule = [
469 'array' => ['is_array', fn($val)=> wpjam_map($val, fn($v)=> $this->prepare($v, $args[1]['items']))],
470 'object' => ['is_array', fn($val)=> wpjam_map($val, fn($v, $k)=> $this->prepare($v, ($args[1]['properties'][$k] ?? [])))],
471 'null' => ['is_blank', fn()=> null],
472 'integer' => ['is_numeric', 'intval'],
473 'number' => ['is_numeric', 'floatval'],
474 'string' => ['is_scalar', 'strval'],
475 'boolean' => [fn($v)=> is_scalar($v) || is_null($v), 'rest_sanitize_boolean']
476 ][$args[1]['type']] ?? '';
477
478 return $rule && $rule[0]($args[0]) ? $rule[1]($args[0]) : $args[0];
479 }
480
481 $value = !empty($args[1]) ? $args[0] : $this->value_callback($args[0]);
482 $value = $this->sanitize($value);
483
484 if($this->is('fieldset')){
485 return array_filter($this->prepare_by_fields($value ?: [], 'value'), fn($v)=> !is_null($v));
486 }elseif($this->is('mu')){
487 return array_map(fn($v)=> $this->prepare_by_item($v, 'value'), $value ?: []);
488 }elseif($this->is('img, image, file')){
489 return wpjam_get_thumbnail($value, $this->size);
490 }
491
492 return $value && $this->parse_required ? $this->parse_by_data_type($value) : $value;
493 }
494
495 public function sanitize($value, $schema=null){
496 return ($schema ??= $this->schema()) ? wpjam_try('rest_sanitize_value_from_schema', ($schema['type'] == 'string' ? (string)$value : $value), $schema, $this->_title) : $value;
497 }
498
499 public function wrap($tag='', $args=[]){
500 $field = $this->render($args);
501 $sep = $args['sep'] ?? '';
502 $for = $this->is('view, mu, fieldset, img, uploader, radio, editor') || $this->multiple ? [] : ['for'=>$this->id];
503 $before = $this->before;
504 $after = $this->after;
505 $buttons= wpjam_join(' ', array_values(wpjam_map($this->buttons ?: [], [self::class, 'create'])));
506
507 foreach(compact('before', 'after') as $k => $v){
508 $t = $this->is('textarea, editor, img, mu, fieldset', true) || ($v && strip_tags($v) !== $v) ? 'p' : 'span';
509 $v = $k == 'after' ? wpjam_join(' ', $v, $buttons) : $v;
510
511 $v && $field->$k($t, [$k], $v);
512 }
513
514 $for && ($this->label || $before || $after || $buttons) && $field->wrap('label', $for);
515
516 $title = $this->title ? wpjam_tag('label', $for, $this->title) : '';
517 $desc = (array)$this->description+['', []];
518 $desc[0] && $field->after('p', ['class'=>'description', 'data-show_if'=>$this->show_if(wpjam_pull($desc[1], 'show_if'))]+$desc[1], $desc[0]);
519
520 if($tag == 'inline'){
521 [$tag, $class] = ($title || $this->is('fields') || !is_null($field->data('label')) || trim($sep)) ? ['div', $tag] : ['', ''];
522 }elseif($tag == 'sub-field'){
523 $title && $title->add_class('sub-field-label') && $field->wrap('div', ['sub-field-detail']);
524
525 [$tag, $class] = ['div', $tag];
526 }elseif($tag == 'tr'){
527 $field->wrap('td', $title && $title->wrap('th', ['scope'=>'row']) ? [] : ['colspan'=>2]);
528 }
529
530 $title && $field->before($title->after($sep));
531 $tag && $field->wrap($tag, ['id'=>$tag.'_'.$this->id, 'class'=>$class ?? '']);
532
533 return $field->data('show_if', $this->show_if())->add_class([$args['wrap_class'] ?? '', $this->wrap_class, $this->disabled, $this->readonly, ($this->is('hidden') ? 'hidden' : '')]);
534 }
535
536 public function query_label($value){
537 if($this->has_class('mix-tag')){
538 $sep = implode('|', array_map(fn($v)=> preg_quote($v, '/'), [' ', ...($this->data('sep') ?: [])]));
539 $value = preg_split('/('.$sep.')/', trim(str_replace("\xc2\xa0", ' ', $value)), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
540 $value = wpjam_filter($value, fn($v)=> trim($v));
541 $value[] = "\xc2\xa0";
542 }
543
544 if($items = $this->data('items')){
545 $this->data('items', wpjam_entries($items, 'value', 'label'));
546
547 return is_array($value) ? wpjam_map($value, fn($v)=> empty($items[$v]) ? $v : ['label'=>$items[$v], 'value'=>$v]) : ($items[$value] ?? null);
548 }
549
550 return $this->query_label_by_data_type($value);
551 }
552
553 public function render($args=[]){
554 $value = $this->value = $this->value_callback($args);
555 $value = $this->is('mu') ? array_values(wpjam_filter((array)$value, fn($v)=> !is_blank($v), true)) : $value;
556
557 if($this->is('fieldset, mu-fields, view')){
558 $data = [];
559 }elseif($this->is('mu-img, img')){
560 $values = wpjam_map((array)$value, fn($v)=> $v ? ['value'=>$v, 'url'=>wpjam_at(wpjam_get_thumbnail($v), '?', 0)] : '');
561 $data = ['value'=>$this->is('mu') ? $values : array_first($values)];
562 }elseif($this->is('cascade')){
563 $data = $this->render_by_data_type($value);
564 }else{
565 $label = $this->query_label($value);
566 $data = is_array($label) ? ['value'=>$label ?: $value] : ['value'=> $value]+array_filter(['label'=>$label]);
567 }
568
569 $data += $this->pick(['key', 'type', 'direction', 'filterable', 'summarization', 'tag_label']);
570 $data += array_filter(wpjam_fill(['schema', 'item_type', 'button_text'], fn($k)=> [$this, $k]()));
571
572 if($this->render){
573 return wpjam_wrap($this->call('render', $args));
574 }elseif($this->is('hr')){
575 return wpjam_tag('hr');
576 }elseif($this->is('view')){
577 $value = (string)$value;
578 $view = array_find(($this->options && $value == strip_tags($value) ? $this->options() : []), fn($v, $k)=> $value ? $k == $value : !$k);
579 $tag = $this->wrap_tag ?? ($this->show_if || isset($view) ? 'span' : '');
580
581 return wpjam_wrap($view ?? $value, $tag, $tag ? ['class'=>'field-key-'.$this->key, 'data'=>['value'=>$value, 'name'=>$this->name]] : []);
582 }elseif($this->is('fieldset')){
583 $tag = $this->wrap_tag;
584 $attr = $this->_mu ? [] : array_filter(['class'=>$this->class, 'data'=>$this->data()]);
585 $field = $this->render_by_fields([
586 'sep' => $this->sep ?? ($tag === 'fieldset' && !$this->group ? '<br />' : ''),
587 'wrap_tag' => $tag === 'fieldset' ? 'inline' : ($tag ? 'sub-field' : '')
588 ]+$args)->wrap($this->_mu ? 'template' : '');
589
590 $this->title && $tag === 'fieldset' && $field->prepend('legend', ['screen-reader-text'], $this->title);
591 $this->summary && $field->before($this->summary, 'summary')->wrap('details');
592
593 return $field->wrap($tag ?: ($attr ? 'div' : ''), $attr)->add_class($this->group ? 'field-group' : '');
594 }elseif($this->is('mu')){
595 $class = ['mu', $this->type, ($this->sortable !== false ? 'sortable' : '')];
596
597 if($this->is('mu-fields')){
598 $text = wpjam_map([...$value, []], fn($v)=> $this->attr_by_item(['value'=>$v])->render());
599 }else{
600 $text = $this->attr_by_item(['id'=>'', 'value'=>null, 'name'=>$this->name.'[]'])->render()->wrap('div');
601 }
602
603 $field = wpjam_tag('div', $this->pick(['id', 'name'])+['class'=>$class])->append($text);
604 }elseif($this->is('toggle')){
605 $field = $this->el('input', ['value'=>1, 'type'=>'checkbox'])->after($this->label ?? $this->pull('description'));
606 }elseif($this->is('radio, select, checkbox, cascade')){
607 $data = $this->pick(['multiple', 'sep'])+($this->is('cascade')
608 ? ['options'=>array_map(fn($v)=> $this->options(false, $v), $data['options'])]
609 : ['options'=>$this->options(false), 'custom'=>$this->custom_input()]
610 )+$data;
611
612 $field = $this->el($this->is('select') && !$this->multiple ? 'select' : 'fieldset');
613 }elseif($this->is('editor, textarea')){
614 if($this->is('editor')){
615 $this->id = 'editor_'.$this->id;
616
617 if(!wp_doing_ajax()){
618 return wpjam_wrap(wpjam_ob('wp_editor', $value ?: '', $this->id, ['textarea_name'=>$this->name]));
619 }
620
621 $data += ['editor'=>['tinymce'=>true, 'quicktags'=>true]];
622 }
623
624 $field = $this->el('textarea')->append(esc_textarea($value ?: ''));
625 }elseif($this->is('img, image, file')){
626 $field = $this->el('input', ['type'=>$this->is('img') ? 'hidden' : 'url']);
627
628 if($this->is('img')){
629 $this->description ??= ($size = wpjam_parse_size($this->size)) && $size['width'] && $size['height'] ? '建议尺寸:'.$size['width'].'x'.$size['height'] : null;
630
631 $field->data('thumb_args', wpjam_get_thumbnail('args', $this->_mu ? [200, 200] : wpjam_constrain_dimensions($this->size ?: '480x0', [480, 480])));
632 }
633
634 if($this->_mu){
635 return $field;
636 }
637 }elseif($this->is('uploader')){
638 $accept = $this->accept ??= 'image/*';
639 $data += [
640 'drag_drop' => $this->pull('drag_drop') && !wp_is_mobile() && !$field->disabled,
641 'max_size' => wp_max_upload_size(),
642 'nonce' => wp_create_nonce('upload-'.$this->key.'-'.$accept)
643 ];
644
645 $field = $this->el('input', ['type'=>'hidden', 'disabled'=>!wpjam_mimes($accept)]);
646 }else{
647 $field = $this->el('input');
648 $data += array_filter(['class'=>$this->class]);
649 }
650
651 return $field->data($data);
652 }
653
654 public static function parse($field){
655 $field = is_string($field) ? ['type'=>'view', 'value'=>$field, 'wrap_tag'=>''] : parent::parse($field);
656 $field = ['options'=>(($field['options'] ?? []) ?: [])]+$field;
657 $type = $field['type'] = ($field['type'] ?? '') ?: (array_find(['options'=>'select', 'label'=>'toggle', 'fields'=>'fieldset'], fn($v, $k)=> !empty($field[$k])) ?: 'text');
658
659 if(($field['filterable'] ?? '') === 'multiple' && in_array($type, ['text', 'number', 'select'])){
660 $field += ['multiple'=>true]+($type == 'select' ? [] : ['unique_items'=>true, 'sortable'=>false]);
661 }
662
663 if(in_array($type, ['fieldset', 'fields', 'mu-fields'])){
664 if($type !== 'mu-fields'){
665 $field['fieldset'] ??= in_array(wpjam_pull($field, 'fieldset_type'), ['array', 'object']) ? 'object' : 'flat';
666 }
667
668 if($type !== 'fields'){
669 $field['wrap_tag'] ??= array_all($field['fields'] ?? [], fn($v)=> empty($v['title'])) ? 'fieldset' : 'div';
670 }
671 }elseif($type == 'size'){
672 $field['fieldset'] ??= 'object';
673 $field['type'] = 'fields';
674 $field['fields'] = wpjam_fill(['width', 'x', 'height'], fn($k)=> $k == 'x' ? '✖️' : (($v = $field['fields'][$k] ?? []) ? (is_string($v) ? self::parse($v) : $v) : [])+['type'=>'number', 'class'=>'small-text']);
675 }elseif(in_array($type, ['number', 'url', 'tel', 'email', 'search'])){
676 $field['inputmode'] ??= $type == 'number' ? ((($step = $field['step'] ?? '') == 'any' || strpos($step, '.')) ? 'decimal': 'numeric') : $type;
677 }elseif($type == 'checkbox'){
678 $field = (empty($field['options']) ? ['type'=>'toggle'] : ['multiple'=>true])+$field;
679 }elseif($type == 'mu-select'){
680 $field['multiple'] = true;
681 $field['type'] = 'select';
682 }elseif($type == 'tag-input' || (!empty($field['multiple']) && in_array($type, ['text', 'number']))){
683 $field['item_type'] ??= $type == 'tag-input' ? 'text' : $type;
684 $field['class'] = wpjam_join(' ', 'tag-input', ...(array)($field['class'] ?? []));
685 $field['type'] = 'mu-text';
686 }
687
688 return $field;
689 }
690
691 public static function create($field, $key=''){
692 $field = self::parse($field);
693 $key = $key && !is_numeric($key) ? $key : (($field['key'] ?? '') ?: ($field['name'] ?? ''));
694
695 if($key && !is_numeric($key)){
696 return new WPJAM_Field(wpjam_fill(['id', 'name', 'key'], fn($k)=> ($field[$k] ?? '') ?: $key)+$field+([
697 'color' => ['data-alpha-enabled'=>wpjam_pull($field, 'alpha')],
698 'timestamp' => ['sanitize_callback'=> fn($v)=> $v ? (is_numeric($v) ? $v : wpjam_strtotime($v)) : '']
699 ][$field['type']] ?? []));
700 }
701
702 trigger_error('Field 的 key 不能为'.(!$key ? '' : '纯数字「'.$key.''));
703 }
704 }
705
706 class WPJAM_Fields extends WPJAM_Attr{
707 public function __invoke($args=[]){
708 return $this->render($args);
709 }
710
711 public function __call($method, $args){
712 $data = [];
713 $fields = $this->mod($method, '_parts') ? wpjam_filter($this->fields, array_shift($args)) : $this->fields;
714
715 if($method == 'validate'){
716 $parent = $this->parent;
717 $prop = $this->prop;
718 $values = $args[0] ?? wpjam_params('post');
719 $type = $args[1] ?? '';
720
721 if($type == 'if_value'){
722 $pk = $prop && !$parent->_mu ? $parent->key.'__' : '';
723 }else{
724 $if = $parent ? (($parent->_mu ?: $parent)->_if ?: []) : [];
725 $if = ((!$if || $prop) ? ['values'=>$this->validate($values, 'if_value')+($if['values'] ?? [])] : [])+$if+['show'=>true];
726 }
727 }
728
729 foreach($fields as $field){
730 $set = $field->is('fieldset');
731 $flat = $field->fieldset == 'flat';
732 $pack = !$flat;
733
734 if($method == 'validate'){
735 $can = !$field->disabled && !$field->readonly && !$field->is('view, button');
736 $value = $flat ? $values : wpjam_get($values, $field->_names);
737
738 if($type == 'if_value'){
739 $value = ($set || $can)
740 ? wpjam_trap([$field, 'validate'], $value, $type, null)
741 : ($field->disabled ? null : $field->value_callback($this->_args));
742
743 $value = $set ? $value : [$pk.$field->key => $value]; // show_if 基于key
744 $pack = false;
745 }else{
746 $show = $can && $if['show'] && (!($show_if = $field->show_if()) || wpjam_match($if['values'], $show_if));
747 $value = $can && ($flat || $show) ? $field->attr('_if', ['show'=>$show]+$if)->validate($value, $type) : null;
748
749 if(!$can || (!$show && $prop)){
750 continue;
751 }
752 }
753 }elseif(method_exists($field, $method)){
754 if($method == 'prepare' && $field->show_in_rest === false){
755 continue;
756 }
757
758 $value = [$field, $method.($flat ? '_by_fields' : '')](...($method == 'prepare'
759 ? [!empty($args[1]) ? wpjam_get($args[0], $field->_names) : ($args[0] ?? [])+$this->_args, $args[1] ?? '']
760 : $args
761 ));
762 }elseif($method == 'get_defaults'){
763 $value = $set ? [$field, $method.'_by_fields']() : ($field->disabled ? null : $field->value);
764 }else{
765 trigger_error($method); // del 2026-09-01
766 }
767
768 $data = $pack ? wpjam_set($data, $field->_names, $value) : wpjam_merge($data, ($value ?? []));
769 }
770
771 return $data;
772 }
773
774 public function render($args=[]){
775 $data = [];
776 $args += $this->_args;
777 $parent = $this->parent;
778 $type = $parent ? '' : (wpjam_pull($args, 'fields_type') ?? 'table');
779 $tag = wpjam_pull($args, 'wrap_tag') ?? ['table'=>'tr', 'list'=>'li'][$type] ?? $type;
780 $args += ['sep'=>($tag == 'p' ? '<br />' : '')];
781 $pf = $this->prop ? wpjam_fill(['id', 'key', 'name'], fn($k)=> $parent->$k.($k == 'name' ? ($parent->_mu ? '[i${i}]' : '') : '__'.($parent->_mu ? 'i${i}__' : ''))) : [];
782
783 foreach($this->fields as $field){
784 if(!$parent || !$data || !$field->group || $field->group != $group){
785 $i = ($i ?? -1)+1;
786 $group = $field->group;
787 }
788
789 $data[$i][] = $field->sandbox(fn()=> ($pf ? $field->val(($parent->value ?: [])[$field->name] ?? null)->attr(wpjam_fill(['id', 'key'], fn($k)=> $pf[$k].$field->$k))->init($pf['name']) : $field)->wrap($tag, $args));
790 }
791
792 $data = array_filter(array_map(fn($g)=> count($g) > 1 ? wpjam_tag('div', ['field-group'], implode("\n", $g)) : $g[0], $data));
793 $wrap = wpjam_wrap(implode($args['sep']."\n", $data));
794
795 return $data && $type == 'table' ? $wrap->wrap('tbody')->wrap('table', ['cellspacing'=>0, 'class'=>'form-table']) : ($data && $type == 'list' ? $wrap->wrap('ul') : $wrap);
796 }
797
798 public function wrap($tag='', $args=[]){
799 return $this->render(wpjam_pull($args, 'args', []))->after(wpjam_pull($args, 'button'))->wrap($tag, $args+($tag == 'form' ? ['novalidate', 'method'=>'post', 'action'=>'#'] : []));
800 }
801
802 public function from(...$args){
803 return $args && is_array($args[0])
804 ? $this->wrap('form', ...$args)
805 : $this->render()->after([
806 wp_nonce_field($args[1] ?? '', '_wpnonce', true, false),
807 wp_original_referer_field(false, 'previous'),
808 ($args[2] ??= '') !== false ? get_submit_button($args[2]) : ''
809 ])->wrap('form', ['enctype'=>'multipart/form-data', 'method'=>'post', 'action'=>$args[0]]);
810 }
811
812 public function register_meta($type, $args=[]){
813 $nested = [];
814 $args = (is_array($args) ? $args : ['object_subtype'=>$args])+['single'=>true];
815
816 foreach($this->fields as $field){
817 if($field->fieldset == 'flat'){
818 $field->register_meta_by_fields($type, $args);
819 }elseif($schema = $field->schema()){
820 $names = $field->_names;
821
822 if(count($names) > 1){
823 $temp = &$nested;
824
825 while($n = array_shift($names)){
826 if(empty($names)){
827 $temp[$n] = $schema;
828 }else{
829 $temp[$n] ??= ['type'=>'object', 'properties'=>[]];
830 $temp = &$temp[$n]['properties'];
831 }
832 }
833 }else{
834 $default = $field->get_arg('show_in_rest.default') ?? $field->default;
835 $args += wpjam_then(rest_validate_value_from_schema($default, $schema), ['default'=>$default], []);
836
837 register_meta($type, $names[0], $args+[
838 'type' => $schema['type'],
839 'show_in_rest' => ['schema'=>$schema],
840 'sanitize_callback' => fn($value)=> $field->validate($value)
841 ]);
842 }
843 }
844 }
845
846 wpjam_map($nested, fn($v, $k)=> register_meta($type, $k, $args+[
847 'type' => 'object',
848 'show_in_rest' => ['schema'=>$v],
849 'sanitize_callback' => fn($value)=> wpjam_filter($this->validate_parts(fn($v)=> $v->_names[0] === $k, [$k => $value])[$k], fn($v)=> !is_null($v), true)
850 ]));
851 }
852
853 public function to_block(){
854 $data = [];
855
856 foreach($this->fields as $field){
857 $schema = $field->schema();
858 $set = $field->is('fieldset');
859
860 if(!$schema || ($set && !$schema['properties'])){
861 continue;
862 }
863
864 $block = $field->pick(['name', 'min', 'max', 'step', 'rows', 'placeholder'])+array_filter([
865 'options' => $field->options ? $field->options(false) : [],
866 'multiple' => $field->is('mu') || $field->multiple,
867 'label' => $field->title ?? $field->label ?? null,
868 'help' => $field->description ?? null
869 ]+($set ? [] : ['schema'=>$schema])+wpjam_fill(['show_if', 'item_type', 'button_text'], fn($k)=> [$field, $k]()));
870
871 $field = $field->is('mu') ? $field->_item : $field;
872 $comp = $field->type;
873
874 if($field->is('fieldset')){
875 $block += $field->pick(['fieldset', 'direction'])+['fields'=>$field->to_block_by_fields()];
876 }elseif($field->is('image, img')){
877 $comp = 'Media';
878 }elseif($field->is('uploader')){
879 $block += ['accept'=>$field->accept ?: 'image/*', 'nonce'=>wp_create_nonce('upload-'.$field->name.'-'.$accept)];
880 }elseif(!$field->is('file, toggle, checkbox, select, radio, range, color, search, textarea, timestamp')){
881 $comp = $field->data_type ? 'Combobox' : 'Text';
882 $block += $comp === 'Text' ? $field->pick(['data_type', 'query_args', 'query_nonce']) : ['type'=>$field->type];
883 }
884
885 $data[] = $block+['component'=>ucfirst($comp)];
886 }
887
888 return $data;
889 }
890
891 public function process($items, $args=[]){
892 if(!($sumable = $this->sumable)){
893 $sumable = [1=>[], 2=>[]];
894
895 foreach($this->fields as $k => $v){
896 if($s = $v['sumable'] ?? ''){
897 $sumable[$s][$k] = 0;
898 }
899
900 if(array_filter($f = [$v['format'] ?? '', $v['precision'] ?? null])){
901 $formats[$k] = $f;
902 }
903
904 if(($e = $v['if_error'] ?? '') || is_numeric($e)){
905 $if_errors[$k] = $e;
906 }
907 }
908
909 $formulas = wpjam_formula($this->fields);
910 $sumable[2] = array_intersect_key($formulas, $sumable[2]);
911
912 $this->update_args([
913 'formulas' => $formulas,
914 'sumable' => $sumable,
915 'formats' => $formats ?? [],
916 'if_errors' => $if_errors ?? [],
917 ]);
918 }
919
920 $sum = $args['sum'] ?? true;
921 $acc = $args['to'] ?? [];
922 $field = $sum === 'accumulate' ? ($args['field'] ?? '') : '';
923 $fields = $field ? (!empty($args['fields']) ? array_fill_keys($args['fields'], true) : [$field=>true]) : [];
924 $calc = $args['calc'] ?? $sum !== 'accumulate';
925 $filter = $args['filter'] ?? [];
926 $term = $args['search_term'] ?? '';
927
928 foreach($items as $i => &$item){
929 if($calc && $item && is_array($item)){
930 $item = wpjam_calc($item, ($calc === 'sum' ? $sumable[2] : $this->formulas), $this->if_errors);
931 }
932
933 if($filter && !wpjam_matches($item, $filter)){
934 unset($items[$i]); continue;
935 }
936
937 if($term && array_all($args['search_columns'] ?? [], fn($k)=> empty($item[$k]) || stripos($item[$k], $term) === false)){
938 unset($items[$i]); continue;
939 }
940
941 if($sum){
942 if($_item = array_intersect_key($item, $sumable[1])){
943 if($field){
944 $g = $item[$field] ?? '';
945 $acc[$g] ??= array_intersect_key($item, $fields);
946 }
947
948 foreach($_item as $k => $v){
949 if($v = is_numeric($v) ? $v : wpjam_format($v, '-,', 0)){
950 if($field){
951 $acc[$g][$k] ??= 0;
952 $acc[$g][$k] += $v;
953 }else{
954 $acc[$k] ??= 0;
955 $acc[$k] += $v;
956 }
957 }
958 }
959 }
960 }elseif(!empty($args['format'])){
961 $item = wpjam_format($item, $this->formats);
962 }
963 }
964
965 if($sum === 'accumulate'){
966 unset($items);
967 return $acc;
968 }
969
970 if(!empty($args['orderby'])){
971 $items = wpjam_sort($items, $args['orderby'], $args['order']);
972 }
973
974 if($acc){
975 $acc = wpjam_calc($acc, $sumable[2], $this->if_errors)+(is_array($sum) ? $sum : []);
976 $items = wpjam_add_at($items, 0, '__sum__', (!empty($args['format']) ? wpjam_format($acc, $this->formats) : $acc));
977 }
978
979 return $items;
980 }
981
982 public static function create($fields, $args=[], $parent=null){
983 if($args === 'processor'){
984 return new self(['fields'=>$fields]);
985 }
986
987 $prop = $parent && $parent->fieldset == 'object';
988 $attr = ['_fields_args'=>$args, '_prop'=>$prop]+wpjam_pick($parent ?: [], ['readonly', 'disabled']);
989
990 foreach(self::parse($fields) as $key => $field){
991 if(($field['show_admin_column'] ?? ($field['column']['show'] ?? '')) !== 'only' && ($field = WPJAM_Field::create($attr+$field, $key))){
992 if($prop && (count($field->_names) > 1 || $field->is('fieldset'))){
993 trigger_error($parent->_title.'子字段不�
994 �许'.($field->is('fieldset') ? $field->type : '[]模式').':'.$field->name); continue;
995 }
996
997 $objects[$key] = $field;
998 }
999 }
1000
1001 $objects ??= $prop ? wp_die($parent->_title.'fields不能为空') : [];
1002
1003 return new self(['_args'=>$args, 'parent'=>$parent, 'prop'=>$prop, 'fields'=>$objects]);
1004 }
1005
1006 public static function parse($fields, ...$args){
1007 [$flat, $prefix] = $args+[false, ''];
1008
1009 foreach($fields as $key => $field){
1010 $field = WPJAM_Field::parse($field);
1011 $nkey = ($prefix ? $prefix.'_' : '').$key;
1012 $subs = [];
1013 $prop = false;
1014
1015 if(in_array($field['type'], ['fieldset', 'fields']) && $field['fieldset'] == 'flat'){
1016 $prop = !$flat;
1017 $subs = static::parse($field['fields'], $flat, ($p = wpjam_pull($field, 'prefix')) === true ? $key : (string)$p);
1018 }elseif($field['type'] == 'toggle'){
1019 $subs = wpjam_map((wpjam_pull($field, 'fields') ?: []), fn($v)=> $v+['show_if'=>[$nkey, '=', 1]]);
1020 }elseif(is_array($field['options'])){
1021 $subs = wpjam_reduce($field['options'], fn($carry, $item, $opt)=> array_merge($carry, wpjam_map(
1022 is_array($item) ? ($item['fields'] ?? []) : [],
1023 fn($v, $k)=> $v+['show_if'=>[$nkey, 'IN', [...($carry[$k]['show_if'][2] ?? []), $opt]]]
1024 )), [], 'options');
1025 }
1026
1027 $subs = static::parse($subs, ...$args);
1028 $parsed = array_merge($parsed ?? [], [$nkey=>($prop ? ['fields'=>$subs] : [])+$field], $prop ? [] : $subs);
1029 }
1030
1031 return $parsed ?? [];
1032 }
1033 }
1034
1035 /**
1036 * @config orderby=order order=ASC
1037 * @items_field paths
1038 **/
1039 #[config(orderby:'order', order:'ASC')]
1040 #[items_field('paths')]
1041 class WPJAM_Platform extends WPJAM_Register{
1042 public function __get($key){
1043 return $key == 'path' ? (bool)$this->get_paths() : parent::__get($key);
1044 }
1045
1046 public function __call($method, $args){
1047 if($this->mod($method, '_path')){
1048 $method = ($method == 'add' ? 'update' : $method).'_arg';
1049
1050 return $this->$method('paths['.array_shift($args).']', ...$args);
1051 }elseif($this->mod($method, '_item')){
1052 $item = $args[0];
1053 $suffix = $args[1] ?? '';
1054 $multi = $args[2] ?? false;
1055
1056 $page_key = wpjam_pull($item, 'page_key'.$suffix);
1057
1058 if($page_key == 'none'){
1059 return ($video = $item['video'] ?? '') ? ['type'=>'video', 'video'=>wpjam_get_qqv_id($video) ?: $video] : ['type'=>'none'];
1060 }elseif(!$this->get_path($page_key.'[]')){
1061 return [];
1062 }
1063
1064 $item = $suffix ? wpjam_map($this->get_fields($page_key), fn($v, $k)=> $item[$k.$suffix] ?? null) : $item;
1065 $path = $this->get_path($page_key, $item);
1066 $path = wpjam_if_error($path, $method == 'validate' ? 'throw' : null);
1067
1068 if(is_null($path)){
1069 $backup = str_ends_with($suffix, '_backup');
1070
1071 if($multi && !$backup){
1072 return [$this, $method.'_item']($item, $suffix.'_backup');
1073 }
1074
1075 return $method == 'validate' ? wpjam_throw('invalid_page_key', '无效的'.($backup ? '备用' : '').'页面。') : ['type'=>'none'];
1076 }
1077
1078 return is_array($path) ? $path : ['type'=>'', 'page_key'=>$page_key, 'path'=>$path];
1079 }elseif($this->mod($method, '_by_page_type')){
1080 $item = array_last($args);
1081 $object = wpjam_get_data_type(wpjam_pull($item, 'page_type'), $item);
1082
1083 return $object ? [$object, $method](...$args) : null;
1084 }
1085 }
1086
1087 public function verify(){
1088 return wpjam_call($this->verify);
1089 }
1090
1091 public function get_tabbar($page_key=''){
1092 if(!$page_key){
1093 return wpjam_array($this->get_paths(), fn($k)=> [$k, $this->get_tabbar($k)], true);
1094 }
1095
1096 if($tabbar = $this->get_path($page_key.'[tabbar]')){
1097 return ($tabbar === true ? [] : $tabbar)+['text'=>(string)$this->get_path($page_key.'[title]')];
1098 }
1099 }
1100
1101 public function get_page($page_key=''){
1102 return $page_key ? wpjam_at($this->get_path($page_key.'[path]'), '?', 0) : wpjam_array($this->get_paths(), fn($k)=> [$k, $this->get_page($k)], true);
1103 }
1104
1105 public function get_fields($page_key){
1106 $item = $this->get_path($page_key.'[]');
1107 $fields = $item ? (!empty($item['fields']) ? maybe_callback($item['fields'], $item, $page_key) : $this->get_path_by_page_type('fields', $item)) : [];
1108
1109 return $fields ?: [];
1110 }
1111
1112 public function has_path($page_key, $strict=false){
1113 $item = $this->get_path($page_key.'[]');
1114
1115 return (!$item || ($strict && ($item['path'] ?? '') === false)) ? false : (isset($item['path']) || isset($item['callback']));
1116 }
1117
1118 public function get_path($page_key, $args=[]){
1119 if(is_array($page_key)){
1120 [$page_key, $args] = [wpjam_pull($page_key, 'page_key'), $page_key];
1121 }
1122
1123 if(str_contains($page_key, '[')){
1124 return wpjam_get($this->get_paths(), wpjam_suffix($page_key, '-', '[]'));
1125 }
1126
1127 if($item = $this->get_path($page_key.'[]')){
1128 $cb = wpjam_pull($item, 'callback');
1129 $args = is_array($args) ? array_filter($args, fn($v)=> !is_null($v))+$item : $args;
1130 $path = $cb ? (is_callable($cb) ? ($cb($args, $item) ?: '') : null) : $this->get_path_by_page_type($args, $item);
1131
1132 return isset($path) ? $path : (isset($item['path']) ? (string)$item['path'] : null);
1133 }
1134 }
1135
1136 public function get_paths($page_key=null, $args=[]){
1137 if($page_key){
1138 $item = $this->get_path($page_key.'[]');
1139 $type = $item ? ($item['page_type'] ?? '') : '';
1140 $items = $type ? $this->query_items_by_page_type(array_merge($args, wpjam_pick($item, [$type])), $item) : [];
1141
1142 return $items ? wpjam_array($items, fn($k, $v)=> [$k, wpjam_trap([$this, 'get_path'], $page_key, $v['value'], null)], true) : [];
1143 }
1144
1145 return $this->get_arg('paths[]');
1146 }
1147
1148 public function registered(){
1149 if($this->name == 'template'){
1150 wpjam_register_path('home', 'template', ['title'=>'首页', 'path'=>home_url(), 'group'=>'tabbar']);
1151 wpjam_register_path('category', 'template', ['title'=>'分类页', 'path'=>'', 'page_type'=>'taxonomy']);
1152 wpjam_register_path('post_tag', 'template', ['title'=>'标签页', 'path'=>'', 'page_type'=>'taxonomy']);
1153 wpjam_register_path('author', 'template', ['title'=>'作�
1154 ', 'path'=>'', 'page_type'=>'author']);
1155 wpjam_register_path('post', 'template', ['title'=>'文章详�
1156 ', 'path'=>'', 'page_type'=>'post_type']);
1157 wpjam_register_path('external', 'template', ['title'=>'外部链接', 'path'=>'', 'fields'=>['url'=>['type'=>'url', 'required'=>true, 'placeholder'=>'请输�
1158 �链接地址。']], 'callback'=>fn($args)=> ['type'=>'external', 'url'=>$args['url']]]);
1159 }
1160 }
1161
1162 public static function get_options($output=''){
1163 return wp_list_pluck(self::get_registereds(), 'title', $output);
1164 }
1165
1166 protected static function get_defaults(){
1167 return [
1168 'weapp' => ['bit'=>1, 'order'=>4, 'title'=>'小程序', 'verify'=>'is_weapp'],
1169 'weixin' => ['bit'=>2, 'order'=>4, 'title'=>'微信网页', 'verify'=>'is_weixin'],
1170 'mobile' => ['bit'=>4, 'order'=>8, 'title'=>'移动网页', 'verify'=>'wp_is_mobile'],
1171 'template' => ['bit'=>8, 'order'=>10, 'title'=>'网页', 'verify'=>'__return_true']
1172 ];
1173 }
1174 }
1175
1176 class WPJAM_Path extends WPJAM_Args{
1177 public static function create($name, ...$args){
1178 $object = static::get_instance($name) ?: new static(['name'=>$name]);
1179
1180 [$pf, $args] = count($args) > 1 ? $args : [array_find(wpjam_pull($args[0], ['platform', 'path_type']), fn($v)=> $v), $args[0]];
1181
1182 $args += in_array(($args['page_type'] ?? ''), ['post_type', 'taxonomy']) ? [$args['page_type']=>$name] : [];
1183
1184 foreach((array)$pf as $pf){
1185 if($platform = WPJAM_Platform::get($pf)){
1186 $platform->add_path($name, array_merge($args, ['platform'=>$pf, 'path_type'=>$pf, 'object'=>$object]));
1187 }
1188
1189 $object->update_arg('platform[]', $pf)->update_args($args, false);
1190 }
1191
1192 return $object;
1193 }
1194
1195 public static function remove($name, $pf=''){
1196 if($object = self::get_instance($name)){
1197 foreach($pf ? (array)$pf : $object->get_arg('platform[]') as $pf){
1198 if($platform = WPJAM_Platform::get($pf)){
1199 $platform->delete_path($name);
1200 }
1201
1202 $object->delete_arg('platform[]', $pf);
1203 }
1204
1205 return $pf ? $object : null;
1206 }
1207 }
1208
1209 public static function get_by($args=[]){
1210 $type = wpjam_pull($args, 'path_type');
1211 $args += $type ? ['platform'=>$type] : [];
1212
1213 return wpjam_filter(array_reduce(WPJAM_Platform::get_by(), fn($c, $v)=> array_merge($c, array_column($v->get_paths(), 'object')), []), $args, 'AND');
1214 }
1215
1216 public static function get_instance($name){
1217 foreach(WPJAM_Platform::get_by() as $v){
1218 if($object = $v->get_path($name.'[object]')){
1219 return $object;
1220 }
1221 }
1222 }
1223
1224 public static function __callStatic($method, $args){
1225 $platforms = array_filter(WPJAM_Platform::get_by((array)(array_shift($args) ?? ['path'=>true])));
1226 $multi = count($platforms) > 1;
1227
1228 if(str_ends_with($method, '_item')){
1229 $args[] = $multi;
1230
1231 if($method == 'parse_item'){
1232 return $platforms ? [$multi ? array_find($platforms, fn($v)=> $v->verify()) : array_first($platforms), $method](...$args) : ['type'=>'none'];
1233 }
1234
1235 return array_reduce($platforms, fn($c, $v)=> $c && $v->$method(...$args), true);
1236 }
1237
1238 if(!$platforms){
1239 return [];
1240 }
1241
1242 $args = $args ? (is_array($args[0]) ? $args[0] : ['strict'=>$args[0]]) : [];
1243 $strict = (bool)wpjam_pull($args, 'strict');
1244 $prepend = array_filter(wpjam_pull($args, ['prepend_name']));
1245 $suffix = wpjam_pull($args, 'suffix');
1246 $title = wpjam_pull($args, 'title') ?: '页面';
1247 $key = 'page_key'.$suffix;
1248 $paths = static::get_by($args);
1249 $fields_key = 'fields['.md5(serialize($prepend+['suffix'=>$suffix, 'strict'=>$strict, 'platforms'=>array_keys($platforms), 'page_keys'=>array_keys($paths)])).']';
1250
1251 static $caches = [];
1252
1253 if(!empty($caches[$fields_key])){
1254 [$fields, $show_if] = $caches[$fields_key];
1255 }else{
1256 $pks = [$key=>['OR', $suffix]]+($multi && !$strict ? [$key.'_backup'=>['AND', $suffix.'_backup']] : []);
1257 $fields = wpjam_map($pks, fn()=> ['tabbar'=>['title'=>'菜单栏/常用', 'options'=>($strict ? [] : ['none'=>'只展示不跳转'])]]);
1258
1259 foreach($paths as $path){
1260 $i = 0;
1261 $name = $path->name;
1262 $group = $path->group;
1263
1264 [$group, $label] = is_array($group) ? [$group['key'] ?? '', $group['title'] ?? ''] : [$group, ''];
1265
1266 if(!$group){
1267 $group = $path->tabbar ? 'tabbar' : 'others';
1268 }
1269
1270 foreach($pks as $pk => [$op, $fix]){
1271 if(wpjam_matches($platforms, fn($pf)=> $pf->has_path($name, $strict && $op == 'OR'), $op)){
1272 $i++;
1273
1274 if($label){
1275 $fields = wpjam_set($fields, $pk.'['.$group.'][label]', $label);
1276 }
1277
1278 $fields = wpjam_set($fields, $pk.'['.$group.'][options]['.$name.']', [
1279 'label' => $path->title,
1280 'fields' => wpjam_array(array_reduce($platforms, fn($c, $pf)=> array_merge($c, $pf->get_fields($name)), []), fn($k, $v)=> [$k.$fix, wpjam_except($v, 'title')+$prepend])
1281 ]);
1282 }
1283 }
1284
1285 if($multi && !$strict && $i == 1){
1286 $show_if[] = $name;
1287 }
1288 }
1289
1290 $caches[$fields_key] = [
1291 wpjam_map($fields, fn($v)=> ($o = wpjam_pull($v, 'others')) ? $v+['others'=>$o+['label'=>'�
1292 �他页面']] : $v),
1293 $show_if ?? []
1294 ];
1295 }
1296
1297 return wpjam_array($fields, fn($k, $v)=> [$k.'_set', ['type'=>'fieldset', 'fields'=>[$k=>['options'=>$v]+$prepend]]+($k != $key ? ['title'=>'备用'.$title, 'show_if'=>[$key, 'IN', $show_if]] : ['title'=>$title])]);
1298 }
1299 }