Passed
Branch master (40a760)
by Chris
07:38
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
wmc 220
eloc 707
mnd 124
bc 124
fnc 96
dl 0
loc 1338
rs 1.893
bpm 1.2916
cpm 2.2916
noi 14
c 0
b 0
f 0

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){
0 ignored issues
show
Unused Code introduced by
The parameter undefined is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
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') ) {
0 ignored issues
show
Bug introduced by
The variable wp seems to be never declared. If this is a global, consider adding a /** global: wp */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The variable wp seems to be never declared. If this is a global, consider adding a /** global: wp */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
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 ) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable wp is declared in the current environment, consider using typeof wp === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
261
			return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
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 );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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     = {};
0 ignored issues
show
Unused Code introduced by
The assignment to variable attrs seems to be never used. Consider removing it.
Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
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 } );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
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 } );
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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 ) {
0 ignored issues
show
Best Practice introduced by
If you intend to check if the variable wp is declared in the current environment, consider using typeof wp === "undefined" instead. This is safe if the variable is not actually declared.
Loading history...
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
			});
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
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