Issues (377)

controls/php/class-kirki-control-repeater.php (5 issues)

1
<?php
2
/**
3
 * Customizer Control: repeater.
4
 *
5
 * @package     Kirki
6
 * @subpackage  Controls
7
 * @copyright   Copyright (c) 2017, Aristeides Stathopoulos
8
 * @license    https://opensource.org/licenses/MIT
9
 * @since       2.0
10
 */
11
12
// Exit if accessed directly.
13
if ( ! defined( 'ABSPATH' ) ) {
14
	exit;
15
}
16
17
/**
18
 * Repeater control
19
 */
20
class Kirki_Control_Repeater extends Kirki_Control_Base {
21
22
	/**
23
	 * The control type.
24
	 *
25
	 * @access public
26
	 * @var string
27
	 */
28
	public $type = 'repeater';
29
30
	/**
31
	 * The fields that each container row will contain.
32
	 *
33
	 * @access public
34
	 * @var array
35
	 */
36
	public $fields = array();
37
38
	/**
39
	 * Will store a filtered version of value for advenced fields (like images).
40
	 *
41
	 * @access protected
42
	 * @var array
43
	 */
44
	protected $filtered_value = array();
45
46
	/**
47
	 * The row label
48
	 *
49
	 * @access public
50
	 * @var array
51
	 */
52
	public $row_label = array();
53
54
	/**
55
	 * The button label
56
	 *
57
	 * @access public
58
	 * @var string
59
	 */
60
	public $button_label = '';
61
62
	/**
63
	 * Constructor.
64
	 * Supplied `$args` override class property defaults.
65
	 * If `$args['settings']` is not defined, use the $id as the setting ID.
66
	 *
67
	 * @param WP_Customize_Manager $manager Customizer bootstrap instance.
68
	 * @param string               $id      Control ID.
69
	 * @param array                $args    {@see WP_Customize_Control::__construct}.
70
	 */
71
	public function __construct( $manager, $id, $args = array() ) {
72
		parent::__construct( $manager, $id, $args );
73
74
		// Set up defaults for row labels.
75
		$this->row_label = array(
76
			'type'  => 'text',
77
			'value' => esc_attr__( 'row', 'kirki' ),
78
			'field' => false,
79
		);
80
81
		// Validate row-labels.
82
		$this->row_label( $args );
83
84
		if ( empty( $this->button_label ) ) {
85
			/* translators: %s represents the label of the row. */
86
			$this->button_label = sprintf( esc_html__( 'Add new %s', 'kirki' ), $this->row_label['value'] );
87
		}
88
89
		if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
90
			$args['fields'] = array();
91
		}
92
93
		// An array to store keys of fields that need to be filtered.
94
		$media_fields_to_filter = array();
95
96
		foreach ( $args['fields'] as $key => $value ) {
97
			if ( ! isset( $value['default'] ) ) {
98
				$args['fields'][ $key ]['default'] = '';
99
			}
100
			if ( ! isset( $value['label'] ) ) {
101
				$args['fields'][ $key ]['label'] = '';
102
			}
103
			$args['fields'][ $key ]['id'] = $key;
104
105
			// We check if the filed is an uploaded media ( image , file, video, etc.. ).
106
			if ( isset( $value['type'] ) ) {
107
				switch ( $value['type'] ) {
108
					case 'image':
109
					case 'cropped_image':
110
					case 'upload':
0 ignored issues
show
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
111
112
						// We add it to the list of fields that need some extra filtering/processing.
113
						$media_fields_to_filter[ $key ] = true;
114
						break;
115
116
					case 'dropdown-pages':
0 ignored issues
show
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
117
118
						// If the field is a dropdown-pages field then add it to args.
119
						$dropdown = wp_dropdown_pages(
120
							array(
121
								'name'              => '',
122
								'echo'              => 0,
123
								'show_option_none'  => esc_html__( 'Select a Page', 'kirki' ),
124
								'option_none_value' => '0',
125
								'selected'          => '',
126
							)
127
						);
128
129
						// Hackily add in the data link parameter.
130
						$dropdown = str_replace( '<select', '<select data-field="' . esc_attr( $args['fields'][ $key ]['id'] ) . '"' . $this->get_link(), $dropdown ); // phpcs:ignore Generic.Formatting.MultipleStatementAlignment.NotSameWarning
0 ignored issues
show
Equals sign not aligned with surrounding assignments; expected 27 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
Inline comments must end in full-stops, exclamation marks, or question marks
Loading history...
131
						$args['fields'][ $key ]['dropdown'] = $dropdown;
132
						break;
133
				}
134
			}
135
		}
136
137
		$this->fields = $args['fields'];
138
139
		// Now we are going to filter the fields.
140
		// First we create a copy of the value that would be used otherwise.
141
		$this->filtered_value = $this->value();
142
143
		if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
144
145
			// We iterate over the list of fields.
146
			foreach ( $this->filtered_value as &$filtered_value_field ) {
147
148
				if ( is_array( $filtered_value_field ) && ! empty( $filtered_value_field ) ) {
149
150
					// We iterate over the list of properties for this field.
151
					foreach ( $filtered_value_field as $key => &$value ) {
152
153
						// We check if this field was marked as requiring extra filtering (in this case image, cropped_images, upload).
154
						if ( array_key_exists( $key, $media_fields_to_filter ) ) {
155
156
							// What follows was made this way to preserve backward compatibility.
157
							// The repeater control use to store the URL for images instead of the attachment ID.
158
							// We check if the value look like an ID (otherwise it's probably a URL so don't filter it).
159
							if ( is_numeric( $value ) ) {
160
161
								// "sanitize" the value.
162
								$attachment_id = (int) $value;
163
164
								// Try to get the attachment_url.
165
								$url = wp_get_attachment_url( $attachment_id );
166
167
								$filename = basename( get_attached_file( $attachment_id ) );
168
169
								// If we got a URL.
170
								if ( $url ) {
171
172
									// 'id' is needed for form hidden value, URL is needed to display the image.
173
									$value = array(
174
										'id'       => $attachment_id,
175
										'url'      => $url,
176
										'filename' => $filename,
177
									);
178
								}
179
							}
180
						}
181
					}
182
				}
183
			}
184
		}
