Completed
Push — milestone/2.0 ( 2ab7a4...17042e )
by
unknown
02:47
created

Complex_Field::set_header_template()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 1
dl 0
loc 19
ccs 0
cts 11
cp 0
crap 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Carbon_Fields\Field;
4
5
use Carbon_Fields\Datastore\Datastore_Interface;
6
use Carbon_Fields\Helper\Helper;
7
use Carbon_Fields\Field\Field;
8
use Carbon_Fields\Field\Group_Field;
9
use Carbon_Fields\Value_Set\Value_Set;
10
use Carbon_Fields\Exception\Incorrect_Syntax_Exception;
11
12
/**
13
 * Complex field class.
14
 * Allows nested repeaters with multiple field groups to be created.
15
 */
16
class Complex_Field extends Field {
17
18
	const LAYOUT_GRID = 'grid'; // default
19
20
	const LAYOUT_TABBED_HORIZONTAL = 'tabbed-horizontal';
21
22
	const LAYOUT_TABBED_VERTICAL = 'tabbed-vertical';
23
24
	const GROUP_TYPE_KEY = '_type';
25
26
	/**
27
	 * Default field value
28
	 *
29
	 * @var array
30
	 */
31
	protected $default_value = array();
32
33
	protected $layout = self::LAYOUT_GRID;
34
35
	protected $value_tree = array();
36
37
	protected $fields = array();
38
39
	protected $groups = array();
40
41
	protected $values_min = -1;
42
43
	protected $values_max = -1;
44
45
	protected $collapsed = false;
46
47
	public $labels = array(
48
		'singular_name' => 'Entry',
49
		'plural_name' => 'Entries',
50
	);
51
52
	/**
53
	 * Create a field from a certain type with the specified label.
54
	 * @param string $name  Field name
55
	 * @param string $label Field label
56
	 */
57
	protected function __construct( $name, $label ) {
58
		$this->set_value_set( new Value_Set( Value_Set::TYPE_MULTIPLE_VALUES ) );
59
		parent::__construct( $name, $label );
60
	}
61
62
	/**
63
	 * Activate the field once the container is attached.
64
	 */
65
	public function activate() {
66
		parent::activate();
67
		$fields = $this->get_fields();
68
		foreach ( $fields as $field ) {
69
			$field->activate();
70
		}
71
	}
72
73
	/**
74
	 * Initialization tasks.
75
	 */
76
	public function init() {
77
		$this->labels = array(
78
			'singular_name' => __( 'Entry', \Carbon_Fields\TEXT_DOMAIN ),
79
			'plural_name' => __( 'Entries', \Carbon_Fields\TEXT_DOMAIN ),
80
		);
81
82
		// Include the complex group Underscore templates
83
		$this->add_template( 'Complex-Group', array( $this, 'template_group' ) );
84
		$this->add_template( 'Complex-Group-Tab-Item', array( $this, 'template_group_tab_item' ) );
85
86
		parent::init();
87
	}
88
89
	/**
90
	 * Add a set/group of fields.
91
	 *
92
	 * Accepted param variations:
93
	 *   - array<Field> $fields
94
	 *   - string $group_name, array<Field> $fields
95
	 *   - string $group_name, string $group_label, array<Field> $fields
96
	 * 
97
	 * @return $this
98
	 */
99
	public function add_fields() {
100
		$argv = func_get_args();
101
		$argc = count( $argv );
102
		$fields = $argv[ $argc - 1 ];
103
		$name = '';
104
		$label = null;
105
106
		if ( $argc >= 2 ) {
107
			$name = $argv[0];
108
		}
109
		
110
		if ( $argc >= 3 ) {
111
			$label = $argv[1];
112
		}
113
114
		$name = ! empty( $name ) ? $name : Group_Field::DEFAULT_GROUP_NAME;
115
116
		if ( array_key_exists( $name, $this->groups ) ) {
117
			Incorrect_Syntax_Exception::raise( 'Group with name "' . $name . '" in Complex Field "' . $this->get_label() . '" already exists.' );
118
		}
119
120
		foreach ( $fields as $field ) {
121
			if ( $field->get_base_name() === static::GROUP_TYPE_KEY ) {
122
				Incorrect_Syntax_Exception::raise( '"' . static::GROUP_TYPE_KEY . '" is a reserved keyword for Complex fields and cannot be used for a field name.' );
123
			}
124
			$field->set_hierarchy( array_merge( $this->get_hierarchy(), array( $this->get_base_name() ) ) );
125
		}
126
127
		$group = new Group_Field( $name, $label, $fields );
128
129
		$this->groups[ $group->get_name() ] = $group;
130
131
		return $this;
132
	}
133
134
	/**
135
	 * Set the group label Underscore template.
136
	 *
137
	 * @param  string|callable $template
138
	 * @return $this
139
	 */
140
	public function set_header_template( $template ) {
141
		if ( count( $this->groups ) === 0 ) {
142
			Incorrect_Syntax_Exception::raise( "Can't set group label template. There are no present groups for Complex Field " . $this->get_label() . '.' );
143
		}
144
145
		$template = is_callable( $template ) ? call_user_func( $template ) : $template;
146
147
		// Assign the template to the group that was added last
148
		$values = array_values( $this->groups );
149
		$group = end( $values );
150
		$group->set_label_template( $template );
151
152
		// Include the group label Underscore template
153
		$this->add_template( $group->get_group_id(), array( $group, 'template_label' ) );
154
155
		$this->groups[ $group->get_name() ] = $group;
156
157
		return $this;
158
	}
159
160
	/**
161
	 * Retrieve all groups of fields.
162
	 *
163
	 * @return array $fields
164
	 */
165
	public function get_fields() {
166
		$fields = array();
167
168
		foreach ( $this->groups as $group ) {
169
			$group_fields = $group->get_fields();
170
171
			$fields = array_merge( $fields, $group_fields );
172
		}
173
174
		return $fields;
175
	}
176
177
	/**
178
	 * Set the field labels.
179
	 * Currently supported values:
180
	 *  - singular_name - the singular entry label
181
	 *  - plural_name - the plural entries label
182
	 *
183
	 * @param  array $labels Labels
184
	 */
185
	public function setup_labels( $labels ) {
186
		$this->labels = array_merge( $this->labels, $labels );
187
		return $this;
188
	}
189
190
	/**
191
	 * Set the datastore of this field.
192
	 *
193
	 * @param Datastore_Interface $datastore
194
	 */
195 View Code Duplication
	public function set_datastore( Datastore_Interface $datastore, $set_as_default = false ) {
196
		if ( $set_as_default && ! $this->has_default_datastore() ) {
197
			return $this; // datastore has been overriden with a custom one - abort changing to a default one
198
		}
199
		$this->datastore = $datastore;
200
		$this->has_default_datastore = $set_as_default;
201
202
		foreach ( $this->groups as $group ) {
203
			$group->set_datastore( $this->get_datastore(), true );
204
		}
205
		return $this;
206
	}
207
208
	/**
209
	 * Return a clone of a field with hierarchy settings applied
210
	 *
211
	 * @param Field $field
212
	 * @param Field $parent_field
213
	 * @param int $entry_index
214
	 * @return Field
215
	 **/
216
	public function get_clone_under_field_in_hierarchy( $field, $parent_field, $entry_index = 0 ) {
217
		$clone = clone $field;
218
		$clone->set_hierarchy( array_merge( $parent_field->get_hierarchy(), array( $parent_field->get_base_name() ) ) );
219
		$clone->set_hierarchy_index( array_merge( $parent_field->get_hierarchy_index(), array( $entry_index ) ) );
220
		return $clone;
221
	}
222
223
	/**
224
	 * Load the field value from an input array based on it's name.
225
	 *
226
	 * @param array $input Array of field names and values.
227
	 **/
228
	public function set_value_from_input( $input ) {
229
		if ( ! isset( $input[ $this->get_name() ] ) ) {
230
			return;
231
		}
232
233
		$value_tree = array();
234
		$input_groups = $input[ $this->get_name() ];
235
		$input_group_index = 0;
236
		foreach ( $input_groups as $values ) {
237
			if ( ! isset( $values[ static::GROUP_TYPE_KEY ] ) || ! isset( $this->groups[ $values[ static::GROUP_TYPE_KEY ] ] ) ) {
238
				continue;
239
			}
240
241
			$group = $this->get_group_by_name( $values[ static::GROUP_TYPE_KEY ] );
242
			$value_group = array( static::GROUP_TYPE_KEY => $values[ static::GROUP_TYPE_KEY ] );
243
			unset( $values[ static::GROUP_TYPE_KEY ] );
244
245
			$group_fields = $group->get_fields();
246
247
			// trim input values to those used by the field
248
			$group_field_names = array_flip( $group->get_field_names() );
249
			$values = array_intersect_key( $values, $group_field_names );
250
			
251
			foreach ( $group_fields as $field ) {
252
				$tmp_field = $this->get_clone_under_field_in_hierarchy( $field, $this, $input_group_index );
253
254
				$tmp_field->set_value_from_input( $values );
255
				if ( is_a( $tmp_field, get_class() ) ) {
256
					$value_group[ $tmp_field->get_base_name() ] = $tmp_field->get_value_tree();
257
				} else {
258
					$value_group[ $tmp_field->get_base_name() ] = array(
259
						'value_set' => $tmp_field->get_value_set()->get_set(),
260
					);
261
				}
262
			}
263
264
			$value_tree['groups'][] = $value_group;
265
			$value_tree['value_set'][] = array(
266
				Value_Set::VALUE_PROPERTY => $group->get_name(),
267
			);
268
			$input_group_index++;
269
		}
270
271
		$this->set_value( $value_tree['value_set'] );
272
		$this->set_value_tree( $value_tree );
273
	}
274
275
	protected function get_prefilled_field_groups( $value_tree ) {
276
		$fields = array();
277
278
		if ( empty( $value_tree ) ) {
279
			return $fields;
280
		}
281
282
		foreach ( $value_tree['value_set'] as $entry_index => $value ) {
283
			$group_name = $value[ Value_Set::VALUE_PROPERTY ];
284
			$group = $this->get_group_by_name( $group_name );
285
			$group_fields = $group->get_fields();
286
			$fields[ $entry_index ] = array(
287
				static::GROUP_TYPE_KEY => $group->get_name(),
288
			);
289
			$group_values = array();
290
			if ( isset( $value_tree['groups'][ $entry_index ] ) ) {
291
				$group_values = $value_tree['groups'][ $entry_index ];
292
			}
293
294
			foreach ( $group_fields as $field ) {
295
				$clone = $this->get_clone_under_field_in_hierarchy( $field, $this, $entry_index );
296
				if ( isset( $group_values[ $clone->get_base_name() ] ) ) {
297
					$group_value = $group_values[ $clone->get_base_name() ];
298
					
299
					if ( isset( $group_value['value_set'] ) ) {
300
						$clone->set_value( $group_value['value_set'] );
301
					}
302
303
					if ( is_a( $clone, get_class() ) ) {
304
						$clone->set_value_tree( $group_value );
305
					}
306
				}
307
				$fields[ $entry_index ][] = $clone;
308
			}
309
		}
310
311
		return $fields;
312
	}
313
314
	/**
315
	 * Load all groups of fields and their data.
316
	 */
317
	public function load() {
318
		$raw_value_set_tree = $this->get_datastore()->load( $this );
319
		$value = null;
320
		if ( isset( $raw_value_set_tree[ $this->get_base_name() ] ) ) {
321
			$value = $raw_value_set_tree[ $this->get_base_name() ]['value_set'];
322
		}
323
		$this->set_value( $value );
324
325
		if ( $this->get_value() === null ) {
326
			$this->set_value( $this->get_default_value() );
327
		} else {
328
			$this->set_value_tree( $raw_value_set_tree[ $this->get_base_name() ] );
329
		}
330
	}
331
332
	/**
333
	 * Save all contained groups of fields.
334
	 */
335
	public function save() {
336
		// Only delete root field values as nested field values should be deleted in a cascading manner by the datastore
337
		$hierarchy = $this->get_hierarchy();
338
		if ( empty( $hierarchy ) ) {
339
			$this->delete();
340
		}
341
		
342
		$this->get_datastore()->save( $this );
343
344
		$field_groups = $this->get_prefilled_field_groups( $this->get_value_tree() );
345
346
		foreach ( $field_groups as $entry_index => $fields ) {
347
			foreach ( $fields as $field ) {
348
				if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
349
					continue;
350
				}
351
				$field->save();
352
			}
353
		}
354
	}
