Completed
Pull Request — master (#1710)
by Aristeides
14:05 queued 02:09
created

controls/js/src/repeater.js   F

Complexity

Total Complexity 140
Complexity/F 2.55

Size

Lines of Code 901
Function Count 55

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
nc 37748736
dl 0
loc 901
rs 2.1818
c 0
b 0
f 0
wmc 140
mnd 7
bc 115
fnc 55
bpm 2.0909
cpm 2.5454
noi 32

How to fix   Complexity   

Complexity

Complex classes like controls/js/src/repeater.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/* global kirkiControlLoader */
2
var RepeaterRow = function( rowIndex, container, label, control ) {
3
4
	'use strict';
5
6
	var self        = this;
7
	this.rowIndex   = rowIndex;
8
	this.container  = container;
9
	this.label      = label;
10
	this.header     = this.container.find( '.repeater-row-header' ),
11
12
	this.header.on( 'click', function() {
13
		self.toggleMinimize();
14
	});
15
16
	this.container.on( 'click', '.repeater-row-remove', function() {
17
		self.remove();
18
	});
19
20
	this.header.on( 'mousedown', function() {
21
		self.container.trigger( 'row:start-dragging' );
22
	});
23
24
	this.container.on( 'keyup change', 'input, select, textarea', function( e ) {
25
		self.container.trigger( 'row:update', [ self.rowIndex, jQuery( e.target ).data( 'field' ), e.target ] );
26
	});
27
28
	this.setRowIndex = function( rowIndex ) {
29
		this.rowIndex = rowIndex;
30
		this.container.attr( 'data-row', rowIndex );
31
		this.container.data( 'row', rowIndex );
32
		this.updateLabel();
33
	};
34
35
	this.toggleMinimize = function() {
36
37
		// Store the previous state.
38
		this.container.toggleClass( 'minimized' );
39
		this.header.find( '.dashicons' ).toggleClass( 'dashicons-arrow-up' ).toggleClass( 'dashicons-arrow-down' );
40
	};
41
42
	this.remove = function() {
43
		this.container.slideUp( 300, function() {
44
			jQuery( this ).detach();
45
		});
46
		this.container.trigger( 'row:remove', [ this.rowIndex ] );
47
	};
48
49
	this.updateLabel = function() {
50
		var rowLabelField,
51
		    rowLabel,
52
		    rowLabelSelector;
53
54
		if ( 'field' === this.label.type ) {
55
			rowLabelField = this.container.find( '.repeater-field [data-field="' + this.label.field + '"]' );
56
			if ( _.isFunction( rowLabelField.val ) ) {
57
				rowLabel = rowLabelField.val();
58
				if ( '' !== rowLabel ) {
59
					if ( ! _.isUndefined( control.params.fields[ this.label.field ] ) ) {
60
						if ( ! _.isUndefined( control.params.fields[ this.label.field ].type ) ) {
61
							if ( 'select' === control.params.fields[ this.label.field ].type ) {
62
								if ( ! _.isUndefined( control.params.fields[ this.label.field ].choices ) && ! _.isUndefined( control.params.fields[ this.label.field ].choices[ rowLabelField.val() ] ) ) {
63
									rowLabel = control.params.fields[ this.label.field ].choices[ rowLabelField.val() ];
64
								}
65
							} else if ( 'radio' === control.params.fields[ this.label.field ].type || 'radio-image' === control.params.fields[ this.label.field ].type ) {
66
								rowLabelSelector = control.selector + ' [data-row="' + this.rowIndex + '"] .repeater-field [data-field="' + this.label.field + '"]:checked';
67
								rowLabel = jQuery( rowLabelSelector ).val();
68
							}
69
						}
70
					}
71
					this.header.find( '.repeater-row-label' ).text( rowLabel );
72
					return;
73
				}
74
			}
75
		}
76
		this.header.find( '.repeater-row-label' ).text( this.label.value + ' ' + ( this.rowIndex + 1 ) );
77
	};
78
	this.updateLabel();
79
};
80
81
wp.customize.controlConstructor.repeater = wp.customize.Control.extend({
82
83
	// When we're finished loading continue processing
84
	ready: function() {
85
86
		'use strict';
87
88
		var control = this;
89
90
		// Init the control.
91
		if ( ! _.isUndefined( window.kirkiControlLoader ) && _.isFunction( kirkiControlLoader ) ) {
92
			kirkiControlLoader( control );
93
		} else {
94
			control.initKirkiControl();
95
		}
96
	},
97
98
	initKirkiControl: function() {
99
100
		'use strict';
101
102
		var control = this,
103
		    limit,
104
		    theNewRow;
105
106
		// The current value set in Control Class (set in Kirki_Customize_Repeater_Control::to_json() function)
107
		var settingValue = this.params.value;
108
109
		control.container.find( '.kirki-controls-loading-spinner' ).hide();
110
111
		// The hidden field that keeps the data saved (though we never update it)
112
		this.settingField = this.container.find( '[data-customize-setting-link]' ).first();
113
114
		// Set the field value for the first time, we'll fill it up later
115
		this.setValue( [], false );
116
117
		// The DIV that holds all the rows
118
		this.repeaterFieldsContainer = this.container.find( '.repeater-fields' ).first();
119
120
		// Set number of rows to 0
121
		this.currentIndex = 0;
122
123
		// Save the rows objects
124
		this.rows = [];
125
126
		// Default limit choice
127
		limit = false;
128
		if ( ! _.isUndefined( this.params.choices.limit ) ) {
129
			limit = ( 0 >= this.params.choices.limit ) ? false : parseInt( this.params.choices.limit, 10 );
130
		}
131
132
		this.container.on( 'click', 'button.repeater-add', function( e ) {
133
			e.preventDefault();
134
			if ( ! limit || control.currentIndex < limit ) {
135
				theNewRow = control.addRow();
136
				theNewRow.toggleMinimize();
137
				control.initColorPicker();
138
				control.initSelect( theNewRow );
139
			} else {
140
				jQuery( control.selector + ' .limit' ).addClass( 'highlight' );
141
			}
142
		});
143
144
		this.container.on( 'click', '.repeater-row-remove', function() {
145
			control.currentIndex--;
146
			if ( ! limit || control.currentIndex < limit ) {
147
				jQuery( control.selector + ' .limit' ).removeClass( 'highlight' );
148
			}
149
		});
150
151
		this.container.on( 'click keypress', '.repeater-field-image .upload-button,.repeater-field-cropped_image .upload-button,.repeater-field-upload .upload-button', function( e ) {
152
			e.preventDefault();
153
			control.$thisButton = jQuery( this );
154
			control.openFrame( e );
155
		});
156
157
		this.container.on( 'click keypress', '.repeater-field-image .remove-button,.repeater-field-cropped_image .remove-button', function( e ) {
158
			e.preventDefault();
159
			control.$thisButton = jQuery( this );
160
			control.removeImage( e );
161
		});
162
163
		this.container.on( 'click keypress', '.repeater-field-upload .remove-button', function( e ) {
164
			e.preventDefault();
165
			control.$thisButton = jQuery( this );
166
			control.removeFile( e );
167
		});
168
169
		/**
170
		 * Function that loads the Mustache template
171
		 */
172
		this.repeaterTemplate = _.memoize( function() {
173
			var compiled,
174
			    /*
175
			     * Underscore's default ERB-style templates are incompatible with PHP
176
			     * when asp_tags is enabled, so WordPress uses Mustache-inspired templating syntax.
177
			     *
178
			     * @see trac ticket #22344.
179
			     */
180
			    options = {
181
					evaluate: /<#([\s\S]+?)#>/g,
182
					interpolate: /\{\{\{([\s\S]+?)\}\}\}/g,
183
					escape: /\{\{([^\}]+?)\}\}(?!\})/g,
184
					variable: 'data'
185
			    };
186
187
			return function( data ) {
188
				compiled = _.template( control.container.find( '.customize-control-repeater-content' ).first().html(), null, options );
189
				return compiled( data );
190
			};
191
		});
192
193
		// When we load the control, the fields have not been filled up
194
		// This is the first time that we create all the rows
195
		if ( settingValue.length ) {
196
			_.each( settingValue, function( subValue ) {
197
				theNewRow = control.addRow( subValue );
198
				control.initColorPicker();
199
				control.initSelect( theNewRow, subValue );
200
			});
201
		}
202
203
		// Once we have displayed the rows, we cleanup the values
204
		this.setValue( settingValue, true, true );
205
206
		this.repeaterFieldsContainer.sortable({
207
			handle: '.repeater-row-header',
208
			update: function() {
209
				control.sort();
210
			}
211
		});
212
213
	},
214
215
	/**
216
	 * Open the media modal.
217
	 */
218
	openFrame: function( event ) {
219
220
		'use strict';
221
222
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
223
			return;
224
		}
225
226
		if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-cropped_image' ) ) {
227
			this.initCropperFrame();
228
		} else {
229
			this.initFrame();
230
		}
