PluginProbe
Virtue/Ascend/Pinnacle Toolkit / 4.9.12.2
Virtue/Ascend/Pinnacle Toolkit v4.9.12.2
4.9.12.2 trunk 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 3.0 3.1 3.2 3.3 3.4 3.7 All 48 releases
virtue-toolkit / cmb / js / cmb2.js

cmb2.js in Virtue/Ascend/Pinnacle Toolkit 4.9.12.2, at cmb/js/cmb2.js

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