Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save IAmAdamTaylor/044d15dd9d1dcbd3a5a30981033b5bda to your computer and use it in GitHub Desktop.
Save IAmAdamTaylor/044d15dd9d1dcbd3a5a30981033b5bda to your computer and use it in GitHub Desktop.
ACF Connect Flexible Content Fields Across Repeater
<?php
/**
* Main plugin file.
*
* @package ACF Connect Flexible Content Fields Across Repeater
* @license GPL2
* Author: Adam Taylor <hi@adamtaylor.dev>
*/
/**
* Plugin Name: ACF Connect Flexible Content Fields Across Repeater
* Description: Allows drag and drop of ACF Flexible Content layouts between different parent Repeater rows.
* Plugin URI: https://gist.github.com/IAmAdamTaylor/044d15dd9d1dcbd3a5a30981033b5bda
* Version: 1.0.0
* Author: Adam Taylor <hi@adamtaylor.dev>
* Author URI: https://adamtaylor.dev
* License: GPL2
*/
/* Copyright 2020 Adam Taylor (email : hi@adamtaylor.dev)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
add_action('admin_head', function () {
// pad empty containers to allow drag and drop into them as well
?>
<style id="acf-drag-flexible-content-across-repeaters-styles">
.acf-flexible-content.-empty .values {
padding: 21px 0; /* same as ACF empty message */
margin-top: -42px;
}
</style>
<?php
});
add_action('acf/input/admin_footer', function () {
?>
<script id="acf-drag-flexible-content-across-repeaters-scripts">
(function connectFlexibleContentFields($) {
// initial unique ID counter for dropped fields
// used to prevent naming collisions when dropping a group into a container which already has fields
var uuid = 0;
function getUuid() {
uuid++; // increment counter
return 'dropped-row-'+ uuid;
}
/**
* Get all flexible content containers on page
* split by field name.
* @return {Object} An object with keys = field name, values = containers for that field.
*/
function getFlexibleContentContainers() {
// split flexible content fields by name so that groups can only be dragged between compatible parents.
var containers = $('.acf-field.acf-field-flexible-content').toArray().reduce(function(map, current) {
var name = current.getAttribute('data-name');
if ( !map[name] ) {
map[name] = [];
}
map[name].push(current);
return map;
}, {});
return containers;
}
/**
* Get the ancestor fields of the current dropped layout.
* Always in the format:
* acf[field_key][row-id][field-key][row-id]...
* @return {String}
*/
function getAcfAncestorsPrefix($layout) {
var ancestorIds = [];
$layout.parents().each(function(index, node) {
if ( node.getAttribute('data-key') ) {
ancestorIds.push(node.getAttribute('data-key'));
}
if ( node.getAttribute('data-id') ) {
ancestorIds.push(node.getAttribute('data-id'));
}
// stop once reached the top of the current ACF meta box
if ( $(node).hasClass('acf-postbox') ) {
return false;
}
});
// need to reverse as parents() gives closest ancestor first
return 'acf[' + ancestorIds.reverse().join('][') + ']';
}
function onStartDrag(event, ui) {
acf.doAction('sortstart', ui.item, ui.placeholder);
}
function onStopDrag(event, ui) {
acf.doAction('sortstop', ui.item, ui.placeholder);
$(this).find('.mce-tinymce').each(function() {
tinyMCE.execCommand('mceRemoveControl', true, $(this).attr('id'));
tinyMCE.execCommand('mceAddControl', true, $(this).attr('id'));
});
}
acf.addAction('ready', function($el) {
// connect same named flexible content fields, across repeater rows, with each other
$.each(getFlexibleContentContainers(), function(name, fields) {
var $layouts = $(fields).find('.values');
$layouts.sortable({ // from jQuery UI
connectWith: $layouts,
start: onStartDrag,
stop: onStopDrag
});
});
// store previous container when start dragging, only update if dropped elsewhere
var $prevContainer = null;
acf.addAction('sortstart', function($el) {
$prevContainer = $el.closest('.values');
});
// on stop (item dropped into place)
acf.addAction('sortstop', function($el) {
var $newContainer = $el.closest('.values');
if ( $newContainer.is($prevContainer) ) {
return;
}
// remove -empty class if dropped there
var emptyContainer = $el.closest('.acf-flexible-content.-empty');
if ( emptyContainer.length ) {
emptyContainer.removeClass('-empty');
}
var prevAncestorPrefix = getAcfAncestorsPrefix($prevContainer);
prevContainer = null; // reset var for next run
// rekey name attrs on inputs inside the dropped item
var newAncestorPrefix = getAcfAncestorsPrefix($el);
var newUuid = getUuid();
$el.find('[name^="acf[field_"]').each(function(index, node) {
var name = node.getAttribute('name');
var unprefixedName = name.replace(prevAncestorPrefix, '');
// force layout id to be unique
// e.g. when dragging layout 1 into another container which already has a layout 1
// sort order of layouts is determined by source order, so new id does not need to be numbered correctly, any unique string will do
var newName = unprefixedName.replace(/^\[([^\[\]]+)\]/, '['+ newUuid +']');
node.setAttribute('name', newAncestorPrefix + newName);
});
// update all layouts in the dropped container
$el.closest('.values').children().each(function(index, node) {
// update visible layout number
// this is the number at the start of each layout on the admin screen, e.g. 1) Text Group, 2) Image Group
// previous container is already handled by ACF core
$(node).find('.acf-fc-layout-order').html(index + 1);
// click already selected buttons to trigger conditional logics
$(node).find('.acf-button-group label.selected').trigger('click');
});
});
})
})(jQuery);
</script>
<?php
});
@IAmAdamTaylor
Copy link
Author

This plugin will allow you to drag and drop ACF Flexible Content fields across different parent Repeater rows.

What I mean by that is if you have a field setup like:

Repeater
	Flexible Content
		Layout 1
		Layout 2
		etc

Normally when you create multiple rows in the Repeater, you would not be able to drag Layout 1 from the first row of the Repeater to the second row of the Repeater. With this plugin installed and active you can!

I've tested this with all of the core ACF fields and on a site with multiple complex layout groups across at least 10 repeater rows and have had no data loss or fields displaying incorrectly.

That said, if you do want to use this on your site, please take a backup of your database before installing!
I am not responsible for any data loss caused by installing this plugin.

If you find any bugs please let me know and I will do my best to help out :)

To install

  1. Create a new file in /wp-content/mu-plugins/ and add the code from the Gist.
  2. Save the file.
  3. Done! Since the file was placed in mu-plugins WordPress will start using it straight away, no need to activate.

Gotchas

I haven't tested with advanced Conditional Logic such as a field from one layout affecting the visibility of a field in a subsequent layout. This may not update in real-time as you drag and drop layouts.
As a workaround, once you've moved a layout, save and refresh the post which should cause ACF to reevaluate the Conditional Logic rules.

There are a small number of awkward drag and drop interactions, due to how empty rows function.

When dragging and dropping a layout into an empty Repeater row, the drop placeholder will appear off centre. This is a side effect of how the ACF placeholder is shown as a separate element, not part of the Flexible Content container.

When dragging the last item out of a Flexible Content container to another row, once you drag over the new row the placeholder in the previous row will disappear. This means that you can no longer drag it back to its previous row without placing it first.
To workaround this, drop the layout somewhere and then pick it back up again - this will allow you to drop it onto the, now empty, previous row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment