1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-15 11:14:12 +02:00

Add support for family-friendly depth settings in InputfieldRepeater. Can be enabled in repeater field settings (for matrix or regular repeater). This makes depth act as parent/child relationships in page editor so that dragging/sorting a parent also drags children. It also prevents more than 1 depth level increase between parent and child (i.e. converts a parent-to-grandchild relationship to parent-to-child relationship).

This commit is contained in:
Ryan Cramer
2021-01-19 11:39:06 -05:00
parent ecb7694312
commit e28d2e67e7
5 changed files with 171 additions and 26 deletions

View File

@@ -9,7 +9,7 @@
* /wire/core/Fieldtype.php
* /wire/core/FieldtypeMulti.php
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @todo: automatic sorting.
@@ -34,7 +34,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' => 106,
'version' => 107,
'autoload' => true,
'installs' => 'InputfieldRepeater'
);

View File

@@ -3,7 +3,7 @@
*
* Maintains a collection of fields that are repeated for any number of times.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
*/
@@ -528,6 +528,8 @@ function InputfieldRepeater($) {
}
}
/*** DEPTH FUNCTIONS **********************************************************************************/
/**
* Determine the sortable depth of a repeater item and either return it or apply it
*
@@ -539,7 +541,8 @@ function InputfieldRepeater($) {
*/
function sortableDepth(ui, maxDepth, updateNow) {
var $depth = ui.item.find('.InputfieldRepeaterDepth');
var $wrap = ui.item.children('.InputfieldContent').children('.Inputfields').children('.InputfieldRepeaterItemDepth');
var $depth = $wrap.find('input');
var depth = -1;
var prevDepth = parseInt($depth.val());
var left = ui.position.left;
@@ -553,40 +556,129 @@ function InputfieldRepeater($) {
// console.log('increase depth to: ' + depth);
}
if(depth < 1) {
depth = 0;
} else if(depth > maxDepth) {
depth = maxDepth;
}
if(updateNow) {
if(depth) {
ui.item.css('margin-left', (depth * depthSize) + 'px');
} else {
ui.item.css('margin-left', 0);
}
$depth.val(depth);
depth = setItemDepth(ui.item, depth, maxDepth);
ui.item.children('.InputfieldHeader').removeClass('ui-state-error');
}
return depth;
}
/**
* Set repeater item depth
*
* @param $item Repeater item
* @param int depth Depth to set
* @param int maxDepth Max depth (you can optionally omit this if depth is already validated for the max)
* @param bool noValidate Specify true to prevent depth validation, otherwise omit
* @returns int Returns adjusted depth or -1 on fail
*
*/
function setItemDepth($item, depth, maxDepth, noValidate) {
noValidate = typeof noValidate === "undefined" ? false : noValidate;
if(depth < 1) depth = 0;
if(typeof maxDepth !== 'undefined' && depth > maxDepth) depth = maxDepth;
if(!$item.hasClass('InputfieldRepeaterItem')) $item = $item.closest('.InputfieldRepeaterItem');
if(!$item.length) return -1;
var $depthInput = $item.children('.InputfieldContent').children('.Inputfields')
.children('.InputfieldRepeaterItemDepth').find('input');
if(!$depthInput.length) {
console.log('Cannot find depth input for ' + $item.attr('id'));
}
if(!noValidate && $item.closest('.InputfieldRepeater').hasClass('InputfieldRepeaterFamilyFriendly')) {
var $prevItem = $item.prev('.InputfieldRepeaterItem:not(.InputfieldRepeaterNewItem)');
if($prevItem.length) {
var prevItemDepth = parseInt($prevItem.attr('data-depth'));
if(depth - prevItemDepth > 1) depth = prevItemDepth + 1;
} else {
depth = 0;
}
}
$depthInput.val(depth);
$item.attr('data-depth', depth);
if(depth > 0) {
$item.css('margin-left', (depth * depthSize) + 'px');
} else {
$item.css('margin-left', 0);
}
return depth;
}
/**
* Get repeater item depth
*
* @param $item Repeater item
* @returns int Returns depth or -1 on fail
*
*/
function getItemDepth($item) {
if(!$item.hasClass('InputfieldRepeaterItem')) $item = $item.closest('.InputfieldRepeaterItem');
if(!$item.length) return -1;
return parseInt($item.attr('data-depth'));
}
/**
* Get all depth children for given repeater item
*
* @param $item Repeater item
* @returns {Array}
*
*/
function getDepthChildren($item) {
var children = [];
var n = 0;
var startDepth = parseInt($item.attr('data-depth'));
var pageId = $item.attr('data-page');
var pageIdClass = 'Inputfield_repeater_item_' + pageId;
// ui.sortable adds additional copies of $item, so make sure we have the last one
while($item.hasClass(pageIdClass)) {
var $nextItem = $item.next('.InputfieldRepeaterItem:not(.InputfieldRepeaterNewItem)');
if(!$nextItem.length || !$nextItem.hasClass(pageIdClass)) break;
$item = $nextItem;
}
do {
// var $child = $item.next('.InputfieldRepeaterItem:not(.' + pageIdClass + '):not(.InputfieldRepeaterNewItem)');
var $child = $item.next('.InputfieldRepeaterItem:not(.InputfieldRepeaterNewItem)');
if(!$child.length) break;
var childDepth = parseInt($child.attr('data-depth'));
if(!childDepth || childDepth <= startDepth) break;
$item = $child;
children[n] = $child;
n++;
} while(true);
return children;
}
/*** INIT FUNCTIONS **********************************************************************************/
/**
* Initialize repeater item depths
*
* Applies a left-margin to repeater items consistent with with value in
* each item's input.InputfieldRepeaterDepth hidden input.
* each item's '.InputfieldRepeaterItemDepth input' hidden input.
*
* @param $inputfieldRepeater
*
*/
function initDepths($inputfieldRepeater) {
$inputfieldRepeater.find('.InputfieldRepeaterDepth').each(function() {
var $depth = $(this);
$inputfieldRepeater.find('.InputfieldRepeaterItemDepth').each(function() {
var $wrap = $(this);
var $depth = $wrap.find('input');
var depth = $depth.val();
var $item = $depth.closest('.InputfieldRepeaterItem');
var currentLeft = $item.css('margin-left');
@@ -610,6 +702,9 @@ function InputfieldRepeater($) {
function initSortable($inputfieldRepeater, $inputfields) {
var maxDepth = parseInt($inputfieldRepeater.attr('data-depth'));
var depthChildren = [];
var startDepth = 0;
var familyFriendly = $inputfieldRepeater.hasClass('InputfieldRepeaterFamilyFriendly');
var sortableOptions = {
items: '> li:not(.InputfieldRepeaterNewItem)',
handle: '.InputfieldRepeaterDrag',
@@ -628,6 +723,15 @@ function InputfieldRepeater($) {
ui.item.find('.InputfieldTinyMCE textarea').each(function() {
tinyMCE.execCommand('mceRemoveControl', false, $(this).attr('id'));
});
if(familyFriendly && maxDepth > 0) {
// remember and hide depth children
startDepth = parseInt(ui.item.attr('data-depth'));
depthChildren = getDepthChildren(ui.item);
for(var n = 0; n < depthChildren.length; n++) {
depthChildren[n].slideUp('fast');
}
}
},
stop: function(e, ui) {
@@ -635,6 +739,24 @@ function InputfieldRepeater($) {
sortableDepth(ui, maxDepth, true);
}
// update/move and show depth children
if(maxDepth > 0 && familyFriendly && depthChildren.length) {
var $item = ui.item;
var stopDepth = parseInt($item.attr('data-depth'));
var diffDepth = stopDepth - startDepth;
for(var n = 0; n < depthChildren.length; n++) {
var $child = depthChildren[n];
if(diffDepth != 0) {
var itemDepth = getItemDepth($child);
setItemDepth($child, itemDepth + diffDepth, maxDepth, true);
}
$item.after($child);
$child.slideDown('fast');
$item = $child;
}
depthChildren = [];
}
ui.item.find('.InputfieldHeader').removeClass("ui-state-highlight");
$(this).children().each(function(n) {
$(this).find('.InputfieldRepeaterSort').slice(0,1).attr('value', n);
@@ -649,6 +771,7 @@ function InputfieldRepeater($) {
ui.item.find('.InputfieldTinyMCE textarea').each(function() {
tinyMCE.execCommand('mceAddControl', false, $(this).attr('id'));
});
}
};

File diff suppressed because one or more lines are too long

View File

@@ -5,12 +5,13 @@
*
* Maintains a collection of fields that are repeated for any number of times.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @property int $repeaterMaxItems
* @property int $repeaterMinItems
* @property int $repeaterDepth
* @property bool|int $familyFriendly
* @property bool $accordionMode
* @property bool $singleMode
*
@@ -25,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' => 106,
'version' => 107,
'requires' => 'FieldtypeRepeater',
);
}
@@ -100,6 +101,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$this->set('repeaterMaxItems', 0);
$this->set('repeaterMinItems', 0);
$this->set('repeaterDepth', 0);
$this->set('familyFriendly', 0);
$this->set('accordionMode', false);
$this->set('singleMode', false);
}
@@ -291,6 +293,10 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$label = $this->field->getLabel();
if(!$label) $label = ucfirst($this->field->name);
if((int) $this->repeaterDepth > 1 && (int) $this->familyFriendly) {
$this->addClass('InputfieldRepeaterFamilyFriendly', 'wrapClass');
}
// remember which repeater items are open (as stored in cookie), when enabled
$openIDs = array();
if((int) $this->field->get('rememberOpen')) {
@@ -371,7 +377,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($this->repeaterDepth > 0) {
$depth = $this->wire('modules')->get('InputfieldHidden');
$depth->attr('id+name', "depth_repeater{$page->id}");
$depth->class = 'InputfieldRepeaterDepth';
$depth->addClass('InputfieldRepeaterItemDepth', 'wrapClass');
$depth->label = $this->_('Depth');
$depthValue = $page->getDepth();
$depth->attr('value', $depthValue);
@@ -405,6 +411,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$wrap->wrapAttr('data-type', $itemType);
$wrap->wrapAttr('data-typeName', $itemTypeName);
$wrap->wrapAttr('data-fnsx', "_repeater$page->id"); // fnsx=field name suffix
$wrap->wrapAttr('data-depth', $depth ? $depth->val() : '0');
//$wrap->wrapAttr('data-editorPage', $this->page->id);
//$wrap->wrapAttr('data-parentPage', $page->parent->id);
$wrap->wrapAttr('data-editUrl', $page->editUrl()); // if needed by any Inputfields within like InputfieldFile/InputfieldImage

View File

@@ -252,6 +252,21 @@ class FieldtypeRepeaterConfigHelper extends Wire {
$f->notes = $this->_('Depths are zero-based, meaning a depth of 3 allows depths 0, 1, 2 and 3.');
$f->notes .= ' ' . $this->_('Depth can be accessed from a repeater page item via `$item->depth`.');
$f->icon = 'indent';
$f->columnWidth = 50;
$inputfields->add($f);
/** @var InputfieldToggle $f */
$f = $this->wire()->modules->get('InputfieldToggle');
$f->attr('name', 'familyFriendly');
$f->label = $this->_('Use family-friendly item depth?');
$f->description =
$this->_('This setting makes the admin page editor treat item depth as a parent/child relationship.') . ' ' .
$this->_('This means that moving/sorting an item includes child items too.') . ' ' .
$this->_('It also prevents a child item from being dragged to have a depth that exceeds its parent by more than 1.');
$f->val((int) $field->get('familyFriendly'));
$f->icon = 'indent';
$f->showIf = 'repeaterDepth>1';
$f->columnWidth = 50;
$inputfields->add($f);
// -------------------------------------------------