1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-10 16:54:44 +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( return array(
'title' => __('Repeater', __FILE__), // Module Title 'title' => __('Repeater', __FILE__), // Module Title
'summary' => __('Maintains a collection of fields that are repeated for any number of times.', __FILE__), // Module Summary 'summary' => __('Maintains a collection of fields that are repeated for any number of times.', __FILE__), // Module Summary
'version' => 105, 'version' => 106,
'autoload' => true, 'autoload' => true,
'installs' => 'InputfieldRepeater' 'installs' => 'InputfieldRepeater'
); );
@@ -496,7 +496,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule {
$inputfield = $this->wire('modules')->get($this->getInputfieldClass()); $inputfield = $this->wire('modules')->get($this->getInputfieldClass());
$inputfield->set('page', $page); $inputfield->set('page', $page);
$inputfield->set('field', $field); $inputfield->set('field', $field);
$inputfield->set('repeaterMaxItems', (int) $field->get('repeaterMaxItems')); $inputfield->set('repeaterMaxItems', (int) $field->get('repeaterMaxItems'));
$inputfield->set('repeaterMinItems', (int) $field->get('repeaterMinItems'));
$inputfield->set('repeaterDepth', (int) $field->get('repeaterDepth')); $inputfield->set('repeaterDepth', (int) $field->get('repeaterDepth'));
$inputfield->set('repeaterReadyItems', 0); // ready items deprecated $inputfield->set('repeaterReadyItems', 0); // ready items deprecated

View File

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

View File

@@ -26,6 +26,13 @@ function InputfieldRepeater($) {
* *
*/ */
var isReno = $('body').hasClass('AdminThemeReno'); var isReno = $('body').hasClass('AdminThemeReno');
/**
* Event timer for double clicks
*
*/
var doubleClickTimer = null;
/*** EVENTS ********************************************************************************************/ /*** EVENTS ********************************************************************************************/
@@ -55,14 +62,17 @@ function InputfieldRepeater($) {
* *
*/ */
var eventDeleteClick = function(e) { var eventDeleteClick = function(e) {
var $header = $(this).closest('.InputfieldHeader'); var $this = $(this);
var $header = $this.closest('.InputfieldHeader');
var $item = $header.parent(); var $item = $header.parent();
if(isActionDisabled($this)) return false;
if($item.hasClass('InputfieldRepeaterNewItem')) { if($item.hasClass('InputfieldRepeaterNewItem')) {
// delete new item (noAjaxAdd mode) // delete new item (noAjaxAdd mode)
var $numAddInput = $item.children('.InputfieldContent').children('.InputfieldRepeaterAddItem').children('input'); 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(); $item.remove();
} else { } else {
@@ -87,20 +97,22 @@ function InputfieldRepeater($) {
$header.find('.InputfieldRepeaterItemControls').css('background-color', $header.css('background-color')); $header.find('.InputfieldRepeaterItemControls').css('background-color', $header.css('background-color'));
} }
checkMax($item.closest('.InputfieldRepeater')); checkMinMax($item.closest('.InputfieldRepeater'));
e.stopPropagation(); e.stopPropagation();
}; };
/** /**
* Event handler for when the "delete" link is double clicked * 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 $li = $(this).closest('li');
var undelete = $li.hasClass('InputfieldRepeaterDeletePending'); var undelete = $li.hasClass('InputfieldRepeaterDeletePending');
if(isActionDisabled($this)) return false;
function selectAll() { function selectAll() {
$li.parent().children('li').each(function() { $li.parent().children('li').each(function() {
var $item = $(this); var $item = $(this);
@@ -123,12 +135,13 @@ function InputfieldRepeater($) {
/** /**
* Event handler for when the "clone" repeater item action is clicked * Event handler for when the "clone" repeater item action is clicked
* *
* @param e
* @returns {boolean} * @returns {boolean}
* *
*/ */
var eventCloneClick = function(e) { var eventCloneClick = function() {
var $item = $(this).closest('.InputfieldRepeaterItem'); var $this = $(this);
if(isActionDisabled($this)) return false;
var $item = $this.closest('.InputfieldRepeaterItem');
ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.clone, function() { ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.clone, function() {
var itemID = $item.attr('data-page'); var itemID = $item.attr('data-page');
var $addLink = $item.closest('.InputfieldRepeater').children('.InputfieldContent') var $addLink = $item.closest('.InputfieldRepeater').children('.InputfieldContent')
@@ -152,26 +165,29 @@ function InputfieldRepeater($) {
var $item = $this.closest('.InputfieldRepeaterItem'); var $item = $this.closest('.InputfieldRepeaterItem');
var $input = $item.find('.InputfieldRepeaterPublish'); var $input = $item.find('.InputfieldRepeaterPublish');
if($this.hasClass(toggleOn)) { if(doubleClickTimer) clearTimeout(doubleClickTimer);
$this.removeClass(toggleOn).addClass(toggleOff); doubleClickTimer = setTimeout(function() {
$item.addClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff'); if(isActionDisabled($this)) return false;
$input.val('-1'); if($this.hasClass(toggleOn)) {
} else { $this.removeClass(toggleOn).addClass(toggleOff);
$this.removeClass(toggleOff).addClass(toggleOn); $item.addClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$item.removeClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff'); $input.val('-1');
$input.val('1'); } else {
} $this.removeClass(toggleOff).addClass(toggleOn);
$item.removeClass('InputfieldRepeaterUnpublished InputfieldRepeaterOff');
$input.val('1');
}
checkMinMax($item.closest('.InputfieldRepeater'));
}, 250);
e.stopPropagation(); e.stopPropagation();
}; };
/** /**
* Event handler for when a repeater item is about to be opened * 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 $item = $(this);
var $loaded = $item.find(".InputfieldRepeaterLoaded"); var $loaded = $item.find(".InputfieldRepeaterLoaded");
if(parseInt($loaded.val()) > 0) return; // item already loaded 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) * 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 $item = $(this);
var $loaded = $item.find(".InputfieldRepeaterLoaded"); var $loaded = $item.find(".InputfieldRepeaterLoaded");
updateState($item); 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'); $loaded.val('1');
@@ -221,8 +239,10 @@ function InputfieldRepeater($) {
initRepeater($(this)); initRepeater($(this));
}); });
$content.slideDown('fast', function() { $content.slideDown('fast', function() {
$spinner.removeClass('fa-spin fa-spinner').addClass('fa-arrows'); $spinner.removeClass('fa-spin fa-spinner').addClass('fa-arrows');
updateAccordion($item);
}); });
setTimeout(function() { setTimeout(function() {
$inputfields.find('.Inputfield').trigger('reloaded', ['InputfieldRepeaterItemEdit']); $inputfields.find('.Inputfield').trigger('reloaded', ['InputfieldRepeaterItemEdit']);
@@ -234,10 +254,8 @@ function InputfieldRepeater($) {
/** /**
* Event handler for when a repeater item is closed * Event handler for when a repeater item is closed
* *
* @param e
*
*/ */
var eventItemClosed = function(e) { var eventItemClosed = function() {
updateState($(this)); updateState($(this));
}; };
@@ -246,11 +264,10 @@ function InputfieldRepeater($) {
* *
* Handles adding repeater items and initializing them * Handles adding repeater items and initializing them
* *
* @param e
* @returns {boolean} * @returns {boolean}
* *
*/ */
var eventAddLinkClick = function(e) { var eventAddLinkClick = function() {
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');
@@ -278,10 +295,10 @@ function InputfieldRepeater($) {
newItemTotal = $newItem.length; newItemTotal = $newItem.length;
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);
addRepeaterItem($addItem); addRepeaterItem($addItem);
$numAddInput.attr('value', newItemTotal); $numAddInput.attr('value', newItemTotal);
checkMax($inputfieldRepeater); checkMinMax($inputfieldRepeater);
} }
return false; 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) // 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) { if($unpublishedItems.length) {
ajaxURL += '&repeater_not='; ajaxURL += '&repeater_not=';
$unpublishedItems.each(function() { $unpublishedItems.each(function() {
@@ -328,8 +345,9 @@ function InputfieldRepeater($) {
scrollTop: $addItem.offset().top scrollTop: $addItem.offset().top
}, 500, 'swing'); }, 500, 'swing');
updateState($addItem); updateState($addItem);
checkMax($inputfieldRepeater); checkMinMax($inputfieldRepeater);
$nestedRepeaters = $addItem.find('.InputfieldRepeater'); updateAccordion($addItem);
var $nestedRepeaters = $addItem.find('.InputfieldRepeater');
if($nestedRepeaters.length) { if($nestedRepeaters.length) {
$nestedRepeaters.each(function() { $nestedRepeaters.each(function() {
initRepeater($(this)); initRepeater($(this));
@@ -348,9 +366,14 @@ function InputfieldRepeater($) {
* *
*/ */
var eventOpenAllClick = function(e) { var eventOpenAllClick = function(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if(doubleClickTimer) clearTimeout(doubleClickTimer);
if($(this).closest('.InputfieldRepeater').hasClass('InputfieldRepeaterAccordion')) return false;
var $repeater = $(this).closest('.InputfieldRepeater'); var $repeater = $(this).closest('.InputfieldRepeater');
var $items = $repeater.children('.InputfieldContent').children('.Inputfields').children('.InputfieldRepeaterItem'); var $items = $repeater.children('.InputfieldContent').children('.Inputfields').children('.InputfieldRepeaterItem');
if(!$items.length) return false; if(!$items.length) return false;
@@ -372,6 +395,51 @@ function InputfieldRepeater($) {
}; };
/*** GENERAL FUNCTIONS **********************************************************************************/ /*** 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 * Given an InputfieldRepeaterItem update the label consistent with any present formatting sting
@@ -379,28 +447,32 @@ function InputfieldRepeater($) {
* Primarily adjusts item count(s) and allowed for {secondary} text appearance * Primarily adjusts item count(s) and allowed for {secondary} text appearance
* *
* @param $item An .InputfieldRepeaterItem * @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) { 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 = $label.html();
var _labelHTML = labelHTML; var _labelHTML = labelHTML;
if(doIncrement && labelHTML.indexOf('#') > -1) { if(typeof labelHTML != "undefined") {
num = $item.siblings('.InputfieldRepeaterItem:visible').length + 1; if(doIncrement && labelHTML.indexOf('#') > -1) {
labelHTML = labelHTML.replace(/#[0-9]+/, '#' + num); var num = $item.siblings('.InputfieldRepeaterItem:visible').length + 1;
} labelHTML = labelHTML.replace(/#[0-9]+/, '#' + num);
}
if(labelHTML.indexOf('{') > -1) { while(labelHTML.indexOf('}') > -1) {
// parts of the label wrapped in {brackets} get different appearance // 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 class="ui-priority-secondary" style="font-weight:normal">');
labelHTML = labelHTML.replace(/}/, '</span>'); labelHTML = labelHTML.replace(/}/, '</span>');
} }
if(labelHTML != _labelHTML) { if(labelHTML != _labelHTML) {
$label.html(labelHTML); $label.html(labelHTML);
}
} }
} }
@@ -557,6 +629,56 @@ function InputfieldRepeater($) {
$inputfields.sortable(sortableOptions); $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 * Initialize a repeater
* *
@@ -578,58 +700,17 @@ function InputfieldRepeater($) {
} }
if($inputfields.hasClass('InputfieldRepeaterInit')) return; if($inputfields.hasClass('InputfieldRepeaterInit')) return;
var renderValueMode = $inputfields.closest('.InputfieldRenderValueMode').length > 0;
$inputfields.addClass('InputfieldRepeaterInit'); $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);
}
$("input.InputfieldRepeaterDelete", $this).parents('.InputfieldCheckbox').hide(); $("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) { if(isItem) {
initHeaders($this.children('.InputfieldHeader')); initHeaders($this.children('.InputfieldHeader'), $inputfieldRepeater, renderValueMode);
} else { } else {
initHeaders($(".InputfieldRepeaterItem > .InputfieldHeader", $this)); initHeaders($(".InputfieldRepeaterItem > .InputfieldHeader", $this), $inputfieldRepeater, renderValueMode);
} }
if(renderValueMode) { if(renderValueMode) {
@@ -662,28 +743,87 @@ function InputfieldRepeater($) {
// check for maximum items // check for maximum items
if($inputfieldRepeater.hasClass('InputfieldRepeaterMax')) { if($inputfieldRepeater.hasClass('InputfieldRepeaterMax')) {
checkMax($inputfieldRepeater); checkMinMax($inputfieldRepeater);
} }
} }
/** /**
* When "max items" setting is used, this toggles whether or not "add" links are visible * 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 * @param $inputfieldRepeater .InputfieldRepeater
* *
*/ */
function checkMax($inputfieldRepeater) { function checkMinMax($inputfieldRepeater) {
if(!$inputfieldRepeater.hasClass('InputfieldRepeaterMax')) return;
if(!$inputfieldRepeater.hasClass('InputfieldRepeaterMax')
&& !$inputfieldRepeater.hasClass('InputfieldRepeaterMin')) return;
var max = parseInt($inputfieldRepeater.attr('data-max')); 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 $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'); var $addItem = $content.children('.InputfieldRepeaterAddItem');
if(num > max) { var cloneChange = '';
$addItem.hide(); var trashChange = '';
} else if(!$addItem.is(":visible")) {
$addItem.show(); 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 * https://processwire.com
* *
* @property int $repeaterMaxItems * @property int $repeaterMaxItems
* @property int $repeaterMinItems
* @property int $repeaterDepth * @property int $repeaterDepth
* @property bool $accordionMode
* *
* @method string renderRepeaterLabel($label, $cnt, Page $page) * @method string renderRepeaterLabel($label, $cnt, Page $page)
* *
@@ -22,7 +24,7 @@ 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' => 105, 'version' => 106,
'requires' => 'FieldtypeRepeater', 'requires' => 'FieldtypeRepeater',
); );
} }
@@ -86,8 +88,10 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
// these are part of the Fieldtype's config, and automatically set from it // these are part of the Fieldtype's config, and automatically set from it
$this->set('repeaterMaxItems', 0); $this->set('repeaterMaxItems', 0);
$this->set('repeaterMinItems', 0);
$this->set('repeaterDepth', 0); $this->set('repeaterDepth', 0);
$this->set('accordionMode', false);
} }
/** /**
@@ -278,9 +282,22 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$openIDs = array(); $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'); $repeaterCollapse = (int) $this->field->get('repeaterCollapse');
$cnt = 0; $cnt = 0;
$numVisible = 0; $numVisible = 0;
$numOpen = 0;
// create field for each repeater iteration // create field for each repeater iteration
foreach($value as $key => $page) { foreach($value as $key => $page) {
@@ -293,6 +310,9 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
$isReadyItem = $isHidden && $isUnpublished; $isReadyItem = $isHidden && $isUnpublished;
$isClone = $page->get('_repeater_clone'); $isClone = $page->get('_repeater_clone');
$isOpen = in_array($page->id, $openIDs) || $isClone; $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 // get the inputfields for the repeater page
if(is_null($loadInputsForIDs) || in_array($page->id, $loadInputsForIDs) || $isOpen) { if(is_null($loadInputsForIDs) || in_array($page->id, $loadInputsForIDs) || $isOpen) {
@@ -354,8 +374,11 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($isOpen) { if($isOpen) {
$wrap->collapsed = Inputfield::collapsedNo; $wrap->collapsed = Inputfield::collapsedNo;
$numOpen++;
} else if($repeaterCollapse == FieldtypeRepeater::collapseExisting && !$page->get('_repeater_new') && !$isHidden) { } else if($repeaterCollapse == FieldtypeRepeater::collapseExisting && !$page->get('_repeater_new') && !$isHidden) {
$wrap->collapsed = Inputfield::collapsedYes; $wrap->collapsed = Inputfield::collapsedYes;
} else if($repeaterCollapse == FieldtypeRepeater::collapseExisting && $isMinItem) {
$wrap->collapsed = Inputfield::collapsedYes;
} else if($repeaterCollapse == FieldtypeRepeater::collapseAll) { } else if($repeaterCollapse == FieldtypeRepeater::collapseAll) {
$wrap->collapsed = Inputfield::collapsedYes; $wrap->collapsed = Inputfield::collapsedYes;
} }
@@ -393,6 +416,12 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($depth) $wrap->prepend($depth); if($depth) $wrap->prepend($depth);
$wrap->prepend($loaded); $wrap->prepend($loaded);
if($isMinItem) {
// allow this ready item to be added so that minimum is met
$wrap->addClass('InputfieldRepeaterMinItem');
$isReadyItem = false;
}
if(!$isReadyItem) { if(!$isReadyItem) {
$form->add($wrap); $form->add($wrap);
$numVisible++; $numVisible++;
@@ -532,11 +561,18 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList {
if($this->repeaterMaxItems > 0) { if($this->repeaterMaxItems > 0) {
$this->addClass('InputfieldRepeaterMax', 'wrapClass'); $this->addClass('InputfieldRepeaterMax', 'wrapClass');
} }
if($this->repeaterMinItems > 0) {
$this->addClass('InputfieldRepeaterMin', 'wrapClass');
}
if($this->repeaterDepth > 0) { if($this->repeaterDepth > 0) {
$this->addClass('InputfieldRepeaterDepth', 'wrapClass'); $this->addClass('InputfieldRepeaterDepth', 'wrapClass');
} }
if($this->accordionMode) {
$this->addClass('InputfieldRepeaterAccordion', 'wrapClass');
}
$this->wrapAttr('data-page', $this->page->id); $this->wrapAttr('data-page', $this->page->id);
$this->wrapAttr('data-max', (int) $this->repeaterMaxItems); $this->wrapAttr('data-max', (int) $this->repeaterMaxItems);
$this->wrapAttr('data-min', (int) $this->repeaterMinItems);
$this->wrapAttr('data-depth', (int) $this->repeaterDepth); $this->wrapAttr('data-depth', (int) $this->repeaterDepth);
list($editorUrl, $queryString) = explode('?', $this->page->editUrl()); 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'), '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'), 'clone' => $this->_x('Clone this item?', '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'),
'disabledMinMax' => $this->_('This action is disabled per min and/or max item settings.')
) )
)); ));

View File

@@ -12,45 +12,56 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
.InputfieldRepeaterItemLabel { .InputfieldRepeaterItemLabel,
display: inline-block;
padding-left: 0.25em;
}
.InputfieldRepeaterItemControls { .InputfieldRepeaterItemControls {
padding-right: 0.5em; display: none;
padding-left: 0.5em; }
margin-top: 0.5em;
position: absolute; &.InputfieldRepeaterHeaderInit {
top: 0; .InputfieldRepeaterItemLabel {
right: 0; display: inline-block;
z-index: 1; padding-left: 0.25em;
display: block; }
white-space: nowrap; .InputfieldRepeaterItemControls {
height: 100%; display: block;
padding-right: 0.5em;
.InputfieldRepeaterClone, padding-left: 0.5em;
.InputfieldRepeaterToggle, margin-top: 0.5em;
.InputfieldRepeaterTrash, 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 { .toggle-icon {
cursor: pointer; line-height: 1em;
float: right; margin-right: 0.5em;
}
.InputfieldRepeaterTrash {
padding-right: 3px;
}
.InputfieldRepeaterToggle {
margin-right: 1em;
}
.InputfieldRepeaterClone {
margin-right: 1em;
} }
} }
.toggle-icon {
line-height: 1em;
margin-right: 0.5em;
}
} }
.InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending) { .InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending) {

View File

@@ -186,13 +186,21 @@ class FieldtypeRepeaterConfigHelper extends Wire {
$f->label = $this->_('Remember which repeater items are open?'); $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->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'; $f->icon = 'lightbulb-o';
if((int) $field->get('rememberOpen')) { if((int) $field->get('rememberOpen')) $f->attr('checked', 'checked');
$f->attr('checked', 'checked'); $f->columnWidth = 50;
} else {
$f->collapsed = Inputfield::collapsedYes;
}
$inputfields->add($f); $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);
// ------------------------------------------------- // -------------------------------------------------
$value = (int) $field->get('repeaterMaxItems'); $value = (int) $field->get('repeaterMaxItems');
@@ -200,9 +208,21 @@ class FieldtypeRepeaterConfigHelper extends Wire {
$f->attr('name', 'repeaterMaxItems'); $f->attr('name', 'repeaterMaxItems');
$f->attr('value', $value > 0 ? $value : ''); $f->attr('value', $value > 0 ? $value : '');
$f->label = $this->_('Maximum number of items'); $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->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->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); $inputfields->add($f);
// ------------------------------------------------- // -------------------------------------------------

View File

@@ -1035,7 +1035,7 @@ function InputfieldStates($target) {
if($newTab.hasClass('collapsed10')) InputfieldStateAjaxClick($newTab); 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 $t = $(this);
var $li = $t.closest('.Inputfield'); var $li = $t.closest('.Inputfield');
@@ -1043,7 +1043,13 @@ function InputfieldStates($target) {
var $icon = isIcon ? $t : $li.children('.InputfieldHeader, .ui-widget-header').find('.toggle-icon'); var $icon = isIcon ? $t : $li.children('.InputfieldHeader, .ui-widget-header').find('.toggle-icon');
var isCollapsed = $li.hasClass("InputfieldStateCollapsed"); var isCollapsed = $li.hasClass("InputfieldStateCollapsed");
var wasCollapsed = $li.hasClass("InputfieldStateWasCollapsed"); var wasCollapsed = $li.hasClass("InputfieldStateWasCollapsed");
var duration = 100;
if($li.hasClass('InputfieldAjaxLoading')) return false; 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(isCollapsed && ($li.hasClass('collapsed10') || $li.hasClass('collapsed11'))) {
if(InputfieldStateAjaxClick($li)) return false; if(InputfieldStateAjaxClick($li)) return false;
@@ -1052,7 +1058,7 @@ function InputfieldStates($target) {
if(isCollapsed || wasCollapsed || isIcon) { if(isCollapsed || wasCollapsed || isIcon) {
$li.addClass('InputfieldStateWasCollapsed'); // this class only used here $li.addClass('InputfieldStateWasCollapsed'); // this class only used here
$li.trigger(isCollapsed ? 'openReady' : 'closeReady'); $li.trigger(isCollapsed ? 'openReady' : 'closeReady');
$li.toggleClass('InputfieldStateCollapsed', 100, function() { $li.toggleClass('InputfieldStateCollapsed', duration, function() {
if(isCollapsed) { if(isCollapsed) {
$li.trigger('opened'); $li.trigger('opened');
if($li.hasClass('InputfieldColumnWidth')) $li.children('.InputfieldContent').show(); if($li.hasClass('InputfieldColumnWidth')) $li.children('.InputfieldContent').show();

File diff suppressed because one or more lines are too long