185
	}
186
187
	/**
188
	 * Refresh the parameters passed to the JavaScript via JSON.
189
	 *
190
	 * @access public
191
	 */
192
	public function to_json() {
193
		parent::to_json();
194
195
		$fields = $this->fields;
196
197
		$this->json['fields']    = $fields;
198
		$this->json['row_label'] = $this->row_label;
199
200
		// If filtered_value has been set and is not empty we use it instead of the actual value.
201
		if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
202
			$this->json['value'] = $this->filtered_value;
203
		}
204
		$this->json['value'] = apply_filters( "kirki_controls_repeater_value_{$this->id}", $this->json['value'] );
205
	}
206
207
	/**
208
	 * Render the control's content.
209
	 * Allows the content to be overriden without having to rewrite the wrapper in $this->render().
210
	 *
211
	 * @access protected
212
	 */
213
	protected function render_content() {
214
		?>
215
		<label>
216
			<?php if ( ! empty( $this->label ) ) : ?>
217
				<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
218
			<?php endif; ?>
219
			<?php if ( ! empty( $this->description ) ) : ?>
220
				<span class="description customize-control-description"><?php echo wp_kses_post( $this->description ); ?></span>
221
			<?php endif; ?>
222
			<input type="hidden" {{{ data.inputAttrs }}} value="" <?php echo wp_kses_post( $this->get_link() ); ?> />
223
		</label>
224
225
		<ul class="repeater-fields"></ul>
226
227
		<?php if ( isset( $this->choices['limit'] ) ) : ?>
228
			<?php /* translators: %s represents the number of rows we're limiting the repeater to allow. */ ?>
0 ignored issues
show
Inline PHP statement must end with a semicolon
Loading history...
229
			<p class="limit"><?php printf( esc_html__( 'Limit: %s rows', 'kirki' ), esc_html( $this->choices['limit'] ) ); ?></p>
230
		<?php endif; ?>
231
		<button class="button-secondary repeater-add"><?php echo esc_html( $this->button_label ); ?></button>
232
233
		<?php
234
235
		$this->repeater_js_template();
236
237
	}
238
239
	/**
240
	 * An Underscore (JS) template for this control's content (but not its container).
241
	 * Class variables for this control class are available in the `data` JS object.
242
	 *
243
	 * @access public
244
	 */