231
232
		this.frame.open();
233
	},
234
235
	initFrame: function() {
236
237
		'use strict';
238
239
		var libMediaType = this.getMimeType();
240
241
		this.frame = wp.media({
242
			states: [
243
			new wp.media.controller.Library({
244
					library:  wp.media.query({ type: libMediaType }),
245
					multiple: false,
246
					date:     false
247
				})
248
			]
249
		});
250
251
		// When a file is selected, run a callback.
252
		this.frame.on( 'select', this.onSelect, this );
253
	},
254
	/**
255
	 * Create a media modal select frame, and store it so the instance can be reused when needed.
256
	 * This is mostly a copy/paste of Core api.CroppedImageControl in /wp-admin/js/customize-control.js
257
	 */
258
	initCropperFrame: function() {
259
260
		'use strict';
261
262
		// We get the field id from which this was called
263
		var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' ),
264
		    attrs          = [ 'width', 'height', 'flex_width', 'flex_height' ], // A list of attributes to look for
265
		    libMediaType   = this.getMimeType();
266
267
		// Make sure we got it
268
		if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
269
270
			// Make fields is defined and only do the hack for cropped_image
271
			if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'cropped_image' === this.params.fields[ currentFieldId ].type ) {
272
273
				//Iterate over the list of attributes
274
				attrs.forEach( function( el ) {
275
276
					// If the attribute exists in the field
277
					if ( ! _.isUndefined( this.params.fields[ currentFieldId ][ el ] ) ) {
278
279
						// Set the attribute in the main object
280
						this.params[ el ] = this.params.fields[ currentFieldId ][ el ];
281
					}
282
				}.bind( this ) );
283
			}
