Completed
Push — develop ( 7e2b7e...23e5a1 )
by Aristeides
03:57
created

Kirki_Control_Repeater::l10n()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 18
nc 2
nop 1
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * Customizer Control: repeater.
4
 *
5
 * @package     Kirki
6
 * @subpackage  Controls
7
 * @copyright   Copyright (c) 2017, Aristeides Stathopoulos
8
 * @license     http://opensource.org/licenses/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 WP_Customize_Control {
21
22
	/**
23
	 * The control type.
24
	 *
25
	 * @access public
26
	 * @var string
27
	 */
28
	public $type = 'repeater';
29
30
	/**
31
	 * Used to automatically generate all CSS output.
32
	 *
33
	 * @access public
34
	 * @var array
35
	 */
36
	public $output = array();
37
38
	/**
39
	 * Data type
40
	 *
41
	 * @access public
42
	 * @var string
43
	 */
44
	public $option_type = 'theme_mod';
45
46
	/**
47
	 * The kirki_config we're using for this control
48
	 *
49
	 * @access public
50
	 * @var string
51
	 */
52
	public $kirki_config = 'global';
53
54
	/**
55
	 * The fields that each container row will contain.
56
	 *
57
	 * @access public
58
	 * @var array
59
	 */
60
	public $fields = array();
61
62
	/**
63
	 * Will store a filtered version of value for advenced fields (like images).
64
	 *
65
	 * @access protected
66
	 * @var array
67
	 */
68
	protected $filtered_value = array();
69
70
	/**
71
	 * The row label
72
	 *
73
	 * @access public
74
	 * @var array
75
	 */
76
	public $row_label = array();
77
78
	/**
79
	 * Constructor.
80
	 * Supplied `$args` override class property defaults.
81
	 * If `$args['settings']` is not defined, use the $id as the setting ID.
82
	 *
83
	 * @param WP_Customize_Manager $manager Customizer bootstrap instance.
84
	 * @param string               $id      Control ID.
85
	 * @param array                $args    {@see WP_Customize_Control::__construct}.
86
	 */
87
	public function __construct( $manager, $id, $args = array() ) {
88
89
		parent::__construct( $manager, $id, $args );
90
91
		// Set up defaults for row labels.
92
		$this->row_label = array(
93
			'type' => 'text',
94
			'value' => esc_attr__( 'row', 'kirki' ),
95
			'field' => false,
96
		);
97
98
		// Validate row-labels.
99
		$this->row_label( $args );
100
101
		if ( empty( $this->button_label ) ) {
102
			$this->button_label = sprintf( esc_attr__( 'Add new %s', 'kirki' ), $this->row_label['value'] );
103
		}
104
105
		if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
106
			$args['fields'] = array();
107
		}
108
109
		// An array to store keys of fields that need to be filtered.
110
		$media_fields_to_filter = array();
111
112
		foreach ( $args['fields'] as $key => $value ) {
113
			if ( ! isset( $value['default'] ) ) {
114
				$args['fields'][ $key ]['default'] = '';
115
			}
116
			if ( ! isset( $value['label'] ) ) {
117
				$args['fields'][ $key ]['label'] = '';
118
			}
119
			$args['fields'][ $key ]['id']      = $key;
120
121
			// We check if the filed is an uploaded media ( image , file, video, etc.. ).
122
			if ( isset( $value['type'] ) ) {
123
				switch ( $value['type'] ) {
124
					case 'image':
125
					case 'cropped_image':
126
					case 'upload':
127
						// We add it to the list of fields that need some extra filtering/processing.
128
						$media_fields_to_filter[ $key ] = true;
129
						break;
130
131
					case 'dropdown-pages':
132
						// If the field is a dropdown-pages field then add it to args.
133
						$dropdown = wp_dropdown_pages(
134
							array(
135
								'name'              => '',
136
								'echo'              => 0,
137
								'show_option_none'  => esc_attr__( 'Select a Page', 'kirki' ),
138
								'option_none_value' => '0',
139
								'selected'          => '',
140
							)
141
						);
142
						// Hackily add in the data link parameter.
143
						$dropdown = str_replace( '<select', '<select data-field="' . esc_attr( $args['fields'][ $key ]['id'] ) . '"' . $this->get_link(), $dropdown );
144
						$args['fields'][ $key ]['dropdown'] = $dropdown;
145
						break;
146
				}
147
			}
148
		} // End foreach().
149
150
		$this->fields = $args['fields'];
151
152
		// Now we are going to filter the fields.
153
		// First we create a copy of the value that would be used otherwise.
154
		$this->filtered_value = $this->value();
155
156
		if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
157
158
			// We iterate over the list of fields.
159
			foreach ( $this->filtered_value as &$filtered_value_field ) {
160
161
				if ( is_array( $filtered_value_field ) && ! empty( $filtered_value_field ) ) {
162
163
					// We iterate over the list of properties for this field.
164
					foreach ( $filtered_value_field as $key => &$value ) {
165
166
						// We check if this field was marked as requiring extra filtering (in this case image, cropped_images, upload).
167
						if ( array_key_exists( $key, $media_fields_to_filter ) ) {
168
169
							// What follows was made this way to preserve backward compatibility.
170
							// The repeater control use to store the URL for images instead of the attachment ID.
171
							// We check if the value look like an ID (otherwise it's probably a URL so don't filter it).
172
							if ( is_numeric( $value ) ) {
173
174
								// "sanitize" the value.
175
								$attachment_id = (int) $value;
176
177
								// Try to get the attachment_url.
178
								$url = wp_get_attachment_url( $attachment_id );
179
180
								$filename = basename( get_attached_file( $attachment_id ) );
181
182
								// If we got a URL.
183
								if ( $url ) {
184
185
									// 'id' is needed for form hidden value, URL is needed to display the image.
186
									$value = array(
187
										'id'  => $attachment_id,
188
										'url' => $url,
189
										'filename' => $filename,
190
									);
191
								}
192
							}
193
						}
194
					}
195
				}
196
			} // End foreach().
197
		} // End if().
198
	}
199
200
	/**
201
	 * Refresh the parameters passed to the JavaScript via JSON.
202
	 *
203
	 * @access public
204
	 */
205
	public function to_json() {
206
		parent::to_json();
207
208
		$this->json['default'] = ( isset( $this->default ) ) ? $this->default : $this->setting->default;
209
		$this->json['output']  = $this->output;
210
		$this->json['value']   = $this->value();
211
		$this->json['choices'] = $this->choices;
212
		$this->json['link']    = $this->get_link();
213
		$this->json['id']      = $this->id;
214
215
		$this->json['inputAttrs'] = '';
216
		foreach ( $this->input_attrs as $attr => $value ) {
217
			$this->json['inputAttrs'] .= $attr . '="' . esc_attr( $value ) . '" ';
218
		}
219
220
		$fields = $this->fields;
221
222
		$this->json['fields'] = $fields;
223
		$this->json['row_label'] = $this->row_label;
224
225
		// If filtered_value has been set and is not empty we use it instead of the actual value.
226
		if ( is_array( $this->filtered_value ) && ! empty( $this->filtered_value ) ) {
227
			$this->json['value'] = $this->filtered_value;
228
		}
229
	}
230
231
	/**
232
	 * Enqueue control related scripts/styles.
233
	 *
234
	 * @access public
235
	 */