355
356
	/**
357
	 * Return the full value tree of all groups and their fields
358
	 *
359
	 * @return mixed
360
	 **/
361
	public function get_value_tree() {
362
		return (array) $this->value_tree;
363
	}
364
365
	/**
366
	 * Set the full value tree of all groups and their fields
367
	 *
368
	 * Tree Schema
369
	 * 'value_set' => array(
370
	 * 		array(
371
	 * 			'value' => '_',
372
	 * 		),
373
	 * 		...
374
	 * ),
375
	 * 'groups' => array(
376
	 * 		array(
377
	 * 			'field1' => array(
378
	 * 				'value_set'=>array(
379
	 * 					array(
380
	 * 						'value' => ...,
381
	 * 						...
382
	 * 					),
383
	 * 					...
384
	 * 				),
385
	 * 			),
386
	 * 			...
387
	 * 		),
388
	 * 		...
389
	 * ),
390
	 * 
391
	 * @return mixed
392
	 **/
393
	public function set_value_tree( $value_tree ) {
394
		return $this->value_tree = $value_tree;
395
	}
396
397
	/**
398
	 * Return a differently formatted value for end-users
399
	 *
400
	 * @return mixed
401
	 **/
402
	public function get_formatted_value() {
403
		$field_groups = $this->get_prefilled_field_groups( $this->get_value_tree() );
404
		
405
		$value = array();
406
		foreach ( $field_groups as $entry_index => $field_group ) {
407
			$value[ $entry_index ] = array();
408
			foreach ( $field_group as $key => $field ) {
409
				if ( is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
410
					$value[ $entry_index ][ $field->get_base_name() ] = $field->get_formatted_value(); 
411
				} else {
412
					$value[ $entry_index ][ $key ] = $field;
413
				}
414
			}
415
		}
416
		return $value;
417
	}