284
		}
285
286
		this.frame = wp.media({
287
			button: {
288
				text: 'Select and Crop',
289
				close: false
290
			},
291
			states: [
292
				new wp.media.controller.Library({
293
					library:         wp.media.query({ type: libMediaType }),
294
					multiple:        false,
295
					date:            false,
296
					suggestedWidth:  this.params.width,
297
					suggestedHeight: this.params.height
298
				}),
299
				new wp.media.controller.CustomizeImageCropper({
300
					imgSelectOptions: this.calculateImageSelectOptions,
301
					control: this
302
				})
303
			]
304
		});
305
306
		this.frame.on( 'select', this.onSelectForCrop, this );
307
		this.frame.on( 'cropped', this.onCropped, this );
308
		this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
309
310
	},
311
312
	onSelect: function() {
313
314
		'use strict';
315
316
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
317
318
		if ( this.$thisButton.closest( '.repeater-field' ).hasClass( 'repeater-field-upload' ) ) {
319
			this.setFileInRepeaterField( attachment );
320
		} else {
321
			this.setImageInRepeaterField( attachment );
322
		}
323
	},
324
325
	/**
326
	 * After an image is selected in the media modal, switch to the cropper
327
	 * state if the image isn't the right size.
328
	 */
329
330
	onSelectForCrop: function() {
331
332
		'use strict';
333
334
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
335
336
		if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
337
			this.setImageInRepeaterField( attachment );
338
		} else {
339
			this.frame.setState( 'cropper' );
340
		}
341
	},
342
343
	/**
344
	 * After the image has been cropped, apply the cropped image data to the setting.
345
	 *
346
	 * @param {object} croppedImage Cropped attachment data.
347
	 */
348
	onCropped: function( croppedImage ) {
349
350
		'use strict';
351
352
		this.setImageInRepeaterField( croppedImage );
353
354
	},
355
356
	/**
357
	 * Returns a set of options, computed from the attached image data and
358
	 * control-specific data, to be fed to the imgAreaSelect plugin in
359
	 * wp.media.view.Cropper.
360
	 *
361
	 * @param {wp.media.model.Attachment} attachment
362
	 * @param {wp.media.controller.Cropper} controller
363
	 * @returns {Object} Options
364
	 */