245
	public function repeater_js_template() {
246
		?>
247
		<script type="text/html" class="customize-control-repeater-content">
248
			<# var field; var index = data.index; #>
249
250
			<li class="repeater-row minimized" data-row="{{{ index }}}">
251
252
				<div class="repeater-row-header">
253
					<span class="repeater-row-label"></span>
254
					<i class="dashicons dashicons-arrow-down repeater-minimize"></i>
255
				</div>
256
				<div class="repeater-row-content">
257
					<# _.each( data, function( field, i ) { #>
258
259
						<div class="repeater-field repeater-field-{{{ field.type }}} repeater-field-{{ field.id }}">
260
261
							<# if ( 'text' === field.type || 'url' === field.type || 'link' === field.type || 'email' === field.type || 'tel' === field.type || 'date' === field.type || 'number' === field.type ) { #>
262
								<# var fieldExtras = ''; #>
263
								<# if ( 'link' === field.type ) { #>
264
									<# field.type = 'url' #>
265
								<# } #>
266
267
								<# if ( 'number' === field.type ) { #>
268
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.min ) ) { #>
269
										<# fieldExtras += ' min="' + field.choices.min + '"'; #>
270
									<# } #>
271
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.max ) ) { #>
272
										<# fieldExtras += ' max="' + field.choices.max + '"'; #>
273
									<# } #>
274
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.step ) ) { #>
275
										<# fieldExtras += ' step="' + field.choices.step + '"'; #>
276
									<# } #>
277
								<# } #>
278
279
								<label>
280
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
281
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
282
									<input type="{{field.type}}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ fieldExtras }}>
283
								</label>
284
285
							<# } else if ( 'number' === field.type ) { #>
286
287
								<label>
288
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
289
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
290
									<input type="{{ field.type }}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ numberFieldExtras }}>
291
								</label>
292
293
							<# } else if ( 'hidden' === field.type ) { #>
294
295
								<input type="hidden" data-field="{{{ field.id }}}" <# if ( field.default ) { #> value="{{{ field.default }}}" <# } #> />
296
297
							<# } else if ( 'checkbox' === field.type ) { #>
298
299
								<label>
300
									<input type="checkbox" value="{{{ field.default }}}" data-field="{{{ field.id }}}" <# if ( field.default ) { #> checked="checked" <# } #> /> {{{ field.label }}}
301
									<# if ( field.description ) { #>{{{ field.description }}}<# } #>
302
								</label>
303
304
							<# } else if ( 'select' === field.type ) { #>
305
306
								<label>
307
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
308
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
309
									<select data-field="{{{ field.id }}}"<# if ( ! _.isUndefined( field.multiple ) && false !== field.multiple ) { #> multiple="multiple" data-multiple="{{ field.multiple }}"<# } #>>
310
										<# _.each( field.choices, function( choice, i ) { #>
311
											<option value="{{{ i }}}" <# if ( -1 !== jQuery.inArray( i, field.default ) || field.default == i ) { #> selected="selected" <# } #>>{{ choice }}</option>
312
										<# }); #>
313
									</select>
314
								</label>
315
316
							<# } else if ( 'dropdown-pages' === field.type ) { #>
317
318
								<label>
319
									<# if ( field.label ) { #><span class="customize-control-title">{{{ data.label }}}</span><# } #>
320
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
321
									<div class="customize-control-content repeater-dropdown-pages">{{{ field.dropdown }}}</div>
322
								</label>
323
324
							<# } else if ( 'radio' === field.type ) { #>
325
326
								<label>
327
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
328
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
329
330
									<# _.each( field.choices, function( choice, i ) { #>
331
										<label><input type="radio" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>> {{ choice }} <br/></label>
332
									<# }); #>
333
								</label>
334
335
							<# } else if ( 'radio-image' === field.type ) { #>
336
337
								<label>
338
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
339
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
340
341
									<# _.each( field.choices, function( choice, i ) { #>
342
										<input type="radio" id="{{{ field.id }}}_{{ index }}_{{{ i }}}" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>>
343
											<label for="{{{ field.id }}}_{{ index }}_{{{ i }}}"><img src="{{ choice }}"></label>
344
										</input>
345
									<# }); #>
346
								</label>
347
348
							<# } else if ( 'color' === field.type ) { #>
349
350
								<label>
351
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
352
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
353
								</label>
354
								<# var defaultValue = '';
355
								if ( field.default ) {
356
									if ( -1 === field.default.indexOf( 'rgba' ) ) {
357
										defaultValue = ( '#' !== field.default.substring( 0, 1 ) ) ? '#' + field.default : field.default;
358
										defaultValue = ' data-default-color=' + defaultValue; // Quotes added automatically.
359
									} else {
360
										defaultValue = ' data-default-color="' + defaultValue + '" data-alpha="true"';
361
									}
362
								} #>
363
								<input class="color-picker-hex" type="text" maxlength="7" value="{{{ field.default }}}" data-field="{{{ field.id }}}" {{ defaultValue }} />
364
365
							<# } else if ( 'textarea' === field.type ) { #>
366
367
								<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
368
								<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
369
								<textarea rows="5" data-field="{{{ field.id }}}">{{ field.default }}</textarea>
370
371
							<# } else if ( field.type === 'image' || field.type === 'cropped_image' ) { #>
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
								</label>
377
378
								<figure class="kirki-image-attachment" data-placeholder="<?php esc_attr_e( 'No Image Selected', 'kirki' ); ?>" >
379
									<# if ( field.default ) { #>
380
										<# var defaultImageURL = ( field.default.url ) ? field.default.url : field.default; #>
381
										<img src="{{{ defaultImageURL }}}">
382
									<# } else { #>
383
										<?php esc_html_e( 'No Image Selected', 'kirki' ); ?>
384
									<# } #>
385
								</figure>
386
387
								<div class="actions">
388
									<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
389
									<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' ); ?>" >
390
										<# if ( field.default ) { #>
391
											<?php esc_html_e( 'Change Image', 'kirki' ); ?>
392
										<# } else { #>
393
											<?php esc_html_e( 'Add Image', 'kirki' ); ?>
394
										<# } #>
395
									</button>
396
									<# if ( field.default.id ) { #>
397
										<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
398
									<# } else { #>
399
										<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
400
									<# } #>
401
								</div>
402
403
							<# } else if ( field.type === 'upload' ) { #>
404
405
								<label>
406
									<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
407
									<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
408
								</label>
409
410
								<figure class="kirki-file-attachment" data-placeholder="<?php esc_attr_e( 'No File Selected', 'kirki' ); ?>" >
411
									<# if ( field.default ) { #>
412
										<# var defaultFilename = ( field.default.filename ) ? field.default.filename : field.default; #>
413
										<span class="file"><span class="dashicons dashicons-media-default"></span> {{ defaultFilename }}</span>
414
									<# } else { #>
415
										<?php esc_html_e( 'No File Selected', 'kirki' ); ?>
416
									<# } #>
417
								</figure>
418
419
								<div class="actions">
420
									<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
421
									<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' ); ?>">
422
										<# if ( field.default ) { #>
423
											<?php esc_html_e( 'Change File', 'kirki' ); ?>
424
										<# } else { #>
425
											<?php esc_html_e( 'Add File', 'kirki' ); ?>
426
										<# } #>
427
									</button>
428
									<# if ( field.default.id ) { #>
429
										<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
430
									<# } else { #>
431
										<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
432
									<# } #>
433
								</div>
434
435
							<# } else if ( 'custom' === field.type ) { #>
436
437
								<# if ( field.label ) { #><span class="customize-control-title">{{{ field.label }}}</span><# } #>
438
								<# if ( field.description ) { #><span class="description customize-control-description">{{{ field.description }}}</span><# } #>
439
								<div data-field="{{{ field.id }}}">{{{ field.default }}}</div>
440
441
							<# } #>
442
443
						</div>
444
					<# }); #>
445
					<button type="button" class="button-link repeater-row-remove"><?php esc_html_e( 'Remove', 'kirki' ); ?></button>
446
				</div>
447
			</li>
448
		</script>
449
		<?php
450
	}
