PluginProbe
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI / 7.9.8
GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI v7.9.8
8.0.4 8.0.3 8.0.1 8.0.2 8.0.0 7.9.9.7 7.9.9.6 7.9.9.5 7.9.9.4 7.9.9.3 7.9.9.2 7.9.9.1 7.9.9 7.9.8 7.9.7 7.9.6 7.9.5 7.9.4 7.9.3 7.9.2 7.9.1 7.9.0 7.8.9 7.8.8 7.8.7 All 46 releases
gamipress / libraries / cmb2 / js / cmb2.js

cmb2.js in GamiPress – Gamification plugin to reward points, badges & ranks in WordPress, now with AI 7.9.8, at libraries/cmb2/js/cmb2.js

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