365
	calculateImageSelectOptions: function( attachment, controller ) {
366
367
		'use strict';
368
369
		var control    = controller.get( 'control' ),
370
		    flexWidth  = !! parseInt( control.params.flex_width, 10 ),
371
		    flexHeight = !! parseInt( control.params.flex_height, 10 ),
372
		    realWidth  = attachment.get( 'width' ),
373
		    realHeight = attachment.get( 'height' ),
374
		    xInit      = parseInt( control.params.width, 10 ),
375
		    yInit      = parseInt( control.params.height, 10 ),
376
		    ratio      = xInit / yInit,
377
		    xImg       = realWidth,
378
		    yImg       = realHeight,
379
		    x1,
380
		    y1,
381
		    imgSelectOptions;
382
383
		controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
384
385
		if ( xImg / yImg > ratio ) {
386
			yInit = yImg;
387
			xInit = yInit * ratio;
388
		} else {
389
			xInit = xImg;
390
			yInit = xInit / ratio;
391
		}
392
393
		x1 = ( xImg - xInit ) / 2;
394
		y1 = ( yImg - yInit ) / 2;
395
396
		imgSelectOptions = {
397
			handles:     true,
398
			keys:        true,
399
			instance:    true,
400
			persistent:  true,
401
			imageWidth:  realWidth,
402
			imageHeight: realHeight,
403
			x1:          x1,
404
			y1:          y1,
405
			x2:          xInit + x1,
406
			y2:          yInit + y1
407
		};
408
409
		if ( false === flexHeight && false === flexWidth ) {
410
			imgSelectOptions.aspectRatio = xInit + ':' + yInit;
411
		}
412
		if ( false === flexHeight ) {
413
			imgSelectOptions.maxHeight = yInit;
414
		}
415
		if ( false === flexWidth ) {
416
			imgSelectOptions.maxWidth = xInit;
417
		}
418
419
		return imgSelectOptions;
420
	},
421
422
	/**
423
	 * Return whether the image must be cropped, based on required dimensions.
424
	 *
425
	 * @param {bool} flexW
426
	 * @param {bool} flexH
427
	 * @param {int}  dstW
428
	 * @param {int}  dstH
429
	 * @param {int}  imgW
430
	 * @param {int}  imgH
431
	 * @return {bool}
432
	 */
433
	mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
434
435
		'use strict';
436
437
		if ( ( true === flexW && true === flexH ) || ( true === flexW && dstH === imgH ) || ( true === flexH && dstW === imgW ) || ( dstW === imgW && dstH === imgH ) || ( imgW <= dstW ) ) {
438
			return false;
439
		}
440
441
		return true;
442
	},
443
444
	/**
445
	 * If cropping was skipped, apply the image data directly to the setting.
446
	 */
447
	onSkippedCrop: function() {
448
449
		'use strict';
450
451
		var attachment = this.frame.state().get( 'selection' ).first().toJSON();
452
		this.setImageInRepeaterField( attachment );
453
454
	},
455
456
	/**
457
	 * Updates the setting and re-renders the control UI.
458
	 *
459
	 * @param {object} attachment
460
	 */
461
	setImageInRepeaterField: function( attachment ) {
462
463
		'use strict';
464
465
		var $targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image' );
466
467
		$targetDiv.find( '.kirki-image-attachment' ).html( '<img src="' + attachment.url + '">' ).hide().slideDown( 'slow' );
468
469
		$targetDiv.find( '.hidden-field' ).val( attachment.id );
470
		this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
471
		$targetDiv.find( '.remove-button' ).show();
472
473
		//This will activate the save button
474
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
475
		this.frame.close();
476
477
	},
478
479
	/**
480
	 * Updates the setting and re-renders the control UI.
481
	 *
482
	 * @param {object} attachment
483
	 */
484
	setFileInRepeaterField: function( attachment ) {
485
486
		'use strict';
487
488
		var $targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
489
490
		$targetDiv.find( '.kirki-file-attachment' ).html( '<span class="file"><span class="dashicons dashicons-media-default"></span> ' + attachment.filename + '</span>' ).hide().slideDown( 'slow' );
491
492
		$targetDiv.find( '.hidden-field' ).val( attachment.id );
493
		this.$thisButton.text( this.$thisButton.data( 'alt-label' ) );
494
		$targetDiv.find( '.upload-button' ).show();
495
		$targetDiv.find( '.remove-button' ).show();
496
497
		//This will activate the save button
498
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
499
		this.frame.close();
500
501
	},
