/** * Controls the behaviours of custom metabox fields. * * @author CMB2 team * @see https://github.com/CMB2/CMB2 */ /** * Custom jQuery for Custom Metaboxes and Fields */ window.CMB2 = window.CMB2 || {}; (function(window, document, $, cmb, undefined){ 'use strict'; // localization strings var l10n = window.cmb2_l10; var setTimeout = window.setTimeout; var $document; var $id = function( selector ) { return $( document.getElementById( selector ) ); }; var defaults = { idNumber : false, repeatEls : 'input:not([type="button"],[id^=filelist]),select,textarea,.cmb2-media-status', noEmpty : 'input:not([type="button"]):not([type="radio"]):not([type="checkbox"]),textarea', repeatUpdate : 'input:not([type="button"]),select,textarea,label', styleBreakPoint : 450, mediaHandlers : {}, defaults : { time_picker : l10n.defaults.time_picker, date_picker : l10n.defaults.date_picker, color_picker : l10n.defaults.color_picker || {}, code_editor : l10n.defaults.code_editor, }, media : { frames : {}, }, }; cmb.metabox = function() { if ( cmb.$metabox ) { return cmb.$metabox; } cmb.$metabox = $('.cmb2-wrap > .cmb2-metabox'); return cmb.$metabox; }; cmb.init = function() { $document = $( document ); // Setup the CMB2 object defaults. $.extend( cmb, defaults ); cmb.trigger( 'cmb_pre_init' ); var $metabox = cmb.metabox(); var $repeatGroup = $metabox.find('.cmb-repeatable-group'); // Init time/date/color pickers cmb.initPickers( $metabox.find('input[type="text"].cmb2-timepicker'), $metabox.find('input[type="text"].cmb2-datepicker'), $metabox.find('input[type="text"].cmb2-colorpicker') ); // Init code editors. cmb.initCodeEditors( $metabox.find( '.cmb2-textarea-code:not(.disable-codemirror)' ) ); // Insert toggle button into DOM wherever there is multicheck. credit: Genesis Framework $( '
' ).insertBefore( '.cmb2-checkbox-list:not(.no-select-all)' ); // Make File List drag/drop sortable: cmb.makeListSortable(); // Make Repeatable fields drag/drop sortable: cmb.makeRepeatableSortable(); $metabox .on( 'change', '.cmb2_upload_file', function() { cmb.media.field = $( this ).attr( 'id' ); $id( cmb.media.field + '_id' ).val(''); }) // Media/file management .on( 'click', '.cmb-multicheck-toggle', cmb.toggleCheckBoxes ) .on( 'click', '.cmb2-upload-button', cmb.handleMedia ) .on( 'click', '.cmb-attach-list li, .cmb2-media-status .img-status img, .cmb2-media-status .file-status > span', cmb.handleFileClick ) .on( 'click', '.cmb2-remove-file-button', cmb.handleRemoveMedia ) // Repeatable content .on( 'click', '.cmb-add-group-row', cmb.addGroupRow ) .on( 'click', '.cmb-add-row-button', cmb.addAjaxRow ) .on( 'click', '.cmb-remove-group-row', cmb.removeGroupRow ) .on( 'click', '.cmb-remove-row-button', cmb.removeAjaxRow ) // Ajax oEmbed display .on( 'keyup paste focusout', '.cmb2-oembed', cmb.maybeOembed ) // Reset titles when removing a row .on( 'cmb2_remove_row', '.cmb-repeatable-group', cmb.resetTitlesAndIterator ) .on( 'click', '.cmbhandle, .cmbhandle + .cmbhandle-title', cmb.toggleHandle ); if ( $repeatGroup.length ) { $repeatGroup .on( 'cmb2_add_row', cmb.emptyValue ) .on( 'cmb2_add_row', cmb.setDefaults ) .filter('.sortable').each( function() { // Add sorting arrows $( this ).find( '.cmb-remove-group-row-button' ).before( ' ' ); }) .on( 'click', '.cmb-shift-rows', cmb.shiftRows ); } // on pageload setTimeout( cmb.resizeoEmbeds, 500); // and on window resize $( window ).on( 'resize', cmb.resizeoEmbeds ); if ( $id( 'addtag' ).length ) { cmb.listenTagAdd(); } cmb.trigger( 'cmb_init' ); }; cmb.listenTagAdd = function() { $document.ajaxSuccess( function( evt, xhr, settings ) { if ( settings.data && settings.data.length && -1 !== settings.data.indexOf( 'action=add-tag' ) ) { cmb.resetBoxes( $id( 'addtag' ).find( '.cmb2-wrap > .cmb2-metabox' ) ); } }); }; cmb.resetBoxes = function( $boxes ) { $.each( $boxes, function() { cmb.resetBox( $( this ) ); }); }; cmb.resetBox = function( $box ) { $box.find( '.wp-picker-clear' ).trigger( 'click' ); $box.find( '.cmb2-remove-file-button' ).trigger( 'click' ); $box.find( '.cmb-row.cmb-repeatable-grouping:not(:first-of-type) .cmb-remove-group-row' ).click(); $box.find( '.cmb-repeat-row:not(:first-child)' ).remove(); $box.find( 'input:not([type="button"]),select,textarea' ).each( function() { var $element = $( this ); var tagName = $element.prop('tagName'); if ( 'INPUT' === tagName ) { var elType = $element.attr( 'type' ); if ( 'checkbox' === elType || 'radio' === elType ) { $element.prop( 'checked', false ); } else { $element.val( '' ); } } if ( 'SELECT' === tagName ) { $( 'option:selected', this ).prop( 'selected', false ); } if ( 'TEXTAREA' === tagName ) { $element.html( '' ); } }); }; cmb.resetTitlesAndIterator = function( evt ) { if ( ! evt.group ) { return; } // Loop repeatable group tables $( '.cmb-repeatable-group.repeatable' ).each( function() { var $table = $( this ); var groupTitle = $table.find( '.cmb-add-group-row' ).data( 'grouptitle' ); // Loop repeatable group table rows $table.find( '.cmb-repeatable-grouping' ).each( function( rowindex ) { var $row = $( this ); var $rowTitle = $row.find( 'h3.cmb-group-title' ); // Reset rows iterator $row.data( 'iterator', rowindex ); // Reset rows title if ( $rowTitle.length ) { $rowTitle.text( groupTitle.replace( '{#}', ( rowindex + 1 ) ) ); } }); }); }; cmb.toggleHandle = function( evt ) { evt.preventDefault(); cmb.trigger( 'postbox-toggled', $( this ).parent('.postbox').toggleClass('closed') ); }; cmb.toggleCheckBoxes = function( evt ) { evt.preventDefault(); var $this = $( this ); var $multicheck = $this.closest( '.cmb-td' ).find( 'input[type=checkbox]:not([disabled])' ); // If the button has already been clicked once... if ( $this.data( 'checked' ) ) { // clear the checkboxes and remove the flag $multicheck.prop( 'checked', false ); $this.data( 'checked', false ); } // Otherwise mark the checkboxes and add a flag else { $multicheck.prop( 'checked', true ); $this.data( 'checked', true ); } }; cmb.handleMedia = function( evt ) { evt.preventDefault(); var $el = $( this ); cmb.attach_id = ! $el.hasClass( 'cmb2-upload-list' ) ? $el.closest( '.cmb-td' ).find( '.cmb2-upload-file-id' ).val() : false; // Clean up default 0 value cmb.attach_id = '0' !== cmb.attach_id ? cmb.attach_id : false; cmb._handleMedia( $el.prev('input.cmb2-upload-file').attr('id'), $el.hasClass( 'cmb2-upload-list' ) ); }; cmb.handleFileClick = function( evt ) { if ( $( evt.target ).is( 'a' ) ) { return; } evt.preventDefault(); var $el = $( this ); var $td = $el.closest( '.cmb-td' ); var isList = $td.find( '.cmb2-upload-button' ).hasClass( 'cmb2-upload-list' ); cmb.attach_id = isList ? $el.find( 'input[type="hidden"]' ).data( 'id' ) : $td.find( '.cmb2-upload-file-id' ).val(); if ( cmb.attach_id ) { cmb._handleMedia( $td.find( 'input.cmb2-upload-file' ).attr( 'id' ), isList, cmb.attach_id ); } }; cmb._handleMedia = function( id, isList ) { if ( ! wp ) { return; } var media, handlers; handlers = cmb.mediaHandlers; media = cmb.media; media.field = id; media.$field = $id( media.field ); media.fieldData = media.$field.data(); media.previewSize = media.fieldData.previewsize; media.sizeName = media.fieldData.sizename; media.fieldName = media.$field.attr('name'); media.isList = isList; // If this field's media frame already exists, reopen it. if ( id in media.frames ) { return media.frames[ id ].open(); } // Create the media frame. media.frames[ id ] = wp.media( { title: cmb.metabox().find('label[for="' + id + '"]').text(), library : media.fieldData.queryargs || {}, button: { text: l10n.strings[ isList ? 'upload_files' : 'upload_file' ] }, multiple: isList ? 'add' : false } ); // Enable the additional media filters: https://github.com/CMB2/CMB2/issues/873 media.frames[ id ].states.first().set( 'filterable', 'all' ); cmb.trigger( 'cmb_media_modal_init', media ); handlers.list = function( selection, returnIt ) { // Setup our fileGroup array var fileGroup = []; var attachmentHtml; if ( ! handlers.list.templates ) { handlers.list.templates = { image : wp.template( 'cmb2-list-image' ), file : wp.template( 'cmb2-list-file' ), }; } // Loop through each attachment selection.each( function( attachment ) { // Image preview or standard generic output if it's not an image. attachmentHtml = handlers.getAttachmentHtml( attachment, 'list' ); // Add our file to our fileGroup array fileGroup.push( attachmentHtml ); }); if ( ! returnIt ) { // Append each item from our fileGroup array to .cmb2-media-status media.$field.siblings( '.cmb2-media-status' ).append( fileGroup ); } else { return fileGroup; } }; handlers.single = function( selection ) { if ( ! handlers.single.templates ) { handlers.single.templates = { image : wp.template( 'cmb2-single-image' ), file : wp.template( 'cmb2-single-file' ), }; } // Only get one file from the uploader var attachment = selection.first(); media.$field.val( attachment.get( 'url' ) ); $id( media.field +'_id' ).val( attachment.get( 'id' ) ); // Image preview or standard generic output if it's not an image. var attachmentHtml = handlers.getAttachmentHtml( attachment, 'single' ); // add/display our output media.$field.siblings( '.cmb2-media-status' ).slideDown().html( attachmentHtml ); }; handlers.getAttachmentHtml = function( attachment, templatesId ) { var isImage = 'image' === attachment.get( 'type' ); var data = handlers.prepareData( attachment, isImage ); // Image preview or standard generic output if it's not an image. return handlers[ templatesId ].templates[ isImage ? 'image' : 'file' ]( data ); }; handlers.prepareData = function( data, image ) { if ( image ) { // Set the correct image size data handlers.getImageData.call( data, 50 ); } data = data.toJSON(); data.mediaField = media.field; data.mediaFieldName = media.fieldName; data.stringRemoveImage = l10n.strings.remove_image; data.stringFile = l10n.strings.file; data.stringDownload = l10n.strings.download; data.stringRemoveFile = l10n.strings.remove_file; return data; }; handlers.getImageData = function( fallbackSize ) { // Preview size dimensions var previewW = media.previewSize[0] || fallbackSize; var previewH = media.previewSize[1] || fallbackSize; // Image dimensions and url var url = this.get( 'url' ); var width = this.get( 'width' ); var height = this.get( 'height' ); var sizes = this.get( 'sizes' ); // Get the correct dimensions and url if a named size is set and exists // fallback to the 'large' size if ( sizes ) { if ( sizes[ media.sizeName ] ) { url = sizes[ media.sizeName ].url; width = sizes[ media.sizeName ].width; height = sizes[ media.sizeName ].height; } else if ( sizes.large ) { url = sizes.large.url; width = sizes.large.width; height = sizes.large.height; } } // Fit the image in to the preview size, keeping the correct aspect ratio if ( width > previewW ) { height = Math.floor( previewW * height / width ); width = previewW; } if ( height > previewH ) { width = Math.floor( previewH * width / height ); height = previewH; } if ( ! width ) { width = previewW; } if ( ! height ) { height = 'svg' === this.get( 'filename' ).split( '.' ).pop() ? '100%' : previewH; } this.set( 'sizeUrl', url ); this.set( 'sizeWidth', width ); this.set( 'sizeHeight', height ); return this; }; handlers.selectFile = function() { var selection = media.frames[ id ].state().get( 'selection' ); var type = isList ? 'list' : 'single'; if ( cmb.attach_id && isList ) { $( '[data-id="'+ cmb.attach_id +'"]' ).parents( 'li' ).replaceWith( handlers.list( selection, true ) ); } else { handlers[type]( selection ); } cmb.trigger( 'cmb_media_modal_select', selection, media ); }; handlers.openModal = function() { var selection = media.frames[ id ].state().get( 'selection' ); var attach; if ( ! cmb.attach_id ) { selection.reset(); } else { attach = wp.media.attachment( cmb.attach_id ); attach.fetch(); selection.set( attach ? [ attach ] : [] ); } cmb.trigger( 'cmb_media_modal_open', selection, media ); }; // When a file is selected, run a callback. media.frames[ id ] .on( 'select', handlers.selectFile ) .on( 'open', handlers.openModal ); // Finally, open the modal media.frames[ id ].open(); }; cmb.handleRemoveMedia = function( evt ) { evt.preventDefault(); var $this = $( this ); if ( $this.is( '.cmb-attach-list .cmb2-remove-file-button' ) ) { $this.parents( '.cmb2-media-item' ).remove(); return false; } cmb.media.field = $this.attr('rel'); cmb.metabox().find( document.getElementById( cmb.media.field ) ).val(''); cmb.metabox().find( document.getElementById( cmb.media.field + '_id' ) ).val(''); $this.parents('.cmb2-media-status').html(''); return false; }; cmb.cleanRow = function( $row, prevNum, group ) { var $elements = $row.find( cmb.repeatUpdate ); if ( group ) { var $other = $row.find( '[id]' ).not( cmb.repeatUpdate ); // Remove extra ajaxed rows $row.find('.cmb-repeat-table .cmb-repeat-row:not(:first-child)').remove(); // Update all elements w/ an ID if ( $other.length ) { $other.each( function() { var $_this = $( this ); var oldID = $_this.attr( 'id' ); var newID = oldID.replace( '_'+ prevNum, '_'+ cmb.idNumber ); var $buttons = $row.find('[data-selector="'+ oldID +'"]'); $_this.attr( 'id', newID ); // Replace data-selector vars if ( $buttons.length ) { $buttons.attr( 'data-selector', newID ).data( 'selector', newID ); } }); } } $elements.filter( ':checked' ).removeAttr( 'checked' ); $elements.find( ':checked' ).removeAttr( 'checked' ); $elements.filter( ':selected' ).removeAttr( 'selected' ); $elements.find( ':selected' ).removeAttr( 'selected', false ); if ( $row.find('h3.cmb-group-title').length ) { $row.find( 'h3.cmb-group-title' ).text( $row.data( 'title' ).replace( '{#}', ( cmb.idNumber + 1 ) ) ); } $elements.each( function() { cmb.elReplacements( $( this ), prevNum, group ); } ); return cmb; }; cmb.elReplacements = function( $newInput, prevNum, group ) { var oldFor = $newInput.attr( 'for' ); var oldVal = $newInput.val(); var type = $newInput.prop( 'type' ); var defVal = cmb.getFieldArg( $newInput, 'default' ); var newVal = 'undefined' !== typeof defVal && false !== defVal ? defVal : ''; var tagName = $newInput.prop('tagName'); var checkable = 'radio' === type || 'checkbox' === type ? oldVal : false; var attrs = {}; var newID, oldID; if ( oldFor ) { attrs = { 'for' : oldFor.replace( '_'+ prevNum, '_'+ cmb.idNumber ) }; } else { var oldName = $newInput.attr( 'name' ); var newName; oldID = $newInput.attr( 'id' ); // Handle adding groups vs rows. if ( group ) { // Expect another bracket after group's index closing bracket. newName = oldName ? oldName.replace( '['+ prevNum +'][', '['+ cmb.idNumber +'][' ) : ''; // Expect another underscore after group's index trailing underscore. newID = oldID ? oldID.replace( '_' + prevNum + '_', '_' + cmb.idNumber + '_' ) : ''; } else { // Row indexes are at the very end of the string. newName = oldName ? cmb.replaceLast( oldName, '[' + prevNum + ']', '[' + cmb.idNumber + ']' ) : ''; newID = oldID ? cmb.replaceLast( oldID, '_' + prevNum, '_' + cmb.idNumber ) : ''; } attrs = { id: newID, name: newName }; } // Clear out textarea values if ( 'TEXTAREA' === tagName ) { $newInput.html( newVal ); } if ( 'SELECT' === tagName && undefined !== typeof defVal ) { var $toSelect = $newInput.find( '[value="'+ defVal + '"]' ); if ( $toSelect.length ) { $toSelect.attr( 'selected', 'selected' ).prop( 'selected', 'selected' ); } } if ( checkable ) { $newInput.removeAttr( 'checked' ); if ( undefined !== typeof defVal && oldVal === defVal ) { $newInput.attr( 'checked', 'checked' ).prop( 'checked', 'checked' ); } } if ( ! group && $newInput[0].hasAttribute( 'data-iterator' ) ) { attrs['data-iterator'] = cmb.idNumber; } $newInput .removeClass( 'hasDatepicker' ) .val( checkable ? checkable : newVal ).attr( attrs ); return $newInput; }; cmb.newRowHousekeeping = function( $row ) { var $colorPicker = $row.find( '.wp-picker-container' ); var $list = $row.find( '.cmb2-media-status' ); if ( $colorPicker.length ) { // Need to clean-up colorpicker before appending $colorPicker.each( function() { var $td = $( this ).parent(); $td.html( $td.find( 'input[type="text"].cmb2-colorpicker' ).attr('style', '') ); }); } // Need to clean-up colorpicker before appending if ( $list.length ) { $list.empty(); } return cmb; }; cmb.afterRowInsert = function( $row ) { // Init pickers from new row cmb.initPickers( $row.find('input[type="text"].cmb2-timepicker'), $row.find('input[type="text"].cmb2-datepicker'), $row.find('input[type="text"].cmb2-colorpicker') ); }; cmb.updateNameAttr = function () { var $this = $( this ); var name = $this.attr( 'name' ); // get current name // If name is defined if ( typeof name !== 'undefined' ) { var prevNum = parseInt( $this.parents( '.cmb-repeatable-grouping' ).data( 'iterator' ), 10 ); var newNum = prevNum - 1; // Subtract 1 to get new iterator number // Update field name attributes so data is not orphaned when a row is removed and post is saved var $newName = name.replace( '[' + prevNum + ']', '[' + newNum + ']' ); // New name with replaced iterator $this.attr( 'name', $newName ); } }; cmb.emptyValue = function( evt, row ) { $( cmb.noEmpty, row ).val( '' ); }; cmb.setDefaults = function( evt, row ) { $( cmb.noEmpty, row ).each( function() { var $el = $(this); var defVal = cmb.getFieldArg( $el, 'default' ); if ( 'undefined' !== typeof defVal && false !== defVal ) { $el.val( defVal ); } }); }; cmb.addGroupRow = function( evt ) { evt.preventDefault(); var $this = $( this ); // before anything significant happens cmb.triggerElement( $this, 'cmb2_add_group_row_start', $this ); var $table = $id( $this.data('selector') ); var $oldRow = $table.find('.cmb-repeatable-grouping').last(); var prevNum = parseInt( $oldRow.data('iterator'), 10 ); cmb.idNumber = parseInt( prevNum, 10 ) + 1; var $row = $oldRow.clone(); // Make sure the next number doesn't exist. while ( $table.find( '.cmb-repeatable-grouping[data-iterator="'+ cmb.idNumber +'"]' ).length > 0 ) { cmb.idNumber++; } cmb.newRowHousekeeping( $row.data( 'title', $this.data( 'grouptitle' ) ) ).cleanRow( $row, prevNum, true ); $row.find( '.cmb-add-row-button' ).prop( 'disabled', false ); var $newRow = $( '