451
452
	/**
453
	 * Validate row-labels.
454
	 *
455
	 * @access protected
456
	 * @since 3.0.0
457
	 * @param array $args {@see WP_Customize_Control::__construct}.
458
	 */
459
	protected function row_label( $args ) {
460
461
		// Validating args for row labels.
462
		if ( isset( $args['row_label'] ) && is_array( $args['row_label'] ) && ! empty( $args['row_label'] ) ) {
463
464
			// Validating row label type.
465
			if ( isset( $args['row_label']['type'] ) && ( 'text' === $args['row_label']['type'] || 'field' === $args['row_label']['type'] ) ) {
466
				$this->row_label['type'] = $args['row_label']['type'];
467
			}
468
469
			// Validating row label type.
470
			if ( isset( $args['row_label']['value'] ) && ! empty( $args['row_label']['value'] ) ) {
471
				$this->row_label['value'] = esc_html( $args['row_label']['value'] );
472
			}
473
474
			// Validating row label field.
475
			if ( isset( $args['row_label']['field'] ) && ! empty( $args['row_label']['field'] ) && isset( $args['fields'][ sanitize_key( $args['row_label']['field'] ) ] ) ) {
476
				$this->row_label['field'] = esc_html( $args['row_label']['field'] );
477
			} else {
478
				// If from field is not set correctly, making sure standard is set as the type.
479
				$this->row_label['type'] = 'text';
480
			}
481
		}
482
	}
483
}
484