502
503
	getMimeType: function() {
504
505
		'use strict';
506
507
		// We get the field id from which this was called
508
		var currentFieldId = this.$thisButton.siblings( 'input.hidden-field' ).attr( 'data-field' );
509
510
		// Make sure we got it
511
		if ( _.isString( currentFieldId ) && '' !== currentFieldId ) {
512
513
			// Make fields is defined and only do the hack for cropped_image
514
			if ( _.isObject( this.params.fields[ currentFieldId ] ) && 'upload' === this.params.fields[ currentFieldId ].type ) {
515
516
				// If the attribute exists in the field
517
				if ( ! _.isUndefined( this.params.fields[ currentFieldId ].mime_type ) ) {
518
519
					// Set the attribute in the main object
520
					return this.params.fields[ currentFieldId ].mime_type;
521
				}
522
			}
523
		}
524
		return 'image';
525
526
	},
527
528
	removeImage: function( event ) {
529
530
		'use strict';
531
532
		var $targetDiv,
533
		    $uploadButton;
534
535
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
536
			return;
537
		}
538
539
		$targetDiv = this.$thisButton.closest( '.repeater-field-image,.repeater-field-cropped_image,.repeater-field-upload' );
540
		$uploadButton = $targetDiv.find( '.upload-button' );
541
542
		$targetDiv.find( '.kirki-image-attachment' ).slideUp( 'fast', function() {
543
			jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
544
		});
545
		$targetDiv.find( '.hidden-field' ).val( '' );
546
		$uploadButton.text( $uploadButton.data( 'label' ) );
547
		this.$thisButton.hide();
548
549
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
550
551
	},
552
553
	removeFile: function( event ) {
554
555
		'use strict';
556
557
		var $targetDiv,
558
		    $uploadButton;
559
560
		if ( wp.customize.utils.isKeydownButNotEnterEvent( event ) ) {
561
			return;
562
		}
563
564
		$targetDiv = this.$thisButton.closest( '.repeater-field-upload' );
565
		$uploadButton = $targetDiv.find( '.upload-button' );
566
567
		$targetDiv.find( '.kirki-file-attachment' ).slideUp( 'fast', function() {
568
			jQuery( this ).show().html( jQuery( this ).data( 'placeholder' ) );
569
		});
570
		$targetDiv.find( '.hidden-field' ).val( '' );
571
		$uploadButton.text( $uploadButton.data( 'label' ) );
572
		this.$thisButton.hide();
573
574
		$targetDiv.find( 'input, textarea, select' ).trigger( 'change' );
575
576
	},
577
578
	/**
579
	 * Get the current value of the setting
580
	 *
581
	 * @return Object
582
	 */
583
	getValue: function() {
584
585
		'use strict';
586
587
		// The setting is saved in JSON
588
		return JSON.parse( decodeURI( this.setting.get() ) );
589
590
	},
591
592
	/**
593
	 * Set a new value for the setting
594
	 *
595
	 * @param newValue Object
596
	 * @param refresh If we want to refresh the previewer or not
597
	 */
598
	setValue: function( newValue, refresh, filtering ) {
599
600
		'use strict';
601
602
		// We need to filter the values after the first load to remove data requrired for diplay but that we don't want to save in DB
603
		var filteredValue = newValue,
604
		    filter        = [];
605
606
		if ( filtering ) {
607
			jQuery.each( this.params.fields, function( index, value ) {
608
				if ( 'image' === value.type || 'cropped_image' === value.type || 'upload' === value.type ) {
609
					filter.push( index );
610
				}
611
			});
612
			jQuery.each( newValue, function( index, value ) {
613
				jQuery.each( filter, function( ind, field ) {
614
					if ( ! _.isUndefined( value[ field ] ) && ! _.isUndefined( value[ field ].id ) ) {
615
						filteredValue[index][ field ] = value[ field ].id;
616
					}
617
				});
618
			});
619
		}
620
621
		this.setting.set( encodeURI( JSON.stringify( filteredValue ) ) );
622
623
		if ( refresh ) {
624
625
			// Trigger the change event on the hidden field so
626
			// previewer refresh the website on Customizer
627
			this.settingField.trigger( 'change' );
628
		}
629
	},
