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:
@@ -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
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
@@ -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.')
|
||||
)
|
||||
));
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
// -------------------------------------------------
|
||||
|
@@ -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
Reference in New Issue
Block a user