Passed
Push — add/multiplan ( f03a15...eb47db )
by Chris
04:55
created

vendor/CMB2/js/cmb2.js   F

Complexity

Total Complexity 220
Complexity/F 2.29

Size

Lines of Code 1338
Function Count 96

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 707
c 0
b 0
f 0
dl 0
loc 1338
rs 1.893
wmc 220
mnd 124
bc 124
fnc 96
bpm 1.2916
cpm 2.2916
noi 14

How to fix   Complexity   

Complexity

Complex classes like vendor/CMB2/js/cmb2.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
/**
2
 * Controls the behaviours of custom metabox fields.
3
 *
4
 * @author CMB2 team
5
 * @see    https://github.com/CMB2/CMB2
6
 */
7
8
/**
9
 * Custom jQuery for Custom Metaboxes and Fields
10
 */
11
window.CMB2 = window.CMB2 || {};
12
(function(window, document, $, cmb, undefined){
13
	'use strict';
14
15
	// localization strings
16
	var l10n = window.cmb2_l10;
17
	var setTimeout = window.setTimeout;
18
	var $document;
19
	var $id = function( selector ) {
20
		return $( document.getElementById( selector ) );
21
	};
22
	cmb.$id = $id;
23
	var defaults = {
24
		idNumber        : false,
25
		repeatEls       : 'input:not([type="button"],[id^=filelist]),select,textarea,.cmb2-media-status',
26
		noEmpty         : 'input:not([type="button"]):not([type="radio"]):not([type="checkbox"]),textarea',
27
		repeatUpdate    : 'input:not([type="button"]),select,textarea,label',
28
		styleBreakPoint : 450,
29
		mediaHandlers   : {},
30
		defaults : {
31
			time_picker  : l10n.defaults.time_picker,
32
			date_picker  : l10n.defaults.date_picker,
33
			color_picker : l10n.defaults.color_picker || {},
34
			code_editor  : l10n.defaults.code_editor,
35
		},
36
		media : {
37
			frames : {},
38
		},
39
	};
40
41
	cmb.init = function() {
42
		$document = $( document );
43
44
		// Setup the CMB2 object defaults.
45
		$.extend( cmb, defaults );
46
47
		cmb.trigger( 'cmb_pre_init' );
48
49
		var $metabox     = cmb.metabox();
50
		var $repeatGroup = $metabox.find('.cmb-repeatable-group');
51
52
		 // Init time/date/color pickers
53
		cmb.initPickers( $metabox.find('input[type="text"].cmb2-timepicker'), $metabox.find('input[type="text"].cmb2-datepicker'), $metabox.find('input[type="text"].cmb2-colorpicker') );
54
55
		// Init code editors.
56
		cmb.initCodeEditors( $metabox.find( '.cmb2-textarea-code:not(.disable-codemirror)' ) );
57
58
		// Insert toggle button into DOM wherever there is multicheck. credit: Genesis Framework
59
		$( '<p><span class="button-secondary cmb-multicheck-toggle">' + l10n.strings.check_toggle + '</span></p>' ).insertBefore( '.cmb2-checkbox-list:not(.no-select-all)' );
60
61
		// Make File List drag/drop sortable:
62
		cmb.makeListSortable();
63
		// Make Repeatable fields drag/drop sortable:
64
		cmb.makeRepeatableSortable();
65
66
		$metabox
67
			.on( 'change', '.cmb2_upload_file', function() {
68
				cmb.media.field = $( this ).attr( 'id' );
69
				$id( cmb.media.field + '_id' ).val('');
70
			})
71
			// Media/file management
72
			.on( 'click', '.cmb-multicheck-toggle', cmb.toggleCheckBoxes )
73
			.on( 'click', '.cmb2-upload-button', cmb.handleMedia )
74
			.on( 'click', '.cmb-attach-list li, .cmb2-media-status .img-status img, .cmb2-media-status .file-status > span', cmb.handleFileClick )
75
			.on( 'click', '.cmb2-remove-file-button', cmb.handleRemoveMedia )
76
			// Repeatable content
77
			.on( 'click', '.cmb-add-group-row', cmb.addGroupRow )
78
			.on( 'click', '.cmb-add-row-button', cmb.addAjaxRow )
79
			.on( 'click', '.cmb-remove-group-row', cmb.removeGroupRow )
80
			.on( 'click', '.cmb-remove-row-button', cmb.removeAjaxRow )
81
			// Ajax oEmbed display
82
			.on( 'keyup paste focusout', '.cmb2-oembed', cmb.maybeOembed )
83
			// Reset titles when removing a row
84
			.on( 'cmb2_remove_row', '.cmb-repeatable-group', cmb.resetTitlesAndIterator )
85
			.on( 'click', '.cmbhandle, .cmbhandle + .cmbhandle-title', cmb.toggleHandle );
86
87
		if ( $repeatGroup.length ) {
88
			$repeatGroup
89
				.on( 'cmb2_add_row', cmb.emptyValue )
90
				.on( 'cmb2_add_row', cmb.setDefaults )
91
				.filter('.sortable').each( function() {
92
					// Add sorting arrows
93
					$( this ).find( '.cmb-remove-group-row-button' ).before( '<a class="button-secondary cmb-shift-rows move-up alignleft" href="#"><span class="'+ l10n.up_arrow_class +'"></span></a> <a class="button-secondary cmb-shift-rows move-down alignleft" href="#"><span class="'+ l10n.down_arrow_class +'"></span></a>' );
94
				})
95
				.on( 'click', '.cmb-shift-rows', cmb.shiftRows );
96
		}
97
98
		// on pageload
99
		setTimeout( cmb.resizeoEmbeds, 500);
100
		// and on window resize
101
		$( window ).on( 'resize', cmb.resizeoEmbeds );
102
103
		if ( $id( 'addtag' ).length ) {
104
			cmb.listenTagAdd();
105
		}
106
107
		$( document ).on( 'cmb_init', cmb.mceEnsureSave );
108
109
		cmb.trigger( 'cmb_init' );
110
	};
111
112
	// Handles updating tiny mce instances when saving a gutenberg post.
113
	// https://github.com/CMB2/CMB2/issues/1156
114
	cmb.mceEnsureSave = function() {
115
		// If no wp.data, do not proceed (no gutenberg)
116
		if ( ! wp.data || ! wp.data.hasOwnProperty('subscribe') ) {
117
			return;
118
		}
119
120
		// If the current user cannot richedit, or MCE is not available, bail.
121
		if ( ! cmb.canTinyMCE() ) {
122
			return;
123
		}
124
125
		wp.data.subscribe( function() {
126
			var editor = wp.data.hasOwnProperty('select') ? wp.data.select( 'core/editor' ) : null;
127
128
			// the post is currently being saved && we have tinymce editors
129
			if ( editor && editor.isSavingPost && editor.isSavingPost() && window.tinyMCE.editors.length ) {
130
				for ( var i = 0; i < window.tinyMCE.editors.length; i++ ) {
131
					if ( window.tinyMCE.activeEditor !== window.tinyMCE.editors[i] ) {
132
						window.tinyMCE.editors[i].save();
133
					}
134
				}
135
			}
136
		});
137
	};
138
139
	cmb.canTinyMCE = function() {
140
		return l10n.user_can_richedit && window.tinyMCE;
141
	};
142
143
	cmb.listenTagAdd = function() {
144
		$document.ajaxSuccess( function( evt, xhr, settings ) {
145
			if ( settings.data && settings.data.length && -1 !== settings.data.indexOf( 'action=add-tag' ) ) {
146
				cmb.resetBoxes( $id( 'addtag' ).find( '.cmb2-wrap > .cmb2-metabox' ) );
147
			}
148
		});
149
	};
150
151
	cmb.resetBoxes = function( $boxes ) {
152
		$.each( $boxes, function() {
153
			cmb.resetBox( $( this ) );
154
		});
155
	};
156
157
	cmb.resetBox = function( $box ) {
158
		$box.find( '.wp-picker-clear' ).trigger( 'click' );
159
		$box.find( '.cmb2-remove-file-button' ).trigger( 'click' );
160
		$box.find( '.cmb-row.cmb-repeatable-grouping:not(:first-of-type) .cmb-remove-group-row' ).click();
161
		$box.find( '.cmb-repeat-row:not(:first-child)' ).remove();
162
163
		$box.find( 'input:not([type="button"]),select,textarea' ).each( function() {
164
			var $element = $( this );
165
			var tagName = $element.prop('tagName');
166
167
			if ( 'INPUT' === tagName ) {
168
				var elType = $element.attr( 'type' );
169
				if ( 'checkbox' === elType || 'radio' === elType ) {
170
					$element.prop( 'checked', false );
171
				} else {
172
					$element.val( '' );
173
				}
174
			}
175
			if ( 'SELECT' === tagName ) {
176
				$( 'option:selected', this ).prop( 'selected', false );
177
			}
178
			if ( 'TEXTAREA' === tagName ) {
179
				$element.html( '' );
180
			}
181
		});
182
	};
183
184
	cmb.resetTitlesAndIterator = function( evt ) {
185
		if ( ! evt.group ) {
186
			return;
187
		}
188
189
		// Loop repeatable group tables
190
		$( '.cmb-repeatable-group.repeatable' ).each( function() {
191
			var $table = $( this );
192
			var groupTitle = $table.find( '.cmb-add-group-row' ).data( 'grouptitle' );
193
194
			// Loop repeatable group table rows
195
			$table.find( '.cmb-repeatable-grouping' ).each( function( rowindex ) {
196
				var $row = $( this );
197
				var $rowTitle = $row.find( 'h3.cmb-group-title' );
198
				// Reset rows iterator
199
				$row.data( 'iterator', rowindex );
200
				// Reset rows title
201
				if ( $rowTitle.length ) {
202
					$rowTitle.text( groupTitle.replace( '{#}', ( rowindex + 1 ) ) );
203
				}
204
			});
205
		});
206
	};
207
208
	cmb.toggleHandle = function( evt ) {
209
		evt.preventDefault();
210
		cmb.trigger( 'postbox-toggled', $( this ).parent('.postbox').toggleClass('closed') );
211
	};
212
213
	cmb.toggleCheckBoxes = function( evt ) {
214
		evt.preventDefault();
215
		var $this = $( this );
216
		var $multicheck = $this.closest( '.cmb-td' ).find( 'input[type=checkbox]:not([disabled])' );
217
218
		// If the button has already been clicked once...
219
		if ( $this.data( 'checked' ) ) {
220
			// clear the checkboxes and remove the flag
221
			$multicheck.prop( 'checked', false );
222
			$this.data( 'checked', false );
223
		}
224
		// Otherwise mark the checkboxes and add a flag
225
		else {
226
			$multicheck.prop( 'checked', true );
227
			$this.data( 'checked', true );
228
		}
229
	};
230
231
	cmb.handleMedia = function( evt ) {
232
		evt.preventDefault();
233
234
		var $el = $( this );
235
		cmb.attach_id = ! $el.hasClass( 'cmb2-upload-list' ) ? $el.closest( '.cmb-td' ).find( '.cmb2-upload-file-id' ).val() : false;
236
		// Clean up default 0 value
237
		cmb.attach_id = '0' !== cmb.attach_id ? cmb.attach_id : false;
238
239
		cmb._handleMedia( $el.prev('input.cmb2-upload-file').attr('id'), $el.hasClass( 'cmb2-upload-list' ) );
240
	};
241
242
	cmb.handleFileClick = function( evt ) {
243
		if ( $( evt.target ).is( 'a' ) ) {
244
			return;
245
		}
246
247
		evt.preventDefault();
248
249
		var $el    = $( this );
250
		var $td    = $el.closest( '.cmb-td' );
251
		var isList = $td.find( '.cmb2-upload-button' ).hasClass( 'cmb2-upload-list' );
252
		cmb.attach_id = isList ? $el.find( 'input[type="hidden"]' ).data( 'id' ) : $td.find( '.cmb2-upload-file-id' ).val();
253
254
		if ( cmb.attach_id ) {
255
			cmb._handleMedia( $td.find( 'input.cmb2-upload-file' ).attr( 'id' ), isList, cmb.attach_id );
256
		}
257
	};
258
259
	cmb._handleMedia = function( id, isList ) {
260
		if ( ! wp ) {
261
			return;
262
		}
263
264
		var media, handlers;
265
266
		handlers          = cmb.mediaHandlers;
267
		media             = cmb.media;
268
		media.field       = id;
269
		media.$field      = $id( media.field );
270
		media.fieldData   = media.$field.data();
271
		media.previewSize = media.fieldData.previewsize;
272
		media.sizeName    = media.fieldData.sizename;
273
		media.fieldName   = media.$field.attr('name');
274
		media.isList      = isList;
275
276
		// If this field's media frame already exists, reopen it.
277
		if ( id in media.frames ) {
278
			return media.frames[ id ].open();
279
		}
280
281
		// Create the media frame.
282
		media.frames[ id ] = wp.media( {
283
			title: cmb.metabox().find('label[for="' + id + '"]').text(),
284
			library : media.fieldData.queryargs || {},
285
			button: {
286
				text: l10n.strings[ isList ? 'upload_files' : 'upload_file' ]
287
			},
288
			multiple: isList ? 'add' : false
289
		} );
290
291
		// Enable the additional media filters: https://github.com/CMB2/CMB2/issues/873
292
		media.frames[ id ].states.first().set( 'filterable', 'all' );
293
294
		cmb.trigger( 'cmb_media_modal_init', media );
295
296
		handlers.list = function( selection, returnIt ) {
297
298
			// Setup our fileGroup array
299
			var fileGroup = [];
300
			var attachmentHtml;
301
302
			if ( ! handlers.list.templates ) {
303
				handlers.list.templates = {
304
					image : wp.template( 'cmb2-list-image' ),
305
					file  : wp.template( 'cmb2-list-file' ),
306
				};
307
			}
308
309
			// Loop through each attachment
310
			selection.each( function( attachment ) {
311
312
				// Image preview or standard generic output if it's not an image.
313
				attachmentHtml = handlers.getAttachmentHtml( attachment, 'list' );
314
315
				// Add our file to our fileGroup array
316
				fileGroup.push( attachmentHtml );
317
			});
318
319
			if ( ! returnIt ) {
320
				// Append each item from our fileGroup array to .cmb2-media-status
321
				media.$field.siblings( '.cmb2-media-status' ).append( fileGroup );
322
			} else {
323
				return fileGroup;
324
			}
325
326
		};
327
328
		handlers.single = function( selection ) {
329
			if ( ! handlers.single.templates ) {
330
				handlers.single.templates = {
331
					image : wp.template( 'cmb2-single-image' ),
332
					file  : wp.template( 'cmb2-single-file' ),
333
				};
334
			}
335
336
			// Only get one file from the uploader
337
			var attachment = selection.first();
338
339
			media.$field.val( attachment.get( 'url' ) );
340
			$id( media.field +'_id' ).val( attachment.get( 'id' ) );
341
342
			// Image preview or standard generic output if it's not an image.
343
			var attachmentHtml = handlers.getAttachmentHtml( attachment, 'single' );
344
345
			// add/display our output
346
			media.$field.siblings( '.cmb2-media-status' ).slideDown().html( attachmentHtml );
347
		};
348
349
		handlers.getAttachmentHtml = function( attachment, templatesId ) {
350
			var isImage = 'image' === attachment.get( 'type' );
351
			var data    = handlers.prepareData( attachment, isImage );
352
353
			// Image preview or standard generic output if it's not an image.
354
			return handlers[ templatesId ].templates[ isImage ? 'image' : 'file' ]( data );
355
		};
356
357
		handlers.prepareData = function( data, image ) {
358
			if ( image ) {
359
				// Set the correct image size data
360
				handlers.getImageData.call( data, 50 );
361
			}
362
363
			data                   = data.toJSON();
364
			data.mediaField        = media.field;
365
			data.mediaFieldName    = media.fieldName;
366
			data.stringRemoveImage = l10n.strings.remove_image;
367
			data.stringFile        = l10n.strings.file;
368
			data.stringDownload    = l10n.strings.download;
369
			data.stringRemoveFile  = l10n.strings.remove_file;
370
371
			return data;
372
		};
373
374
		handlers.getImageData = function( fallbackSize ) {
375
376
			// Preview size dimensions
377
			var previewW = media.previewSize[0] || fallbackSize;
378
			var previewH = media.previewSize[1] || fallbackSize;
379
380
			// Image dimensions and url
381
			var url    = this.get( 'url' );
382
			var width  = this.get( 'width' );
383
			var height = this.get( 'height' );
384
			var sizes  = this.get( 'sizes' );
385
386
			// Get the correct dimensions and url if a named size is set and exists
387
			// fallback to the 'large' size
388
			if ( sizes ) {
389
				if ( sizes[ media.sizeName ] ) {
390
					url    = sizes[ media.sizeName ].url;
391
					width  = sizes[ media.sizeName ].width;
392
					height = sizes[ media.sizeName ].height;
393
				} else if ( sizes.large ) {
394
					url    = sizes.large.url;
395
					width  = sizes.large.width;
396
					height = sizes.large.height;
397
				}
398
			}
399
400
			// Fit the image in to the preview size, keeping the correct aspect ratio
401
			if ( width > previewW ) {
402
				height = Math.floor( previewW * height / width );
403
				width = previewW;
404
			}
405
406
			if ( height > previewH ) {
407
				width = Math.floor( previewH * width / height );
408
				height = previewH;
409
			}
410
411
			if ( ! width ) {
412
				width = previewW;
413
			}
414
415
			if ( ! height ) {
416
				height = 'svg' === this.get( 'filename' ).split( '.' ).pop() ? '100%' : previewH;
417
			}
418
419
			this.set( 'sizeUrl', url );
420
			this.set( 'sizeWidth', width );
421
			this.set( 'sizeHeight', height );
422
423
			return this;
424
		};
425
426
		handlers.selectFile = function() {
427
			var selection = media.frames[ id ].state().get( 'selection' );
428
			var type = isList ? 'list' : 'single';
429
430
			if ( cmb.attach_id && isList ) {
431
				$( '[data-id="'+ cmb.attach_id +'"]' ).parents( 'li' ).replaceWith( handlers.list( selection, true ) );
432
			} else {
433
				handlers[type]( selection );
434
			}
435
436
			cmb.trigger( 'cmb_media_modal_select', selection, media );
437
		};
438
439
		handlers.openModal = function() {
440
			var selection = media.frames[ id ].state().get( 'selection' );
441
			var attach;
442
443
			if ( ! cmb.attach_id ) {
444
				selection.reset();
445
			} else {
446
				attach = wp.media.attachment( cmb.attach_id );
447
				attach.fetch();
448
				selection.set( attach ? [ attach ] : [] );
449
			}
450
451
			cmb.trigger( 'cmb_media_modal_open', selection, media );
452
		};
453
454
		// When a file is selected, run a callback.
455
		media.frames[ id ]
456
			.on( 'select', handlers.selectFile )
457
			.on( 'open', handlers.openModal );
458
459
		// Finally, open the modal
460
		media.frames[ id ].open();
461
	};
462
463
	cmb.handleRemoveMedia = function( evt ) {
464
		evt.preventDefault();
465
		var $this = $( this );
466
		if ( $this.is( '.cmb-attach-list .cmb2-remove-file-button' ) ) {
467
			$this.parents( '.cmb2-media-item' ).remove();
468
			return false;
469
		}
470
471
		cmb.media.field = $this.attr('rel');
472
473
		cmb.metabox().find( document.getElementById( cmb.media.field ) ).val('');
474
		cmb.metabox().find( document.getElementById( cmb.media.field + '_id' ) ).val('');
475
		$this.parents('.cmb2-media-status').html('');
476
477
		return false;
478
	};
479
480
	cmb.cleanRow = function( $row, prevNum, group ) {
481
		var $elements = $row.find( cmb.repeatUpdate );
482
		if ( group ) {
483
484
			var $other = $row.find( '[id]' ).not( cmb.repeatUpdate );
485
486
			// Remove extra ajaxed rows
487
			$row.find('.cmb-repeat-table .cmb-repeat-row:not(:first-child)').remove();
488
489
			// Update all elements w/ an ID
490
			if ( $other.length ) {
491
				$other.each( function() {
492
					var $_this = $( this );
493
					var oldID = $_this.attr( 'id' );
494
					var newID = oldID.replace( '_'+ prevNum, '_'+ cmb.idNumber );
495
					var $buttons = $row.find('[data-selector="'+ oldID +'"]');
496
					$_this.attr( 'id', newID );
497
498
					// Replace data-selector vars
499
					if ( $buttons.length ) {
500
						$buttons.attr( 'data-selector', newID ).data( 'selector', newID );
501
					}
502
				});
503
			}
504
		}
505
506
		$elements.filter( ':checked' ).removeAttr( 'checked' );
507
		$elements.find( ':checked' ).removeAttr( 'checked' );
508
		$elements.filter( ':selected' ).removeAttr( 'selected' );
509
		$elements.find( ':selected' ).removeAttr( 'selected', false );
510
511
		if ( $row.find('h3.cmb-group-title').length ) {
512
			$row.find( 'h3.cmb-group-title' ).text( $row.data( 'title' ).replace( '{#}', ( cmb.idNumber + 1 ) ) );
513
		}
514
515
		$elements.each( function() {
516
			cmb.elReplacements( $( this ), prevNum, group );
517
		} );
518
519
		return cmb;
520
	};
521
522
	cmb.elReplacements = function( $newInput, prevNum, group ) {
523
		var oldFor    = $newInput.attr( 'for' );
524
		var oldVal    = $newInput.val();
525
		var type      = $newInput.prop( 'type' );
526
		var defVal    = cmb.getFieldArg( $newInput, 'default' );
527
		var newVal    = 'undefined' !== typeof defVal && false !== defVal ? defVal : '';
528
		var tagName   = $newInput.prop('tagName');
529
		var checkable = 'radio' === type || 'checkbox' === type ? oldVal : false;
530
		var attrs     = {};
531
		var newID, oldID;
532
		if ( oldFor ) {
533
			attrs = { 'for' : oldFor.replace( '_'+ prevNum, '_'+ cmb.idNumber ) };
534
		} else {
535
			var oldName = $newInput.attr( 'name' );
536
			var newName;
537
			oldID = $newInput.attr( 'id' );
538
539
			// Handle adding groups vs rows.
540
			if ( group ) {
541
				// Expect another bracket after group's index closing bracket.
542
				newName = oldName ? oldName.replace( '['+ prevNum +'][', '['+ cmb.idNumber +'][' ) : '';
543
				// Expect another underscore after group's index trailing underscore.
544
				newID   = oldID ? oldID.replace( '_' + prevNum + '_', '_' + cmb.idNumber + '_' ) : '';
545
			}
546
			else {
547
				// Row indexes are at the very end of the string.
548
				newName = oldName ? cmb.replaceLast( oldName, '[' + prevNum + ']', '[' + cmb.idNumber + ']' ) : '';
549
				newID   = oldID ? cmb.replaceLast( oldID, '_' + prevNum, '_' + cmb.idNumber ) : '';
550
			}
551
552
			attrs = {
553
				id: newID,
554
				name: newName
555
			};
556
557
		}
558
559
		// Clear out textarea values
560
		if ( 'TEXTAREA' === tagName ) {
561
			$newInput.html( newVal );
562
		}
563
564
		if ( 'SELECT' === tagName && 'undefined' !== typeof defVal ) {
565
			var $toSelect = $newInput.find( '[value="'+ defVal + '"]' );
566
			if ( $toSelect.length ) {
567
				$toSelect.attr( 'selected', 'selected' ).prop( 'selected', 'selected' );
568
			}
569
		}
570
571
		if ( checkable ) {
572
			$newInput.removeAttr( 'checked' );
573
			if ( 'undefined' !== typeof defVal && oldVal === defVal ) {
574
				$newInput.attr( 'checked', 'checked' ).prop( 'checked', 'checked' );
575
			}
576
		}
577
578
		if ( ! group && $newInput[0].hasAttribute( 'data-iterator' ) ) {
579
			attrs['data-iterator'] = cmb.idNumber;
580
		}
581
582
		$newInput
583
			.removeClass( 'hasDatepicker' )
584
			.val( checkable ? checkable : newVal ).attr( attrs );
585
586
		return $newInput;
587
	};
588
589
	cmb.newRowHousekeeping = function( $row ) {
590
		var $colorPicker = $row.find( '.wp-picker-container' );
591
		var $list        = $row.find( '.cmb2-media-status' );
592
593
		if ( $colorPicker.length ) {
594
			// Need to clean-up colorpicker before appending
595
			$colorPicker.each( function() {
596
				var $td = $( this ).parent();
597
				$td.html( $td.find( 'input[type="text"].cmb2-colorpicker' ).attr('style', '') );
598
			});
599
		}
600
601
		// Need to clean-up colorpicker before appending
602
		if ( $list.length ) {
603
			$list.empty();
604
		}
605
606
		return cmb;
607
	};
608
609
	cmb.afterRowInsert = function( $row ) {
610
		// Init pickers from new row
611
		cmb.initPickers( $row.find('input[type="text"].cmb2-timepicker'), $row.find('input[type="text"].cmb2-datepicker'), $row.find('input[type="text"].cmb2-colorpicker') );
612
	};
613
614
	cmb.updateNameAttr = function () {
615
		var $this = $( this );
616
		var name  = $this.attr( 'name' ); // get current name
617
618
		// If name is defined
619
		if ( 'undefined' !== typeof name ) {
620
			var prevNum = parseInt( $this.parents( '.cmb-repeatable-grouping' ).data( 'iterator' ), 10 );
621
			var newNum  = prevNum - 1; // Subtract 1 to get new iterator number
622
623
			// Update field name attributes so data is not orphaned when a row is removed and post is saved
624
			var $newName = name.replace( '[' + prevNum + ']', '[' + newNum + ']' );
625
626
			// New name with replaced iterator
627
			$this.attr( 'name', $newName );
628
		}
629
	};
630
631
	cmb.emptyValue = function( evt, row ) {
632
		$( cmb.noEmpty, row ).val( '' );
633
	};
634
635
	cmb.setDefaults = function( evt, row ) {
636
		$( cmb.noEmpty, row ).each( function() {
637
			var $el = $(this);
638
			var defVal = cmb.getFieldArg( $el, 'default' );
639
			if ( 'undefined' !== typeof defVal && false !== defVal ) {
640
				$el.val( defVal );
641
			}
642
		});
643
	};
644
645
	cmb.addGroupRow = function( evt ) {
646
		evt.preventDefault();
647
648
		var $this = $( this );
649
650
		// before anything significant happens
651
		cmb.triggerElement( $this, 'cmb2_add_group_row_start', $this );
652
653
		var $table   = $id( $this.data('selector') );
654
		var $oldRow  = $table.find('.cmb-repeatable-grouping').last();
655
		var prevNum  = parseInt( $oldRow.data('iterator'), 10 );
656
		cmb.idNumber = parseInt( prevNum, 10 ) + 1;
657
		var $row     = $oldRow.clone();
658
		var nodeName = $row.prop('nodeName') || 'div';
659
		var getRowId = function( id ) {
660
			id = id.split('-');
661
			id.splice(id.length - 1, 1);
662
			id.push( cmb.idNumber );
663
			return id.join('-');
664
		};
665
666
		// Make sure the next number doesn't exist.
667
		while ( $table.find( '.cmb-repeatable-grouping[data-iterator="'+ cmb.idNumber +'"]' ).length > 0 ) {
668
			cmb.idNumber++;
669
		}
670
671
		cmb.newRowHousekeeping( $row.data( 'title', $this.data( 'grouptitle' ) ) ).cleanRow( $row, prevNum, true );
672
		$row.find( '.cmb-add-row-button' ).prop( 'disabled', false );
673
674
		var $newRow = $( '<' + nodeName + ' id="'+ getRowId( $oldRow.attr('id') ) +'" class="postbox cmb-row cmb-repeatable-grouping" data-iterator="'+ cmb.idNumber +'">'+ $row.html() +'</' + nodeName + '>' );
675
		$oldRow.after( $newRow );
676
677
		cmb.afterRowInsert( $newRow );
678
679
		cmb.triggerElement( $table, { type: 'cmb2_add_row', group: true }, $newRow );
680
681
	};
682
683
	cmb.addAjaxRow = function( evt ) {
684
		evt.preventDefault();
685
686
		var $this     = $( this );
687
		var $table    = $id( $this.data('selector') );
688
		var $row      = $table.find('.empty-row');
689
		var prevNum   = parseInt( $row.find('[data-iterator]').data('iterator'), 10 );
690
		cmb.idNumber  = parseInt( prevNum, 10 ) + 1;
691
		var $emptyrow = $row.clone();
692
693
		cmb.newRowHousekeeping( $emptyrow ).cleanRow( $emptyrow, prevNum );
694
695
		$row.removeClass('empty-row hidden').addClass('cmb-repeat-row');
696
		$row.after( $emptyrow );
697
698
		cmb.afterRowInsert( $emptyrow );
699
700
		cmb.triggerElement( $table, { type: 'cmb2_add_row', group: false }, $emptyrow, $row );
701
	};
702
703
	cmb.removeGroupRow = function( evt ) {
704
		evt.preventDefault();
705
706
		var $this        = $( this );
707
		var confirmation = $this.data('confirm');
708
709
		// Process further only if deletion confirmation enabled and user agreed.
710
		if ( confirmation && ! window.confirm( confirmation ) ) {
711
			return;
712
		}
713
714
		var $table  = $id( $this.data('selector') );
715
		var $parent = $this.parents('.cmb-repeatable-grouping');
716
		var number  = $table.find('.cmb-repeatable-grouping').length;
717
718
		if ( number < 2 ) {
719
			return cmb.resetRow( $parent.parents('.cmb-repeatable-group').find( '.cmb-add-group-row' ), $this );
720
		}
721
722
		cmb.triggerElement( $table, 'cmb2_remove_group_row_start', $this );
723
724
		// When a group is removed, loop through all next groups and update fields names.
725
		$parent.nextAll( '.cmb-repeatable-grouping' ).find( cmb.repeatEls ).each( cmb.updateNameAttr );
726
727
		$parent.remove();
728
729
		cmb.triggerElement( $table, { type: 'cmb2_remove_row', group: true } );
730
	};
731
732
	cmb.removeAjaxRow = function( evt ) {
733
		evt.preventDefault();
734
735
		var $this = $( this );
736
737
		// Check if disabled
738
		if ( $this.hasClass( 'button-disabled' ) ) {
739
			return;
740
		}
741
742
		var $parent = $this.parents('.cmb-row');
743
		var $table  = $this.parents('.cmb-repeat-table');
744
		var number  = $table.find('.cmb-row').length;
745
746
		if ( number <= 2 ) {
747
			return cmb.resetRow( $parent.find( '.cmb-add-row-button' ), $this );
748
		}
749
750
		if ( $parent.hasClass('empty-row') ) {
751
			$parent.prev().addClass( 'empty-row' ).removeClass('cmb-repeat-row');
752
		}
753
754
		$this.parents('.cmb-repeat-table .cmb-row').remove();
755
756
757
		cmb.triggerElement( $table, { type: 'cmb2_remove_row', group: false } );
758
	};
759
760
	cmb.resetRow = function( $addNewBtn, $removeBtn ) {
761
		// Click the "add new" button followed by the "remove this" button
762
		// in order to reset the repeat row to empty values.
763
		$addNewBtn.trigger( 'click' );
764
		$removeBtn.trigger( 'click' );
765
	};
766
767
	cmb.shiftRows = function( evt ) {
768
769
		evt.preventDefault();
770
771
		var $this = $( this );
772
		var $from = $this.parents( '.cmb-repeatable-grouping' );
773
		var $goto = $this.hasClass( 'move-up' ) ? $from.prev( '.cmb-repeatable-grouping' ) : $from.next( '.cmb-repeatable-grouping' );
774
775
		// Before shift occurs.
776
		cmb.triggerElement( $this, 'cmb2_shift_rows_enter', $this, $from, $goto );
777
778
		if ( ! $goto.length ) {
779
			return;
780
		}
781
782
		// About to shift
783
		cmb.triggerElement( $this, 'cmb2_shift_rows_start', $this, $from, $goto );
784
785
		var inputVals = [];
786
		// Loop this item's fields
787
		$from.find( cmb.repeatEls ).each( function() {
788
			var $element = $( this );
789
			var elType = $element.attr( 'type' );
790
			var val;
791
792
			if ( $element.hasClass('cmb2-media-status') ) {
793
				// special case for image previews
794
				val = $element.html();
795
			} else if ( 'checkbox' === elType || 'radio' === elType ) {
796
				val = $element.is(':checked');
797
			} else if ( 'select' === $element.prop('tagName') ) {
798
				val = $element.is(':selected');
799
			} else {
800
				val = $element.val();
801
			}
802
803
			// Get all the current values per element
804
			inputVals.push( { val: val, $: $element } );
805
		});
806
		// And swap them all
807
		$goto.find( cmb.repeatEls ).each( function( index ) {
808
			var $element = $( this );
809
			var elType = $element.attr( 'type' );
810
			var val;
811
812
			if ( $element.hasClass('cmb2-media-status') ) {
813
				var toRowId = $element.closest('.cmb-repeatable-grouping').attr('data-iterator');
814
				var fromRowId = inputVals[ index ].$.closest('.cmb-repeatable-grouping').attr('data-iterator');
815
816
				// special case for image previews
817
				val = $element.html();
818
				$element.html( inputVals[ index ].val );
819
				inputVals[ index ].$.html( val );
820
821
				inputVals[ index ].$.find( 'input' ).each(function() {
822
					var name = $( this ).attr( 'name' );
823
					name = name.replace( '['+toRowId+']', '['+fromRowId+']' );
824
					$( this ).attr( 'name', name );
825
				});
826
				$element.find('input').each(function() {
827
					var name = $( this ).attr('name');
828
					name = name.replace('['+fromRowId+']', '['+toRowId+']');
829
					$( this ).attr('name', name);
830
				});
831
832
			}
833
			// handle checkbox swapping
834
			else if ( 'checkbox' === elType  ) {
835
				inputVals[ index ].$.prop( 'checked', $element.is(':checked') );
836
				$element.prop( 'checked', inputVals[ index ].val );
837
			}
838
			// handle radio swapping
839
			else if ( 'radio' === elType  ) {
840
				if ( $element.is( ':checked' ) ) {
841
					inputVals[ index ].$.attr( 'data-checked', 'true' );
842
				}
843
				if ( inputVals[ index ].$.is( ':checked' ) ) {
844
					$element.attr( 'data-checked', 'true' );
845
				}
846
			}
847
			// handle select swapping
848
			else if ( 'select' === $element.prop('tagName') ) {
849
				inputVals[ index ].$.prop( 'selected', $element.is(':selected') );
850
				$element.prop( 'selected', inputVals[ index ].val );
851
			}
852
			// handle normal input swapping
853
			else {
854
				inputVals[ index ].$.val( $element.val() );
855
				$element.val( inputVals[ index ].val );
856
			}
857
		});
858
859
		$from.find( 'input[data-checked=true]' ).prop( 'checked', true ).removeAttr( 'data-checked' );
860
		$goto.find( 'input[data-checked=true]' ).prop( 'checked', true ).removeAttr( 'data-checked' );
861
862
		// trigger color picker change event
863
		$from.find( 'input[type="text"].cmb2-colorpicker' ).trigger( 'change' );
864
		$goto.find( 'input[type="text"].cmb2-colorpicker' ).trigger( 'change' );
865
866
		// shift done
867
		cmb.triggerElement( $this, 'cmb2_shift_rows_complete', $this, $from, $goto );
868
	};
869
870
	cmb.initPickers = function( $timePickers, $datePickers, $colorPickers ) {
871
		cmb.trigger( 'cmb_init_pickers', {
872
			time: $timePickers,
873
			date: $datePickers,
874
			color: $colorPickers
875
		} );
876
877
		// Initialize jQuery UI timepickers
878
		cmb.initDateTimePickers( $timePickers, 'timepicker', 'time_picker' );
879
		// Initialize jQuery UI datepickers
880
		cmb.initDateTimePickers( $datePickers, 'datepicker', 'date_picker' );
881
		// Initialize color picker
882
		cmb.initColorPickers( $colorPickers );
883
	};
884
885
	cmb.initDateTimePickers = function( $selector, method, defaultKey ) {
886
		if ( $selector.length ) {
887
			$selector[ method ]( 'destroy' ).each( function() {
888
				var $this     = $( this );
889
				var fieldOpts = $this.data( method ) || {};
890
				var options   = $.extend( {}, cmb.defaults[ defaultKey ], fieldOpts );
891
				$this[ method ]( cmb.datePickerSetupOpts( fieldOpts, options, method ) );
892
			} );
893
		}
894
	};
895
896
	cmb.datePickerSetupOpts = function( fieldOpts, options, method ) {
897
		var existing = $.extend( {}, options );
898
899
		options.beforeShow = function( input, inst ) {
900
			if ( 'timepicker' === method ) {
901
				cmb.addTimePickerClasses( inst.dpDiv );
902
			}
903
904
			// Wrap datepicker w/ class to narrow the scope of jQuery UI CSS and prevent conflicts
905
			$id( 'ui-datepicker-div' ).addClass( 'cmb2-element' );
906
907
			// Let's be sure to call beforeShow if it was added
908
			if ( 'function' === typeof existing.beforeShow ) {
909
				existing.beforeShow( input, inst );
910
			}
911
		};
912
913
		if ( 'timepicker' === method ) {
914
			options.onChangeMonthYear = function( year, month, inst, picker ) {
915
				cmb.addTimePickerClasses( inst.dpDiv );
916
917
				// Let's be sure to call onChangeMonthYear if it was added
918
				if ( 'function' === typeof existing.onChangeMonthYear ) {
919
					existing.onChangeMonthYear( year, month, inst, picker );
920
				}
921
			};
922
		}
923
924
		options.onClose = function( dateText, inst ) {
925
			// Remove the class when we're done with it (and hide to remove FOUC).
926
			var $picker = $id( 'ui-datepicker-div' ).removeClass( 'cmb2-element' ).hide();
927
			if ( 'timepicker' === method && ! $( inst.input ).val() ) {
928
				// Set the timepicker field value if it's empty.
929
				inst.input.val( $picker.find( '.ui_tpicker_time' ).text() );
930
			}
931
932
			// Let's be sure to call onClose if it was added
933
			if ( 'function' === typeof existing.onClose ) {
934
				existing.onClose( dateText, inst );
935
			}
936
		};
937
938
		return options;
939
	};
940
941
	// Adds classes to timepicker buttons.
942
	cmb.addTimePickerClasses = function( $picker ) {
943
		var func = cmb.addTimePickerClasses;
944
		func.count = func.count || 0;
945
946
		// Wait a bit to let the timepicker render, since these are pre-render events.
947
		setTimeout( function() {
948
			if ( $picker.find( '.ui-priority-secondary' ).length ) {
949
				$picker.find( '.ui-priority-secondary' ).addClass( 'button-secondary' );
950
				$picker.find( '.ui-priority-primary' ).addClass( 'button-primary' );
951
				func.count = 0;
952
			} else if ( func.count < 5 ) {
953
				func.count++;
954
				func( $picker );
955
			}
956
		}, 10 );
957
	};
958
959
	cmb.initColorPickers = function( $selector ) {
960
		if ( ! $selector.length ) {
961
			return;
962
		}
963
		if ( 'object' === typeof jQuery.wp && 'function' === typeof jQuery.wp.wpColorPicker ) {
964
965
			$selector.each( function() {
966
				var $this = $( this );
967
				var fieldOpts = $this.data( 'colorpicker' ) || {};
968
				$this.wpColorPicker( $.extend( {}, cmb.defaults.color_picker, fieldOpts ) );
969
			} );
970
971
		} else {
972
			$selector.each( function( i ) {
973
				$( this ).after( '<div id="picker-' + i + '" style="z-index: 1000; background: #EEE; border: 1px solid #CCC; position: absolute; display: block;"></div>' );
974
				$id( 'picker-' + i ).hide().farbtastic( $( this ) );
975
			} )
976
			.focus( function() {
977
				$( this ).next().show();
978
			} )
979
			.blur( function() {
980
				$( this ).next().hide();
981
			} );
982
		}
983
	};
984
985
	cmb.initCodeEditors = function( $selector ) {
986
		cmb.trigger( 'cmb_init_code_editors', $selector );
987
988
		if ( ! cmb.defaults.code_editor || ! wp || ! wp.codeEditor || ! $selector.length ) {
989
			return;
990
		}
991
992
		$selector.each( function() {
993
			wp.codeEditor.initialize(
994
				this.id,
995
				cmb.codeEditorArgs( $( this ).data( 'codeeditor' ) )
996
			);
997
		} );
998
	};
999
1000
	cmb.codeEditorArgs = function( overrides ) {
1001
		var props = [ 'codemirror', 'csslint', 'jshint', 'htmlhint' ];
1002
		var args = $.extend( {}, cmb.defaults.code_editor );
1003
		overrides = overrides || {};
1004
1005
		for ( var i = props.length - 1; i >= 0; i-- ) {
1006
			if ( overrides.hasOwnProperty( props[i] ) ) {
1007
				args[ props[i] ] = $.extend( {}, args[ props[i] ] || {}, overrides[ props[i] ] );
1008
			}
1009
		}
1010
1011
		return args;
1012
	};
1013
1014
	cmb.makeListSortable = function() {
1015
		var $filelist = cmb.metabox().find( '.cmb2-media-status.cmb-attach-list' );
1016
		if ( $filelist.length ) {
1017
			$filelist.sortable({ cursor: 'move' }).disableSelection();
1018
		}
1019
	};
1020
1021
	cmb.makeRepeatableSortable = function() {
1022
		var $repeatables = cmb.metabox().find( '.cmb-repeat-table .cmb-field-list' );
1023
1024
		if ( $repeatables.length ) {
1025
			$repeatables.sortable({
1026
				items : '.cmb-repeat-row',
1027
				cursor: 'move',
1028
				// The default "cancel" attributes are: "input,textarea,button,select,option".
1029
				// We are appending .CodeMirror.
1030
				// See https://api.jqueryui.com/sortable/#option-cancel
1031
				cancel: 'input,textarea,button,select,option,.CodeMirror'
1032
			});
1033
		}
1034
	};
1035
1036
	cmb.maybeOembed = function( evt ) {
1037
		var $this = $( this );
1038
1039
		var m = {
1040
			focusout : function() {
1041
				setTimeout( function() {
1042
					// if it's been 2 seconds, hide our spinner
1043
					cmb.spinner( '.cmb2-metabox', true );
1044
				}, 2000);
1045
			},
1046
			keyup : function() {
1047
				var betw = function( min, max ) {
1048
					return ( evt.which <= max && evt.which >= min );
1049
				};
1050
				// Only Ajax on normal keystrokes
1051
				if ( betw( 48, 90 ) || betw( 96, 111 ) || betw( 8, 9 ) || evt.which === 187 || evt.which === 190 ) {
1052
					// fire our ajax function
1053
					cmb.doAjax( $this, evt );
1054
				}
1055
			},
1056
			paste : function() {
1057
				// paste event is fired before the value is filled, so wait a bit
1058
				setTimeout( function() { cmb.doAjax( $this ); }, 100);
1059
			}
1060
		};
1061
1062
		m[ evt.type ]();
1063
	};
1064
1065
	/**
1066
	 * Resize oEmbed videos to fit in their respective metaboxes
1067
	 *
1068
	 * @since  0.9.4
1069
	 *
1070
	 * @return {return}
1071
	 */
1072
	cmb.resizeoEmbeds = function() {
1073
		cmb.metabox().each( function() {
1074
			var $this      = $( this );
1075
			var $tableWrap = $this.parents('.inside');
1076
			var isSide     = $this.parents('.inner-sidebar').length || $this.parents( '#side-sortables' ).length;
1077
			var isSmall    = isSide;
1078
			var isSmallest = false;
1079
			if ( ! $tableWrap.length )  {
1080
				return true; // continue
1081
			}
1082
1083
			// Calculate new width
1084
			var tableW = $tableWrap.width();
1085
1086
			if ( cmb.styleBreakPoint > tableW ) {
1087
				isSmall    = true;
1088
				isSmallest = ( cmb.styleBreakPoint - 62 ) > tableW;
1089
			}
1090
1091
			tableW = isSmall ? tableW : Math.round(($tableWrap.width() * 0.82)*0.97);
1092
			var newWidth = tableW - 30;
1093
			if ( isSmall && ! isSide && ! isSmallest ) {
1094
				newWidth = newWidth - 75;
1095
			}
1096
			if ( newWidth > 639 ) {
1097
				return true; // continue
1098
			}
1099
1100
			var $embeds   = $this.find('.cmb-type-oembed .embed-status');
1101
			var $children = $embeds.children().not('.cmb2-remove-wrapper');
1102
			if ( ! $children.length ) {
1103
				return true; // continue
1104
			}
1105
1106
			$children.each( function() {
1107
				var $this     = $( this );
1108
				var iwidth    = $this.width();
1109
				var iheight   = $this.height();
1110
				var _newWidth = newWidth;
1111
				if ( $this.parents( '.cmb-repeat-row' ).length && ! isSmall ) {
1112
					// Make room for our repeatable "remove" button column
1113
					_newWidth = newWidth - 91;
1114
					_newWidth = 785 > tableW ? _newWidth - 15 : _newWidth;
1115
				}
1116
				// Calc new height
1117
				var newHeight = Math.round((_newWidth * iheight)/iwidth);
1118
				$this.width(_newWidth).height(newHeight);
1119
			});
1120
		});
1121
	};
1122
1123
	// function for running our ajax
1124
	cmb.doAjax = function( $obj ) {
1125
		// get typed value
1126
		var oembed_url = $obj.val();
1127
		// only proceed if the field contains more than 6 characters
1128
		if ( oembed_url.length < 6 ) {
1129
			return;
1130
		}
1131
1132
		// get field id
1133
		var field_id         = $obj.attr('id');
1134
		var $context         = $obj.closest( '.cmb-td' );
1135
		var $embed_container = $context.find( '.embed-status' );
1136
		var $embed_wrap      = $context.find( '.embed_wrap' );
1137
		var $child_el        = $embed_container.find( ':first-child' );
1138
		var oembed_width     = $embed_container.length && $child_el.length ? $child_el.width() : $obj.width();
1139
1140
		cmb.log( 'oembed_url', oembed_url, field_id );
1141
1142
		// show our spinner
1143
		cmb.spinner( $context );
1144
		// clear out previous results
1145
		$embed_wrap.html('');
1146
		// and run our ajax function
1147
		setTimeout( function() {
1148
			// if they haven't typed in 500 ms
1149
			if ( $( '.cmb2-oembed:focus' ).val() !== oembed_url ) {
1150
				return;
1151
			}
1152
			$.ajax({
1153
				type : 'post',
1154
				dataType : 'json',
1155
				url : l10n.ajaxurl,
1156
				data : {
1157
					'action'          : 'cmb2_oembed_handler',
1158
					'oembed_url'      : oembed_url,
1159
					'oembed_width'    : oembed_width > 300 ? oembed_width : 300,
1160
					'field_id'        : field_id,
1161
					'object_id'       : $obj.data( 'objectid' ),
1162
					'object_type'     : $obj.data( 'objecttype' ),
1163
					'cmb2_ajax_nonce' : l10n.ajax_nonce
1164
				},
1165
				success: function(response) {
1166
					cmb.log( response );
1167
					// hide our spinner
1168
					cmb.spinner( $context, true );
1169
					// and populate our results from ajax response
1170
					$embed_wrap.html( response.data );
1171
				}
1172
			});
1173
1174
		}, 500);
1175
1176
	};
1177
1178
	/**
1179
	 * Gets jQuery object containing all CMB metaboxes. Caches the result.
1180
	 *
1181
	 * @since  1.0.2
1182
	 *
1183
	 * @return {Object} jQuery object containing all CMB metaboxes.
1184
	 */
1185
	cmb.metabox = function() {
1186
		if ( cmb.$metabox ) {
1187
			return cmb.$metabox;
1188
		}
1189
		cmb.$metabox = $('.cmb2-wrap > .cmb2-metabox');
1190
		return cmb.$metabox;
1191
	};
1192
1193
	/**
1194
	 * Starts/stops contextual spinner.
1195
	 *
1196
	 * @since  1.0.1
1197
	 *
1198
	 * @param  {object} $context The jQuery parent/context object.
1199
	 * @param  {bool} hide       Whether to hide the spinner (will show by default).
1200
	 *
1201
	 * @return {void}
1202
	 */
1203
	cmb.spinner = function( $context, hide ) {
1204
		var m = hide ? 'removeClass' : 'addClass';
1205
		$('.cmb-spinner', $context )[ m ]( 'is-active' );
1206
	};
1207
1208
	/**
1209
	 * Triggers a jQuery event on the document object.
1210
	 *
1211
	 * @since  2.2.3
1212
	 *
1213
	 * @param  {string} evtName The name of the event to trigger.
1214
	 *
1215
	 * @return {void}
1216
	 */
1217
	cmb.trigger = function( evtName ) {
1218
		var args = Array.prototype.slice.call( arguments, 1 );
1219
		args.push( cmb );
1220
		$document.trigger( evtName, args );
1221
	};
1222
1223
	/**
1224
	 * Triggers a jQuery event on the given jQuery object.
1225
	 *
1226
	 * @since  2.2.3
1227
	 *
1228
	 * @param  {object} $el     The jQuery element object.
1229
	 * @param  {string} evtName The name of the event to trigger.
1230
	 *
1231
	 * @return {void}
1232
	 */
1233
	cmb.triggerElement = function( $el, evtName ) {
1234
		var args = Array.prototype.slice.call( arguments, 2 );
1235
		args.push( cmb );
1236
		$el.trigger( evtName, args );
1237
	};
1238
1239
	/**
1240
	 * Get an argument for a given field.
1241
	 *
1242
	 * @since  2.5.0
1243
	 *
1244
	 * @param  {string|object} hash The field hash, id, or a jQuery object for a field.
1245
	 * @param  {string}        arg  The argument to get on the field.
1246
	 *
1247
	 * @return {mixed}              The argument value.
1248
	 */
1249
	cmb.getFieldArg = function( hash, arg ) {
1250
		return cmb.getField( hash )[ arg ];
1251
	};
1252
1253
	/**
1254
	 * Get a field object instances. Can be filtered by passing in a filter callback function.
1255
	 * e.g. `const fileFields = CMB2.getFields(f => 'file' === f.type);`
1256
	 *
1257
	 * @since  2.5.0
1258
	 *
1259
	 * @param  {mixed} filterCb An optional filter callback function.
1260
	 *
1261
	 * @return array            An array of field object instances.
1262
	 */
1263
	cmb.getFields = function( filterCb ) {
1264
		if ( 'function' === typeof filterCb ) {
1265
			var fields = [];
1266
			$.each( l10n.fields, function( hash, field ) {
1267
				if ( filterCb( field, hash ) ) {
1268
					fields.push( field );
1269
				}
1270
			});
1271
			return fields;
1272
		}
1273
1274
		return l10n.fields;
1275
	};
1276
1277
	/**
1278
	 * Get a field object instance by hash or id.
1279
	 *
1280
	 * @since  2.5.0
1281
	 *
1282
	 * @param  {string|object} hash The field hash, id, or a jQuery object for a field.
1283
	 *
1284
	 * @return {object}        The field object or an empty object.
1285
	 */
1286
	cmb.getField = function( hash ) {
1287
		var field = {};
1288
		hash = hash instanceof jQuery ? hash.data( 'hash' ) : hash;
1289
		if ( hash ) {
1290
			try {
1291
				if ( l10n.fields[ hash ] ) {
1292
					throw new Error( hash );
1293
				}
1294
1295
				cmb.getFields( function( field ) {
1296
					if ( 'function' === typeof hash ) {
1297
						if ( hash( field ) ) {
1298
							throw new Error( field.hash );
1299
						}
1300
					} else  if ( field.id && field.id === hash ) {
1301
						throw new Error( field.hash );
1302
					}
1303
				});
1304
			} catch( e ) {
1305
				field = l10n.fields[ e.message ];
1306
			}
1307
		}
1308
1309
		return field;
1310
	};
1311
1312
	/**
1313
	 * Safely log things if query var is set. Accepts same parameters as console.log.
1314
	 *
1315
	 * @since  1.0.0
1316
	 *
1317
	 * @return {void}
1318
	 */
1319
	cmb.log = function() {
1320
		if ( l10n.script_debug && console && 'function' === typeof console.log ) {
1321
			console.log.apply(console, arguments);
1322
		}
1323
	};
1324
1325
	/**
1326
	 * Replace the last occurrence of a string.
1327
	 *
1328
	 * @since  2.2.6
1329
	 *
1330
	 * @param  {string} string  String to search/replace.
1331
	 * @param  {string} search  String to search.
1332
	 * @param  {string} replace String to replace search with.
1333
	 *
1334
	 * @return {string}         Possibly modified string.
1335
	 */
1336
	cmb.replaceLast = function( string, search, replace ) {
1337
		// find the index of last time word was used
1338
		var n = string.lastIndexOf( search );
1339
1340
		// slice the string in 2, one from the start to the lastIndexOf
1341
		// and then replace the word in the rest
1342
		return string.slice( 0, n ) + string.slice( n ).replace( search, replace );
1343
	};
1344
1345
	// Kick it off!
1346
	$( cmb.init );
1347
1348
})(window, document, jQuery, window.CMB2);
1349