236
	public function enqueue() {
237
238
		// If we have a color picker field we need to enqueue the Wordpress Color Picker style and script.
239
		if ( is_array( $this->fields ) && ! empty( $this->fields ) ) {
240
			foreach ( $this->fields as $field ) {
241
				if ( isset( $field['type'] ) ) {
242
243
					// Some field-types require extra scripts.
244
					switch ( $field['type'] ) {
245
						case 'color':
246
							wp_enqueue_script( 'wp-color-picker' );
247
							wp_enqueue_style( 'wp-color-picker' );
248
							break;
249
						case 'select':
250
						case 'dropdown-pages':
251
							wp_enqueue_script( 'select2', trailingslashit( Kirki::$url ) . 'assets/vendor/select2/js/select2.full.js', array( 'jquery' ), '4.0.3', true );
252
							wp_enqueue_style( 'select2', trailingslashit( Kirki::$url ) . 'assets/vendor/select2/css/select2.css', array(), '4.0.3' );
253
							wp_enqueue_style( 'kirki-select2', trailingslashit( Kirki::$url ) . 'assets/vendor/select2/kirki.css', null );
254
							break;
255
					}
256
				}
257
			}
258
		}
259
260
		wp_enqueue_script( 'kirki-repeater', trailingslashit( Kirki::$url ) . 'controls/repeater/repeater.js', array( 'jquery', 'customize-base', 'jquery-ui-core', 'jquery-ui-sortable' ), false, true );
261
		wp_enqueue_style( 'kirki-repeater-css', trailingslashit( Kirki::$url ) . 'controls/repeater/repeater.css', null );
262
	}
263
264
	/**
265
	 * Render the control's content.
266
	 * Allows the content to be overriden without having to rewrite the wrapper in $this->render().
267
	 *
268
	 * @access protected
269
	 */
270
	protected function render_content() {
271
		?>
272
		<div class="kirki-controls-loading-spinner"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>
273
		<label>
274
			<?php if ( ! empty( $this->label ) ) : ?>
275
				<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
276
			<?php endif; ?>
277
			<?php if ( ! empty( $this->description ) ) : ?>
278
				<span class="description customize-control-description"><?php echo wp_kses_post( $this->description ); ?></span>
279
			<?php endif; ?>
280
			<input type="hidden" {{{ data.inputAttrs }}} value="" <?php echo wp_kses_post( $this->get_link() ); ?> />
281
		</label>
282
283
		<ul class="repeater-fields"></ul>
284
285
		<?php if ( isset( $this->choices['limit'] ) ) : ?>
286
			<?php /* translators: %s represents the number of rows we're limiting the repeater to allow. */ ?>
287
			<p class="limit"><?php printf( esc_attr__( 'Limit: %s rows', 'kirki' ), esc_html( $this->choices['limit'] ) ); ?></p>
288
		<?php endif; ?>
289
		<button class="button-secondary repeater-add"><?php echo esc_html( $this->button_label ); ?></button>
290
291
		<?php
292
293
		$this->repeater_js_template();
294
295
	}
296
297
	/**
298
	 * An Underscore (JS) template for this control's content (but not its container).
299
	 * Class variables for this control class are available in the `data` JS object.
300
	 *
301
	 * @access public
302
	 */
303
	public function repeater_js_template() {
304
		?>
305
		<script type="text/html" class="customize-control-repeater-content">
306
			<# var field; var index = data.index; #>
307
308
			<li class="repeater-row minimized" data-row="{{{ index }}}">
309
310
				<div class="repeater-row-header">
311
					<span class="repeater-row-label"></span>
312
					<i class="dashicons dashicons-arrow-down repeater-minimize"></i>
313
				</div>
314
				<div class="repeater-row-content">
315
					<# _.each( data, function( field, i ) { #>
316
317
						<div class="repeater-field repeater-field-{{{ field.type }}}">
318
319
							<# if ( 'text' === field.type || 'url' === field.type || 'link' === field.type || 'email' === field.type || 'tel' === field.type || 'date' === field.type || 'number' === field.type ) { #>
320
								<# var fieldExtras = ''; #>
321
								<# if ( 'link' === field.type ) { #>
322
									<# field.type = 'url' #>
323
								<# } #>
324
325
								<# if ( 'number' === field.type ) { #>
326
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.min ) ) { #>
327
										<# fieldExtras += ' min="' + field.choices.min + '"'; #>
328
									<# } #>
329
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.max ) ) { #>
330
										<# fieldExtras += ' max="' + field.choices.max + '"'; #>
331
									<# } #>
332
									<# if ( ! _.isUndefined( field.choices ) && ! _.isUndefined( field.choices.step ) ) { #>
333
										<# fieldExtras += ' step="' + field.choices.step + '"'; #>
334
									<# } #>
335
								<# } #>
336
337
								<label>
338
									<# if ( field.label ) { #>
339
										<span class="customize-control-title">{{ field.label }}</span>
340
									<# } #>
341
									<# if ( field.description ) { #>
342
										<span class="description customize-control-description">{{ field.description }}</span>
343
									<# } #>
344
									<input type="{{field.type}}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ fieldExtras }}>
345
								</label>
346
347
							<# } else if ( 'number' === field.type ) { #>
348
349
								<label>
350
									<# if ( field.label ) { #>
351
										<span class="customize-control-title">{{ field.label }}</span>
352
									<# } #>
353
									<# if ( field.description ) { #>
354
										<span class="description customize-control-description">{{ field.description }}</span>
355
									<# } #>
356
									<input type="{{ field.type }}" name="" value="{{{ field.default }}}" data-field="{{{ field.id }}}"{{ numberFieldExtras }}>
357
								</label>
358
359
							<# } else if ( 'hidden' === field.type ) { #>
360
361
								<input type="hidden" data-field="{{{ field.id }}}" <# if ( field.default ) { #> value="{{{ field.default }}}" <# } #> />
362
363
							<# } else if ( 'checkbox' === field.type ) { #>
364
365
								<label>
366
									<input type="checkbox" value="true" data-field="{{{ field.id }}}" <# if ( field.default ) { #> checked="checked" <# } #> /> {{ field.label }}
367
									<# if ( field.description ) { #>
368
										{{ field.description }}
369
									<# } #>
370
								</label>
371
372
							<# } else if ( 'select' === field.type ) { #>
373
374
								<label>
375
									<# if ( field.label ) { #>
376
										<span class="customize-control-title">{{ field.label }}</span>
377
									<# } #>
378
									<# if ( field.description ) { #>
379
										<span class="description customize-control-description">{{ field.description }}</span>
380
									<# } #>
381
									<select data-field="{{{ field.id }}}"<# if ( ! _.isUndefined( field.multiple ) && false !== field.multiple ) { #> multiple="multiple" data-multiple="{{ field.multiple }}"<# } #>>
382
										<# _.each( field.choices, function( choice, i ) { #>
383
											<option value="{{{ i }}}" <# if ( field.default == i ) { #> selected="selected" <# } #>>{{ choice }}</option>
384
										<# }); #>
385
									</select>
386
								</label>
387
388
							<# } else if ( 'dropdown-pages' === field.type ) { #>
389
390
								<label>
391
									<# if ( field.label ) { #>
392
										<span class="customize-control-title">{{{ data.label }}}</span>
393
									<# } #>
394
									<# if ( field.description ) { #>
395
										<span class="description customize-control-description">{{{ field.description }}}</span>
396
									<# } #>
397
									<div class="customize-control-content repeater-dropdown-pages">{{{ field.dropdown }}}</div>
398
								</label>
399
400
							<# } else if ( 'radio' === field.type ) { #>
401
402
								<label>
403
									<# if ( field.label ) { #>
404
										<span class="customize-control-title">{{ field.label }}</span>
405
									<# } #>
406
									<# if ( field.description ) { #>
407
										<span class="description customize-control-description">{{ field.description }}</span>
408
									<# } #>
409
410
									<# _.each( field.choices, function( choice, i ) { #>
411
										<label>
412
											<input type="radio" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>> {{ choice }} <br/>
413
										</label>
414
									<# }); #>
415
								</label>
416
417
							<# } else if ( 'radio-image' === field.type ) { #>
418
419
								<label>
420
									<# if ( field.label ) { #>
421
										<span class="customize-control-title">{{ field.label }}</span>
422
									<# } #>
423
									<# if ( field.description ) { #>
424
										<span class="description customize-control-description">{{ field.description }}</span>
425
									<# } #>
426
427
									<# _.each( field.choices, function( choice, i ) { #>
428
										<input type="radio" id="{{{ field.id }}}_{{ index }}_{{{ i }}}" name="{{{ field.id }}}{{ index }}" data-field="{{{ field.id }}}" value="{{{ i }}}" <# if ( field.default == i ) { #> checked="checked" <# } #>>
429
											<label for="{{{ field.id }}}_{{ index }}_{{{ i }}}">
430
												<img src="{{ choice }}">
431
											</label>
432
										</input>
433
									<# }); #>
434
								</label>
435
436
							<# } else if ( 'color' === field.type ) { #>
437
438
								<# var defaultValue = '';
439
								if ( field.default ) {
440
									if ( '#' !== field.default.substring( 0, 1 ) ) {
441
										defaultValue = '#' + field.default;
442
									} else {
443
										defaultValue = field.default;
444
									}
445
									defaultValue = ' data-default-color=' + defaultValue; // Quotes added automatically.
446
								} #>
447
								<label>
448
									<# if ( field.label ) { #>
449
										<span class="customize-control-title">{{{ field.label }}}</span>
450
									<# } #>
451
									<# if ( field.description ) { #>
452
										<span class="description customize-control-description">{{{ field.description }}}</span>
453
									<# } #>
454
									<input class="color-picker-hex" type="text" maxlength="7" placeholder="<?php esc_attr_e( 'Hex Value', 'kirki' ); ?>"  value="{{{ field.default }}}" data-field="{{{ field.id }}}" {{ defaultValue }} />
455
456
								</label>
457
458
							<# } else if ( 'textarea' === field.type ) { #>
459
460
								<# if ( field.label ) { #>
461
									<span class="customize-control-title">{{ field.label }}</span>
462
								<# } #>
463
								<# if ( field.description ) { #>
464
									<span class="description customize-control-description">{{ field.description }}</span>
465
								<# } #>
466
								<textarea rows="5" data-field="{{{ field.id }}}">{{ field.default }}</textarea>
467
468
							<# } else if ( field.type === 'image' || field.type === 'cropped_image' ) { #>
469
470
								<label>
471
									<# if ( field.label ) { #>
472
										<span class="customize-control-title">{{ field.label }}</span>
473
									<# } #>
474
									<# if ( field.description ) { #>
475
										<span class="description customize-control-description">{{ field.description }}</span>
476
									<# } #>
477
								</label>
478
479
								<figure class="kirki-image-attachment" data-placeholder="<?php esc_attr_e( 'No Image Selected', 'kirki' ); ?>" >
480
									<# if ( field.default ) { #>
481
										<# var defaultImageURL = ( field.default.url ) ? field.default.url : field.default; #>
482
										<img src="{{{ defaultImageURL }}}">
483
									<# } else { #>
484
										<?php esc_attr_e( 'No Image Selected', 'kirki' ); ?>
485
									<# } #>
486
								</figure>
487
488
								<div class="actions">
489
									<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"><?php esc_attr_e( 'Remove', 'kirki' ); ?></button>
490
									<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' ); ?>" >
491
										<# if ( field.default ) { #>
492
											<?php esc_attr_e( 'Change Image', 'kirki' ); ?>
493
										<# } else { #>
494
											<?php esc_attr_e( 'Add Image', 'kirki' ); ?>
495
										<# } #>
496
									</button>
497
									<# if ( field.default.id ) { #>
498
										<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
499
									<# } else { #>
500
										<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
501
									<# } #>
502
								</div>
503
504
							<# } else if ( field.type === 'upload' ) { #>
505
506
								<label>
507
									<# if ( field.label ) { #>
508
										<span class="customize-control-title">{{ field.label }}</span>
509
									<# } #>
510
									<# if ( field.description ) { #>
511
										<span class="description customize-control-description">{{ field.description }}</span>
512
									<# } #>
513
								</label>
514
515
								<figure class="kirki-file-attachment" data-placeholder="<?php esc_attr_e( 'No File Selected', 'kirki' ); ?>" >
516
									<# if ( field.default ) { #>
517
										<# var defaultFilename = ( field.default.filename ) ? field.default.filename : field.default; #>
518
										<span class="file"><span class="dashicons dashicons-media-default"></span> {{ defaultFilename }}</span>
519
									<# } else { #>
520
										<?php esc_attr_e( 'No File Selected', 'kirki' ); ?>
521
									<# } #>
522
								</figure>
523
524
								<div class="actions">
525
									<button type="button" class="button remove-button<# if ( ! field.default ) { #> hidden<# } #>"></button>
526
									<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' ); ?>">
527
										<# if ( field.default ) { #>
528
											<?php esc_attr_e( 'Change File', 'kirki' ); ?>
529
										<# } else { #>
530
											<?php esc_attr_e( 'Add File', 'kirki' ); ?>
531
										<# } #>
532
									</button>
533
									<# if ( field.default.id ) { #>
534
										<input type="hidden" class="hidden-field" value="{{{ field.default.id }}}" data-field="{{{ field.id }}}" >
535
									<# } else { #>
536
										<input type="hidden" class="hidden-field" value="{{{ field.default }}}" data-field="{{{ field.id }}}" >
537
									<# } #>
538
								</div>
539
540
							<# } else if ( 'custom' === field.type ) { #>
541
542
								<# if ( field.label ) { #>
543
									<span class="customize-control-title">{{ field.label }}</span>
544
								<# } #>
545
								<# if ( field.description ) { #>
546
									<span class="description customize-control-description">{{ field.description }}</span>
547
								<# } #>
548
								<div data-field="{{{ field.id }}}">{{{ field.default }}}</div>
549
550
							<# } #>
551
552
						</div>
553
					<# }); #>
554
					<button type="button" class="button-link repeater-row-remove"><?php esc_attr_e( 'Remove', 'kirki' ); ?></button>
555
				</div>
556
			</li>
557
		</script>
558
		<?php
559
	}
