PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.1.1
Kirki – Freeform Page Builder, Website Builder & Customizer v6.1.1
6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / customizer / packages / controls / repeater / src / Control / Repeater.php
kirki / customizer / packages / controls / repeater / src / Control Last commit date
Repeater.php 3 months ago
Repeater.php
541 lines
1 <?php
2 /**
3 * Customizer Control: repeater.
4 *
5 * @package kirki-framework/control-repeater
6 * @copyright Copyright (c) 2023, Themeum
7 * @license https://opensource.org/licenses/MIT
8 * @since 1.0
9 */
10
11 namespace Kirki\Control;
12
13 use Kirki\Control\Base;
14 use Kirki\URL;
15 /**
16 * Repeater control
17 *
18 * @since 1.0
19 */
20 class Repeater extends Base {
21
22 /**
23 * The control type.
24 *
25 * @access public
26 * @since 1.0
27 * @var string
28 */
29 public $type = 'repeater';
30
31 /**
32 * The fields that each container row will contain.
33 *
34 * @access public
35 * @since 1.0
36 * @var array
37 */
38 public $fields = [];
39
40 /**
41 * Will store a filtered version of value for advenced fields (like images).
42 *
43 * @access protected
44 * @since 1.0
45 * @var array
46 */
47 protected $filtered_value = [];
48
49 /**
50 * The row label
51 *
52 * @access public
53 * @since 1.0
54 * @var array
55 */
56 public $row_label = [];
57
58 /**
59 * The button label
60 *
61 * @access public
62 * @since 1.0
63 * @var string
64 */
65 public $button_label = '';
66
67 /**
68 * The version. Used in scripts & styles for cache-busting.
69 *
70 * @static
71 * @access public
72 * @since 1.0
73 * @var string
74 */
75 public static $control_ver = '1.0.5';
76
77 /**
78 * Constructor.
79 * Supplied `$args` override class property defaults.
80 * If `$args['settings']` is not defined, use the $id as the setting ID.
81 *
82 * @access public
83 * @since 1.0
84 * @param WP_Customize_Manager $manager Customizer bootstrap instance.
85 * @param string $id Control ID.
86 * @param array $args {@see WP_Customize_Control::__construct}.
87 */
88 public function __construct( $manager, $id, $args = [] ) {
89
90 parent::__construct( $manager, $id, $args );
91
92 // Set up defaults for row labels.
93 $this->row_label = [
94 'type' => 'text',
95 'value' => esc_attr__( 'row', 'kirki' ),
96 'field' => false,
97 ];
98
99 // Validate row-labels.
100 $this->row_label( $args );
101
102 if ( empty( $this->button_label ) ) {
103 /* translators: %s represents the label of the row. */
104 $this->button_label = sprintf( esc_html__( 'Add new %s', 'kirki' ), $this->row_label['value'] );
105 }
106
107 if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
108 $args['fields'] = [];
109 }
110
111 // An array to store keys of fields that need to be filtered.
112 $media_fields_to_filter = [];
113
114 foreach ( $args['fields'] as $key => $value ) {
115 if ( ! isset( $value['default'] ) ) {
116 $args['fields'][ $key ]['default'] = '';
117 }
118 if ( ! isset( $value['label'] ) ) {
119 $args['fields'][ $key ]['label'] = '';
120 }
121 $args['fields'][ $key ]['id'] = $key;
122
123 // We check if the filed is an uploaded media ( image , file, video, etc.. ).
124 if ( isset( $value['type'] ) ) {
125 switch ( $value['type'] ) {
126 case 'image':
127 case 'cropped_image':
128 case 'upload':
129 // We add it to the list of fields that need some extra filtering/processing.
130 $media_fields_to_filter[ $key ] = true;
131 break;
132
133 case 'dropdown-pages':
134 // If the field is a dropdown-pages field then add it to args.
135 $dropdown = wp_dropdown_pages(
136 [
137 'name' => '',
138 'echo' => 0,
139 'show_option_none' => esc_html__( 'Select a Page', 'kirki' ),
140 'option_none_value' => '0',
141 'selected' => '',
142 ]
143 );
144
145 // Hackily add in the data link parameter.
146 $dropdown = str_replace( '<select', '<select data-field="' . esc_attr( $args['fields'][ $key ]['id'] ) . '"' . $this->get_link(), $dropdown ); // phpcs:ignore Generic.Formatting.MultipleStatementAlignment
147 $args['fields'][ $key ]['dropdown'] = $dropdown;
148 break;
149 }
150 }
151 }
152
153 $this->fields = $args['fields'];
154
155 // Now we are going to filter the fields.
156 // First we create a copy of the value that would be used otherwise.
157 $this->filtered_value = $this->value();
158
159 if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
160
161 // We iterate over the list of fields.
162 foreach ( $this->filtered_value as &$filtered_value_field ) {
163
164 if ( is_array( $filtered_value_field ) && ! empty( $filtered_value_field ) ) {
165
166 // We iterate over the list of properties for this field.
167 foreach ( $filtered_value_field as $key => &$value ) {
168
169 // We check if this field was marked as requiring extra filtering (in this case image, cropped_images, upload).
170 if ( array_key_exists( $key, $media_fields_to_filter ) ) {
171
172 // What follows was made this way to preserve backward compatibility.
173 // The repeater control use to store the URL for images instead of the attachment ID.
174 // We check if the value look like an ID (otherwise it's probably a URL so don't filter it).
175 if ( is_numeric( $value ) ) {
176
177 // "sanitize" the value.
178 $attachment_id = (int) $value;
179
180 // Try to get the attachment_url.
181 $url = wp_get_attachment_url( $attachment_id );
182
183 $filename = basename( get_attached_file( $attachment_id ) );
184
185 // If we got a URL.
186 if ( $url ) {
187
188 // 'id' is needed for form hidden value, URL is needed to display the image.
189 $value = [
190 'id' => $attachment_id,
191 'url' => $url,
192 'filename' => $filename,
193 ];
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201
202 }
203
204 /**
205 * Enqueue control related scripts/styles.
206 *
207 * @access public
208 * @since 1.0
209 * @return void
210 */
211
212
213 /**
214 * Refresh the parameters passed to the JavaScript via JSON.
215 *
216 * @access public
217 * @since 1.0
218 * @return void
219 */
220 public function to_json() {
221
222 parent::to_json();
223
224 $fields = $this->fields;
225
226 $this->json['fields'] = $fields;
227 $this->json['row_label'] = $this->row_label;
228
229 // If filtered_value has been set and is not empty we use it instead of the actual value.
230 if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
231 $this->json['value'] = $this->filtered_value;
232 }
233
234 $this->json['value'] = apply_filters( "kirki_controls_repeater_value_{$this->id}", $this->json['value'] );
235
236 }
237
238 /**
239 * Render the control's content.
240 * Allows the content to be overridden without having to rewrite the wrapper in $this->render().
241 *
242 * @access protected
243 * @since 1.0
244 * @return void
245 */
246 protected function render_content() {
247
248 ?>
249 <label>
250 <?php if ( ! empty( $this->label ) ) : ?>
251 <span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
252 <?php endif; ?>
253 <?php if ( ! empty( $this->description ) ) : ?>
254 <span class="description customize-control-description"><?php echo wp_kses_post( $this->description ); ?></span>
255 <?php endif; ?>
256 <input type="hidden" {{{ data.inputAttrs }}} value="" <?php echo wp_kses_post( $this->get_link() ); ?> />
257 </label>
258
259 <ul class="repeater-fields"></ul>
260
261 <?php if ( isset( $this->choices['limit'] ) ) : ?>
262 <?php /* translators: %s represents the number of rows we're limiting the repeater to allow. */ ?>
263 <p class="limit"><?php printf( esc_html__( 'Limit: %s rows', 'kirki' ), esc_html( $this->choices['limit'] ) ); ?></p>
264 <?php endif; ?>
265 <button class="button-secondary repeater-add"><?php echo esc_html( $this->button_label ); ?></button>
266
267 <?php
268 $this->repeater_js_template();
269
270 }
271
272 /**
273 * An Underscore (JS) template for this control's content (but not its container).
274 * Class variables for this control class are available in the `data` JS object.
275 *
276 * @access public
277 * @since 1.0
278 * @return void
279 */
280 public function repeater_js_template() {
281 ?>
282
283 <script type="text/html" class="customize-control-repeater-content">
284 <# var field; var index = data.index; #>
285
286 <li class="repeater-row minimized" data-row="{{{ index }}}">
287
288 <div class="repeater-row-header">
289 <span class="repeater-row-label"></span>
290 <i class="dashicons dashicons-arrow-down repeater-minimize"></i>
291 </div>
292 <div class="repeater-row-content">
293 <# _.each( data, function( field, i ) { #>
294
295 <div class="repeater-field repeater-field-{{{ field.type }}} repeater-field-{{ field.id }}">
296
297 <# if ( 'text' === field.type || 'url' === field.type || 'link' === field.type || 'email' === field.type || 'tel' === field.type || 'date' === field.type || 'number' === field.type ) { #>
298 <# var fieldExtras = ''; #>
299 <# if ( 'link' === field.type ) { #>
300 <# field.type = 'url' #>
301 <# } #>
302
303 <# if ( 'number' === field.type ) { #>
304 <# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.min ) ) { #>
305 <# fieldExtras += ' min="' + field.choices.min + '"'; #>
306 <# } #>
307 <# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.max ) ) { #>
308 <# fieldExtras += ' max="' + field.choices.max + '"'; #>
309 <# } #>
310 <# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.step ) ) { #>
311 <# fieldExtras += ' step="' + field.choices.step + '"'; #>
312 <# } #>
313 <# } #>
314
315 <label>
316 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
317 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
318 <input type="{{field.type}}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ fieldExtras }}>
319 </label>
320
321 <# } else if ( 'number' === field.type ) { #>
322
323 <label>
324 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
325 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
326 <input type="{{ field.type }}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ numberFieldExtras }}>
327 </label>
328
329 <# } else if ( 'hidden' === field.type ) { #>
330
331 <input type="hidden" data-field="{{{ field.id }}}" <# if ( field.default ) { #> value="{{{ field.default }}}" <# } #> />
332
333 <# } else if ( 'checkbox' === field.type ) { #>
334
335 <label>
336 <input type="checkbox" value="{{{ field.default }}}" data-field="{{{ field.id }}}" <# if ( field.default ) { #> checked="checked" <# } #> /> {{{ field.label }}}
337 <# if ( field.description ) { #>{{{ field.description }}}<# } #>
338 </label>
339
340 <# } else if ( 'select' === field.type ) { #>
341
342 <label>
343 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
344 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
345 <select data-field="{{{ field.id }}}"<# if ( ! _.isUndefined( field.multiple ) && false !== field.multiple ) { #> multiple="multiple" data-multiple="{{ field.multiple }}"<# } #>>
346 <# _.each( field.choices, function( choice, i ) { #>
347 <option value="{{{ i }}}" <# if ( -1 !== jQuery.inArray( i, field.default ) || field.default == i ) { #> selected="selected" <# } #>>{{ choice }}</option>
348 <# }); #>
349 </select>
350 </label>
351
352 <# } else if ( 'dropdown-pages' === field.type ) { #>
353
354 <label>
355 <# if ( field.label ) { #><span class="customize-control-title">{{{ data.label }}}</span><# } #>
356 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
357 <div class="customize-control-content repeater-dropdown-pages">{{{ field.dropdown }}}</div>
358 </label>
359
360 <# } else if ( 'radio' === field.type ) { #>
361
362 <label>
363 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
364 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
365
366 <# _.each( field.choices, function( choice, i ) { #>
367 <label><input type="radio" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>> {{ choice }} <br/></label>
368 <# }); #>
369 </label>
370
371 <# } else if ( 'radio-image' === field.type ) { #>
372
373 <label>
374 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
375 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
376
377 <# _.each( field.choices, function( choice, i ) { #>
378 <input type="radio" id="{{{ field.id }}}_{{ index }}_{{{ i }}}" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>>
379 <label for="{{{ field.id }}}_{{ index }}_{{{ i }}}"><img src="{{ choice }}"></label>
380 </input>
381 <# }); #>
382 </label>
383
384 <# } else if ( 'color' === field.type ) { #>
385
386 <label>
387 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
388 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
389 </label>
390
391 <#
392 var defaultValue = '';
393 if ( field.default ) {
394 if ( -1 !== field.default.indexOf( 'rgb' ) || -1 !== field.default.indexOf( '#' ) ) {
395 defaultValue = field.default;
396
397 if (-1 !== field.default.indexOf('rgba')) {
398 if (!field.choices) field.choices = {};
399 field.choices.alpha = true;
400 }
401 } else {
402 if (field.default.length >= 3) {
403 defaultValue = '#' + field.default;
404 }
405 }
406 }
407 #>
408
409 <#
410 var alphaEnabledAttr = '';
411 if ( field.choices && field.choices.alpha ) {
412 alphaEnabledAttr = ' data-alpha-enabled=true';
413 }
414 #>
415
416 <input class="kirki-classic-color-picker" type="text" maxlength="7" value="{{{ field.default }}}" data-field="{{{ field.id }}}" data-default-color="{{{ defaultValue }}}" {{alphaEnabledAttr}} />
417
418 <# } else if ( 'textarea' === field.type ) { #>
419
420 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
421 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
422 <textarea rows="5" data-field="{{{ field.id }}}">{{ field.default }}</textarea>
423
424 <# } else if ( field.type === 'image' || field.type === 'cropped_image' ) { #>
425
426 <label>
427 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
428 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
429 </label>
430
431 <figure class="kirki-image-attachment" data-placeholder="<?php esc_attr_e( 'No Image Selected', 'kirki' ); ?>" >
432 <# if ( field.default ) { #>
433 <# var defaultImageURL = ( field.default.url ) ? field.default.url : field.default; #>
434 <img src="{{{ defaultImageURL }}}">
435 <# } else { #>
436 <?php esc_html_e( 'No Image Selected', 'kirki' ); ?>
437 <# } #>
438 </figure>
439
440 <div class="actions">
441 <button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
442 <button type="button" class="button upload-button" data-label=" <?php esc_attr_e( 'Add Image', 'kirki' ); ?>" data-alt-label="<?php echo esc_attr_e( 'Change Image', 'kirki' ); ?>" >
443 <# if ( field.default ) { #>
444 <?php esc_html_e( 'Change Image', 'kirki' ); ?>
445 <# } else { #>
446 <?php esc_html_e( 'Add Image', 'kirki' ); ?>
447 <# } #>
448 </button>
449 <# if ( field.default.id ) { #>
450 <input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
451 <# } else { #>
452 <input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
453 <# } #>
454 </div>
455
456 <# } else if ( field.type === 'upload' ) { #>
457
458 <label>
459 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
460 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
461 </label>
462
463 <figure class="kirki-file-attachment" data-placeholder="<?php esc_attr_e( 'No File Selected', 'kirki' ); ?>" >
464 <# if ( field.default ) { #>
465 <# var defaultFilename = ( field.default.filename ) ? field.default.filename : field.default; #>
466 <span class="file"><span class="dashicons dashicons-media-default"></span> {{ defaultFilename }}</span>
467 <# } else { #>
468 <?php esc_html_e( 'No File Selected', 'kirki' ); ?>
469 <# } #>
470 </figure>
471
472 <div class="actions">
473 <button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
474 <button type="button" class="button upload-button" data-label="<?php esc_attr_e( 'Add File', 'kirki' ); ?>" data-alt-label="<?php esc_attr_e( 'Change File', 'kirki' ); ?>">
475 <# if ( field.default ) { #>
476 <?php esc_html_e( 'Change File', 'kirki' ); ?>
477 <# } else { #>
478 <?php esc_html_e( 'Add File', 'kirki' ); ?>
479 <# } #>
480 </button>
481 <# if ( field.default.id ) { #>
482 <input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
483 <# } else { #>
484 <input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
485 <# } #>
486 </div>
487
488 <# } else if ( 'custom' === field.type ) { #>
489
490 <# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
491 <# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
492 <div data-field="{{{ field.id }}}">{{{ field.default }}}</div>
493
494 <# } #>
495
496 </div>
497 <# }); #>
498 <button type="button" class="button-link repeater-row-remove"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
499 </div>
500 </li>
501 </script>
502
503 <?php
504 }
505
506 /**
507 * Validate row-labels.
508 *
509 * @access protected
510 * @since 1.0
511 * @param array $args {@see WP_Customize_Control::__construct}.
512 * @return void
513 */
514 protected function row_label( $args ) {
515
516 // Validating args for row labels.
517 if ( isset( $args['row_label'] ) && is_array( $args['row_label'] ) && ! empty( $args['row_label'] ) ) {
518
519 // Validating row label type.
520 if ( isset( $args['row_label']['type'] ) && ( 'text' === $args['row_label']['type'] || 'field' === $args['row_label']['type'] ) ) {
521 $this->row_label['type'] = $args['row_label']['type'];
522 }
523
524 // Validating row label type.
525 if ( isset( $args['row_label']['value'] ) && ! empty( $args['row_label']['value'] ) ) {
526 $this->row_label['value'] = esc_html( $args['row_label']['value'] );
527 }
528
529 // Validating row label field.
530 if ( isset( $args['row_label']['field'] ) && ! empty( $args['row_label']['field'] ) && isset( $args['fields'][ sanitize_key( $args['row_label']['field'] ) ] ) ) {
531 $this->row_label['field'] = esc_html( $args['row_label']['field'] );
532 } else {
533 // If from field is not set correctly, making sure standard is set as the type.
534 $this->row_label['type'] = 'text';
535 }
536 }
537
538 }
539
540 }
541