1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-12 01:34:31 +02:00

Add support for copy and paste of repeater items between pages (or same page). To copy, click the copy/clone icon and a dialog will appear asking what action you want. Use the same dialog action to paste. This also updates the clone feature to support insert before/after.

This commit is contained in:
Ryan Cramer
2021-10-29 11:23:44 -04:00
parent a16c43f010
commit a19a224c20
9 changed files with 298 additions and 21 deletions

View File

@@ -332,7 +332,7 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod
* @return Page
*
*/
protected function getRepeaterPageParent(Page $page, Field $field, $create = true) {
public function getRepeaterPageParent(Page $page, Field $field, $create = true) {
return $this->getRepeaterParent($field);
}

View File

@@ -33,7 +33,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule {
return array(
'title' => __('Repeater', __FILE__), // Module Title
'summary' => __('Maintains a collection of fields that are repeated for any number of times.', __FILE__), // Module Summary
'version' => 108,
'version' => 110,
'autoload' => true,
'installs' => 'InputfieldRepeater'
);
@@ -1114,7 +1114,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule {
* @return Page|NullPage
*
*/
protected function getRepeaterPageParent(Page $page, Field $field, $create = true) {
public function getRepeaterPageParent(Page $page, Field $field, $create = true) {
$repeaterParent = $this->getRepeaterParent($field);
$parentName = self::repeaterPageNamePrefix . $page->id; // for-page-123
@@ -1149,7 +1149,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule {
* @throws WireException
*
*/
protected function getRepeaterParent(Field $field) {
public function getRepeaterParent(Field $field) {
$pages = $this->wire()->pages;
$parentID = (int) $field->get('parent_id');

File diff suppressed because one or more lines are too long

View File

@@ -189,7 +189,76 @@ function InputfieldRepeater($) {
});
return false;
};
/**
* Event when the copy/clone/paste action is clicked
*
* @returns {boolean}
*
*/
var eventCopyCloneClick = function() {
if(isActionDisabled($(this))) return false;
var labels = ProcessWire.config.InputfieldRepeater.labels;
var $item = $(this).closest('.InputfieldRepeaterItem');
var itemID = $item.attr('data-page');
var $inputfield = $item.closest('.InputfieldRepeater');
var fieldName = $inputfield.attr('data-name');
var cookieName = copyPasteCookieName(fieldName);
var copyValue = jQuery.cookie(cookieName);
var itemLabel = getItemLabel($item).text();
var pasteID = copyValue ? parseInt(copyValue.item) : '';
var pasteDisabled = copyValue ? '' : 'disabled ';
var pasteSelected = pasteID > 0 ? 'selected ' : '';
var note = '';
if(pasteID > 0) {
note = "<div><i class='fa fa-paste fa-fw'></i>" + labels.copyInMemory + ' (id ' + pasteID + ')</div>';
}
var input =
'<option value="copy">' + labels.copy + '</option>' +
'<option value="clone-before">' + labels.cloneBefore + '</option>' +
'<option value="clone-after">' + labels.cloneAfter + '</option>' +
'<option ' + pasteDisabled + 'value="paste-before">' + labels.pasteBefore + '</option>' +
'<option ' + pasteDisabled + pasteSelected + 'value="paste-after">' + labels.pasteAfter + '</option>' +
'<option ' + pasteDisabled + 'value="clear">' + labels.clear + '</option>';
if(note.length) note = "<span class='detail'>" + note + "</span>";
var options = {
message: labels.selectAction + ' (id ' + itemID + ')', // message displayed at top
input: '<select name="action" class="uk-select">' + input + '</select>' + note, // HTML content that is to be displayed
callback: function(value) {
var action = value.action;
if(action === 'copy') {
copyRepeaterItem($item);
$item.fadeOut('fast', function() { $item.fadeIn('fast') });
$inputfield.addClass('InputfieldRepeaterCanPaste');
} else if(action === 'clone-before') {
cloneRepeaterItem($item, true);
} else if(action === 'clone-after') {
cloneRepeaterItem($item, false);
} else if(action === 'paste-before') {
pasteRepeaterItem($item, true);
} else if(action === 'paste-after') {
pasteRepeaterItem($item, false);
} else if(action === 'clear') {
jQuery.cookie(cookieName, null);
$inputfield.removeClass('InputfieldRepeaterCanPaste');
} else {
console.log('unknown action: ' + action);
}
},
};
// open the add-type selection dialog
vex.dialog.open(options);
return false;
};
var eventSettingsClick = function(e) {
var $this = $(this);
var $item = $this.closest('.InputfieldRepeaterItem');
@@ -357,8 +426,10 @@ function InputfieldRepeater($) {
var newItemTotal = 0; // for noAjaxAdd mode
var useAjax = $addLink.attr('data-noajax').length == 0;
var cloneID = $addLink.attr('data-clone');
var pageID = 0;
var depth = 0;
var redoSortAll = false;
var inputfieldPageID = parseInt($inputfieldRepeater.attr('data-page'));
function addRepeaterItem($addItem) {
// make sure it has a unique ID
@@ -382,7 +453,16 @@ function InputfieldRepeater($) {
}
if(typeof cloneID == "undefined" || !cloneID) cloneID = null;
if(cloneID) $addLink.removeAttr('data-clone');
if(cloneID) {
$addLink.removeAttr('data-clone');
// when data-clone contains pageID:itemID it is from a previous copy operation
if(cloneID.indexOf(':') > 0) {
var a = cloneID.split(':');
pageID = parseInt(a[0]); // for copy/paste
cloneID = parseInt(a[1]);
}
}
if(!useAjax) {
var $newItem = $inputfields.children('.InputfieldRepeaterNewItem'); // for noAjaxAdd mode, non-editable new item
@@ -399,8 +479,9 @@ function InputfieldRepeater($) {
return false;
}
// get addItem from ajax
var pageID = $inputfieldRepeater.attr('data-page');
if(!pageID) pageID = inputfieldPageID;
var fieldName = $inputfieldRepeater.attr('id').replace('wrap_Inputfield_', '');
var $spinner = $addLink.parent().find('.InputfieldRepeaterSpinner');
var ajaxURL = ProcessWire.config.InputfieldRepeater.editorUrl + '?id=' + pageID + '&field=' + fieldName;
@@ -408,7 +489,7 @@ function InputfieldRepeater($) {
$spinner.removeClass($spinner.attr('data-off')).addClass($spinner.attr('data-on'));
if(cloneID) {
ajaxURL += '&repeater_clone=' + cloneID;
ajaxURL += '&repeater_clone=' + cloneID + '&repeater_clone_to=' + inputfieldPageID;
} else {
ajaxURL += '&repeater_add=' + $addLink.attr('data-type');
}
@@ -568,6 +649,18 @@ function InputfieldRepeater($) {
}
}
/**
* Event called when the "Paste" link in the footer is clicked
*
*/
var eventPasteClick = function(e) {
var $inputfield = $(this).closest('.InputfieldRepeater');
// use the InputfieldRepeaterNewItem as our substitute for a contextual item
var $newItem = $inputfield.children('.InputfieldContent').children('.Inputfields').children('.InputfieldRepeaterNewItem');
pasteRepeaterItem($newItem, false);
return false;
};
/**
* Event when mouseout of insert before/after action
*
@@ -1084,10 +1177,11 @@ function InputfieldRepeater($) {
function initHeaders($headers, $inputfieldRepeater, renderValueMode) {
var $clone = $("<i class='fa fa-copy InputfieldRepeaterClone'></i>").css('display', 'block');
// var $paste = $("<i class='fa fa-paste InputfieldRepeaterPaste'></i>").css('display', 'block');
var $delete = $("<i class='fa fa-trash InputfieldRepeaterTrash'></i>");
var $toggle = $("<i class='fa InputfieldRepeaterToggle' data-on='fa-toggle-on' data-off='fa-toggle-off'></i>");
var $insertAfter = $("<i class='fa fa-download xfa-arrow-circle-down InputfieldRepeaterInsertAfter'></i>");
var $insertBefore = $("<i class='fa fa-upload xfa-arrow-circle-up InputfieldRepeaterInsertBefore'></i>");
var $insertAfter = $("<i class='fa fa-download InputfieldRepeaterInsertAfter'></i>");
var $insertBefore = $("<i class='fa fa-upload InputfieldRepeaterInsertBefore'></i>");
var cfg = ProcessWire.config.InputfieldRepeater;
var allowClone = !$inputfieldRepeater.hasClass('InputfieldRepeaterNoAjaxAdd');
var allowSettings = $inputfieldRepeater.hasClass('InputfieldRepeaterHasSettings');
@@ -1096,6 +1190,7 @@ function InputfieldRepeater($) {
$toggle.attr('title', cfg.labels.toggle);
$delete.attr('title', cfg.labels.remove);
$clone.attr('title', cfg.labels.clone);
// $paste.attr('title', 'Paste'); // @todo
$insertBefore.attr('title', cfg.labels.insertBefore);
$insertAfter.attr('title', cfg.labels.insertAfter);
}
@@ -1134,7 +1229,10 @@ function InputfieldRepeater($) {
.attr('title', cfg.labels.settings);
$controls.prepend($settingsToggle);
}
if(allowClone) $controls.prepend($clone.clone(true));
if(allowClone) {
$controls.prepend($clone.clone(true));
// $controls.prepend($paste.clone(true));
}
$controls.prepend($toggleControl);
$controls.prepend($deleteControl);
$t.prepend($controls);
@@ -1340,6 +1438,107 @@ function InputfieldRepeater($) {
$.cookie('repeaters_open', val);
}
/**
* Clone a repeater item in place
*
* @param $item
* @param pasteValue Optional cookie object value that was previously copied
*
*/
function cloneRepeaterItem($item, insertBefore, pasteValue) {
if(typeof pasteValue === "undefined") pasteValue = null;
var actionName = pasteValue === null ? 'clone' : 'paste';
var $addLink = $item.closest('.InputfieldRepeater').children('.InputfieldContent')
.children('.InputfieldRepeaterAddItem').find('.InputfieldRepeaterAddLink:eq(0)');
// $('html, body').animate({ scrollTop: $addLink.offset().top - 100}, 250, 'swing');
$item.siblings('.InputfieldRepeaterInsertItem').remove();
var depth = getItemDepth($item);
var $newItem = $item.hasClass('InputfieldRepeaterNewItem') ? $item.clone() : $item.siblings('.InputfieldRepeaterNewItem').clone();
var $nextItem = $item.next('.InputfieldRepeaterItem');
var nextItemDepth = $nextItem.length ? getItemDepth($nextItem) : depth;
var $prevItem = $item.prev('.InputfieldRepeaterItem');
var prevItemDepth = $prevItem.length ? getItemDepth($prevItem) : depth;
if(typeof insertBefore === "undefined") {
insertBefore = depth < nextItemDepth;
}
$newItem.addClass('InputfieldRepeaterInsertItem').attr('id', $newItem.attr('id') + '-' + actionName); // .removeClass('InputfieldRepeaterNewItem); ?
$newItem.find('.InputfieldHeader').html("<i class='fa fa-spin fa-spinner'></i>");
if(insertBefore) {
depth = getInsertBeforeItemDepth($item);
$newItem.addClass('InputfieldRepeaterInsertItemBefore');
$newItem.insertBefore($item);
} else {
depth = getInsertAfterItemDepth($item);
$newItem.addClass('InputfieldRepeaterInsertItemAfter');
$newItem.insertAfter($item);
}
setItemDepth($newItem, depth);
$newItem.show();
if(actionName === 'paste') {
// data-clone attribute with 'pageID:itemID' indicates page ID and item ID to clone
$addLink.attr('data-clone', pasteValue.page + ':' + pasteValue.item).click();
} else {
// current page ID is implied when only itemID is supplied
$addLink.attr('data-clone', $item.attr('data-page')).click();
}
}
/**
* Paste previously copied item
*
* @param $item Item to insert before or after
* @param insertBefore True to insert before, false to insert after
*
*/
function pasteRepeaterItem($item, insertBefore) {
var $inputfield = $item.closest('.InputfieldRepeater');
var fieldName = $inputfield.attr('data-name');
var cookieName = copyPasteCookieName(fieldName);
var copyValue = jQuery.cookie(cookieName);
if(copyValue) cloneRepeaterItem($item, insertBefore, copyValue);
}
/**
* Copy a repeater item to memory
*
* @param $item
*
*/
function copyRepeaterItem($item) {
var $title = $('#Inputfield_title');
var $name = $('#Inputfield__pw_page_name');
var $inputfield = $item.closest('.InputfieldRepeater');
var fieldName = $inputfield.attr('data-name');
var copyValue = {
page: parseInt($inputfield.attr('data-page')),
item: parseInt($item.attr('data-page')),
field: fieldName,
};
var cookieName = copyPasteCookieName(fieldName);
jQuery.cookie(cookieName, copyValue);
}
/**
* Get the copy/paste cookie name
*
* @param fieldName
* @returns {string}
*
*/
function copyPasteCookieName(fieldName) {
return fieldName + '_copy';
}
/**
* Initialization for document.ready
*
@@ -1354,7 +1553,9 @@ function InputfieldRepeater($) {
.on('reloaded', '.InputfieldRepeater', eventReloaded)
.on('click', '.InputfieldRepeaterTrash', eventDeleteClick)
.on('dblclick', '.InputfieldRepeaterTrash', eventDeleteDblClick)
.on('click', '.InputfieldRepeaterClone', eventCloneClick)
//.on('click', '.InputfieldRepeaterClone', eventCloneClick)
.on('click', '.InputfieldRepeaterClone', eventCopyCloneClick)
.on('click', '.InputfieldRepeaterPaste', eventPasteClick)
.on('click', '.InputfieldRepeaterSettingsToggle', eventSettingsClick)
.on('dblclick', '.InputfieldRepeaterToggle', eventOpenAllClick)
.on('click', '.InputfieldRepeaterToggle', eventToggleClick)

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
return array(
'title' => __('Repeater', __FILE__), // Module Title
'summary' => __('Repeats fields from another template. Provides the input for FieldtypeRepeater.', __FILE__), // Module Summary
'version' => 109,
'version' => 110,
'requires' => 'FieldtypeRepeater',
);
}
@@ -620,14 +620,16 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
* Render a new item for ajax after 'add new' link clicked
*
* @param int $cloneItemID
* @param int $cloneToParentID
* @return string
*
*/
public function renderAjaxNewItem($cloneItemID = 0) {
public function renderAjaxNewItem($cloneItemID = 0, $cloneToParentID = 0) {
/** @var PageArray $value */
$value = $this->attr('value');
$clonePage = null;
$cloneToParent = null;
$readyPage = null;
if($cloneItemID) {
@@ -637,12 +639,23 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
break;
}
}
if($cloneToParentID && $cloneToParentID != $this->page->id) {
$cloneToParent = $this->wire()->pages->get((int) $cloneToParentID);
if($cloneToParent->id && $cloneToParent->hasField($this->field) && $cloneToParent->editable($this->field)) {
// ok
$fieldtype = $this->field->type; /** @var FieldtypeRepeater $fieldtype */
// convert from /path/to/page having repeater to /processwire/repeaters/for-field-123/for-page-456/
$cloneToParent = $fieldtype->getRepeaterPageParent($cloneToParent, $this->field);
} else {
$cloneToParent = null;
}
}
}
if($clonePage && $clonePage->id) {
/** @var FieldtypeRepeater $fieldtype */
$fieldtype = $this->field->type;
$readyPage = $this->wire()->pages->clone($clonePage, null, true,
$readyPage = $this->wire()->pages->clone($clonePage, $cloneToParent, true,
array('set' => array(
'name' => $fieldtype->getUniqueRepeaterPageName() . 'c', // trailing "c" indicates clone
'sort' => count($value)+1,
@@ -685,6 +698,17 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if(!strlen($addLabel)) $addLabel = $this->_('Add New');
return $addLabel;
}
protected function renderPasteLabel() {
return $this->_('Paste');
}
protected function renderPasteLink() {
$icon = wireIconMarkup('paste', 'fw');
$label = $this->renderPasteLabel();
$out = "<a class='InputfieldRepeaterPaste' href='#'>$icon $label</a>";
return $out;
}
/**
* Called before render() or renderValue() method by InputfieldWrapper, before Inputfield-specific CSS/JS files added
@@ -730,6 +754,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($this->accordionMode) {
$this->addClass('InputfieldRepeaterAccordion', 'wrapClass');
}
if(!empty($_COOKIE[$this->copyPasteCookieName()])) {
$this->addClass('InputfieldRepeaterCanPaste', 'wrapClass');
}
$this->wrapAttr('data-name', $this->field->name);
$this->wrapAttr('data-page', $this->page->id);
$this->wrapAttr('data-max', (int) $this->repeaterMaxItems);
$this->wrapAttr('data-min', (int) $this->repeaterMinItems);
@@ -757,7 +786,15 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
'insertBefore' => $this->_x('Insert new item before this one', 'repeater-item-action'),
'insertAfter' => $this->_x('Insert new item after this one', 'repeater-item-action'),
'insertHere' => $this->_x('Insert new item here', 'repeater-item-action'),
'disabledMinMax' => $this->_('This action is disabled per min and/or max item settings.')
'disabledMinMax' => $this->_('This action is disabled per min and/or max item settings.'),
'selectAction' => $this->_x('Select an action for this item ', 'dialog-header'),
'copy' => $this->_x('COPY this item in memory to paste elsewhere', 'repeater-item-action'),
'copyInMemory' => $this->_x('Item in copy/paste memory', 'dialog-note'),
'cloneBefore' => $this->_x('CLONE this item and insert ABOVE this', 'repeater-item-action'),
'cloneAfter' => $this->_x('CLONE this item and insert BELOW this', 'repeater-item-action'),
'pasteBefore' => $this->_x('PASTE copied item ABOVE this', 'repeater-item-action'),
'pasteAfter' => $this->_x('PASTE copied item BELOW this', 'repeater-item-action'),
'clear' => $this->_x('CLEAR copy/paste memory', 'repeater-item-action'),
)
));
@@ -774,6 +811,13 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
protected function renderFooter($noAjaxAdd) {
// a hidden checkbox with link that we use to identify when items have been added
if($this->singleMode) return '';
if(!empty($_COOKIE[$this->copyPasteCookieName()])) {
$paste = ' &nbsp; ' . $this->renderPasteLink();
} else {
$paste = '';
}
$out =
"<p class='InputfieldRepeaterAddItem'>" .
"<input class='InputfieldRepeaterAddItemsQty' type='text' name='_{$this->name}_add_items' value='0' />" . // for noAjaxAdd
@@ -781,8 +825,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
"<i class='fa fa-fw fa-plus-circle InputfieldRepeaterSpinner' " .
"data-on='fa-spin fa-spinner' data-off='fa-plus-circle'></i>" .
$this->renderAddLabel() .
"</a>" .
"</a>" . $paste .
"</p>";
return $out;
}
@@ -802,9 +847,10 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$repeaterAdd = $input->get('repeater_add');
$repeaterEdit = (int) $input->get('repeater_edit');
$repeaterClone = (int) $input->get('repeater_clone');
$repeaterCloneTo = (int) $input->get('repeater_clone_to');
if($input->get('inrvm')) $this->renderValueMode = true;
if($repeaterClone) {
return $this->renderValueMode ? '' : $this->renderAjaxNewItem($repeaterClone);
return $this->renderValueMode ? '' : $this->renderAjaxNewItem($repeaterClone, $repeaterCloneTo);
} else if($repeaterAdd !== null && !$noAjaxAdd) {
return $this->renderValueMode ? '' : $this->renderAjaxNewItem();
} else if($repeaterEdit) {
@@ -1139,6 +1185,17 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
return $this->wrappers;
}
/**
* Get the copy/paste cookie name
*
* @return string
* @since 3.0.188
*
*/
public function copyPasteCookieName() {
return $this->field->name . '_copy';
}
/**
* @return InputfieldWrapper
*

View File

@@ -7,6 +7,21 @@
}
}
.InputfieldRepeaterPaste {
display: none;
}
&.InputfieldRepeaterCanPaste {
.InputfieldRepeaterPaste {
display: inline-block;
}
}
// prevents longer item headers from going outside Inputfield box horizontally
.InputfieldRepeaterItem {
max-width: 100%;
overflow-x: hidden;
}
.InputfieldRepeaterItem > .InputfieldHeader {
line-height: 1em;
padding: 0.5em 0 0.5em 0.4em;

File diff suppressed because one or more lines are too long

View File

@@ -640,6 +640,10 @@ $focusPointCircleSize: 40px;
}
}
.AdminThemeUikit .InputfieldImageValidExtensions {
position: relative;
top: -15px;
}
// Narrow mode applies when width of Inputfield is under 500px
.InputfieldImageNarrow {