418
419
	/**
420
	 * Returns an array that holds the field data, suitable for JSON representation.
421
	 * This data will be available in the Underscore template and the Backbone Model.
422
	 *
423
	 * @param bool $load  Should the value be loaded from the database or use the value from the current instance.
424
	 * @return array
425
	 */
426
	public function to_json( $load ) {
427
		$complex_data = parent::to_json( $load );
428
429
		$groups_data = array();
430
		foreach ( $this->groups as $group ) {
431
			$groups_data[] = $group->to_json( false );
432
		}
433
434
		$field_groups = $this->get_prefilled_field_groups( $this->get_value_tree() );
435
		$value_data = array();
436
		foreach ( $field_groups as $entry_index => $fields ) {
437
			$group = $this->get_group_by_name( $fields[ static::GROUP_TYPE_KEY ] );
438
439
			$data = array(
440
				'name' => $group->get_name(),
441
				'label' => $group->get_label(),
442
				'group_id' => $group->get_group_id(),
443
				'fields' => array(),
444
			);
445
446
			foreach ( $fields as $field ) {
447
				if ( ! is_a( $field, __NAMESPACE__ . '\\Field' ) ) {
448
					continue;
449
				}
450
				$data['fields'][] = $field->to_json( false );
451
			}
452
453
			$value_data[] = $data;
454
		}
455
456
		$complex_data = array_merge( $complex_data, array(
457
			'layout' => $this->layout,
458
			'labels' => $this->labels,
459
			'min' => $this->get_min(),
460
			'max' => $this->get_max(),
461
			'multiple_groups' => count( $groups_data ) > 1,
462
			'groups' => $groups_data,
463
			'value' => $value_data,
464
			'collapsed' => $this->collapsed,
465
		) );
466
		return $complex_data;
467
	}