630
631
	/**
632
	 * Add a new row to repeater settings based on the structure.
633
	 *
634
	 * @param data (Optional) Object of field => value pairs (undefined if you want to get the default values)
635
	 */
636
	addRow: function( data ) {
637
638
		'use strict';
639
640
		var control       = this,
641
		    template      = control.repeaterTemplate(), // The template for the new row (defined on Kirki_Customize_Repeater_Control::render_content() ).
642
		    settingValue  = this.getValue(), // Get the current setting value.
643
		    newRowSetting = {}, // Saves the new setting data.
644
		    templateData, // Data to pass to the template
645
		    newRow,
646
		    i;
647
648
		if ( template ) {
649
650
			// The control structure is going to define the new fields
651
			// We need to clone control.params.fields. Assigning it
652
			// ould result in a reference assignment.
653
			templateData = jQuery.extend( true, {}, control.params.fields );
654
655
			// But if we have passed data, we'll use the data values instead
656
			if ( data ) {
657
				for ( i in data ) {
658
					if ( data.hasOwnProperty( i ) && templateData.hasOwnProperty( i ) ) {
659
						templateData[ i ]['default'] = data[ i ];
660
					}
661
				}
662
			}
663
664
			templateData.index = this.currentIndex;
665
666
			// Append the template content
667
			template = template( templateData );
668
669
			// Create a new row object and append the element
670
			newRow = new RepeaterRow(
671
				control.currentIndex,
672
				jQuery( template ).appendTo( control.repeaterFieldsContainer ),
673
				control.params.row_label,
674
				control
675
			);
676
677
			newRow.container.on( 'row:remove', function( e, rowIndex ) {
678
				control.deleteRow( rowIndex );
679
			});
680
681
			newRow.container.on( 'row:update', function( e, rowIndex, fieldName, element ) {
682
				control.updateField.call( control, e, rowIndex, fieldName, element );
683
				newRow.updateLabel();
684
			});
685
686
			// Add the row to rows collection
687
			this.rows[ this.currentIndex ] = newRow;
688
689
			for ( i in templateData ) {
690
				if ( templateData.hasOwnProperty( i ) ) {
691
					newRowSetting[ i ] = templateData[ i ]['default'];
692
				}
693
			}
694
695
			settingValue[ this.currentIndex ] = newRowSetting;
696
			this.setValue( settingValue, true );
697
698
			this.currentIndex++;
699
700
			return newRow;
701
		}
702
	},
703
704
	sort: function() {
705
706
		'use strict';
707
708
		var control     = this,
709
		    $rows       = this.repeaterFieldsContainer.find( '.repeater-row' ),
710
		    newOrder    = [],
711
		    settings    = control.getValue(),
712
		    newRows     = [],
713
		    newSettings = [];
714
715
		$rows.each( function( i, element ) {
716
			newOrder.push( jQuery( element ).data( 'row' ) );
717
		});
718
719
		jQuery.each( newOrder, function( newPosition, oldPosition ) {
720
			newRows[ newPosition ] = control.rows[ oldPosition ];
721
			newRows[ newPosition ].setRowIndex( newPosition );
722
723
			newSettings[ newPosition ] = settings[ oldPosition ];
724
		});
725
726
		control.rows = newRows;
727
		control.setValue( newSettings );
728
729
	},
730
731
	/**
732
	 * Delete a row in the repeater setting
733
	 *
734
	 * @param index Position of the row in the complete Setting Array
735
	 */
736
	deleteRow: function( index ) {
737
738
		'use strict';
739
740
		var currentSettings = this.getValue(),
741
		    row,
742
		    i,
743
		    prop;
744
745
		if ( currentSettings[ index ] ) {
746
747
			// Find the row
748
			row = this.rows[ index ];
749
			if ( row ) {
750
751
				// Remove the row settings
752
				delete currentSettings[ index ];
753
754
				// Remove the row from the rows collection
755
				delete this.rows[ index ];
756
757
				// Update the new setting values
758
				this.setValue( currentSettings, true );
759
760
			}
761
762
		}
763
764
		// Remap the row numbers
765
		i = 1;
766
		for ( prop in this.rows ) {
767
			if ( this.rows.hasOwnProperty( prop ) && this.rows[ prop ] ) {
768
				this.rows[ prop ].updateLabel();
769
				i++;
770
			}
771
		}
772
	},
773
774
	/**
775
	 * Update a single field inside a row.
776
	 * Triggered when a field has changed
777
	 *
778
	 * @param e Event Object
779
	 */
