1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 16:26:59 +02:00

Additional repeater updates including addition of a "minimum items" option, and support for an accordion mode.

This commit is contained in:
Ryan Cramer
2016-12-09 10:08:55 -05:00
parent 6027e87a5e
commit 880810c6bb
9 changed files with 387 additions and 166 deletions

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' => 105,
'version' => 106,
'autoload' => true,
'installs' => 'InputfieldRepeater'
);
@@ -497,6 +497,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule {
$inputfield->set('page', $page);
$inputfield->set('field', $field);
$inputfield->set('repeaterMaxItems', (int) $field->get('repeaterMaxItems'));
$inputfield->set('repeaterMinItems', (int) $field->get('repeaterMinItems'));
$inputfield->set('repeaterDepth', (int) $field->get('repeaterDepth'));
$inputfield->set('repeaterReadyItems', 0); // ready items deprecated

View File

@@ -6,10 +6,14 @@
white-space: nowrap;
overflow: hidden;
position: relative; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemLabel {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemLabel,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls {
display: none; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemLabel {
display: inline-block;
padding-left: 0.25em; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls {
display: block;
padding-right: 0.5em;
padding-left: 0.5em;
margin-top: 0.5em;
@@ -20,19 +24,21 @@
display: block;
white-space: nowrap;
height: 100%; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterClone,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterToggle,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterTrash,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .toggle-icon {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterClone,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterToggle,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterTrash,
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .toggle-icon {
cursor: pointer;
float: right; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterTrash {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterTrash {
padding-right: 3px; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterToggle {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterToggle {
margin-right: 1em; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .InputfieldRepeaterItemControls .InputfieldRepeaterClone {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .InputfieldRepeaterClone {
margin-right: 1em; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .toggle-icon {
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .InputfieldRepeaterItemControls .pw-icon-disabled {
opacity: 0.3; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader.InputfieldRepeaterHeaderInit .toggle-icon {
line-height: 1em;
margin-right: 0.5em; }
.Inputfields .InputfieldRepeater .InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending).InputfieldStateCollapsed > .InputfieldHeader {

View File

@@ -27,6 +27,13 @@ function InputfieldRepeater($) {
*/
var isReno = $('body').hasClass('AdminThemeReno');
/**
* Event timer for double clicks
*
*/
var doubleClickTimer = null;
/*** EVENTS ********************************************************************************************/
/**
@@ -56,13 +63,16 @@ function InputfieldRepeater($) {
*/
var eventDeleteClick = function(e) {
var $header = $(this).closest('.InputfieldHeader');
var $this = $(this);
var $header = $this.closest('.InputfieldHeader');
var $item = $header.parent();
if(isActionDisabled($this)) return false;
if($item.hasClass('InputfieldRepeaterNewItem')) {
// delete new item (noAjaxAdd mode)
var $numAddInput = $item.children('.InputfieldContent').children('.InputfieldRepeaterAddItem').children('input');
$numAddInput.attr('value', parseInt($numAddInput.attr('value')-1)); // total number of new items to add, minus 1
$numAddInput.attr('value', parseInt($numAddInput.attr('value') - 1)); // total number of new items to add, minus 1
$item.remove();
} else {
@@ -87,20 +97,22 @@ function InputfieldRepeater($) {
$header.find('.InputfieldRepeaterItemControls').css('background-color', $header.css('background-color'));
}
checkMax($item.closest('.InputfieldRepeater'));
checkMinMax($item.closest('.InputfieldRepeater'));
e.stopPropagation();
};
/**
* Event handler for when the "delete" link is double clicked
*
* @param e
*
*/
var eventDeleteDblClick = function(e) {
var eventDeleteDblClick = function() {
var $this = $(this);
var $li = $(this).closest('li');
var undelete = $li.hasClass('InputfieldRepeaterDeletePending');
if(isActionDisabled($this)) return false;
function selectAll() {
$li.parent().children('li').each(function() {
var $item = $(this);
@@ -123,12 +135,13 @@ function InputfieldRepeater($) {
/**
* Event handler for when the "clone" repeater item action is clicked
*
* @param e
* @returns {boolean}
*
*/
var eventCloneClick = function(e) {
var $item = $(this).closest('.InputfieldRepeaterItem');
var eventCloneClick = function() {
var $this = $(this);
if(isActionDisabled($this)) return false;
var $item = $this.closest('.InputfieldRepeaterItem');
ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.clone, function() {
var itemID = $item.attr('data-page');
var $addLink = $item.closest('.InputfieldRepeater').children('.InputfieldContent')
@@ -152,15 +165,20 @@ function InputfieldRepeater($) {
var $item = $this.closest('.InputfieldRepeaterItem');
var $input = $item.find('.InputfieldRepeaterPublish');
if($this.hasClass(toggleOn)) {
$this.removeClass(toggleOn).addClass(toggleOff);
$item.addClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$input.val('-1');
} else {
$this.removeClass(toggleOff).addClass(toggleOn);
$item.removeClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$input.val('1');
}
if(doubleClickTimer) clearTimeout(doubleClickTimer);
doubleClickTimer = setTimeout(function() {
if(isActionDisabled($this)) return false;
if($this.hasClass(toggleOn)) {
$this.removeClass(toggleOn).addClass(toggleOff);
$item.addClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$input.val('-1');
} else {
$this.removeClass(toggleOff).addClass(toggleOn);
$item.removeClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$input.val('1');
}
checkMinMax($item.closest('.InputfieldRepeater'));
}, 250);
e.stopPropagation();
};
@@ -168,10 +186,8 @@ function InputfieldRepeater($) {
/**
* Event handler for when a repeater item is about to be opened
*
* @param e
*
*/
var eventItemOpenReady = function(e) {
var eventItemOpenReady = function() {
var $item = $(this);
var $loaded = $item.find(".InputfieldRepeaterLoaded");
if(parseInt($loaded.val()) > 0) return; // item already loaded
@@ -181,16 +197,18 @@ function InputfieldRepeater($) {
/**
* Event handler for when a repeater item is opened (primarily focused on ajax loaded items)
*
* @param e
*
*/
var eventItemOpened = function(e) {
var eventItemOpened = function() {
var $item = $(this);
var $loaded = $item.find(".InputfieldRepeaterLoaded");
updateState($item);
if(parseInt($loaded.val()) > 0) return; // item already loaded
if(parseInt($loaded.val()) > 0) {
updateAccordion($item);
return; // item already loaded
}
$loaded.val('1');
@@ -221,8 +239,10 @@ function InputfieldRepeater($) {
initRepeater($(this));
});
$content.slideDown('fast', function() {
$spinner.removeClass('fa-spin fa-spinner').addClass('fa-arrows');
updateAccordion($item);
});
setTimeout(function() {
$inputfields.find('.Inputfield').trigger('reloaded', ['InputfieldRepeaterItemEdit']);
@@ -234,10 +254,8 @@ function InputfieldRepeater($) {
/**
* Event handler for when a repeater item is closed
*
* @param e
*
*/
var eventItemClosed = function(e) {
var eventItemClosed = function() {
updateState($(this));
};
@@ -246,11 +264,10 @@ function InputfieldRepeater($) {
*
* Handles adding repeater items and initializing them
*
* @param e
* @returns {boolean}
*
*/
var eventAddLinkClick = function(e) {
var eventAddLinkClick = function() {
var $addLink = $(this);
var $inputfields = $addLink.parent('p').prev('ul.Inputfields');
var $inputfieldRepeater = $addLink.closest('.InputfieldRepeater');
@@ -278,10 +295,10 @@ function InputfieldRepeater($) {
newItemTotal = $newItem.length;
if(newItemTotal > 0) {
if(newItemTotal > 1) $newItem = $newItem.slice(0, 1);
var $addItem = $newItem.clone(true)
var $addItem = $newItem.clone(true);
addRepeaterItem($addItem);
$numAddInput.attr('value', newItemTotal);
checkMax($inputfieldRepeater);
checkMinMax($inputfieldRepeater);
}
return false;
}
@@ -301,7 +318,7 @@ function InputfieldRepeater($) {
}
// determine which page IDs we don't accept for new items (because we already have them rendered)
var $unpublishedItems = $inputfields.find('.InputfieldRepeaterUnpublished');
var $unpublishedItems = $inputfields.find('.InputfieldRepeaterUnpublished:not(.InputfieldRepeaterMinItem)');
if($unpublishedItems.length) {
ajaxURL += '&repeater_not=';
$unpublishedItems.each(function() {
@@ -328,8 +345,9 @@ function InputfieldRepeater($) {
scrollTop: $addItem.offset().top
}, 500, 'swing');
updateState($addItem);
checkMax($inputfieldRepeater);
$nestedRepeaters = $addItem.find('.InputfieldRepeater');
checkMinMax($inputfieldRepeater);
updateAccordion($addItem);
var $nestedRepeaters = $addItem.find('.InputfieldRepeater');
if($nestedRepeaters.length) {
$nestedRepeaters.each(function() {
initRepeater($(this));
@@ -348,9 +366,14 @@ function InputfieldRepeater($) {
*
*/
var eventOpenAllClick = function(e) {
e.stopPropagation();
e.preventDefault();
if(doubleClickTimer) clearTimeout(doubleClickTimer);
if($(this).closest('.InputfieldRepeater').hasClass('InputfieldRepeaterAccordion')) return false;
var $repeater = $(this).closest('.InputfieldRepeater');
var $items = $repeater.children('.InputfieldContent').children('.Inputfields').children('.InputfieldRepeaterItem');
if(!$items.length) return false;
@@ -373,34 +396,83 @@ function InputfieldRepeater($) {
/*** GENERAL FUNCTIONS **********************************************************************************/
/**
* Returns whether or not the given icon action is disabled
*
* @param $this The '.fa-' icon that represents the action
* @returns {boolean}
*
*/
function isActionDisabled($this) {
if($this.hasClass('pw-icon-disabled')) {
ProcessWire.alert(ProcessWire.config.InputfieldRepeater.labels.disabledMinMax);
return true;
}
return false;
}
function updateAccordion($item) {
if(!$item.closest('.InputfieldRepeater').hasClass('InputfieldRepeaterAccordion')) return false;
var itemID = $item.attr('id');
var useScroll = false;
var $siblings = $item.parent().children('.InputfieldRepeaterItem');
var itemHasPassed = false;
var hasOpen = false;
$siblings.each(function() {
var $sibling = $(this);
if($sibling.attr('id') == itemID) {
itemHasPassed = true;
return;
}
if($sibling.hasClass('InputfieldStateCollapsed')) return;
if(!$sibling.is(':visible')) return;
if(!itemHasPassed) useScroll = true;
$sibling.children('.InputfieldHeader').find('.toggle-icon').trigger('click', [ { duration: 0 }]);
hasOpen = true;
});
if(useScroll && hasOpen) {
$('html, body').animate({scrollTop: $item.offset().top - 10}, 0);
}
return true;
}
/**
* Given an InputfieldRepeaterItem update the label consistent with any present formatting sting
*
* Primarily adjusts item count(s) and allowed for {secondary} text appearance
*
* @param $item An .InputfieldRepeaterItem
* @param bool doIncrement Specify true to increment the item count value (like for new items)
* @param {boolean} doIncrement Specify true to increment the item count value (like for new items)
*
*/
function adjustItemLabel($item, doIncrement) {
var $label = $item.children('label');
var $label;
$label = $item.children('.InputfieldHeader').find('.InputfieldRepeaterItemLabel');
if(typeof $label == "undefined") $label = $item.children('label');
var labelHTML = $label.html();
var _labelHTML = labelHTML;
if(doIncrement && labelHTML.indexOf('#') > -1) {
num = $item.siblings('.InputfieldRepeaterItem:visible').length + 1;
labelHTML = labelHTML.replace(/#[0-9]+/, '#' + num);
}
if(typeof labelHTML != "undefined") {
if(doIncrement && labelHTML.indexOf('#') > -1) {
var num = $item.siblings('.InputfieldRepeaterItem:visible').length + 1;
labelHTML = labelHTML.replace(/#[0-9]+/, '#' + num);
}
if(labelHTML.indexOf('{') > -1) {
// parts of the label wrapped in {brackets} get different appearance
labelHTML = labelHTML.replace(/\{/, '<span class="ui-priority-secondary" style="font-weight:normal">');
labelHTML = labelHTML.replace(/}/, '</span>');
}
while(labelHTML.indexOf('}') > -1) {
// parts of the label wrapped in {brackets} get different appearance
labelHTML = labelHTML.replace(/\{/, '<span class="ui-priority-secondary" style="font-weight:normal">');
labelHTML = labelHTML.replace(/}/, '</span>');
}
if(labelHTML != _labelHTML) {
$label.html(labelHTML);
if(labelHTML != _labelHTML) {
$label.html(labelHTML);
}
}
}
@@ -557,6 +629,56 @@ function InputfieldRepeater($) {
$inputfields.sortable(sortableOptions);
}
/**
* Initialize the .InputfieldHeader for .InputfieldRepeaterItem elements
*
* @param $headers The .InputfieldHeader elements
* @param $inputfieldRepeater The parent .InputfieldRepeater
* @param {boolean} renderValueMode Whether or not this is value-only rendering mode
*
*/
function initHeaders($headers, $inputfieldRepeater, renderValueMode) {
var $clone = $("<i class='fa fa-copy InputfieldRepeaterClone'></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 cfg = ProcessWire.config.InputfieldRepeater;
var allowClone = !$inputfieldRepeater.hasClass('InputfieldRepeaterNoAjaxAdd');
if(cfg) {
$toggle.attr('title', cfg.labels.toggle);
$delete.attr('title', cfg.labels.remove);
$clone.attr('title', cfg.labels.clone);
}
$headers.each(function() {
var $t = $(this);
if($t.hasClass('InputfieldRepeaterHeaderInit')) return;
var icon = 'fa-arrows';
var $item = $t.parent();
if($item.hasClass('InputfieldRepeaterNewItem')) {
// noAjaxAdd mode
icon = 'fa-plus';
$t.addClass('ui-priority-secondary');
}
$t.addClass('ui-state-default InputfieldRepeaterHeaderInit');
$t.prepend("<i class='fa fa-fw " + icon + " InputfieldRepeaterDrag'></i>");
if(!renderValueMode) {
var $controls = $("<span class='InputfieldRepeaterItemControls'></span>");
var $toggleControl = $toggle.clone(true)
.addClass($t.parent().hasClass('InputfieldRepeaterOff') ? 'fa-toggle-off' : 'fa-toggle-on');
var $deleteControl = $delete.clone(true);
var $collapseControl = $t.find('.toggle-icon');
$controls.prepend($collapseControl);
if(allowClone) $controls.prepend($clone.clone(true));
$controls.prepend($toggleControl).prepend($deleteControl);
$t.prepend($controls);
$controls.css('background-color', $t.css('background-color'));
}
adjustItemLabel($item, false);
});
}
/**
* Initialize a repeater
*
@@ -579,57 +701,16 @@ function InputfieldRepeater($) {
if($inputfields.hasClass('InputfieldRepeaterInit')) return;
$inputfields.addClass('InputfieldRepeaterInit');
var renderValueMode = $inputfields.closest('.InputfieldRenderValueMode').length > 0;
var $clone = $("<i class='fa fa-copy InputfieldRepeaterClone'></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 cfg = ProcessWire.config.InputfieldRepeater;
var allowClone = !$inputfieldRepeater.hasClass('InputfieldRepeaterNoAjaxAdd');
if(cfg) {
$toggle.attr('title', cfg.labels.toggle);
$delete.attr('title', cfg.labels.remove);
$clone.attr('title', cfg.labels.clone);
}
$inputfields.addClass('InputfieldRepeaterInit');
$("input.InputfieldRepeaterDelete", $this).parents('.InputfieldCheckbox').hide();
function initHeaders($headers) {
$headers.each(function() {
var $t = $(this);
if($t.hasClass('InputfieldRepeaterHeaderInit')) return;
var icon = 'fa-arrows';
var $item = $t.parent();
if($item.hasClass('InputfieldRepeaterNewItem')) {
// noAjaxAdd mode
icon = 'fa-plus';
$t.addClass('ui-priority-secondary');
}
$t.addClass('ui-state-default InputfieldRepeaterHeaderInit');
$t.prepend("<i class='fa fa-fw " + icon + " InputfieldRepeaterDrag'></i>")
if(!renderValueMode) {
//if(allowClone) $t.prepend($clone.clone(true));
var $controls = $("<span class='InputfieldRepeaterItemControls'></span>");
var $toggleControl = $toggle.clone(true).addClass($t.parent().hasClass('InputfieldRepeaterOff') ? 'fa-toggle-off' : 'fa-toggle-on');
var $deleteControl = $delete.clone(true);
var $collapseControl = $t.find('.toggle-icon');
//$collapseControl.addClass('InputfieldRepeaterCollapse').removeClass('toggle-icon');
$controls.prepend($collapseControl);
if(allowClone) $controls.prepend($clone.clone(true));
$controls.prepend($toggleControl).prepend($deleteControl);
$t.prepend($controls);
$controls.css('background-color', $t.css('background-color'));
}
adjustItemLabel($item, false);
});
}
if(isItem) {
initHeaders($this.children('.InputfieldHeader'));
initHeaders($this.children('.InputfieldHeader'), $inputfieldRepeater, renderValueMode);
} else {
initHeaders($(".InputfieldRepeaterItem > .InputfieldHeader", $this));
initHeaders($(".InputfieldRepeaterItem > .InputfieldHeader", $this), $inputfieldRepeater, renderValueMode);
}
if(renderValueMode) {
@@ -662,28 +743,87 @@ function InputfieldRepeater($) {
// check for maximum items
if($inputfieldRepeater.hasClass('InputfieldRepeaterMax')) {
checkMax($inputfieldRepeater);
checkMinMax($inputfieldRepeater);
}
}
/**
* When "max items" setting is used, this toggles whether or not "add" links are visible
*
* @todo Make this toggle the clone links as well
* @param $inputfieldRepeater .InputfieldRepeater
*
*/
function checkMax($inputfieldRepeater) {
if(!$inputfieldRepeater.hasClass('InputfieldRepeaterMax')) return;
function checkMinMax($inputfieldRepeater) {
if(!$inputfieldRepeater.hasClass('InputfieldRepeaterMax')
&& !$inputfieldRepeater.hasClass('InputfieldRepeaterMin')) return;
var max = parseInt($inputfieldRepeater.attr('data-max'));
if(max <= 0) return;
var min = parseInt($inputfieldRepeater.attr('data-min'));
if(max <= 0 && min <= 0) return;
var $content = $inputfieldRepeater.children('.InputfieldContent');
var num = $content.children('.Inputfields').children('li:not(.InputfieldRepeaterDeletePending)').length;
var num = $content.children('.Inputfields')
.children('li:not(.InputfieldRepeaterDeletePending):not(.InputfieldRepeaterOff):visible').length;
var $addItem = $content.children('.InputfieldRepeaterAddItem');
if(num > max) {
$addItem.hide();
} else if(!$addItem.is(":visible")) {
$addItem.show();
var cloneChange = '';
var trashChange = '';
if(max > 0) {
if(num >= max) {
$addItem.hide();
cloneChange = 'hide';
} else if(!$addItem.is(":visible")) {
$addItem.show();
cloneChange = 'show';
}
}
if(min > 0) {
if(num <= min) {
trashChange = 'hide';
$content.addClass('InputfieldRepeaterTrashHidden');
} else if($content.hasClass('InputfieldRepeaterTrashHidden')) {
$content.removeClass('InputfieldRepeaterTrashHidden');
trashChange = 'show';
}
}
if(cloneChange.length || trashChange.length) {
var $items = $content.children('.Inputfields').children('.InputfieldRepeaterItem');
if(cloneChange.length) {
// update the visibility of clone actions
$items.each(function() {
var $clone = $(this).children('.InputfieldHeader').find('.InputfieldRepeaterClone');
if(cloneChange === 'show') {
$clone.removeClass('pw-icon-disabled');
} else {
$clone.addClass('pw-icon-disabled');
}
});
}
if(trashChange.length) {
// update visibility of trash actions
$items.each(function() {
var $header = $(this).children('.InputfieldHeader');
var $trash = $header.find('.InputfieldRepeaterTrash');
var $toggle = $header.find('.InputfieldRepeaterToggle.fa-toggle-on');
if(trashChange === 'show') {
$trash.removeClass('pw-icon-disabled');
$toggle.removeClass('pw-icon-disabled');
} else {
$trash.addClass('pw-icon-disabled');
$toggle.addClass('pw-icon-disabled');
}
});
if(trashChange == 'hide') {
$content.children('.Inputfields').children('li.InputfieldRepeaterDeletePending').each(function() {
var $trash = $(this).children('.InputfieldHeader').find('.InputfieldRepeaterTrash');
$trash.removeClass('pw-icon-disabled');
});
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,9 @@
* https://processwire.com
*
* @property int $repeaterMaxItems
* @property int $repeaterMinItems
* @property int $repeaterDepth
* @property bool $accordionMode
*
* @method string renderRepeaterLabel($label, $cnt, Page $page)
*
@@ -22,7 +24,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' => 105,
'version' => 106,
'requires' => 'FieldtypeRepeater',
);
}
@@ -87,7 +89,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
parent::__construct();
// these are part of the Fieldtype's config, and automatically set from it
$this->set('repeaterMaxItems', 0);
$this->set('repeaterMinItems', 0);
$this->set('repeaterDepth', 0);
$this->set('accordionMode', false);
}
/**
@@ -278,9 +282,22 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$openIDs = array();
}
$minItems = $this->repeaterMinItems;
// if there are a minimum required number of items, set them up now
if(!$itemID && $minItems > 0) {
$notIDs = $value->explode('id');
while($value->count() < $this->repeaterMinItems) {
$item = $this->getNextReadyPage($notIDs);
$value->add($item);
$notIDs[] = $item->id;
}
}
$repeaterCollapse = (int) $this->field->get('repeaterCollapse');
$cnt = 0;
$numVisible = 0;
$numOpen = 0;
// create field for each repeater iteration
foreach($value as $key => $page) {
@@ -293,6 +310,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$isReadyItem = $isHidden && $isUnpublished;
$isClone = $page->get('_repeater_clone');
$isOpen = in_array($page->id, $openIDs) || $isClone;
$isMinItem = $isReadyItem && $minItems && $cnt < $minItems;
if($isOpen && $numOpen > 0 && $this->accordionMode) $isOpen = false;
// get the inputfields for the repeater page
if(is_null($loadInputsForIDs) || in_array($page->id, $loadInputsForIDs) || $isOpen) {
@@ -354,8 +374,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($isOpen) {
$wrap->collapsed = Inputfield::collapsedNo;
$numOpen++;
} else if($repeaterCollapse == FieldtypeRepeater::collapseExisting && !$page->get('_repeater_new') && !$isHidden) {
$wrap->collapsed = Inputfield::collapsedYes;
} else if($repeaterCollapse == FieldtypeRepeater::collapseExisting && $isMinItem) {
$wrap->collapsed = Inputfield::collapsedYes;
} else if($repeaterCollapse == FieldtypeRepeater::collapseAll) {
$wrap->collapsed = Inputfield::collapsedYes;
}
@@ -393,6 +416,12 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($depth) $wrap->prepend($depth);
$wrap->prepend($loaded);
if($isMinItem) {
// allow this ready item to be added so that minimum is met
$wrap->addClass('InputfieldRepeaterMinItem');
$isReadyItem = false;
}
if(!$isReadyItem) {
$form->add($wrap);
$numVisible++;
@@ -532,11 +561,18 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($this->repeaterMaxItems > 0) {
$this->addClass('InputfieldRepeaterMax', 'wrapClass');
}
if($this->repeaterMinItems > 0) {
$this->addClass('InputfieldRepeaterMin', 'wrapClass');
}
if($this->repeaterDepth > 0) {
$this->addClass('InputfieldRepeaterDepth', 'wrapClass');
}
if($this->accordionMode) {
$this->addClass('InputfieldRepeaterAccordion', 'wrapClass');
}
$this->wrapAttr('data-page', $this->page->id);
$this->wrapAttr('data-max', (int) $this->repeaterMaxItems);
$this->wrapAttr('data-min', (int) $this->repeaterMinItems);
$this->wrapAttr('data-depth', (int) $this->repeaterDepth);
list($editorUrl, $queryString) = explode('?', $this->page->editUrl());
@@ -550,7 +586,8 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
'toggle' => $this->_x('Click to turn item on/off, or double-click to open/collapse all items', 'repeater-item-action'),
'clone' => $this->_x('Clone this item?', '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'),
'disabledMinMax' => $this->_('This action is disabled per min and/or max item settings.')
)
));

View File

@@ -12,43 +12,54 @@
overflow: hidden;
position: relative;
.InputfieldRepeaterItemLabel {
display: inline-block;
padding-left: 0.25em;
}
.InputfieldRepeaterItemLabel,
.InputfieldRepeaterItemControls {
padding-right: 0.5em;
padding-left: 0.5em;
margin-top: 0.5em;
position: absolute;
top: 0;
right: 0;
z-index: 1;
display: block;
white-space: nowrap;
height: 100%;
.InputfieldRepeaterClone,
.InputfieldRepeaterToggle,
.InputfieldRepeaterTrash,
.toggle-icon {
cursor: pointer;
float: right;
}
.InputfieldRepeaterTrash {
padding-right: 3px;
}
.InputfieldRepeaterToggle {
margin-right: 1em;
}
.InputfieldRepeaterClone {
margin-right: 1em;
}
display: none;
}
.toggle-icon {
line-height: 1em;
margin-right: 0.5em;
&.InputfieldRepeaterHeaderInit {
.InputfieldRepeaterItemLabel {
display: inline-block;
padding-left: 0.25em;
}
.InputfieldRepeaterItemControls {
display: block;
padding-right: 0.5em;
padding-left: 0.5em;
margin-top: 0.5em;
position: absolute;
top: 0;
right: 0;
z-index: 1;
display: block;
white-space: nowrap;
height: 100%;
.InputfieldRepeaterClone,
.InputfieldRepeaterToggle,
.InputfieldRepeaterTrash,
.toggle-icon {
cursor: pointer;
float: right;
}
.InputfieldRepeaterTrash {
padding-right: 3px;
}
.InputfieldRepeaterToggle {
margin-right: 1em;
}
.InputfieldRepeaterClone {
margin-right: 1em;
}
.pw-icon-disabled {
opacity: 0.3;
}
}
.toggle-icon {
line-height: 1em;
margin-right: 0.5em;
}
}
}

View File

@@ -186,11 +186,19 @@ class FieldtypeRepeaterConfigHelper extends Wire {
$f->label = $this->_('Remember which repeater items are open?');
$f->description = $this->_('When checked, opened repeater items remain open after saving or reloading from the page editor (unless the user closes them).');
$f->icon = 'lightbulb-o';
if((int) $field->get('rememberOpen')) {
$f->attr('checked', 'checked');
} else {
$f->collapsed = Inputfield::collapsedYes;
}
if((int) $field->get('rememberOpen')) $f->attr('checked', 'checked');
$f->columnWidth = 50;
$inputfields->add($f);
// -------------------------------------------------
$f = $this->wire('modules')->get('InputfieldCheckbox');
$f->attr('name', 'accordionMode');
$f->label = $this->_('Use accordion mode?');
$f->description = $this->_('When checked, only one repeater item will be open at a time.');
$f->icon = 'map-o';
if((int) $field->get('accordionMode')) $f->attr('checked', 'checked');
$f->columnWidth = 50;
$inputfields->add($f);
// -------------------------------------------------
@@ -200,9 +208,21 @@ class FieldtypeRepeaterConfigHelper extends Wire {
$f->attr('name', 'repeaterMaxItems');
$f->attr('value', $value > 0 ? $value : '');
$f->label = $this->_('Maximum number of items');
$f->collapsed = Inputfield::collapsedBlank;
$f->description = $this->_('If you need to limit the number of items allowed, enter the limit here (0=no limit).');
$f->icon = 'hand-stop-o';
$f->columnWidth = 50;
$inputfields->add($f);
// -------------------------------------------------
$value = (int) $field->get('repeaterMinItems');
$f = $this->wire('modules')->get('InputfieldInteger');
$f->attr('name', 'repeaterMinItems');
$f->attr('value', $value > 0 ? $value : '');
$f->label = $this->_('Minimum number of items');
$f->description = $this->_('This many items will always be open and ready-to-edit (0=no minimum).');
$f->icon = 'hand-peace-o';
$f->columnWidth = 50;
$inputfields->add($f);
// -------------------------------------------------

View File

@@ -1035,7 +1035,7 @@ function InputfieldStates($target) {
if($newTab.hasClass('collapsed10')) InputfieldStateAjaxClick($newTab);
});
$(document).on('click', '.InputfieldStateToggle, .toggle-icon', function() {
$(document).on('click', '.InputfieldStateToggle, .toggle-icon', function(event, data) {
var $t = $(this);
var $li = $t.closest('.Inputfield');
@@ -1043,8 +1043,14 @@ function InputfieldStates($target) {
var $icon = isIcon ? $t : $li.children('.InputfieldHeader, .ui-widget-header').find('.toggle-icon');
var isCollapsed = $li.hasClass("InputfieldStateCollapsed");
var wasCollapsed = $li.hasClass("InputfieldStateWasCollapsed");
var duration = 100;
if($li.hasClass('InputfieldAjaxLoading')) return false;
if(typeof data != "undefined") {
if(typeof data.duration != "undefined") duration = data.duration;
}
if(isCollapsed && ($li.hasClass('collapsed10') || $li.hasClass('collapsed11'))) {
if(InputfieldStateAjaxClick($li)) return false;
}
@@ -1052,7 +1058,7 @@ function InputfieldStates($target) {
if(isCollapsed || wasCollapsed || isIcon) {
$li.addClass('InputfieldStateWasCollapsed'); // this class only used here
$li.trigger(isCollapsed ? 'openReady' : 'closeReady');
$li.toggleClass('InputfieldStateCollapsed', 100, function() {
$li.toggleClass('InputfieldStateCollapsed', duration, function() {
if(isCollapsed) {
$li.trigger('opened');
if($li.hasClass('InputfieldColumnWidth')) $li.children('.InputfieldContent').show();

File diff suppressed because one or more lines are too long