468
469
	/**
470
	 * The main Underscore template.
471
	 */
472
	public function template() {
473
		?>
474
		<div class="carbon-subcontainer carbon-grid {{ multiple_groups ? 'multiple-groups' : '' }}">
475
			<div class="carbon-empty-row carbon-empty-row-visible">
476
				{{{ crbl10n.complex_no_rows.replace('%s', labels.plural_name) }}}
477
			</div>
478
479
			<div class="groups-wrapper layout-{{ layout }}">
480
				<# if (layout === '<?php echo static::LAYOUT_TABBED_HORIZONTAL ?>' || layout === '<?php echo static::LAYOUT_TABBED_VERTICAL ?>' ) { #>
481
					<div class="group-tabs-nav-holder">
482
						<ul class="group-tabs-nav"></ul>
483
484
						<div class="carbon-actions">
485
							<div class="carbon-button">
486
								<a href="#" class="button" data-group="{{{ multiple_groups ? '' : groups[0].name }}}">
487
									+
488
								</a>
489
490
								<# if (multiple_groups) { #>
491
									<ul>
492
										<# _.each(groups, function(group) { #>
493
											<li><a href="#" data-group="{{{ group.name }}}">{{{ group.label }}}</a></li>
494
										<# }); #>
495
									</ul>
496
								<# } #>
497
							</div>
498
						</div>
499
					</div><!-- /.group-tabs-nav-holder -->
500
				<# } #>
501
502
				<div class="carbon-groups-holder"></div>
503
				<div class="clear"></div>
504
			</div>
505
506
			<div class="carbon-actions">
507
				<div class="carbon-button">
508
					<a href="#" class="button" data-group="{{{ multiple_groups ? '' : groups[0].name }}}">
509
						{{{ crbl10n.complex_add_button.replace('%s', labels.singular_name) }}}
510
					</a>
511
512
					<# if (multiple_groups) { #>
513
						<ul>
514
							<# _.each(groups, function(group) { #>
515
								<li><a href="#" data-group="{{{ group.name }}}">{{{ group.label }}}</a></li>
516
							<# }); #>
517
						</ul>
518
					<# } #>
519
				</div>
520
			</div>
521
		</div>
522
		<?php
523
	}
