PluginProbe
UpStream: a Project Management Plugin for WordPress / 2.1.0
UpStream: a Project Management Plugin for WordPress v2.1.0
trunk 1.39.0 1.39.1 1.39.2 1.39.3 2.0.7 2.1.0
upstream / includes / libraries / cmb2 / js / cmb2.js

cmb2.js in UpStream: a Project Management Plugin for WordPress 2.1.0, at includes/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