PluginProbe
WPGet API – Connect to any external REST API / 2.2.10
WPGet API – Connect to any external REST API v2.2.10
1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.3 2.1.4 2.1.5 2.2.0 2.2.1 2.2.10 2.2.2 2.2.3 All 97 releases
wpgetapi / lib / cmb2 / js / cmb2.js

cmb2.js in WPGet API – Connect to any external REST API 2.2.10, at lib/cmb2/js/cmb2.js

1,351 lines 40.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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', 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' ).prop( 'checked', false );
507 $elements.find( ':checked' ).prop( 'checked', false );
508 $elements.filter( ':selected' ).prop( 'selected', false );
509 $elements.find( ':selected' ).prop( '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.prop( 'checked', false );
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 cmb.makeRepeatableSortable( $newRow );
679
680 cmb.triggerElement( $table, { type: 'cmb2_add_row', group: true }, $newRow );
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 ( ! cmb.resetRow.resetting && 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 cmb.resetRow.resetting = true;
762 // Click the "add new" button followed by the "remove this" button
763 // in order to reset the repeat row to empty values.
764 $addNewBtn.trigger( 'click' );
765 $removeBtn.trigger( 'click' );
766 cmb.resetRow.resetting = false;
767 };
768
769 cmb.shiftRows = function( evt ) {
770
771 evt.preventDefault();
772
773 var $this = $( this );
774 var $from = $this.parents( '.cmb-repeatable-grouping' );
775 var $goto = $this.hasClass( 'move-up' ) ? $from.prev( '.cmb-repeatable-grouping' ) : $from.next( '.cmb-repeatable-grouping' );
776
777 // Before shift occurs.
778 cmb.triggerElement( $this, 'cmb2_shift_rows_enter', $this, $from, $goto );
779
780 if ( ! $goto.length ) {
781 return;
782 }
783
784 // About to shift
785 cmb.triggerElement( $this, 'cmb2_shift_rows_start', $this, $from, $goto );
786
787 var inputVals = [];
788 // Loop this item's fields
789 $from.find( cmb.repeatEls ).each( function() {
790 var $element = $( this );
791 var elType = $element.attr( 'type' );
792 var val;
793
794 if ( $element.hasClass('cmb2-media-status') ) {
795 // special case for image previews
796 val = $element.html();
797 } else if ( 'checkbox' === elType || 'radio' === elType ) {
798 val = $element.is(':checked');
799 } else if ( 'select' === $element.prop('tagName') ) {
800 val = $element.is(':selected');
801 } else {
802 val = $element.val();
803 }
804
805 // Get all the current values per element
806 inputVals.push( { val: val, $: $element } );
807 });
808 // And swap them all
809 $goto.find( cmb.repeatEls ).each( function( index ) {
810 var $element = $( this );
811 var elType = $element.attr( 'type' );
812 var val;
813
814 if ( $element.hasClass('cmb2-media-status') ) {
815 var toRowId = $element.closest('.cmb-repeatable-grouping').attr('data-iterator');
816 var fromRowId = inputVals[ index ].$.closest('.cmb-repeatable-grouping').attr('data-iterator');
817
818 // special case for image previews
819 val = $element.html();
820 $element.html( inputVals[ index ].val );
821 inputVals[ index ].$.html( val );
822
823 inputVals[ index ].$.find( 'input' ).each(function() {
824 var name = $( this ).attr( 'name' );
825 name = name.replace( '['+toRowId+']', '['+fromRowId+']' );
826 $( this ).attr( 'name', name );
827 });
828 $element.find('input').each(function() {
829 var name = $( this ).attr('name');
830 name = name.replace('['+fromRowId+']', '['+toRowId+']');
831 $( this ).attr('name', name);
832 });
833
834 }
835 // handle checkbox swapping
836 else if ( 'checkbox' === elType ) {
837 inputVals[ index ].$.prop( 'checked', $element.is(':checked') );
838 $element.prop( 'checked', inputVals[ index ].val );
839 }
840 // handle radio swapping
841 else if ( 'radio' === elType ) {
842 if ( $element.is( ':checked' ) ) {
843 inputVals[ index ].$.attr( 'data-checked', 'true' );
844 }
845 if ( inputVals[ index ].$.is( ':checked' ) ) {
846 $element.attr( 'data-checked', 'true' );
847 }
848 }
849 // handle select swapping
850 else if ( 'select' === $element.prop('tagName') ) {
851 inputVals[ index ].$.prop( 'selected', $element.is(':selected') );
852 $element.prop( 'selected', inputVals[ index ].val );
853 }
854 // handle normal input swapping
855 else {
856 inputVals[ index ].$.val( $element.val() );
857 $element.val( inputVals[ index ].val );
858 }
859 });
860
861 $from.find( 'input[data-checked=true]' ).prop( 'checked', true ).removeAttr( 'data-checked' );
862 $goto.find( 'input[data-checked=true]' ).prop( 'checked', true ).removeAttr( 'data-checked' );
863
864 // trigger color picker change event
865 $from.find( 'input[type="text"].cmb2-colorpicker' ).trigger( 'change' );
866 $goto.find( 'input[type="text"].cmb2-colorpicker' ).trigger( 'change' );
867
868 // shift done
869 cmb.triggerElement( $this, 'cmb2_shift_rows_complete', $this, $from, $goto );
870 };
871
872 cmb.initPickers = function( $timePickers, $datePickers, $colorPickers ) {
873 cmb.trigger( 'cmb_init_pickers', {
874 time: $timePickers,
875 date: $datePickers,
876 color: $colorPickers
877 } );
878
879 // Initialize jQuery UI timepickers
880 cmb.initDateTimePickers( $timePickers, 'timepicker', 'time_picker' );
881 // Initialize jQuery UI datepickers
882 cmb.initDateTimePickers( $datePickers, 'datepicker', 'date_picker' );
883 // Initialize color picker
884 cmb.initColorPickers( $colorPickers );
885 };
886
887 cmb.initDateTimePickers = function( $selector, method, defaultKey ) {
888 if ( $selector.length ) {
889 $selector[ method ]( 'destroy' ).each( function() {
890 var $this = $( this );
891 var fieldOpts = $this.data( method ) || {};
892 var options = $.extend( {}, cmb.defaults[ defaultKey ], fieldOpts );
893 $this[ method ]( cmb.datePickerSetupOpts( fieldOpts, options, method ) );
894 } );
895 }
896 };
897
898 cmb.datePickerSetupOpts = function( fieldOpts, options, method ) {
899 var existing = $.extend( {}, options );
900
901 options.beforeShow = function( input, inst ) {
902 if ( 'timepicker' === method ) {
903 cmb.addTimePickerClasses( inst.dpDiv );
904 }
905
906 // Wrap datepicker w/ class to narrow the scope of jQuery UI CSS and prevent conflicts
907 $id( 'ui-datepicker-div' ).addClass( 'cmb2-element' );
908
909 // Let's be sure to call beforeShow if it was added
910 if ( 'function' === typeof existing.beforeShow ) {
911 existing.beforeShow( input, inst );
912 }
913 };
914
915 if ( 'timepicker' === method ) {
916 options.onChangeMonthYear = function( year, month, inst, picker ) {
917 cmb.addTimePickerClasses( inst.dpDiv );
918
919 // Let's be sure to call onChangeMonthYear if it was added
920 if ( 'function' === typeof existing.onChangeMonthYear ) {
921 existing.onChangeMonthYear( year, month, inst, picker );
922 }
923 };
924 }
925
926 options.onClose = function( dateText, inst ) {
927 // Remove the class when we're done with it (and hide to remove FOUC).
928 var $picker = $id( 'ui-datepicker-div' ).removeClass( 'cmb2-element' ).hide();
929 if ( 'timepicker' === method && ! $( inst.input ).val() ) {
930 // Set the timepicker field value if it's empty.
931 inst.input.val( $picker.find( '.ui_tpicker_time' ).text() );
932 }
933
934 // Let's be sure to call onClose if it was added
935 if ( 'function' === typeof existing.onClose ) {
936 existing.onClose( dateText, inst );
937 }
938 };
939
940 return options;
941 };
942
943 // Adds classes to timepicker buttons.
944 cmb.addTimePickerClasses = function( $picker ) {
945 var func = cmb.addTimePickerClasses;
946 func.count = func.count || 0;
947
948 // Wait a bit to let the timepicker render, since these are pre-render events.
949 setTimeout( function() {
950 if ( $picker.find( '.ui-priority-secondary' ).length ) {
951 $picker.find( '.ui-priority-secondary' ).addClass( 'button-secondary' );
952 $picker.find( '.ui-priority-primary' ).addClass( 'button-primary' );
953 func.count = 0;
954 } else if ( func.count < 5 ) {
955 func.count++;
956 func( $picker );
957 }
958 }, 10 );
959 };
960
961 cmb.initColorPickers = function( $selector ) {
962 if ( ! $selector.length ) {
963 return;
964 }
965 if ( 'object' === typeof jQuery.wp && 'function' === typeof jQuery.wp.wpColorPicker ) {
966
967 $selector.each( function() {
968 var $this = $( this );
969 var fieldOpts = $this.data( 'colorpicker' ) || {};
970 $this.wpColorPicker( $.extend( {}, cmb.defaults.color_picker, fieldOpts ) );
971 } );
972
973 } else {
974 $selector.each( function( i ) {
975 $( this ).after( '<div id="picker-' + i + '" style="z-index: 1000; background: #EEE; border: 1px solid #CCC; position: absolute; display: block;"></div>' );
976 $id( 'picker-' + i ).hide().farbtastic( $( this ) );
977 } )
978 .focus( function() {
979 $( this ).next().show();
980 } )
981 .blur( function() {
982 $( this ).next().hide();
983 } );
984 }
985 };
986
987 cmb.initCodeEditors = function( $selector ) {
988 cmb.trigger( 'cmb_init_code_editors', $selector );
989
990 if ( ! cmb.defaults.code_editor || ! wp || ! wp.codeEditor || ! $selector.length ) {
991 return;
992 }
993
994 $selector.each( function() {
995 wp.codeEditor.initialize(
996 this.id,
997 cmb.codeEditorArgs( $( this ).data( 'codeeditor' ) )
998 );
999 } );
1000 };
1001
1002 cmb.codeEditorArgs = function( overrides ) {
1003 var props = [ 'codemirror', 'csslint', 'jshint', 'htmlhint' ];
1004 var args = $.extend( {}, cmb.defaults.code_editor );
1005 overrides = overrides || {};
1006
1007 for ( var i = props.length - 1; i >= 0; i-- ) {
1008 if ( overrides.hasOwnProperty( props[i] ) ) {
1009 args[ props[i] ] = $.extend( {}, args[ props[i] ] || {}, overrides[ props[i] ] );
1010 }
1011 }
1012
1013 return args;
1014 };
1015
1016 cmb.makeListSortable = function() {
1017 var $filelist = cmb.metabox().find( '.cmb2-media-status.cmb-attach-list' );
1018 if ( $filelist.length ) {
1019 $filelist.sortable({ cursor: 'move' }).disableSelection();
1020 }
1021 };
1022
1023 cmb.makeRepeatableSortable = function( $row ) {
1024 var $repeatables = ($row || cmb.metabox()).find( '.cmb-repeat-table .cmb-field-list' );
1025
1026 if ( $repeatables.length ) {
1027 $repeatables.sortable({
1028 items : '.cmb-repeat-row',
1029 cursor: 'move',
1030 // The default "cancel" attributes are: "input,textarea,button,select,option".
1031 // We are appending .CodeMirror.
1032 // See https://api.jqueryui.com/sortable/#option-cancel
1033 cancel: 'input,textarea,button,select,option,.CodeMirror'
1034 });
1035 }
1036 };
1037
1038 cmb.maybeOembed = function( evt ) {
1039 var $this = $( this );
1040
1041 var m = {
1042 focusout : function() {
1043 setTimeout( function() {
1044 // if it's been 2 seconds, hide our spinner
1045 cmb.spinner( '.cmb2-metabox', true );
1046 }, 2000);
1047 },
1048 keyup : function() {
1049 var betw = function( min, max ) {
1050 return ( evt.which <= max && evt.which >= min );
1051 };
1052 // Only Ajax on normal keystrokes
1053 if ( betw( 48, 90 ) || betw( 96, 111 ) || betw( 8, 9 ) || evt.which === 187 || evt.which === 190 ) {
1054 // fire our ajax function
1055 cmb.doAjax( $this, evt );
1056 }
1057 },
1058 paste : function() {
1059 // paste event is fired before the value is filled, so wait a bit
1060 setTimeout( function() { cmb.doAjax( $this ); }, 100);
1061 }
1062 };
1063
1064 m[ evt.type ]();
1065 };
1066
1067 /**
1068 * Resize oEmbed videos to fit in their respective metaboxes
1069 *
1070 * @since 0.9.4
1071 *
1072 * @return {return}
1073 */
1074 cmb.resizeoEmbeds = function() {
1075 cmb.metabox().each( function() {
1076 var $this = $( this );
1077 var $tableWrap = $this.parents('.inside');
1078 var isSide = $this.parents('.inner-sidebar').length || $this.parents( '#side-sortables' ).length;
1079 var isSmall = isSide;
1080 var isSmallest = false;
1081 if ( ! $tableWrap.length ) {
1082 return true; // continue
1083 }
1084
1085 // Calculate new width
1086 var tableW = $tableWrap.width();
1087
1088 if ( cmb.styleBreakPoint > tableW ) {
1089 isSmall = true;
1090 isSmallest = ( cmb.styleBreakPoint - 62 ) > tableW;
1091 }
1092
1093 tableW = isSmall ? tableW : Math.round(($tableWrap.width() * 0.82)*0.97);
1094 var newWidth = tableW - 30;
1095 if ( isSmall && ! isSide && ! isSmallest ) {
1096 newWidth = newWidth - 75;
1097 }
1098 if ( newWidth > 639 ) {
1099 return true; // continue
1100 }
1101
1102 var $embeds = $this.find('.cmb-type-oembed .embed-status');
1103 var $children = $embeds.children().not('.cmb2-remove-wrapper');
1104 if ( ! $children.length ) {
1105 return true; // continue
1106 }
1107
1108 $children.each( function() {
1109 var $this = $( this );
1110 var iwidth = $this.width();
1111 var iheight = $this.height();
1112 var _newWidth = newWidth;
1113 if ( $this.parents( '.cmb-repeat-row' ).length && ! isSmall ) {
1114 // Make room for our repeatable "remove" button column
1115 _newWidth = newWidth - 91;
1116 _newWidth = 785 > tableW ? _newWidth - 15 : _newWidth;
1117 }
1118 // Calc new height
1119 var newHeight = Math.round((_newWidth * iheight)/iwidth);
1120 $this.width(_newWidth).height(newHeight);
1121 });
1122 });
1123 };
1124
1125 // function for running our ajax
1126 cmb.doAjax = function( $obj ) {
1127 // get typed value
1128 var oembed_url = $obj.val();
1129 // only proceed if the field contains more than 6 characters
1130 if ( oembed_url.length < 6 ) {
1131 return;
1132 }
1133
1134 // get field id
1135 var field_id = $obj.attr('id');
1136 var $context = $obj.closest( '.cmb-td' );
1137 var $embed_container = $context.find( '.embed-status' );
1138 var $embed_wrap = $context.find( '.embed_wrap' );
1139 var $child_el = $embed_container.find( ':first-child' );
1140 var oembed_width = $embed_container.length && $child_el.length ? $child_el.width() : $obj.width();
1141
1142 cmb.log( 'oembed_url', oembed_url, field_id );
1143
1144 // show our spinner
1145 cmb.spinner( $context );
1146 // clear out previous results
1147 $embed_wrap.html('');
1148 // and run our ajax function
1149 setTimeout( function() {
1150 // if they haven't typed in 500 ms
1151 if ( $( '.cmb2-oembed:focus' ).val() !== oembed_url ) {
1152 return;
1153 }
1154 $.ajax({
1155 type : 'post',
1156 dataType : 'json',
1157 url : l10n.ajaxurl,
1158 data : {
1159 'action' : 'cmb2_oembed_handler',
1160 'oembed_url' : oembed_url,
1161 'oembed_width' : oembed_width > 300 ? oembed_width : 300,
1162 'field_id' : field_id,
1163 'object_id' : $obj.data( 'objectid' ),
1164 'object_type' : $obj.data( 'objecttype' ),
1165 'cmb2_ajax_nonce' : l10n.ajax_nonce
1166 },
1167 success: function(response) {
1168 cmb.log( response );
1169 // hide our spinner
1170 cmb.spinner( $context, true );
1171 // and populate our results from ajax response
1172 $embed_wrap.html( response.data );
1173 }
1174 });
1175
1176 }, 500);
1177
1178 };
1179
1180 /**
1181 * Gets jQuery object containing all CMB metaboxes. Caches the result.
1182 *
1183 * @since 1.0.2
1184 *
1185 * @return {Object} jQuery object containing all CMB metaboxes.
1186 */
1187 cmb.metabox = function() {
1188 if ( cmb.$metabox ) {
1189 return cmb.$metabox;
1190 }
1191 cmb.$metabox = $('.cmb2-wrap > .cmb2-metabox');
1192 return cmb.$metabox;
1193 };
1194
1195 /**
1196 * Starts/stops contextual spinner.
1197 *
1198 * @since 1.0.1
1199 *
1200 * @param {object} $context The jQuery parent/context object.
1201 * @param {bool} hide Whether to hide the spinner (will show by default).
1202 *
1203 * @return {void}
1204 */
1205 cmb.spinner = function( $context, hide ) {
1206 var m = hide ? 'removeClass' : 'addClass';
1207 $('.cmb-spinner', $context )[ m ]( 'is-active' );
1208 };
1209
1210 /**
1211 * Triggers a jQuery event on the document object.
1212 *
1213 * @since 2.2.3
1214 *
1215 * @param {string} evtName The name of the event to trigger.
1216 *
1217 * @return {void}
1218 */
1219 cmb.trigger = function( evtName ) {
1220 var args = Array.prototype.slice.call( arguments, 1 );
1221 args.push( cmb );
1222 $document.trigger( evtName, args );
1223 };
1224
1225 /**
1226 * Triggers a jQuery event on the given jQuery object.
1227 *
1228 * @since 2.2.3
1229 *
1230 * @param {object} $el The jQuery element object.
1231 * @param {string} evtName The name of the event to trigger.
1232 *
1233 * @return {void}
1234 */
1235 cmb.triggerElement = function( $el, evtName ) {
1236 var args = Array.prototype.slice.call( arguments, 2 );
1237 args.push( cmb );
1238 $el.trigger( evtName, args );
1239 };
1240
1241 /**
1242 * Get an argument for a given field.
1243 *
1244 * @since 2.5.0
1245 *
1246 * @param {string|object} hash The field hash, id, or a jQuery object for a field.
1247 * @param {string} arg The argument to get on the field.
1248 *
1249 * @return {mixed} The argument value.
1250 */
1251 cmb.getFieldArg = function( hash, arg ) {
1252 return cmb.getField( hash )[ arg ];
1253 };
1254
1255 /**
1256 * Get a field object instances. Can be filtered by passing in a filter callback function.
1257 * e.g. `const fileFields = CMB2.getFields(f => 'file' === f.type);`
1258 *
1259 * @since 2.5.0
1260 *
1261 * @param {mixed} filterCb An optional filter callback function.
1262 *
1263 * @return array An array of field object instances.
1264 */
1265 cmb.getFields = function( filterCb ) {
1266 if ( 'function' === typeof filterCb ) {
1267 var fields = [];
1268 $.each( l10n.fields, function( hash, field ) {
1269 if ( filterCb( field, hash ) ) {
1270 fields.push( field );
1271 }
1272 });
1273 return fields;
1274 }
1275
1276 return l10n.fields;
1277 };
1278
1279 /**
1280 * Get a field object instance by hash or id.
1281 *
1282 * @since 2.5.0
1283 *
1284 * @param {string|object} hash The field hash, id, or a jQuery object for a field.
1285 *
1286 * @return {object} The field object or an empty object.
1287 */
1288 cmb.getField = function( hash ) {
1289 var field = {};
1290 hash = hash instanceof jQuery ? hash.data( 'hash' ) : hash;
1291 if ( hash ) {
1292 try {
1293 if ( l10n.fields[ hash ] ) {
1294 throw new Error( hash );
1295 }
1296
1297 cmb.getFields( function( field ) {
1298 if ( 'function' === typeof hash ) {
1299 if ( hash( field ) ) {
1300 throw new Error( field.hash );
1301 }
1302 } else if ( field.id && field.id === hash ) {
1303 throw new Error( field.hash );
1304 }
1305 });
1306 } catch( e ) {
1307 field = l10n.fields[ e.message ];
1308 }
1309 }
1310
1311 return field;
1312 };
1313
1314 /**
1315 * Safely log things if query var is set. Accepts same parameters as console.log.
1316 *
1317 * @since 1.0.0
1318 *
1319 * @return {void}
1320 */
1321 cmb.log = function() {
1322 if ( l10n.script_debug && console && 'function' === typeof console.log ) {
1323 console.log.apply(console, arguments);
1324 }
1325 };
1326
1327 /**
1328 * Replace the last occurrence of a string.
1329 *
1330 * @since 2.2.6
1331 *
1332 * @param {string} string String to search/replace.
1333 * @param {string} search String to search.
1334 * @param {string} replace String to replace search with.
1335 *
1336 * @return {string} Possibly modified string.
1337 */
1338 cmb.replaceLast = function( string, search, replace ) {
1339 // find the index of last time word was used
1340 var n = string.lastIndexOf( search );
1341
1342 // slice the string in 2, one from the start to the lastIndexOf
1343 // and then replace the word in the rest
1344 return string.slice( 0, n ) + string.slice( n ).replace( search, replace );
1345 };
1346
1347 // Kick it off!
1348 $( cmb.init );
1349
1350 })(window, document, jQuery, window.CMB2);
1351