524
525
	/**
526
	 * The Underscore template for the complex field group.
527
	 */
528
	public function template_group() {
529
		?>
530
		<div id="carbon-{{{ complex_name }}}-complex-container" class="carbon-row carbon-group-row" data-group-id="{{ id }}">
531
			<input type="hidden" name="{{{ complex_name + '[' + index + ']' }}}[_type]" value="{{ name }}" />
532
533
			<div class="carbon-drag-handle">
534
				<span class="group-number">{{{ order + 1 }}}</span><span class="group-name">{{{ label_template || label }}}</span>
535
			</div>
536
537
			<div class="carbon-group-actions carbon-group-actions-{{ layout }}">
538
				<a class="carbon-btn-duplicate dashicons-before dashicons-admin-page" href="#" title="<?php esc_attr_e( 'Clone', \Carbon_Fields\TEXT_DOMAIN ); ?>">
539
					<?php _e( 'Clone', \Carbon_Fields\TEXT_DOMAIN ); ?>
540
				</a>
541
542
				<a class="carbon-btn-remove dashicons-before dashicons-trash" href="#" title="<?php esc_attr_e( 'Remove', \Carbon_Fields\TEXT_DOMAIN ); ?>">
543
					<?php _e( 'Remove', \Carbon_Fields\TEXT_DOMAIN ); ?>
544
				</a>
545
546
				<a class="carbon-btn-collapse dashicons-before dashicons-arrow-up" href="#" title="<?php esc_attr_e( 'Collapse/Expand', \Carbon_Fields\TEXT_DOMAIN ); ?>">
547
					<?php _e( 'Collapse/Expand', \Carbon_Fields\TEXT_DOMAIN ); ?>
548
				</a>
549
			</div>
550
551
			<div class="fields-container">
552
				<# _.each(fields, function(field) { #>
553
					<div class="carbon-row carbon-subrow subrow-{{{ field.type }}} {{{ field.classes.join(' ') }}}">
554
						<label for="{{{ complex_id + '-' + field.id + '-' + index }}}">
555
							{{ field.label }}
556
557
							<# if (field.required) { #>
558
								 <span class="carbon-required">*</span>
559
							<# } #>
560
						</label>
561
562
						<div class="field-holder {{{ complex_id + '-' + field.id + '-' + index }}}"></div>
563
564
						<# if (field.help_text) { #>
565
							<em class="help-text">
566
								{{{ field.help_text }}}
567
							</em>
568
						<# } #>
569
570
						<em class="carbon-error"></em>
571
					</div>
572
				<# }) #>
573
			</div>
574
		</div>
575
		<?php
576
	}