780
	updateField: function( e, rowIndex, fieldId, element ) {
781
782
		'use strict';
783
784
		var type,
785
		    row,
786
		    currentSettings;
787
788
		if ( ! this.rows[ rowIndex ] ) {
789
			return;
790
		}
791
792
		if ( ! this.params.fields[ fieldId ] ) {
793
			return;
794
		}
795
796
		type            = this.params.fields[ fieldId].type;
797
		row             = this.rows[ rowIndex ];
798
		currentSettings = this.getValue();
799
800
		element = jQuery( element );
801
802
		if ( _.isUndefined( currentSettings[ row.rowIndex ][ fieldId ] ) ) {
803
			return;
804
		}
805
806
		if ( 'checkbox' === type ) {
807
			currentSettings[ row.rowIndex ][ fieldId ] = element.is( ':checked' );
808
		} else {
809
810
			// Update the settings
811
			currentSettings[ row.rowIndex ][ fieldId ] = element.val();
812
		}
813
		this.setValue( currentSettings, true );
814
	},
815
816
	/**
817
	 * Init the color picker on color fields
818
	 * Called after AddRow
819
	 *
820
	 */
821
	initColorPicker: function() {
822
823
		'use strict';
824
825
		var control     = this,
826
		    colorPicker = control.container.find( '.color-picker-hex' ),
827
		    options     = {},
828
		    fieldId     = colorPicker.data( 'field' );
829
830
		// We check if the color palette parameter is defined.
831
		if ( ! _.isUndefined( fieldId ) && ! _.isUndefined( control.params.fields[ fieldId ] ) && ! _.isUndefined( control.params.fields[ fieldId ].palettes ) && _.isObject( control.params.fields[ fieldId ].palettes ) ) {
832
			options.palettes = control.params.fields[ fieldId ].palettes;
833
		}
834
835
		// When the color picker value is changed we update the value of the field
836
		options.change = function( event, ui ) {
837
838
			var currentPicker   = jQuery( event.target ),
839
			    row             = currentPicker.closest( '.repeater-row' ),
840
			    rowIndex        = row.data( 'row' ),
841
			    currentSettings = control.getValue();
842
843
			currentSettings[ rowIndex ][ currentPicker.data( 'field' ) ] = ui.color.toString();
844
			control.setValue( currentSettings, true );
845
846
		};
847
848
		// Init the color picker
849
		if ( 0 !== colorPicker.length ) {
850
			colorPicker.wpColorPicker( options );
851
		}
852
	},
853
854
	/**
855
	 * Init the dropdown-pages field with selectWoo
856
	 * Called after AddRow
857
	 *
858
	 * @param {object} theNewRow the row that was added to the repeater
859
	 * @param {object} data the data for the row if we're initializing a pre-existing row
860
	 *
861
	 */
862
	initSelect: function( theNewRow, data ) {
863
864
		'use strict';
865
866
		var control  = this,
867
		    dropdown = theNewRow.container.find( '.repeater-field select' ),
868
		    $select,
869
		    dataField,
870
		    multiple,
871
		    selectWooOptions = {};
872
873
		if ( 0 === dropdown.length ) {
874
			return;
875
		}
876
877
		dataField = dropdown.data( 'field' );
878
		multiple  = jQuery( dropdown ).data( 'multiple' );
879
		if ( 'undefed' !== multiple && jQuery.isNumeric( multiple ) ) {
880
			multiple = parseInt( multiple, 10 );
881
			if ( 1 < multiple ) {
882
				selectWooOptions.maximumSelectionLength = multiple;
883
			}
884
		}
885
886
		data = data || {};
887
		data[ dataField ] = data[ dataField ] || '';
888
889
		$select = jQuery( dropdown ).selectWoo( selectWooOptions ).val( data[ dataField ] );
890
891
		this.container.on( 'change', '.repeater-field select', function( event ) {
892
893
			var currentDropdown = jQuery( event.target ),
894
			    row             = currentDropdown.closest( '.repeater-row' ),
895
			    rowIndex        = row.data( 'row' ),
896
			    currentSettings = control.getValue();
897
898
			currentSettings[ rowIndex ][ currentDropdown.data( 'field' ) ] = jQuery( this ).val();
899
			control.setValue( currentSettings );
900
		});
901
	}
902
});
903