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:
@@ -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'
|
||||
);
|
||||
|
@@ -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,25 +556,113 @@ 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 **********************************************************************************/
|
||||
|
||||
@@ -579,14 +670,15 @@ function InputfieldRepeater($) {
|
||||
* 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,12 +702,15 @@ 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',
|
||||
start: function(e, ui) {
|
||||
ui.item.find('.InputfieldHeader').addClass("ui-state-highlight");
|
||||
|
||||
|
||||
// CKEditor doesn't like being sorted, do destroy when sort starts, and reload after sort
|
||||
ui.item.find('textarea.InputfieldCKEditorNormal.InputfieldCKEditorLoaded').each(function() {
|
||||
$(this).removeClass('InputfieldCKEditorLoaded');
|
||||
@@ -628,12 +723,39 @@ 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) {
|
||||
if(maxDepth > 0) {
|
||||
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) {
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -290,7 +292,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
|
||||
// get field label in user's language if available
|
||||
$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
|
||||
|
@@ -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);
|
||||
|
||||
// -------------------------------------------------
|
||||
|
Reference in New Issue
Block a user