577
578
	 /**
579
	 * The Underscore template for the group item tab.
580
	 */
581
	public function template_group_tab_item() {
582
		?>
583
		<li class="group-tab-item" data-group-id="{{ id }}">
584
			<a href="#">
585
				<span class="group-handle"></span>
586
587
				<# if (label_template || label) { #>
588
					<span class="group-name">{{{ label_template || label }}}</span>
589
				<# } #>
590
				<span class="group-number">{{{ order + 1 }}}</span>
591
				<span class="dashicons dashicons-warning carbon-complex-group-error-badge" ></span>
592
			</a>
593
		</li>
594
		<?php
595
	}
596
597
	/**
598
	 * Modify the layout of this field.
599
	 *
600
	 * @param string $layout
601
	 */
602
	public function set_layout( $layout ) {
603
		$available_layouts = array(
604
			static::LAYOUT_GRID,
605
			static::LAYOUT_TABBED_HORIZONTAL,
606
			static::LAYOUT_TABBED_VERTICAL,
607
		);
608
609
		if ( ! in_array( $layout,  $available_layouts ) ) {
610
			$error_message = 'Incorrect layout ``' . $layout . '" specified. ' .
611
				'Available layouts: ' . implode( ', ', $available_layouts );
612
613
			Incorrect_Syntax_Exception::raise( $error_message );
614
		}
615
616
		$this->layout = $layout;
617
618
		return $this;
619
	}
620
621
	/**
622
	 * Set the minimum number of entries.
623
	 *
624
	 * @param int $min
625
	 */
626
	public function set_min( $min ) {
627
		$this->values_min = intval( $min );
628
		return $this;
629
	}
630
631
	/**
632
	 * Get the minimum number of entries.
633
	 *
634
	 * @return int $min
635
	 */
636
	public function get_min() {
637
		return $this->values_min;
638
	}
639
640
	/**
641
	 * Set the maximum number of entries.
642
	 *
643
	 * @param int $max
644
	 */
645
	public function set_max( $max ) {
646
		$this->values_max = intval( $max );
647
		return $this;
648
	}
649
650
	/**
651
	 * Get the maximum number of entries.
652
	 *
653
	 * @return int $max
654
	 */
655
	public function get_max() {
656
		return $this->values_max;
657
	}
658
659
	/**
660
	 * Change the groups initial collapse state.
661
	 * This state relates to the state of which the groups are rendered.
662
	 *
663
	 * @param bool $collapsed
664
	 */
665
	public function set_collapsed( $collapsed = true ) {
666
		$this->collapsed = $collapsed;
667
668
		return $this;
669
	}
670
671
	/**
672
	 * Retrieve the groups of this field.
673
	 *
674
	 * @return array
675
	 */
676
	public function get_group_names() {
677
		return array_keys( $this->groups );
678
	}
679
680
	/**
681
	 * Retrieve a group by its name.
682
	 *
683
	 * @param  string $group_name        Group name
684
	 * @return Group_Field $group_object Group object
685
	 */
686
	public function get_group_by_name( $group_name ) {
687
		$group_object = null;
688
689
		foreach ( $this->groups as $group ) {
690
			if ( $group->get_name() == $group_name ) {
691
				$group_object = $group;
692
			}
693
		}
694
695
		return $group_object;
696
	}
697
}
698