560
561
	/**
562
	 * Validate row-labels.
563
	 *
564
	 * @access protected
565
	 * @since 3.0.0
566
	 * @param array $args {@see WP_Customize_Control::__construct}.
567
	 */
568
	protected function row_label( $args ) {
569
570
		// Validating args for row labels.
571
		if ( isset( $args['row_label'] ) && is_array( $args['row_label'] ) && ! empty( $args['row_label'] ) ) {
572
573
			// Validating row label type.
574
			if ( isset( $args['row_label']['type'] ) && ( 'text' === $args['row_label']['type'] || 'field' === $args['row_label']['type'] ) ) {
575
				$this->row_label['type'] = $args['row_label']['type'];
576
			}
577
578
			// Validating row label type.
579
			if ( isset( $args['row_label']['value'] ) && ! empty( $args['row_label']['value'] ) ) {
580
				$this->row_label['value'] = esc_attr( $args['row_label']['value'] );
581
			}
582
583
			// Validating row label field.
584
			if ( isset( $args['row_label']['field'] ) && ! empty( $args['row_label']['field'] ) && isset( $args['fields'][ esc_attr( $args['row_label']['field'] ) ] ) ) {
585
				$this->row_label['field'] = esc_attr( $args['row_label']['field'] );
586
			} else {
587
				// If from field is not set correctly, making sure standard is set as the type.
588
				$this->row_label['type'] = 'text';
589
			}
590
		}
591
	}
592
}
593