Created
April 8, 2021 10:31
-
-
Save harkor/f89ad9990b98fd41c7503d3f525e781e to your computer and use it in GitHub Desktop.
Copy Button for groups Metabox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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