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

Upgrade Repeaters with ability to insert before/after existing items

This commit is contained in:
Ryan Cramer
2021-10-15 15:22:16 -04:00
parent 6315a3db60
commit 7e5f6c85ec
7 changed files with 376 additions and 54 deletions

File diff suppressed because one or more lines are too long

View File

@@ -33,6 +33,18 @@ function InputfieldRepeater($) {
*/ */
var doubleClickTimer = null; var doubleClickTimer = null;
/**
* True when an ajax request is currently processing a newly added item
*
*/
var currentlyAddingItem = false;
/**
* Timeout tracker for insert actions
*
*/
var insertTimeout = null;
/*** EVENTS ********************************************************************************************/ /*** EVENTS ********************************************************************************************/
@@ -313,6 +325,8 @@ function InputfieldRepeater($) {
* *
*/ */
var eventAddLinkClick = function() { var eventAddLinkClick = function() {
currentlyAddingItem = true;
var $addLink = $(this); var $addLink = $(this);
var $inputfields = $addLink.parent('p').prev('ul.Inputfields'); var $inputfields = $addLink.parent('p').prev('ul.Inputfields');
var $inputfieldRepeater = $addLink.closest('.InputfieldRepeater'); var $inputfieldRepeater = $addLink.closest('.InputfieldRepeater');
@@ -320,13 +334,25 @@ function InputfieldRepeater($) {
var newItemTotal = 0; // for noAjaxAdd mode var newItemTotal = 0; // for noAjaxAdd mode
var useAjax = $addLink.attr('data-noajax').length == 0; var useAjax = $addLink.attr('data-noajax').length == 0;
var cloneID = $addLink.attr('data-clone'); var cloneID = $addLink.attr('data-clone');
var depth = 0;
var redoSortAll = false;
function addRepeaterItem($addItem) { function addRepeaterItem($addItem) {
// make sure it has a unique ID // make sure it has a unique ID
var id = $addItem.attr('id') + '_'; var id = $addItem.attr('id') + '_';
while($('#' + id).length > 0) id += '_'; while($('#' + id).length > 0) id += '_';
$addItem.attr('id', id); $addItem.attr('id', id);
$inputfields.append($addItem); var $insertItem = $inputfields.children('.InputfieldRepeaterInsertItem');
if($insertItem.length) {
depth = getItemDepth($insertItem);
$addItem.addClass('InputfieldStateCollapsed')
var $toggleIcon = $addItem.children('.InputfieldHeader').find('.toggle-icon');
$toggleIcon.toggleClass($toggleIcon.attr('data-to'));
$insertItem.replaceWith($addItem);
redoSortAll = true;
} else {
$inputfields.append($addItem);
}
$addItem.css('display', 'block'); $addItem.css('display', 'block');
adjustItemLabel($addItem, true); adjustItemLabel($addItem, true);
$addLink.trigger('repeateradd', [ $addItem ]); $addLink.trigger('repeateradd', [ $addItem ]);
@@ -341,10 +367,12 @@ function InputfieldRepeater($) {
if(newItemTotal > 0) { if(newItemTotal > 0) {
if(newItemTotal > 1) $newItem = $newItem.slice(0, 1); if(newItemTotal > 1) $newItem = $newItem.slice(0, 1);
var $addItem = $newItem.clone(true); var $addItem = $newItem.clone(true);
if(depth) setItemDepth($addItem, depth);
addRepeaterItem($addItem); addRepeaterItem($addItem);
$numAddInput.attr('value', newItemTotal); $numAddInput.attr('value', newItemTotal);
checkMinMax($inputfieldRepeater); checkMinMax($inputfieldRepeater);
} }
currentlyAddingItem = false;
return false; return false;
} }
@@ -390,10 +418,21 @@ function InputfieldRepeater($) {
// next line can remove 9/2019, as 'cloned' support will have been in InputfieldTable for awhile // next line can remove 9/2019, as 'cloned' support will have been in InputfieldTable for awhile
$addItem.find('.InputfieldTableRowID').val(0); $addItem.find('.InputfieldTableRowID').val(0);
} }
$addItem.find('.InputfieldRepeaterSort').val($inputfields.children().length); if(redoSortAll) {
$('html, body').animate({ $inputfields.children('.InputfieldRepeaterItem').each(function(n) {
scrollTop: $addItem.offset().top setItemSort($(this), n);
}, 500, 'swing'); });
} else {
setItemSort($addItem, $inputfields.children().length);
}
if(depth) setItemDepth($addItem, depth);
if($addItem.hasClass('InputfieldStateCollapsed')) {
// ok
} else {
$('html, body').animate({
scrollTop: $addItem.offset().top
}, 500, 'swing');
}
updateState($addItem); updateState($addItem);
checkMinMax($inputfieldRepeater); checkMinMax($inputfieldRepeater);
updateAccordion($addItem); updateAccordion($addItem);
@@ -404,6 +443,8 @@ function InputfieldRepeater($) {
}); });
} }
runScripts(data); runScripts(data);
setTimeout(function() { currentlyAddingItem = false; }, 500);
}); });
return false; return false;
@@ -445,6 +486,131 @@ function InputfieldRepeater($) {
}); });
return false; return false;
}; };
/**
* Click the "insert before" button event
*
*/
var eventInsertBeforeClick = function(e) {
var $item = $(this).closest('.InputfieldRepeaterItem');
eventInsertClick($item, true);
e.stopPropagation();
};
/**
* Click the "insert after" button event
*
*/
var eventInsertAfterClick = function(e) {
var $item = $(this).closest('.InputfieldRepeaterItem');
eventInsertClick($item, false);
e.stopPropagation();
};
/**
* Handler for either insert before or insert after click events
*
*/
function eventInsertClick($item, insertBefore) {
if(currentlyAddingItem) return false;
currentlyAddingItem = true;
if(insertTimeout) clearTimeout(insertTimeout);
var depth = getInsertItemDepth($item, insertBefore);
var $insertItem = $item.siblings('.InputfieldRepeaterInsertItem');
if($insertItem.length) {
$insertItem.remove();
}
var $placeholder = $item.siblings('.InputfieldRepeaterNewItem').clone()
.removeClass('.InputfieldRepeaterNewItem').addClass('InputfieldRepeaterInsertItem');
$placeholder.attr('id', $placeholder.attr('id') + '-placeholder');
$placeholder.find('.InputfieldHeader').html("<i class='fa fa-spin fa-spinner'></i>");
if(insertBefore) {
$placeholder.insertBefore($item);
} else {
$placeholder.insertAfter($item);
}
if(depth > 0) setItemDepth($placeholder, depth);
$placeholder.show();
if(!insertBefore && !$item.hasClass('InputfieldStateCollapsed')) scrollToItem($placeholder);
$placeholder.children('.InputfieldHeader').effect('highlight', {}, 500);
// @todo the following line will need to be updated for matrix support
$item.parent('.Inputfields').siblings('.InputfieldRepeaterAddItem').find('.InputfieldRepeaterAddLink:eq(0)').click();
}
/**
* Event when mouseout of insert before/after action
*
*/
var eventInsertMouseout = function(e) {
if(currentlyAddingItem) return;
if(insertTimeout) clearTimeout(insertTimeout);
var $action = $(this);
var $newItem = $action.data('newItem');
$action.removeClass('hov');
// var $newItem = $action.closest('.Inputfields').children('.InputfieldRepeaterInsertItem');
if($newItem && $newItem.length) {
if($newItem.hasClass('hov')) return;
$newItem.remove();
}
};
/**
* Event when mouseover of insert before/after action
*
*/
var eventInsertMouseover = function(e) {
if(currentlyAddingItem) return;
if(insertTimeout) clearTimeout(insertTimeout);
var $action = $(this);
var insertBefore = $action.hasClass('InputfieldRepeaterInsertBefore');
var $item = $(this).closest('.InputfieldRepeaterItem');
var depth = 0;
$item.siblings('.InputfieldRepeaterInsertItem').remove();
var $newItem = $item.siblings('.InputfieldRepeaterNewItem').clone();
$newItem.addClass('InputfieldRepeaterInsertItem').attr('id', $newItem.attr('id') + '-insert');
if(insertBefore) {
depth = getInsertBeforeItemDepth($item);
$newItem.addClass('InputfieldRepeaterInsertItemBefore');//.insertBefore($item);
} else {
depth = getInsertAfterItemDepth($item);
$newItem.addClass('InputfieldRepeaterInsertItemAfter');//.insertAfter($item);
}
$newItem.find('.InputfieldRepeaterItemControls').hide();
$newItem.find('.InputfieldRepeaterItemLabel').text(ProcessWire.config.InputfieldRepeater.labels.insertHere);
$action.addClass('hov').data('newItem', $newItem);
setItemDepth($newItem, depth);
insertTimeout = setTimeout(function() {
insertTimeout = null;
if(!$action.hasClass('hov')) {
$newItem.remove();
return;
} else if(insertBefore) {
$newItem.insertBefore($item);
} else {
$newItem.addClass('hov').insertAfter($item);
}
//$newItem.addClass('hov');
$newItem.on('mouseover', function() {
$(this).addClass('hov');
}).on('click', function(e) {
e.stopPropagation();
eventInsertClick($item, insertBefore);
}).on('mouseout', function() {
$(this).removeClass('hov').remove();
});
$newItem.slideDown();
}, 1000);
};
/*** GENERAL FUNCTIONS **********************************************************************************/ /*** GENERAL FUNCTIONS **********************************************************************************/
@@ -487,7 +653,7 @@ function InputfieldRepeater($) {
}); });
if(useScroll && hasOpen) { if(useScroll && hasOpen) {
$('html, body').animate({scrollTop: $item.offset().top - 10}, 0); scrollToItem($item);
} }
return true; return true;
@@ -527,6 +693,25 @@ function InputfieldRepeater($) {
} }
} }
} }
/*** SORT FUNCTIONS ***********************************************************************************/
function setItemSort($item, sort) {
var $input = getItemSortInput($item);
if($input.length) $input.val(sort);
}
function getItemSort($item) {
var $input = getItemSortInput($item);
if($input.length) return parseInt($input.val());
return -1;
}
function getItemSortInput($item) {
if(!$item.hasClass('InputfieldRepeaterItem')) $item = $item.closest('.InputfieldRepeaterItem');
return $item.children('.InputfieldContent').children('.Inputfields')
.children('.InputfieldRepeaterItemSort').find('.InputfieldRepeaterSort');
}
/*** DEPTH FUNCTIONS **********************************************************************************/ /*** DEPTH FUNCTIONS **********************************************************************************/
@@ -586,7 +771,7 @@ function InputfieldRepeater($) {
var $depthInput = $item.children('.InputfieldContent').children('.Inputfields') var $depthInput = $item.children('.InputfieldContent').children('.Inputfields')
.children('.InputfieldRepeaterItemDepth').find('input'); .children('.InputfieldRepeaterItemDepth').find('input');
if(!$depthInput.length) { if(!$depthInput.length && !$item.hasClass('InputfieldRepeaterNewItem')) {
console.log('Cannot find depth input for ' + $item.attr('id')); console.log('Cannot find depth input for ' + $item.attr('id'));
} }
@@ -625,6 +810,39 @@ function InputfieldRepeater($) {
return parseInt($item.attr('data-depth')); return parseInt($item.attr('data-depth'));
} }
/**
* Get depth for a new item if it were to be inserted before/after given $contextItem
*
* @param $contextItem
* @param insertBefore
* @returns {Number}
*
*/
function getInsertItemDepth($contextItem, insertBefore) {
var depth = 0;
if(insertBefore) {
depth = getItemDepth($contextItem);
} else {
var $nextItem = $contextItem.next('.InputfieldRepeaterItem');
depth = getItemDepth($contextItem);
if($nextItem.hasClass('InputfieldRepeaterNewItem')) {
// the default hidden new item is not useful for identifying depth
if(!$nextItem.hasClass('InputfieldRepeaterInsertItem')) $nextItem = null;
}
var nextDepth = $nextItem && $nextItem.length ? getItemDepth($nextItem) : depth;
if(nextDepth > depth) depth = nextDepth;
}
return depth;
}
function getInsertBeforeItemDepth($item) {
return getInsertItemDepth($item, true);
}
function getInsertAfterItemDepth($item) {
return getInsertItemDepth($item, false);
}
/** /**
* Get all depth children for given repeater item * Get all depth children for given repeater item
* *
@@ -815,6 +1033,8 @@ function InputfieldRepeater($) {
var $clone = $("<i class='fa fa-copy InputfieldRepeaterClone'></i>").css('display', 'block'); var $clone = $("<i class='fa fa-copy InputfieldRepeaterClone'></i>").css('display', 'block');
var $delete = $("<i class='fa fa-trash InputfieldRepeaterTrash'></i>"); 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 $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 cfg = ProcessWire.config.InputfieldRepeater; var cfg = ProcessWire.config.InputfieldRepeater;
var allowClone = !$inputfieldRepeater.hasClass('InputfieldRepeaterNoAjaxAdd'); var allowClone = !$inputfieldRepeater.hasClass('InputfieldRepeaterNoAjaxAdd');
var allowSettings = $inputfieldRepeater.hasClass('InputfieldRepeaterHasSettings'); var allowSettings = $inputfieldRepeater.hasClass('InputfieldRepeaterHasSettings');
@@ -823,6 +1043,8 @@ function InputfieldRepeater($) {
$toggle.attr('title', cfg.labels.toggle); $toggle.attr('title', cfg.labels.toggle);
$delete.attr('title', cfg.labels.remove); $delete.attr('title', cfg.labels.remove);
$clone.attr('title', cfg.labels.clone); $clone.attr('title', cfg.labels.clone);
$insertBefore.attr('title', cfg.labels.insertBefore);
$insertAfter.attr('title', cfg.labels.insertAfter);
} }
if(allowSettings) { if(allowSettings) {
@@ -836,7 +1058,7 @@ function InputfieldRepeater($) {
var $item = $t.parent(); var $item = $t.parent();
if($item.hasClass('InputfieldRepeaterNewItem')) { if($item.hasClass('InputfieldRepeaterNewItem')) {
// noAjaxAdd mode // noAjaxAdd mode
icon = 'fa-plus'; icon = 'fa-plus-circle';
$t.addClass('ui-priority-secondary'); $t.addClass('ui-priority-secondary');
} }
$t.addClass('ui-state-default InputfieldRepeaterHeaderInit'); $t.addClass('ui-state-default InputfieldRepeaterHeaderInit');
@@ -847,7 +1069,11 @@ function InputfieldRepeater($) {
.addClass($t.parent().hasClass('InputfieldRepeaterOff') ? 'fa-toggle-off' : 'fa-toggle-on'); .addClass($t.parent().hasClass('InputfieldRepeaterOff') ? 'fa-toggle-off' : 'fa-toggle-on');
var $deleteControl = $delete.clone(true); var $deleteControl = $delete.clone(true);
var $collapseControl = $t.find('.toggle-icon'); var $collapseControl = $t.find('.toggle-icon');
var $insertBeforeControl = $insertBefore.clone(true);
var $insertAfterControl = $insertAfter.clone(true);
$controls.prepend($collapseControl); $controls.prepend($collapseControl);
$controls.prepend($insertBeforeControl);
$controls.prepend($insertAfterControl);
if($t.closest('.InputfieldRepeater').hasClass('InputfieldRepeaterHasSettings')) { // intentionally not using allowSettings var if($t.closest('.InputfieldRepeater').hasClass('InputfieldRepeaterHasSettings')) { // intentionally not using allowSettings var
var $settingsToggle = $("<i class='fa fa-gear InputfieldRepeaterSettingsToggle ui-priority-secondary'></i>") var $settingsToggle = $("<i class='fa fa-gear InputfieldRepeaterSettingsToggle ui-priority-secondary'></i>")
.attr('title', cfg.labels.settings); .attr('title', cfg.labels.settings);
@@ -858,7 +1084,6 @@ function InputfieldRepeater($) {
$controls.prepend($deleteControl); $controls.prepend($deleteControl);
$t.prepend($controls); $t.prepend($controls);
$controls.css('background-color', $t.css('background-color')); $controls.css('background-color', $t.css('background-color'));
} }
adjustItemLabel($item, false); adjustItemLabel($item, false);
}); });
@@ -1013,6 +1238,15 @@ function InputfieldRepeater($) {
} }
} }
/**
* Scroll to repeater item
*
* @param $item
*/
function scrollToItem($item) {
$('html, body').animate({scrollTop: $item.offset().top - 10}, 250, 'swing');
}
/** /**
* Run any scripts in the given HTML ajax data since jQuery will strip them * Run any scripts in the given HTML ajax data since jQuery will strip them
* *
@@ -1071,7 +1305,12 @@ function InputfieldRepeater($) {
.on('click', '.InputfieldRepeaterToggle', eventToggleClick) .on('click', '.InputfieldRepeaterToggle', eventToggleClick)
.on('opened', '.InputfieldRepeaterItem', eventItemOpened) .on('opened', '.InputfieldRepeaterItem', eventItemOpened)
.on('closed', '.InputfieldRepeaterItem', eventItemClosed) .on('closed', '.InputfieldRepeaterItem', eventItemClosed)
.on('openReady', '.InputfieldRepeaterItem', eventItemOpenReady); .on('openReady', '.InputfieldRepeaterItem', eventItemOpenReady)
.on('click', '.InputfieldRepeaterInsertBefore', eventInsertBeforeClick)
.on('click', '.InputfieldRepeaterInsertAfter', eventInsertAfterClick)
.on('mouseover', '.InputfieldRepeaterInsertBefore', eventInsertMouseover)
.on('mouseover', '.InputfieldRepeaterInsertAfter', eventInsertMouseover)
.on('mouseout', '.InputfieldRepeaterInsertAfter', eventInsertMouseout);
} }
init(); init();

File diff suppressed because one or more lines are too long

View File

@@ -26,9 +26,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
return array( return array(
'title' => __('Repeater', __FILE__), // Module Title 'title' => __('Repeater', __FILE__), // Module Title
'summary' => __('Repeats fields from another template. Provides the input for FieldtypeRepeater.', __FILE__), // Module Summary 'summary' => __('Repeats fields from another template. Provides the input for FieldtypeRepeater.', __FILE__), // Module Summary
'version' => 107, 'version' => 108,
'requires' => 'FieldtypeRepeater', 'requires' => 'FieldtypeRepeater',
); );
} }
/** /**
@@ -112,8 +112,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/ */
public function init() { public function init() {
parent::init(); parent::init();
if(is_null($this->page)) $this->page = $this->wire('pages')->newNullPage(); $pages = $this->wire()->pages;
$this->attr('value', $this->wire('pages')->newPageArray()); if(is_null($this->page)) $this->page = $pages->newNullPage();
$this->attr('value', $pages->newPageArray());
} }
/** /**
@@ -128,11 +129,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
public function ___renderRepeaterLabel($label, $cnt, Page $page) { public function ___renderRepeaterLabel($label, $cnt, Page $page) {
// situations where we skip the render of repeater label because it is not needed // situations where we skip the render of repeater label because it is not needed
$repeaterEditID = (int) $this->wire('input')->get('repeater_edit'); $repeaterEditID = (int) $this->wire()->input->get('repeater_edit');
if($repeaterEditID && $repeaterEditID == $page->id) { if($repeaterEditID && $repeaterEditID === $page->id) {
// edit of item requested in URL that matches given $page // edit of item requested in URL that matches given $page
return $label; return $label;
} else if(count($_POST) && !$this->wire('config')->ajax) { } else if(count($_POST) && !$this->wire()->config->ajax) {
// POST request that is not ajax // POST request that is not ajax
return $label; return $label;
} }
@@ -154,7 +155,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
// formatted {label} // formatted {label}
$out = $page->getMarkup($repeaterTitle); $out = $page->getMarkup($repeaterTitle);
} else if(!$hasCnt && $this->wire('sanitizer')->fieldName($repeaterTitle) === $repeaterTitle) { } else if(!$hasCnt && $this->wire()->sanitizer->fieldName($repeaterTitle) === $repeaterTitle) {
// just a single field name // just a single field name
$value = $page->getFormatted($repeaterTitle); $value = $page->getFormatted($repeaterTitle);
if(is_object($value)) { if(is_object($value)) {
@@ -321,6 +322,10 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
* *
*/ */
protected function buildForm($itemID = 0, $loadInputsForIDs = null) { protected function buildForm($itemID = 0, $loadInputsForIDs = null) {
$input = $this->wire()->input;
$session = $this->wire()->session;
$modules = $this->wire()->modules;
// if it's already been built, then return the cached version // if it's already been built, then return the cached version
if(!is_null($this->form)) return $this->form; if(!is_null($this->form)) return $this->form;
@@ -352,12 +357,12 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$openIDs = array(); $openIDs = array();
if((int) $this->field->get('rememberOpen')) { if((int) $this->field->get('rememberOpen')) {
$this->addClass('InputfieldRepeaterRememberOpen', 'wrapClass'); $this->addClass('InputfieldRepeaterRememberOpen', 'wrapClass');
$openIDs = $this->wire('input')->cookie('repeaters_open'); $openIDs = $input->cookie('repeaters_open');
if($openIDs) $openIDs = explode('|', trim($openIDs, '|')); if($openIDs) $openIDs = explode('|', trim($openIDs, '|'));
if(!is_array($openIDs)) $openIDs = array(); if(!is_array($openIDs)) $openIDs = array();
} }
// merge with any open IDs in session // merge with any open IDs in session
$_openIDs = $this->wire('session')->getFor($this, 'openIDs'); $_openIDs = $session->getFor($this, 'openIDs');
if(is_array($_openIDs) && !empty($_openIDs)) { if(is_array($_openIDs) && !empty($_openIDs)) {
$openIDs = array_merge($openIDs, array_values($_openIDs)); $openIDs = array_merge($openIDs, array_values($_openIDs));
} }
@@ -378,7 +383,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$cnt = 0; $cnt = 0;
$numVisible = 0; $numVisible = 0;
$numOpen = 0; $numOpen = 0;
$isPost = $this->wire('input')->requestMethod('POST'); $isPost = $input->requestMethod('POST');
$isSingle = $this->singleMode; $isSingle = $this->singleMode;
// create field for each repeater iteration // create field for each repeater iteration
@@ -413,20 +418,24 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$depth = null; $depth = null;
} else { } else {
// also add a delete checkbox to the repeater page fields // also add a delete checkbox to the repeater page fields
$delete = $this->wire('modules')->get('InputfieldCheckbox'); /** @var InputfieldCheckbox $delete */
$delete = $modules->get('InputfieldCheckbox');
$delete->attr('id+name', "delete_repeater{$page->id}"); $delete->attr('id+name', "delete_repeater{$page->id}");
$delete->addClass('InputfieldRepeaterDelete', 'wrapClass'); $delete->addClass('InputfieldRepeaterDelete', 'wrapClass');
$delete->label = $this->_('Delete'); $delete->label = $this->_('Delete');
$delete->attr('value', $page->id); $delete->attr('value', $page->id);
$sort = $this->wire('modules')->get('InputfieldHidden'); /** @var InputfieldHidden $sort */
$sort = $modules->get('InputfieldHidden');
$sort->attr('id+name', "sort_repeater{$page->id}"); $sort->attr('id+name', "sort_repeater{$page->id}");
$sort->class = 'InputfieldRepeaterSort'; $sort->class = 'InputfieldRepeaterSort';
$sort->addClass('InputfieldRepeaterItemSort', 'wrapClass');
$sort->label = $this->_('Sort'); $sort->label = $this->_('Sort');
$sort->attr('value', $cnt); $sort->attr('value', $cnt);
if($this->repeaterDepth > 0) { if($this->repeaterDepth > 0) {
$depth = $this->wire('modules')->get('InputfieldHidden'); /** @var InputfieldHidden $depth */
$depth = $modules->get('InputfieldHidden');
$depth->attr('id+name', "depth_repeater{$page->id}"); $depth->attr('id+name', "depth_repeater{$page->id}");
$depth->addClass('InputfieldRepeaterItemDepth', 'wrapClass'); $depth->addClass('InputfieldRepeaterItemDepth', 'wrapClass');
$depth->label = $this->_('Depth'); $depth->label = $this->_('Depth');
@@ -437,14 +446,16 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$depth = null; $depth = null;
} }
} }
$loaded = $this->wire('modules')->get('InputfieldHidden'); /** @var InputfieldHidden $loaded */
$loaded = $modules->get('InputfieldHidden');
$loaded->attr('id+name', "loaded_repeater{$page->id}"); $loaded->attr('id+name', "loaded_repeater{$page->id}");
$loaded->attr('value', $isLoaded ? 1 : 0); $loaded->attr('value', $isLoaded ? 1 : 0);
$loaded->set('renderValueAsInput', true); $loaded->set('renderValueAsInput', true);
$loaded->class = 'InputfieldRepeaterLoaded'; $loaded->class = 'InputfieldRepeaterLoaded';
$wrap = $this->wire('modules')->get('InputfieldFieldset'); /** @var InputfieldFieldset $wrap */
$wrap = $modules->get('InputfieldFieldset');
$wrap->addClass('InputfieldRepeaterItem InputfieldNoFocus'); $wrap->addClass('InputfieldRepeaterItem InputfieldNoFocus');
if(!$isPost) { if(!$isPost) {
$wrap->entityEncodeLabel = false; $wrap->entityEncodeLabel = false;
@@ -495,7 +506,8 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if(!$isSingle) { if(!$isSingle) {
// add a hidden field that will be populated with a positive value for all visible repeater items // add a hidden field that will be populated with a positive value for all visible repeater items
// this is so that processInput can see this item should be a published item // this is so that processInput can see this item should be a published item
$f = $this->wire('modules')->get('InputfieldHidden'); /** @var InputfieldHidden $f */
$f = $modules->get('InputfieldHidden');
$f->attr('name', "publish_repeater{$page->id}"); $f->attr('name', "publish_repeater{$page->id}");
$f->attr('class', 'InputfieldRepeaterPublish'); $f->attr('class', 'InputfieldRepeaterPublish');
@@ -547,7 +559,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
foreach($form->getAll() as $inputfield) { foreach($form->getAll() as $inputfield) {
$idAttr = $inputfield->attr('id'); $idAttr = $inputfield->attr('id');
$this->renderReadyInputfield($inputfield, $form, $this->renderValueMode); $this->renderReadyInputfield($inputfield, $form, $this->renderValueMode);
$jsValue = $this->wire('config')->js($idAttr); $jsValue = $this->wire()->config->js($idAttr);
if(!empty($jsValue)) { if(!empty($jsValue)) {
$inputfield->appendMarkup .= "<script>ProcessWire.config['$idAttr'] = " . json_encode($jsValue) . ';</script>'; $inputfield->appendMarkup .= "<script>ProcessWire.config['$idAttr'] = " . json_encode($jsValue) . ';</script>';
} }
@@ -555,9 +567,12 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
} else if(!$isSingle) { } else if(!$isSingle) {
// create a new/blank item to be used as a template for any new items added // create a new/blank item to be used as a template for any new items added
/** @var InputfieldWrapper $wrap */ /** @var InputfieldWrapper $wrap */
$wrap = $this->wire('modules')->get('InputfieldFieldset'); $wrap = $modules->get('InputfieldFieldset');
$wrap->label = $this->renderRepeaterLabel($label, ++$cnt, new NullPage()); $wrap->entityEncodeLabel = false;
$label = $this->entityEncode($this->renderRepeaterLabel($label, ++$cnt, new NullPage()));
$wrap->label = "<span class='InputfieldRepeaterItemLabel'>$label</span>";
$wrap->class = 'InputfieldRepeaterItem InputfieldRepeaterNewItem'; $wrap->class = 'InputfieldRepeaterItem InputfieldRepeaterNewItem';
$wrap->attr('data-depth', 0);
$wrap->collapsed = Inputfield::collapsedNo; $wrap->collapsed = Inputfield::collapsedNo;
$form->add($wrap); $form->add($wrap);
} }
@@ -618,7 +633,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($clonePage && $clonePage->id) { if($clonePage && $clonePage->id) {
/** @var FieldtypeRepeater $fieldtype */ /** @var FieldtypeRepeater $fieldtype */
$fieldtype = $this->field->type; $fieldtype = $this->field->type;
$readyPage = $this->wire('pages')->clone($clonePage, null, true, $readyPage = $this->wire()->pages->clone($clonePage, null, true,
array('set' => array( array('set' => array(
'name' => $fieldtype->getUniqueRepeaterPageName() . 'c', // trailing "c" indicates clone 'name' => $fieldtype->getUniqueRepeaterPageName() . 'c', // trailing "c" indicates clone
'sort' => count($value)+1, 'sort' => count($value)+1,
@@ -628,7 +643,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
); );
$readyPage->set('_repeater_clone', $clonePage->id); $readyPage->set('_repeater_clone', $clonePage->id);
} else if(!$cloneItemID) { } else if(!$cloneItemID) {
$notIDs = $this->wire('sanitizer')->intArray(explode(',', trim($this->wire('input')->get('repeater_not'), ','))); $notIDs = $this->wire()->sanitizer->intArray(explode(',', trim($this->wire()->input->get('repeater_not'), ',')));
$readyPage = $this->getNextReadyPage($notIDs); $readyPage = $this->getNextReadyPage($notIDs);
$readyPage->removeStatus(Page::statusHidden); $readyPage->removeStatus(Page::statusHidden);
} }
@@ -652,8 +667,8 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/ */
protected function renderAddLabel() { protected function renderAddLabel() {
$addLabel = $this->field->get('repeaterAddLabel'); $addLabel = $this->field->get('repeaterAddLabel');
if($this->wire('languages')) { if($this->wire()->languages) {
$language = $this->wire('user')->language; $language = $this->wire()->user->language;
if(!$language->isDefault()) { if(!$language->isDefault()) {
$addLabel = $this->field->get("repeaterAddLabel$language"); $addLabel = $this->field->get("repeaterAddLabel$language");
} }
@@ -672,11 +687,17 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/ */
public function renderReady(Inputfield $parent = null, $renderValueMode = false) { public function renderReady(Inputfield $parent = null, $renderValueMode = false) {
/** @var User $user */ $user = $this->wire()->user;
$user = $this->wire('user'); $modules = $this->wire()->modules;
/** @var JqueryCore $jQueryCore */
$jQueryCore = $modules->get('JqueryCore');
$jQueryCore->use('cookie');
/** @var JqueryUI $jQueryUI */
$jQueryUI = $modules->get('JqueryUI');
$jQueryUI->use('vex');
$this->wire('modules')->get('JqueryCore')->use('cookie');
$this->wire('modules')->get('JqueryUI')->use('vex');
$this->preloadInputfieldAssets(); $this->preloadInputfieldAssets();
$min = (int) $this->repeaterMinItems; $min = (int) $this->repeaterMinItems;
@@ -714,7 +735,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($queryString) {} if($queryString) {}
$this->wire('config')->js('InputfieldRepeater', array( $this->wire()->config->js('InputfieldRepeater', array(
'editorUrl' => $editorUrl, 'editorUrl' => $editorUrl,
'labels' => array( 'labels' => array(
'remove' => $this->_x('Click to delete this item, or double-click to delete all', 'repeater-item-action'), 'remove' => $this->_x('Click to delete this item, or double-click to delete all', 'repeater-item-action'),
@@ -724,6 +745,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
'settings' => $this->_x('Show settings?', 'repeater-item-action'), 'settings' => $this->_x('Show settings?', 'repeater-item-action'),
'openAll' => $this->_x('Open all items?', 'repeater-item-action'), 'openAll' => $this->_x('Open all items?', 'repeater-item-action'),
'collapseAll' => $this->_x('Collapse all items?', 'repeater-item-action'), 'collapseAll' => $this->_x('Collapse all items?', 'repeater-item-action'),
'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.')
) )
)); ));
@@ -761,11 +785,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/ */
public function ___render() { public function ___render() {
$input = $this->wire('input'); $input = $this->wire()->input;
$noAjaxAdd = $this->field->get('repeaterLoading') == FieldtypeRepeater::loadingOff ? '1' : ''; $noAjaxAdd = $this->field->get('repeaterLoading') == FieldtypeRepeater::loadingOff ? '1' : '';
$ajax = $this->wire('config')->ajax; $ajax = $this->wire()->config->ajax;
if($ajax && $input->get('field') == $this->attr('name')) { if($ajax && $input->get('field') === $this->attr('name')) {
$repeaterAdd = $input->get('repeater_add'); $repeaterAdd = $input->get('repeater_add');
$repeaterEdit = (int) $input->get('repeater_edit'); $repeaterEdit = (int) $input->get('repeater_edit');
$repeaterClone = (int) $input->get('repeater_clone'); $repeaterClone = (int) $input->get('repeater_clone');
@@ -841,7 +865,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$sortChanged = false; $sortChanged = false;
$value->setTrackChanges(true); $value->setTrackChanges(true);
$pageIDs = array(); $pageIDs = array();
$_openIDs = $this->wire('session')->getFor($this, 'openIDs'); $_openIDs = $this->wire()->session->getFor($this, 'openIDs');
if(!is_array($_openIDs)) $_openIDs = array(); if(!is_array($_openIDs)) $_openIDs = array();
$openIDs = $_openIDs; // these two are compared with each other at the end $openIDs = $_openIDs; // these two are compared with each other at the end
$this->numRequiredEmpty = 0; $this->numRequiredEmpty = 0;
@@ -962,7 +986,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
} }
// if openIDs value changed, update the session variable // if openIDs value changed, update the session variable
if($_openIDs !== $openIDs) $this->wire('session')->setFor($this, 'openIDs', $openIDs); if($_openIDs !== $openIDs) $this->wire()->session->setFor($this, 'openIDs', $openIDs);
return $this; return $this;
} }
@@ -979,7 +1003,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
*/ */
protected function formToPage(InputfieldWrapper $wrapper, Page $page, $level = 0) { protected function formToPage(InputfieldWrapper $wrapper, Page $page, $level = 0) {
$languages = $this->wire('languages'); $languages = $this->wire()->languages;
foreach($wrapper as $inputfield) { foreach($wrapper as $inputfield) {
@@ -1064,9 +1088,13 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
* *
*/ */
public function set($key, $value) { public function set($key, $value) {
if($key == 'page') $this->page = $value; if($key === 'page') {
else if($key == 'field') $this->field = $value; $this->page = $value;
else return parent::set($key, $value); } else if($key === 'field') {
$this->field = $value;
} else {
return parent::set($key, $value);
}
return $this; return $this;
} }

View File

@@ -42,9 +42,14 @@
.InputfieldRepeaterClone, .InputfieldRepeaterClone,
.InputfieldRepeaterToggle, .InputfieldRepeaterToggle,
.InputfieldRepeaterTrash, .InputfieldRepeaterTrash,
.InputfieldRepeaterInsertBefore,
.InputfieldRepeaterInsertAfter,
.toggle-icon { .toggle-icon {
cursor: pointer; cursor: pointer;
float: right; float: right;
&:hover {
opacity: 0.8;
}
} }
.InputfieldRepeaterTrash { .InputfieldRepeaterTrash {
padding-right: 3px; padding-right: 3px;
@@ -55,6 +60,12 @@
.InputfieldRepeaterClone { .InputfieldRepeaterClone {
margin-right: 1em; margin-right: 1em;
} }
.InputfieldRepeaterInsertBefore {
margin-right: 0.75em;
}
.InputfieldRepeaterInsertAfter {
margin-right: 1em;
}
.InputfieldRepeaterSettingsToggle { .InputfieldRepeaterSettingsToggle {
margin-right: 1em; margin-right: 1em;
} }
@@ -70,6 +81,10 @@
} }
} }
& > .InputfieldContent > .Inputfields > .InputfieldRepeaterInsertItem {
opacity: 0.5;
}
.InputfieldStateCollapsed .InputfieldRepeaterSettingsToggle { .InputfieldStateCollapsed .InputfieldRepeaterSettingsToggle {
display: none; display: none;

View File

@@ -3,7 +3,7 @@
/** /**
* RepeaterPage represents an individual repeater page item * RepeaterPage represents an individual repeater page item
* *
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
*/ */
@@ -114,7 +114,7 @@ class RepeaterPage extends Page {
$grandparentName = $grandparent->name; $grandparentName = $grandparent->name;
$prefix = FieldtypeRepeater::fieldPageNamePrefix; // for-field- $prefix = FieldtypeRepeater::fieldPageNamePrefix; // for-field-
$forField = null; $forField = null;
$fields = $this->wire('fields'); /** @var Fields $fields */ $fields = $this->wire()->fields;
if(strpos($grandparentName, $prefix) === 0) { if(strpos($grandparentName, $prefix) === 0) {
// determine field from grandparent name in format: for-field-1234 // determine field from grandparent name in format: for-field-1234

View File

@@ -5,7 +5,7 @@
* *
* Special PageArray for use by repeaters that includes a getNewItem() method * Special PageArray for use by repeaters that includes a getNewItem() method
* *
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
*/ */
@@ -28,15 +28,49 @@ class RepeaterPageArray extends PageArray {
*/ */
protected $field = null; protected $field = null;
/**
* Construct
*
* @param Page $parent
* @param Field $field
*
*/
public function __construct(Page $parent, Field $field) { public function __construct(Page $parent, Field $field) {
$this->setParent($parent); $this->setParent($parent);
$this->setField($field); $this->setField($field);
parent::__construct(); parent::__construct();
} }
/**
* Set parent
*
* @param Page $parent
*
*/
public function setParent(Page $parent) { $this->parent = $parent; } public function setParent(Page $parent) { $this->parent = $parent; }
/**
* Get parent
*
* @return Page
*
*/
public function getParent() { return $this->parent; } public function getParent() { return $this->parent; }
/**
* Set field
*
* @param Field $field
*
*/
public function setField(Field $field) { $this->field = $field; } public function setField(Field $field) { $this->field = $field; }
/**
* Get field
*
* @return Field
*
*/
public function getField() { return $this->field; } public function getField() { return $this->field; }
/** /**
@@ -132,6 +166,12 @@ class RepeaterPageArray extends PageArray {
parent::trackRemove($item, $key); parent::trackRemove($item, $key);
} }
/**
* Debug info
*
* @return array
*
*/
public function __debugInfo() { public function __debugInfo() {
$info = array( $info = array(
'field' => $this->field ? $this->field->debugInfoSmall() : '', 'field' => $this->field ? $this->field->debugInfoSmall() : '',