Skip to content

Instantly share code, notes, and snippets.

@harkor
Created April 8, 2021 10:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harkor/f89ad9990b98fd41c7503d3f525e781e to your computer and use it in GitHub Desktop.
Save harkor/f89ad9990b98fd41c7503d3f525e781e to your computer and use it in GitHub Desktop.
Copy Button for groups Metabox
jQuery(function($){
setTimeout(function(){
$('.rwmb-clone').each(function(){
var $this = $(this);
$this.prepend('<a href="#" class="rwmb-button copy-clone"><span class="dashicons dashicons-controls-repeat"></span></a>');
});
}, 500);
});
( function( $, _, document, window, rwmb, i18n ) {
'use strict';
var group = {
toggle: {}, // Toggle module for handling collapsible/expandable groups.
clone: {} // Clone module for handling clone groups.
};
/**
* Handles a click on either the group title or the group collapsible/expandable icon.
* Expects `this` to equal the clicked element.
*
* @param event Click event.
*/
group.toggle.handle = function( event ) {
event.preventDefault();
event.stopPropagation();
var $group = $( this ).closest( '.rwmb-group-clone, .rwmb-group-non-cloneable' ),
state = $group.hasClass( 'rwmb-group-collapsed' ) ? 'expanded' : 'collapsed';
group.toggle.updateState( $group, state );
// Refresh maps to make them visible.
$( window ).trigger( 'rwmb_map_refresh' );
};
/**
* Update the group expanded/collapsed state.
*
* @param $group Group element.
* @param state Force group to have a state.
*/
group.toggle.updateState = function( $group, state ) {
var $input = $group.find( '.rwmb-group-state' ).last().find( 'input' );
if ( ! $input.length && ! state ) {
return;
}
if ( state ) {
$input.val( state );
} else {
state = $input.val();
}
// Store current state. Will be preserved when cloning.
$input.attr( 'data-current', state );
$input.trigger( 'change' );
$group.toggleClass( 'rwmb-group-collapsed', 'collapsed' === state )
.find( '.rwmb-group-toggle-handle' ).first().attr( 'aria-expanded', 'collapsed' !== state );
};
/**
* Update group title.
*
* @param index Group clone index.
* @param element Group element.
*/
group.toggle.updateTitle = function ( index, element ) {
var $group = $( element ),
$title = $group.find( '> .rwmb-group-title-wrapper > .rwmb-group-title, > .rwmb-input > .rwmb-group-title-wrapper > .rwmb-group-title' ),
options = $title.data( 'options' );
if ( 'undefined' === typeof options ) {
return;
}
var content = options.content || '',
fields = options.fields || [];
function processField( field ) {
if ( -1 === content.indexOf( '{' + field + '}' ) ) {
return;
}
var selectors = 'input[name*="[' + field + ']"], textarea[name*="[' + field + ']"], select[name*="[' + field + ']"], button[name*="[' + field + ']"]',
$field = $group.find( selectors );
if ( ! $field.length ) {
return;
}
var fieldValue = $field.val() || '';
if ( $field.is( 'select' ) && fieldValue ) {
fieldValue = $field.find( 'option:selected' ).text();
}
content = content.replace( '{' + field + '}', fieldValue );
// Update title when field's value is changed.
if ( ! $field.data( 'update-group-title' ) ) {
$field.on( 'keyup change', _.debounce( function () {
group.toggle.updateTitle( index, element );
}, 250 ) ).data( 'update-group-title', true );
}
}
content = content.replace( '{#}', index );
fields.forEach( processField );
$title.text( content );
};
/**
* Initialize the title on load or when new clone is added.
*
* @param $container Wrapper (on load) or group element (when new clone is added)
*/
group.toggle.initTitle = function ( $container ) {
$container.find( '.rwmb-group-collapsible' ).each( function () {
// Update group title for non-cloneable groups.
var $this = $( this );
if ( $this.hasClass( 'rwmb-group-non-cloneable' ) ) {
group.toggle.updateTitle( 1, this );
group.toggle.updateState( $this );
return;
}
$this.children( '.rwmb-input' ).each( function () {
var $input = $( this );
// Update group title.
$input.children( '.rwmb-group-clone' ).each( function ( index, clone ) {
group.toggle.updateTitle( index + 1, clone );
group.toggle.updateState( $( clone ) );
} );
// Drag and drop clones via group title.
if ( $input.data( 'ui-sortable' ) ) { // If sortable is initialized.
$input.sortable( 'option', 'handle', '.rwmb-clone-icon + .rwmb-group-title-wrapper' );
} else { // If not.
$input.on( 'sortcreate', function () {
$input.sortable( 'option', 'handle', '.rwmb-clone-icon + .rwmb-group-title-wrapper' );
} );
}
} );
} );
};
/**
* Initialize the collapsible state when first loaded.
* Add class 'rwmb-group-collapsed' to group clones.
* Non-cloneable groups have that class already - added via PHP.
*/
group.toggle.initState = function () {
$( '.rwmb-group-collapsible.rwmb-group-collapsed' ).each( function () {
var $this = $( this );
if ( ! $this.hasClass( 'rwmb-group-non-cloneable' ) ) {
$this.children( '.rwmb-input' ).children( '.rwmb-group-clone' ).addClass( 'rwmb-group-collapsed' );
}
} );
};
/**
* Update group index for inputs
*/
group.clone.updateGroupIndex = function () {
var that = this,
$clones = $( this ).parents( '.rwmb-group-clone' ),
totalLevel = $clones.length;
$clones.each( function ( i, clone ) {
var index = parseInt( $( clone ).parent().data( 'next-index' ) ) - 1,
level = totalLevel - i;
group.clone.replaceName.call( that, level, index );
// Stop each() loop immediately when reach the new clone group.
if ( $( clone ).data( 'clone-group-new' ) ) {
return false;
}
} );
};
group.clone.updateIndex = function() {
// debugger;
var $this = $( this );
// Update index only for sub fields in a group
if ( ! $this.closest( '.rwmb-group-clone' ).length ) {
return;
}
// Do not update index if field is not cloned
if ( ! $this.closest( '.rwmb-input' ).children( '.rwmb-clone' ).length ) {
return;
}
var index = parseInt( $this.closest( '.rwmb-input' ).data( 'next-index' ) ) - 1,
level = $this.parents( '.rwmb-clone' ).length;
group.clone.replaceName.call( this, level, index );
// Stop propagation.
return false;
};
/**
* Helper function to replace the level-nth [\d] with the new index.
* @param level
* @param index
*/
group.clone.replaceName = function ( level, index ) {
var $input = $( this ),
name = $input.attr( 'name' );
if ( ! name ) {
return;
}
var regex = new RegExp( '((?:\\[\\d+\\].*?){' + ( level - 1 ) + '}.*?)(\\[\\d+\\])' ),
newValue = '$1' + '[' + index + ']';
name = name.replace( regex, newValue );
$input.attr( 'name', name );
};
/**
* Helper function to replace the level-nth [\d] with the new index.
* @param level
* @param index
*/
group.clone.replaceId = function ( level, index ) {
var $input = $( this ),
id = $input.attr( 'id' );
if ( ! id ) {
return;
}
var regex = new RegExp( '_(\\d*)$' ),
newValue = '_' + rwmb.uniqid();
if ( regex.test( id ) ) {
id = id.replace( regex, newValue );
} else {
id += newValue;
}
$input.attr( 'id', id );
};
/**
* When clone a group:
* 1) Remove sub fields' clones and keep only their first clone
* 2) Reset sub fields' [data-next-index] to 1
* 3) Set [name] for sub fields (which is done when 'clone' event is fired
* 4) Repeat steps 1)-3) for sub groups
* 5) Set the group title
*
* @param event The clone_instance custom event
* @param index The group clone index
*/
group.clone.processGroup = function ( event, index ) {
var $group = $( this );
if ( ! $group.hasClass( 'rwmb-group-clone' ) ) {
return false; // Do not bubble up.
}
// Do not trigger clone on parents.
event.stopPropagation();
$group
// Add new [data-clone-group-new] to detect which group is cloned. This data is used to update sub inputs' group index
.data( 'clone-group-new', true )
// Remove clones, and keep only their first clone. Reset [data-next-index] to 1
.find( '.rwmb-input' ).each( function () {
$( this ).data( 'next-index', 1 ).children( '.rwmb-clone:gt(0)' ).remove();
} );
// Update [group index] for inputs
$group.find( rwmb.inputSelectors ).each( function () {
group.clone.updateGroupIndex.call( this );
} );
// Preserve the state (via [data-current]).
$group.find( '[name*="[_state]"]' ).each( function() {
$( this ).val( $( this ).data( 'current' ) );
} );
// Update group title for the new clone and set it expanded by default.
if ( $group.closest( '.rwmb-group-collapsible' ).length ) {
group.toggle.updateTitle( index + 1, $group );
group.toggle.updateState( $group );
}
// Sub groups: reset titles, but preserve the state.
group.toggle.initTitle( $group );
rwmb.$document.trigger( 'clone_completed', [$group] );
};
/**
* Remove a group clone
* @param event The click event.
*/
group.clone.remove = function( event ) {
event.preventDefault();
event.stopPropagation();
var ok = confirm( i18n.confirmRemove );
if ( ! ok ) {
return;
}
$( this ).parent().siblings( '.remove-clone' ).trigger( 'click' );
}
function init() {
group.toggle.initState();
group.toggle.initTitle( rwmb.$document );
// Refresh maps to make them visible.
$( window ).trigger( 'rwmb_map_refresh' );
}
rwmb.$document
.on( 'mb_ready', init )
.on( 'click', '.rwmb-group-title-wrapper, .rwmb-group-toggle-handle', group.toggle.handle )
.on( 'clone_instance', '.rwmb-clone', group.clone.processGroup )
.on( 'update_index', rwmb.inputSelectors, group.clone.replaceId )
.on( 'update_index', rwmb.inputSelectors, group.clone.updateIndex )
.on( 'click', '.rwmb-group-remove', group.clone.remove );
} )( jQuery, _, document, window, rwmb, RWMB_Group );
( function ( $, rwmb ) {
'use strict';
// Object holds all methods related to fields' index when clone
var cloneIndex = {
/**
* Set index for fields in a .rwmb-clone
* @param $inputs .rwmb-clone element
* @param index Index value
*/
set: function ( $inputs, index ) {
$inputs.each( function () {
var $field = $( this );
// Name attribute
var name = this.name;
if ( name && ! $field.closest( '.rwmb-group-clone' ).length ) {
$field.attr( 'name', cloneIndex.replace( index, name, '[', ']', false ) );
}
// ID attribute
var id = this.id;
if ( id ) {
$field.attr( 'id', cloneIndex.replace( index, id, '_', '', true, true ) );
}
$field.trigger( 'update_index', index );
} );
},
/**
* Replace an attribute of a field with updated index
* @param index New index value
* @param value Attribute value
* @param before String before returned value
* @param after String after returned value
* @param alternative Check if attribute does not contain any integer, will reset the attribute?
* @param isEnd Check if we find string at the end?
* @return string
*/
replace: function ( index, value, before, after, alternative, isEnd ) {
before = before || '';
after = after || '';
if ( typeof alternative === 'undefined' ) {
alternative = true;
}
var end = isEnd ? '$' : '';
var regex = new RegExp( cloneIndex.escapeRegex( before ) + '(\\d+)' + cloneIndex.escapeRegex( after ) + end ),
newValue = before + index + after;
return regex.test( value ) ? value.replace( regex, newValue ) : (alternative ? value + newValue : value );
},
/**
* Helper function to escape string in regular expression
* @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
* @param string
* @return string
*/
escapeRegex: function ( string ) {
return string.replace( /[.*+?^${}()|[\]\\]/g, "\\$&" );
},
/**
* Helper function to create next index for clones
* @param $container .rwmb-input container
* @return integer
*/
nextIndex: function ( $container ) {
var nextIndex = $container.data( 'next-index' );
$container.data( 'next-index', nextIndex + 1 );
return nextIndex;
}
};
// Object holds all method related to fields' value when clone.
var cloneValue = {
setDefault: function() {
var $field = $( this );
if ( true !== $field.data( 'clone-default' ) ) {
return;
}
var type = $field.attr( 'type' ),
defaultValue = $field.data( 'default' );
if ( 'radio' === type ) {
$field.prop( 'checked', $field.val() === defaultValue );
} else if ( $field.hasClass( 'rwmb-checkbox' ) || $field.hasClass( 'rwmb-switch' ) ) {
$field.prop( 'checked', !! defaultValue );
} else if ( $field.hasClass( 'rwmb-checkbox_list' ) ) {
var value = $field.val();
$field.prop( 'checked', Array.isArray( defaultValue ) ? -1 !== defaultValue.indexOf( value ) : value == defaultValue );
} else if ( 'select' === type ) {
$field.find( 'option[value="' + defaultValue + '"]' ).prop( 'selected', true );
} else if ( ! $field.hasClass( 'rwmb-hidden' ) ) {
$field.val( defaultValue );
}
},
clear: function() {
var $field = $( this ),
type = $field.attr( 'type' );
if ( 'radio' === type || 'checkbox' === type ) {
$field.prop( 'checked', false );
} else if ( 'select' === type ) {
$field.prop( 'selectedIndex', - 1 );
} else if ( ! $field.hasClass( 'rwmb-hidden' ) ) {
$field.val( '' );
}
}
};
/**
* Clone fields
* @param $container A div container which has all fields
*/
function clone( $container, $originalItem ) {
var $last = $container.children( '.rwmb-clone' ).last();
var $clone = $originalItem.clone();
var nextIndex = cloneIndex.nextIndex( $container );
// Clear fields' values.
var $inputs = $clone.find( rwmb.inputSelectors );
// $inputs.each( cloneValue.clear );
// Insert clone.
$clone.insertAfter( $last );
// Trigger custom event for the clone instance. Required for Group extension to update sub fields.
$clone.trigger( 'clone_instance', nextIndex );
// Set fields index. Must run before trigger clone event.
cloneIndex.set( $inputs, nextIndex );
// Set fields' default values: do after index is set to prevent previous radio fields from unchecking.
$inputs.each( cloneValue.setDefault );
// Trigger custom clone event.
$inputs.trigger( 'clone', nextIndex );
// After cloning fields.
$inputs.trigger( 'after_clone', nextIndex );
// Trigger custom change event for MB Blocks to update block attributes.
$inputs.first().trigger( 'mb_change' );
}
/**
* Hide remove buttons when there's only 1 of them
*
* @param $container .rwmb-input container
*/
function toggleRemoveButtons( $container ) {
var $clones = $container.children( '.rwmb-clone' );
$clones.children( '.remove-clone' ).toggle( $clones.length > 1 );
// Recursive for nested groups.
$container.find( '.rwmb-input' ).each( function () {
toggleRemoveButtons( $( this ) );
} );
}
/**
* Toggle add button
* Used with [data-max-clone] attribute. When max clone is reached, the add button is hid and vice versa
*
* @param $container .rwmb-input container
*/
function toggleAddButton( $container ) {
var $button = $container.children( '.add-clone' ),
maxClone = parseInt( $container.data( 'max-clone' ) ),
numClone = $container.children( '.rwmb-clone' ).length;
$button.toggle( isNaN( maxClone ) || ( maxClone && numClone < maxClone ) );
}
function copyClone( e ) {
e.preventDefault();
var $this = $(this);
var $container = $this.closest( '.rwmb-input' );
var $original = $this.closest('.rwmb-group-clone');
clone( $container, $original);
toggleRemoveButtons( $container );
toggleAddButton( $container );
sortClones.apply( $container[0] );
}
/**
* Sort clones.
* Expect this = .rwmb-input element.
*/
function sortClones() {
var $container = $( this );
if ( undefined !== $container.sortable( 'instance' ) ) {
return;
}
if ( 0 === $container.children( '.rwmb-clone' ).length ) {
return;
}
$container.sortable( {
handle: '.rwmb-clone-icon',
placeholder: ' rwmb-clone rwmb-sortable-placeholder',
items: '> .rwmb-clone',
start: function ( event, ui ) {
// Make the placeholder has the same height as dragged item
ui.placeholder.height( ui.item.outerHeight() );
},
stop: function( event, ui ) {
ui.item.trigger( 'mb_init_editors' );
ui.item.find( rwmb.inputSelectors ).first().trigger( 'mb_change' );
}
} );
}
function start() {
var $container = $( this );
toggleRemoveButtons( $container );
toggleAddButton( $container );
$container.data( 'next-index', $container.children( '.rwmb-clone' ).length );
sortClones.apply( this );
}
function init( e ) {
$( e.target ).find( '.rwmb-input' ).each( start );
}
rwmb.$document
// .on( 'mb_ready', init )
.on( 'click', '.copy-clone', copyClone );
} )( jQuery, rwmb );
.rwmb-button.copy-clone {
text-decoration: none;
color: #ccc;
display: inline-block;
position: absolute;
top: 0;
right: 30px;
width: 20px;
height: 20px;
transition: color 200ms;
}
.rwmb-button.copy-clone:hover {
color: rgb(0, 110, 255);
}
<?php
add_action( 'admin_head', function(){
wp_enqueue_script('rwmb_group_copy_js', 'rwmb-copy-group.js', ['jquery'], null, false);
wp_enqueue_style('rwmb_group_copy_css', 'rwmb-group-copy.css');
}, 100);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment