diff --git a/wire/core/AdminTheme.php b/wire/core/AdminTheme.php index 7e6a9fa4..d1f4ec69 100644 --- a/wire/core/AdminTheme.php +++ b/wire/core/AdminTheme.php @@ -62,6 +62,22 @@ abstract class AdminTheme extends WireData implements Module { */ protected $bodyClasses = array(); + /** + * Extra markup regions + * + * @var array + * + */ + protected $extraMarkup = array( + 'head' => '', + 'notices' => '', + 'body' => '', + 'masthead' => '', + 'content' => '', + 'footer' => '', + 'sidebar' => '', // sidebar not used in all admin themes + ); + /** * URLs to place in link prerender tags * @@ -161,15 +177,7 @@ abstract class AdminTheme extends WireData implements Module { * */ public function ___getExtraMarkup() { - $parts = array( - 'head' => '', - 'notices' => '', - 'body' => '', - 'masthead' => '', - 'content' => '', - 'footer' => '', - 'sidebar' => '', // sidebar not used in all admin themes - ); + $parts = $this->extraMarkup; $isLoggedin = $this->wire('user')->isLoggedin(); if($isLoggedin && $this->wire('modules')->isInstalled('InputfieldCKEditor') && $this->wire('process') instanceof WirePageEditor) { @@ -186,11 +194,38 @@ abstract class AdminTheme extends WireData implements Module { } return $parts; } - + + /** + * Add extra markup to a region in the admin theme + * + * @param string $name + * @param string $value + * + */ + public function addExtraMarkup($name, $value) { + if(!empty($this->extraMarkup[$name])) { + $this->extraMarkup[$name] .= "\n$value"; + } else { + $this->extraMarkup[$name] = $value; + } + } + + /** + * Add a
class to the admin theme + * + * @param string $className + * + */ public function addBodyClass($className) { $this->bodyClasses[$className] = $className; } - + + /** + * Get the body[class] attribute string + * + * @return string + * + */ public function getBodyClass() { return trim(implode(' ', $this->bodyClasses)); } @@ -244,6 +279,8 @@ abstract class AdminTheme extends WireData implements Module { /** * Set a pre-render URL or get currently pre-render URL(s) * + * #pw-internal + * * @param string $url * @return array * diff --git a/wire/core/LanguageFunctions.php b/wire/core/LanguageFunctions.php index a13d991a..5881dac7 100644 --- a/wire/core/LanguageFunctions.php +++ b/wire/core/LanguageFunctions.php @@ -36,6 +36,9 @@ function __($text, $textdomain = null, $context = '') { $textdomain = $traces[1]['file']; } if(is_null($textdomain)) $textdomain = 'site'; + } else if($textdomain === 'common') { + // common translation + $textdomain = 'wire/modules/LanguageSupport/LanguageTranslator.php'; } $value = htmlspecialchars($language->translator()->getTranslation($textdomain, $text, $context), ENT_QUOTES, 'UTF-8'); if($value === "=") { diff --git a/wire/core/Modules.php b/wire/core/Modules.php index d2504aa5..307afcaf 100644 --- a/wire/core/Modules.php +++ b/wire/core/Modules.php @@ -34,7 +34,7 @@ * @method bool|int delete($class) * @method bool uninstall($class) * @method bool saveModuleConfigData($className, array $configData) Alias of saveConfig() method #pw-internal - * @method bool saveConfig($class, array $data) + * @method bool saveConfig($class, $data, $value = null) * @method InputfieldWrapper|null getModuleConfigInputfields($moduleName, InputfieldWrapper $form = null) #pw-internal * @method void moduleVersionChanged(Module $module, $fromVersion, $toVersion) #pw-internal * diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index 2f021bd0..53380e09 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -1062,23 +1062,27 @@ class PagesEditor extends Wire { if(is_string($options)) $options = Selectors::keyValueStringToArray($options); if(!isset($options['recursionLevel'])) $options['recursionLevel'] = 0; // recursion level - // if parent is not changing, we have to modify name now - if(is_null($parent)) { - $parent = $page->parent; - $n = 1; - $name = $page->name . '-' . $n; + if(isset($options['set']) && isset($options['set']['name'])) { + $name = $options['set']['name']; + } else { - $name = $page->name; - $n = 0; - } + // if parent is not changing, we have to modify name now + if(is_null($parent) || $parent->id == $page->parent->id) { + $parent = $page->parent; + $n = 1; + $name = $page->name . '-' . $n; + } else { + $name = $page->name; + $n = 0; + } - // make sure that we have a unique name - - while(count($parent->children("name=$name, include=all"))) { - $name = $page->name; - $nStr = "-" . (++$n); - if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) $name = substr($name, 0, Pages::nameMaxLength - strlen($nStr)); - $name .= $nStr; + // make sure that we have a unique name + while(count($parent->children("name=$name, include=all"))) { + $name = $page->name; + $nStr = "-" . (++$n); + if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) $name = substr($name, 0, Pages::nameMaxLength - strlen($nStr)); + $name .= $nStr; + } } // Ensure all data is loaded for the page @@ -1144,7 +1148,7 @@ class PagesEditor extends Wire { $page->filesManager->copyFiles($copy->filesManager->path()); } - // if there are children, then recurisvely clone them too + // if there are children, then recursively clone them too if($page->numChildren && $recursive) { $start = 0; $limit = 200; diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module index cb568159..ef4badcb 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module @@ -468,7 +468,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * @return string * */ - protected function getUniqueRepeaterPageName() { + public function getUniqueRepeaterPageName() { static $cnt = 0; return str_replace('.', '-', microtime(true)) . '-' . (++$cnt); } @@ -496,7 +496,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $inputfield = $this->wire('modules')->get($this->getInputfieldClass()); $inputfield->set('page', $page); $inputfield->set('field', $field); - $inputfield->set('repeaterMaxItems', $field->get('repeaterMaxItems')); + $inputfield->set('repeaterMaxItems', (int) $field->get('repeaterMaxItems')); + $inputfield->set('repeaterDepth', (int) $field->get('repeaterDepth')); $inputfield->set('repeaterReadyItems', 0); // ready items deprecated $pageArray = $page->getUnformatted($field->name); @@ -533,6 +534,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($item->hasStatus(Page::statusUnpublished) && $item->hasStatus(Page::statusHidden) && $item->id + && substr($item->name, -1) !== 'c' // cloned item && !in_array($item->id, $notIDs)) { // existing/unused ready item that we will reuse $readyPage = $item; @@ -638,7 +640,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // load the repeater pages $a = $this->wire('pages')->find($selector); - $class = $this->getPageArrayClass(true); + $class = $this->getPageArrayClass(); $pageArray = $this->wire(new $class($page, $field)); $pageArray->import($a); $pageArray->resetTrackChanges(true); @@ -667,6 +669,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // if $value isn't a PageArray, then abort if(!$value instanceof PageArray) return array(); + + /** @var PageArray $value */ $count = 0; $ids = array(); @@ -957,8 +961,10 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * */ public function ___formatValue(Page $page, Field $field, $value) { + + $maxItems = (int) $field->get('repeaterMaxItems'); - if(!$value instanceof PageArray) return $this->getBlankValue($page, $field); + if(!$value instanceof PageArray) $value = $this->getBlankValue($page, $field); /* TBA if($field->repeaterMaxItems == 1) { @@ -969,29 +975,19 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // used as a clone if a formatted version of $value is different from non-formatted $formatted = null; + $cnt = 0; // remove unpublished and ready items that shouldn't be here foreach($value as $p) { - - /* - if($p->status < Page::statusHidden) continue; - $remove = false; - if($p->isHidden()) { - // hidden pages (assumed to be a ready page) should never be included when page is being viewed (outputFormatting) - $remove = true; - } else if($p->isUnpublished() && !$page->isUnpublished()) { - // unpublished items may only be included if the page is also unpublished (and presumably being previewed) - $remove = true; - } - if($remove) { - */ - - if($p->isHidden() || $p->isUnpublished()) { + $cnt++; + if($p->isHidden() || $p->isUnpublished() || ($maxItems && $cnt > $maxItems)) { if(is_null($formatted)) $formatted = clone $value; + /** @var Page $formatted */ $trackChanges = $formatted->trackChanges(); $formatted->setTrackChanges(false); $formatted->remove($p); $formatted->setTrackChanges($trackChanges); + $cnt--; } } diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.css b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.css index 79760e81..7edcc08e 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.css +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.css @@ -6,13 +6,15 @@ .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader .toggle-icon { line-height: 1em; margin-right: 0.5em; } - .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterToggle, .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterTrash { + .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterClone, .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterToggle, .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterTrash { cursor: pointer; float: right; } .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterTrash { padding-right: 3px; } .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterToggle { margin-right: 1em; } + .Inputfields .InputfieldRepeater .InputfieldRepeaterItem > .InputfieldHeader > .InputfieldRepeaterClone { + margin-right: 1em; } .Inputfields .InputfieldRepeater .InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending).InputfieldStateCollapsed > .InputfieldHeader { opacity: 0.90; } .Inputfields .InputfieldRepeater .InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending).InputfieldStateCollapsed > .InputfieldHeader:hover { diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.js b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.js index bec93919..66819de9 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.js +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.js @@ -8,7 +8,14 @@ * */ +var InputfieldRepeaterDepthSize = 50; + +/** + * Delete click event (single item) + * + */ function InputfieldRepeaterDeleteClick(e) { + var $parent = $(this).parent('label').parent('li'); if($parent.is('.InputfieldRepeaterNewItem')) { @@ -33,9 +40,52 @@ function InputfieldRepeaterDeleteClick(e) { $parent.addClass('InputfieldRepeaterDeletePending'); } } + + InputfieldRepeaterCheckMax($parent.closest('.InputfieldRepeater')); + e.stopPropagation(); } +/** + * Delete double-click event (multi-item) + * + */ +function InputfieldRepeaterDeleteDblClick(e) { + + var $li = $(this).closest('li'); + var undelete = $li.hasClass('InputfieldRepeaterDeletePending'); + + function selectAll() { + $li.parent().children('li').each(function() { + var $item = $(this); + var $trashLink = $item.children('.InputfieldHeader').children('.InputfieldRepeaterTrash'); + if($item.hasClass('InputfieldRepeaterDeletePending')) { + if(undelete) $trashLink.click(); + } else { + if(!undelete) $trashLink.click(); + } + }); + } + + if(undelete) { + selectAll(); + } else { + ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.removeAll, selectAll); + } +} + +function InputfieldRepeaterCloneClick(e) { + 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') + .children('.InputfieldRepeaterAddItem').find('.InputfieldRepeaterAddLink:eq(0)'); + $addLink.attr('data-clone', itemID).click(); + $('html, body').animate({ scrollTop: $addLink.offset().top - 100}, 250, 'swing'); + }); + return false; +} + /** * Event handler for the "publish" toggle in the header of each repeater item * @@ -74,19 +124,46 @@ function InputfieldRepeaterItemOpenReady(e) { * */ function InputfieldRepeaterUpdateState($item) { - - if(!$item.closest('.InputfieldRepeaterRememberOpen').length) return; - - var val = ''; - - $(".InputfieldRepeaterItem:not(.InputfieldStateCollapsed)").each(function() { - var id = parseInt($(this).attr('data-page')); - if(id > 0) { - val += id + '|'; + if($item.closest('.InputfieldRepeaterRememberOpen').length) { + var val = ''; + $(".InputfieldRepeaterItem:not(.InputfieldStateCollapsed)").each(function() { + var id = parseInt($(this).attr('data-page')); + if(id > 0) { + val += id + '|'; + } + }); + $.cookie('repeaters_open', val); + } +} + +function InputfieldRepeaterCheckMax($inputfield) { + if(!$inputfield.hasClass('InputfieldRepeaterMax')) return; + var max = parseInt($inputfield.attr('data-max')); + if(max <= 0) return; + var $content = $inputfield.children('.InputfieldContent'); + var num = $content.children('.Inputfields').children('li:not(.InputfieldRepeaterDeletePending)').length; + var $addItem = $content.children('.InputfieldRepeaterAddItem'); + if(num > max) { + $addItem.hide(); + } else if(!$addItem.is(":visible")) { + $addItem.show(); + } +} + +function InputfieldRepeaterCheckDepths($inputfield) { + $inputfield.find('.InputfieldRepeaterDepth').each(function() { + var $depth = $(this); + var depth = $depth.val(); + var $item = $depth.closest('.InputfieldRepeaterItem'); + var currentLeft = $item.css('margin-left'); + if(currentLeft == 'auto') currentLeft = 0; + currentLeft = parseInt(currentLeft); + var targetLeft = depth * InputfieldRepeaterDepthSize; + if(targetLeft != currentLeft) { + $item.css('margin-left', targetLeft + 'px'); + $item.data('lastLeft', targetLeft); } }); - - $.cookie('repeaters_open', val); } /** @@ -184,10 +261,12 @@ function InputfieldRepeaterInit($this) { if($this.hasClass('InputfieldRepeaterItem')) { // single repeater item var $inputfields = $this; + var $inputfieldRepeater = $this.closest('.InputfieldRepeater'); var isItem = true; } else { // enter repeater var $inputfields = $this.find('.Inputfields:eq(0)'); + var $inputfieldRepeater = $this; var isItem = false; } @@ -196,13 +275,16 @@ function InputfieldRepeaterInit($this) { $inputfields.addClass('InputfieldRepeaterInit'); var renderValueMode = $inputfields.closest('.InputfieldRenderValueMode').length > 0; - var $delete = $("").css('display', 'block'); + var $clone = $("").css('display', 'block'); + var $delete = $(""); var $toggle = $(""); 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(); @@ -221,6 +303,7 @@ function InputfieldRepeaterInit($this) { $t.addClass('ui-state-default InputfieldRepeaterHeaderInit'); $t.prepend("") if(!renderValueMode) { + if(allowClone) $t.prepend($clone.clone(true)); $t.prepend($toggle.clone(true).addClass($t.parent().hasClass('InputfieldRepeaterOff') ? 'fa-toggle-off' : 'fa-toggle-on')); $t.prepend($delete.clone(true)); } @@ -255,9 +338,8 @@ function InputfieldRepeaterInit($this) { $inputfields = $this.closest('.InputfieldRepeater').find('.Inputfields:eq(0)'); } - $inputfields.sortable({ + var sortableOptions = { items: '> li:not(.InputfieldRepeaterNewItem)', - axis: 'y', handle: '.InputfieldRepeaterDrag', start: function(e, ui) { ui.item.find('.InputfieldHeader').addClass("ui-state-highlight"); @@ -275,7 +357,9 @@ function InputfieldRepeaterInit($this) { tinyMCE.execCommand('mceRemoveControl', false, $(this).attr('id')); }); }, + stop: function(e, ui) { + ui.item.find('.InputfieldHeader').removeClass("ui-state-highlight"); $(this).children().each(function(n) { $(this).find('.InputfieldRepeaterSort').slice(0,1).attr('value', n); @@ -291,8 +375,47 @@ function InputfieldRepeaterInit($this) { tinyMCE.execCommand('mceAddControl', false, $(this).attr('id')); }); } - }); + }; + var maxDepth = parseInt($inputfieldRepeater.attr('data-depth')); + if(maxDepth > 0) { + InputfieldRepeaterCheckDepths($inputfieldRepeater); + sortableOptions.grid = [ InputfieldRepeaterDepthSize, 1 ]; + sortableOptions.beforeStop = function(event, ui) { + var lastLeft = ui.item.data('lastLeft'); + if(!lastLeft) lastLeft = 0; + var left = lastLeft + ui.position.left; + left -= InputfieldRepeaterDepthSize / 2; + if(left > 10 && left < InputfieldRepeaterDepthSize) left = InputfieldRepeaterDepthSize; + var depth = Math.round(left / InputfieldRepeaterDepthSize); + if(depth < 1) depth = 0; + if(depth > maxDepth) depth = maxDepth; + if(depth) { + ui.item.css('margin-left', (depth * InputfieldRepeaterDepthSize) + 'px'); + } else { + ui.item.css('margin-left', 0); + } + ui.item.find('.InputfieldRepeaterDepth').val(depth); + ui.item.data('lastLeft', left); + ui.item.children('.InputfieldHeader').removeClass('ui-state-error'); + }; + sortableOptions.sort = function(event, ui) { + var lastLeft = ui.item.data('lastLeft'); + if(!lastLeft) lastLeft = 0; + var left = lastLeft + ui.position.left; + var $header = ui.item.children('.InputfieldHeader'); + if(left > (InputfieldRepeaterDepthSize * maxDepth) + (InputfieldRepeaterDepthSize / 2)) { + // beyond max depth allowed + $header.addClass('ui-state-error'); + } else if($header.hasClass('ui-state-error')) { + $header.removeClass('ui-state-error'); + } + }; + } else { + sortableOptions.axis = 'y'; + } + + $inputfields.sortable(sortableOptions); var $addLinks = $(".InputfieldRepeaterAddLink:not(.InputfieldRepeaterAddLinkInit)", $this); $addLinks.addClass('InputfieldRepeaterAddLinkInit'); @@ -302,20 +425,31 @@ function InputfieldRepeaterInit($this) { var $inputfields = $(this).parent('p').prev('ul.Inputfields'); var $numAddInput = $(this).parent().children('input'); var newItemTotal = 0; // for noAjaxAdd mode + var useAjax = $addLink.attr('data-noajax').length == 0; + var cloneID = $addLink.attr('data-clone'); function addRepeaterItem($addItem) { // make sure it has a unique ID var id = $addItem.attr('id') + '_'; - while($('#' + id).size() > 0) id += '_'; + while($('#' + id).length > 0) id += '_'; $addItem.attr('id', id); $inputfields.append($addItem); $addItem.css('display', 'block'); //$addItem.find('.InputfieldRepeaterTrash').click(InputfieldRepeaterDeleteClick); InputfieldRepeaterAdjustLabel($addItem, true); + /* + $nestedItems = $addItem.find('.InputfieldRepeater').find('.InputfieldRepeaterItem'); + if($nestedItems.length) { + $nestedItems.each(function() { + InputfieldRepeaterAdjustLabel($(this)); + }); + } + */ $addLink.trigger('repeateradd', [ $addItem ]); } - var useAjax = $addLink.attr('data-noajax').length == 0; + if(typeof cloneID == "undefined" || !cloneID) cloneID = null; + if(cloneID) $addLink.removeAttr('data-clone'); if(!useAjax) { var $newItem = $inputfields.children('.InputfieldRepeaterNewItem'); // for noAjaxAdd mode, non-editable new item @@ -325,31 +459,42 @@ function InputfieldRepeaterInit($this) { var $addItem = $newItem.clone(true) addRepeaterItem($addItem); $numAddInput.attr('value', newItemTotal); + InputfieldRepeaterCheckMax($inputfieldRepeater); } } else { // get addItem from ajax var pageID = $addLink.closest('.InputfieldRepeater').attr('data-page'); // $("#Inputfield_id").val(); var fieldName = $addLink.closest('.InputfieldRepeater').attr('id').replace('wrap_Inputfield_', ''); - var ajaxURL = ProcessWire.config.InputfieldRepeater.editorUrl + '?id=' + pageID + '&field=' + fieldName + '&repeater_add=' + $addLink.attr('data-type') + '&repeater_not='; var $spinner = $addLink.parent().find('.InputfieldRepeaterSpinner'); + var ajaxURL = ProcessWire.config.InputfieldRepeater.editorUrl + '?id=' + pageID + '&field=' + fieldName; $spinner.removeClass($spinner.attr('data-off')).addClass($spinner.attr('data-on')); + + if(cloneID) { + ajaxURL += '&repeater_clone=' + cloneID; + } else { + ajaxURL += '&repeater_add=' + $addLink.attr('data-type'); + } // determine which page IDs we don't accept for new items (because we already have them rendered) - $inputfields.find('.InputfieldRepeaterUnpublished').each(function() { - ajaxURL += $(this).attr('data-page') + ','; - }); + var $unpublishedItems = $inputfields.find('.InputfieldRepeaterUnpublished'); + if($unpublishedItems.length) { + ajaxURL += '&repeater_not='; + $unpublishedItems.each(function() { + ajaxURL += $(this).attr('data-page') + ','; + }); + } $.get(ajaxURL, function(data) { + //console.log(data); $spinner.removeClass($spinner.attr('data-on')).addClass($spinner.attr('data-off')); - var $addItem = $(data).find(".InputfieldRepeaterItem.InputfieldRepeaterUnpublished"); + var $addItem = $(data).find(".InputfieldRepeaterItemRequested"); if(!$addItem.length) { // error // console.log("Can't find item: .InputfieldRepeaterItem.InputfieldRepeaterUnpublished"); return; } - //console.log($addItem); addRepeaterItem($addItem); $addItem.wrap(""); // wrap for inputfields.js $target InputfieldsInit($addItem.parent()); @@ -360,8 +505,15 @@ function InputfieldRepeaterInit($this) { $addItem.find('.InputfieldRepeaterSort').val($inputfields.children().length); $('html, body').animate({ scrollTop: $addItem.offset().top - }, 500); + }, 500, 'swing'); InputfieldRepeaterUpdateState($addItem); + InputfieldRepeaterCheckMax($inputfieldRepeater); + $nestedRepeaters = $addItem.find('.InputfieldRepeater'); + if($nestedRepeaters.length) { + $nestedRepeaters.each(function() { + InputfieldRepeaterInit($(this)); + }); + } }); } @@ -369,13 +521,17 @@ function InputfieldRepeaterInit($this) { }); //$(".InputfieldRepeaterUnpublished").children('.InputfieldHeader').addClass('ui-priority-secondary'); + + if($inputfieldRepeater.hasClass('InputfieldRepeaterMax')) InputfieldRepeaterCheckMax($inputfieldRepeater); } $(document).ready(function() { + $(".InputfieldRepeater").each(function() { InputfieldRepeaterInit($(this)); }); + $(document).on('reloaded', '.InputfieldRepeater', function(event, source) { if(typeof source != "undefined") { if(source == 'InputfieldRepeaterItemEdit' || source == 'InputfieldRepeaterItemAdd') { @@ -387,11 +543,14 @@ $(document).ready(function() { } InputfieldRepeaterInit($(this)); }); - $(document).on('click', '.InputfieldRepeaterTrash', InputfieldRepeaterDeleteClick); - $(document).on('click', '.InputfieldRepeaterToggle', InputfieldRepeaterToggleClick); - $(document).on('opened', '.InputfieldRepeaterItem', InputfieldRepeaterItemOpened); - $(document).on('closed', '.InputfieldRepeaterItem', InputfieldRepeaterItemClosed); - $(document).on('openReady', '.InputfieldRepeaterItem', InputfieldRepeaterItemOpenReady); - + + $(document) + .on('click', '.InputfieldRepeaterTrash', InputfieldRepeaterDeleteClick) + .on('dblclick', '.InputfieldRepeaterTrash', InputfieldRepeaterDeleteDblClick) + .on('click', '.InputfieldRepeaterClone', InputfieldRepeaterCloneClick) + .on('click', '.InputfieldRepeaterToggle', InputfieldRepeaterToggleClick) + .on('opened', '.InputfieldRepeaterItem', InputfieldRepeaterItemOpened) + .on('closed', '.InputfieldRepeaterItem', InputfieldRepeaterItemClosed) + .on('openReady', '.InputfieldRepeaterItem', InputfieldRepeaterItemOpenReady); }); diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.min.js b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.min.js index ae21454f..08c76923 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.min.js +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.min.js @@ -1 +1 @@ -function InputfieldRepeaterDeleteClick(d){var c=$(this).parent("label").parent("li");if(c.is(".InputfieldRepeaterNewItem")){var a=c.parent().parent().find(".InputfieldRepeaterAddItem").children("input");a.attr("value",parseInt(a.attr("value")-1));c.remove()}else{var b=c.find(".InputfieldRepeaterDelete");if(b.is(":checked")){b.removeAttr("checked");c.children("label").removeClass("ui-state-error").addClass("ui-state-default");c.removeClass("InputfieldRepeaterDeletePending")}else{b.attr("checked","checked");c.children("label").removeClass("ui-state-default").addClass("ui-state-error");if(!c.hasClass("InputfieldStateCollapsed")){c.toggleClass("InputfieldStateCollapsed",100)}c.addClass("InputfieldRepeaterDeletePending")}}d.stopPropagation()}function InputfieldRepeaterToggleClick(f){var d=$(this);var c=d.attr("data-on");var a=d.attr("data-off");var b=d.closest(".InputfieldRepeaterItem");var g=b.find(".InputfieldRepeaterPublish");if(d.hasClass(c)){d.removeClass(c).addClass(a);b.addClass("InputfieldRepeaterUnpublished InputfieldRepeaterOff");g.val("-1")}else{d.removeClass(a).addClass(c);b.removeClass("InputfieldRepeaterUnpublished InputfieldRepeaterOff");g.val("1")}f.stopPropagation()}function InputfieldRepeaterItemOpenReady(c){var a=$(this);var b=a.find(".InputfieldRepeaterLoaded");if(parseInt(b.val())>0){return}a.addClass("InputfieldRepeaterItemLoading")}function InputfieldRepeaterUpdateState(a){if(!a.closest(".InputfieldRepeaterRememberOpen").length){return}var b="";$(".InputfieldRepeaterItem:not(.InputfieldStateCollapsed)").each(function(){var c=parseInt($(this).attr("data-page"));if(c>0){b+=c+"|"}});$.cookie("repeaters_open",b)}function InputfieldRepeaterItemClosed(a){InputfieldRepeaterUpdateState($(this))}function InputfieldRepeaterItemOpened(h){var j=$(this);var m=j.find(".InputfieldRepeaterLoaded");InputfieldRepeaterUpdateState(j);if(parseInt(m.val())>0){return}m.val("1");var b=j.find(".InputfieldContent").hide();var d=j.closest(".InputfieldRepeater");var i=d.attr("data-page");var c=parseInt(j.attr("data-page"));var l=d.attr("id");var k=l.replace("wrap_Inputfield_","");var a=ProcessWire.config.InputfieldRepeater.editorUrl+"?id="+i+"&field="+k+"&repeater_edit="+c;var f=j.find(".InputfieldRepeaterDrag");var g=m.closest(".Inputfields");f.removeClass("fa-arrows").addClass("fa-spin fa-spinner");l=l.replace(/_repeater\d+$/,"");$.get(a,function(o){var e=$(o).find("#"+l+" > .InputfieldContent > .Inputfields > .InputfieldRepeaterItem > .InputfieldContent > .Inputfields > .InputfieldWrapper > .Inputfields > .Inputfield");g.append(e);j.removeClass("InputfieldRepeaterItemLoading");InputfieldsInit(g);var n=e.filter(".InputfieldRepeater");if(n.length){n.each(function(){InputfieldRepeaterInit($(this))})}b.slideDown("fast",function(){f.removeClass("fa-spin fa-spinner").addClass("fa-arrows")});setTimeout(function(){g.find(".Inputfield").trigger("reloaded",["InputfieldRepeaterItemEdit"])},50)})}function InputfieldRepeaterAdjustLabel(c,d){var b=c.children("label");var e=b.html();var a=e;if(d&&e.indexOf("#")>-1){num=c.siblings(".InputfieldRepeaterItem:visible").length+1;e=e.replace(/#[0-9]+/,"#"+num)}if(e.indexOf("{")>-1){e=e.replace(/\{/,'');e=e.replace(/}/,"")}if(e!=a){b.html(e)}}function InputfieldRepeaterInit(g){if(g.hasClass("InputfieldRepeaterItem")){var d=g;var c=true}else{var d=g.find(".Inputfields:eq(0)");var c=false}if(d.hasClass("InputfieldRepeaterInit")){return}d.addClass("InputfieldRepeaterInit");var b=d.closest(".InputfieldRenderValueMode").length>0;var e=$("").css("display","block");var a=$("");var f=ProcessWire.config.InputfieldRepeater;if(f){a.attr("title",f.labels.toggle);e.attr("title",f.labels.remove)}$("input.InputfieldRepeaterDelete",g).parents(".InputfieldCheckbox").hide();function h(j){j.each(function(){var m=$(this);if(m.hasClass("InputfieldRepeaterHeaderInit")){return}var l="fa-arrows";var k=m.parent();if(k.hasClass("InputfieldRepeaterNewItem")){l="fa-plus";m.addClass("ui-priority-secondary")}m.addClass("ui-state-default InputfieldRepeaterHeaderInit");m.prepend("");if(!b){m.prepend(a.clone(true).addClass(m.parent().hasClass("InputfieldRepeaterOff")?"fa-toggle-off":"fa-toggle-on"));m.prepend(e.clone(true))}InputfieldRepeaterAdjustLabel(k,false)})}if(c){h(g.children(".InputfieldHeader"))}else{h($(".InputfieldRepeaterItem > .InputfieldHeader",g))}if(b){return}$(".InputfieldRepeaterDrag",g).hover(function(){$(this).parent("label").addClass("ui-state-focus")},function(){$(this).parent("label").removeClass("ui-state-focus")});$(".InputfieldRepeaterTrash",g).hover(function(){var j=$(this).parent("label");if(!j.parent().is(".InputfieldRepeaterDeletePending")){j.addClass("ui-state-error")}},function(){var j=$(this).parent("label");if(!j.parent().is(".InputfieldRepeaterDeletePending")){j.removeClass("ui-state-error")}});if(c){d=g.closest(".InputfieldRepeater").find(".Inputfields:eq(0)")}d.sortable({items:"> li:not(.InputfieldRepeaterNewItem)",axis:"y",handle:".InputfieldRepeaterDrag",start:function(k,j){j.item.find(".InputfieldHeader").addClass("ui-state-highlight");j.item.find("textarea.InputfieldCKEditorNormal.InputfieldCKEditorLoaded").each(function(){$(this).removeClass("InputfieldCKEditorLoaded");var l=CKEDITOR.instances[$(this).attr("id")];l.destroy();CKEDITOR.remove($(this).attr("id"))});j.item.find(".InputfieldTinyMCE textarea").each(function(){tinyMCE.execCommand("mceRemoveControl",false,$(this).attr("id"))})},stop:function(k,j){j.item.find(".InputfieldHeader").removeClass("ui-state-highlight");$(this).children().each(function(l){$(this).find(".InputfieldRepeaterSort").slice(0,1).attr("value",l)});j.item.find("textarea.InputfieldCKEditorNormal:not(.InputfieldCKEditorLoaded)").each(function(){$(this).closest(".InputfieldCKEditor").trigger("reloaded",["InputfieldRepeaterSort"])});j.item.find(".InputfieldTinyMCE textarea").each(function(){tinyMCE.execCommand("mceAddControl",false,$(this).attr("id"))})}});var i=$(".InputfieldRepeaterAddLink:not(.InputfieldRepeaterAddLinkInit)",g);i.addClass("InputfieldRepeaterAddLinkInit");i.click(function(){var k=$(this);var q=$(this).parent("p").prev("ul.Inputfields");var l=$(this).parent().children("input");var p=0;function s(v){var w=v.attr("id")+"_";while($("#"+w).size()>0){w+="_"}v.attr("id",w);q.append(v);v.css("display","block");InputfieldRepeaterAdjustLabel(v,true);k.trigger("repeateradd",[v])}var u=k.attr("data-noajax").length==0;if(!u){var m=q.children(".InputfieldRepeaterNewItem");p=m.length;if(p>0){if(p>1){m=m.slice(0,1)}var o=m.clone(true);s(o);l.attr("value",p)}}else{var r=k.closest(".InputfieldRepeater").attr("data-page");var t=k.closest(".InputfieldRepeater").attr("id").replace("wrap_Inputfield_","");var j=ProcessWire.config.InputfieldRepeater.editorUrl+"?id="+r+"&field="+t+"&repeater_add="+k.attr("data-type")+"&repeater_not=";var n=k.parent().find(".InputfieldRepeaterSpinner");n.removeClass(n.attr("data-off")).addClass(n.attr("data-on"));q.find(".InputfieldRepeaterUnpublished").each(function(){j+=$(this).attr("data-page")+","});$.get(j,function(w){n.removeClass(n.attr("data-on")).addClass(n.attr("data-off"));var v=$(w).find(".InputfieldRepeaterItem.InputfieldRepeaterUnpublished");if(!v.length){return}s(v);v.wrap("");InputfieldsInit(v.parent());InputfieldRepeaterInit(v);v.unwrap();v.find(".Inputfield").trigger("reloaded",["InputfieldRepeaterItemAdd"]);v.find(".InputfieldRepeaterSort").val(q.children().length);$("html, body").animate({scrollTop:v.offset().top},500);InputfieldRepeaterUpdateState(v)})}return false})}$(document).ready(function(){$(".InputfieldRepeater").each(function(){InputfieldRepeaterInit($(this))});$(document).on("reloaded",".InputfieldRepeater",function(b,c){if(typeof c!="undefined"){if(c=="InputfieldRepeaterItemEdit"||c=="InputfieldRepeaterItemAdd"){b.stopPropagation();var a=$(this).find(".InputfieldRepeater");if(a.length){InputfieldRepeaterInit(a)}return}}InputfieldRepeaterInit($(this))});$(document).on("click",".InputfieldRepeaterTrash",InputfieldRepeaterDeleteClick);$(document).on("click",".InputfieldRepeaterToggle",InputfieldRepeaterToggleClick);$(document).on("opened",".InputfieldRepeaterItem",InputfieldRepeaterItemOpened);$(document).on("closed",".InputfieldRepeaterItem",InputfieldRepeaterItemClosed);$(document).on("openReady",".InputfieldRepeaterItem",InputfieldRepeaterItemOpenReady)}); \ No newline at end of file +var InputfieldRepeaterDepthSize=50;function InputfieldRepeaterDeleteClick(d){var c=$(this).parent("label").parent("li");if(c.is(".InputfieldRepeaterNewItem")){var a=c.parent().parent().find(".InputfieldRepeaterAddItem").children("input");a.attr("value",parseInt(a.attr("value")-1));c.remove()}else{var b=c.find(".InputfieldRepeaterDelete");if(b.is(":checked")){b.removeAttr("checked");c.children("label").removeClass("ui-state-error").addClass("ui-state-default");c.removeClass("InputfieldRepeaterDeletePending")}else{b.attr("checked","checked");c.children("label").removeClass("ui-state-default").addClass("ui-state-error");if(!c.hasClass("InputfieldStateCollapsed")){c.toggleClass("InputfieldStateCollapsed",100)}c.addClass("InputfieldRepeaterDeletePending")}}InputfieldRepeaterCheckMax(c.closest(".InputfieldRepeater"));d.stopPropagation()}function InputfieldRepeaterDeleteDblClick(c){var d=$(this).closest("li");var a=d.hasClass("InputfieldRepeaterDeletePending");function b(){d.parent().children("li").each(function(){var e=$(this);var f=e.children(".InputfieldHeader").children(".InputfieldRepeaterTrash");if(e.hasClass("InputfieldRepeaterDeletePending")){if(a){f.click()}}else{if(!a){f.click()}}})}if(a){b()}else{ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.removeAll,b)}}function InputfieldRepeaterCloneClick(b){var a=$(this).closest(".InputfieldRepeaterItem");ProcessWire.confirm(ProcessWire.config.InputfieldRepeater.labels.clone,function(){var d=a.attr("data-page");var c=a.closest(".InputfieldRepeater").children(".InputfieldContent").children(".InputfieldRepeaterAddItem").find(".InputfieldRepeaterAddLink:eq(0)");c.attr("data-clone",d).click();$("html, body").animate({scrollTop:c.offset().top-100},250,"swing")});return false}function InputfieldRepeaterToggleClick(f){var d=$(this);var c=d.attr("data-on");var a=d.attr("data-off");var b=d.closest(".InputfieldRepeaterItem");var g=b.find(".InputfieldRepeaterPublish");if(d.hasClass(c)){d.removeClass(c).addClass(a);b.addClass("InputfieldRepeaterUnpublished InputfieldRepeaterOff");g.val("-1")}else{d.removeClass(a).addClass(c);b.removeClass("InputfieldRepeaterUnpublished InputfieldRepeaterOff");g.val("1")}f.stopPropagation()}function InputfieldRepeaterItemOpenReady(c){var a=$(this);var b=a.find(".InputfieldRepeaterLoaded");if(parseInt(b.val())>0){return}a.addClass("InputfieldRepeaterItemLoading")}function InputfieldRepeaterUpdateState(a){if(a.closest(".InputfieldRepeaterRememberOpen").length){var b="";$(".InputfieldRepeaterItem:not(.InputfieldStateCollapsed)").each(function(){var c=parseInt($(this).attr("data-page"));if(c>0){b+=c+"|"}});$.cookie("repeaters_open",b)}}function InputfieldRepeaterCheckMax(d){if(!d.hasClass("InputfieldRepeaterMax")){return}var a=parseInt(d.attr("data-max"));if(a<=0){return}var c=d.children(".InputfieldContent");var b=c.children(".Inputfields").children("li:not(.InputfieldRepeaterDeletePending)").length;var e=c.children(".InputfieldRepeaterAddItem");if(b>a){e.hide()}else{if(!e.is(":visible")){e.show()}}}function InputfieldRepeaterCheckDepths(a){a.find(".InputfieldRepeaterDepth").each(function(){var d=$(this);var e=d.val();var b=d.closest(".InputfieldRepeaterItem");var c=b.css("margin-left");if(c=="auto"){c=0}c=parseInt(c);var f=e*InputfieldRepeaterDepthSize;if(f!=c){b.css("margin-left",f+"px");b.data("lastLeft",f)}})}function InputfieldRepeaterItemClosed(a){InputfieldRepeaterUpdateState($(this))}function InputfieldRepeaterItemOpened(h){var j=$(this);var m=j.find(".InputfieldRepeaterLoaded");InputfieldRepeaterUpdateState(j);if(parseInt(m.val())>0){return}m.val("1");var b=j.find(".InputfieldContent").hide();var d=j.closest(".InputfieldRepeater");var i=d.attr("data-page");var c=parseInt(j.attr("data-page"));var l=d.attr("id");var k=l.replace("wrap_Inputfield_","");var a=ProcessWire.config.InputfieldRepeater.editorUrl+"?id="+i+"&field="+k+"&repeater_edit="+c;var f=j.find(".InputfieldRepeaterDrag");var g=m.closest(".Inputfields");f.removeClass("fa-arrows").addClass("fa-spin fa-spinner");l=l.replace(/_repeater\d+$/,"");$.get(a,function(o){var e=$(o).find("#"+l+" > .InputfieldContent > .Inputfields > .InputfieldRepeaterItem > .InputfieldContent > .Inputfields > .InputfieldWrapper > .Inputfields > .Inputfield");g.append(e);j.removeClass("InputfieldRepeaterItemLoading");InputfieldsInit(g);var n=e.filter(".InputfieldRepeater");if(n.length){n.each(function(){InputfieldRepeaterInit($(this))})}b.slideDown("fast",function(){f.removeClass("fa-spin fa-spinner").addClass("fa-arrows")});setTimeout(function(){g.find(".Inputfield").trigger("reloaded",["InputfieldRepeaterItemEdit"])},50)})}function InputfieldRepeaterAdjustLabel(c,d){var b=c.children("label");var e=b.html();var a=e;if(d&&e.indexOf("#")>-1){num=c.siblings(".InputfieldRepeaterItem:visible").length+1;e=e.replace(/#[0-9]+/,"#"+num)}if(e.indexOf("{")>-1){e=e.replace(/\{/,'');e=e.replace(/}/,"")}if(e!=a){b.html(e)}}function InputfieldRepeaterInit(l){if(l.hasClass("InputfieldRepeaterItem")){var h=l;var b=l.closest(".InputfieldRepeater");var g=true}else{var h=l.find(".Inputfields:eq(0)");var b=l;var g=false}if(h.hasClass("InputfieldRepeaterInit")){return}h.addClass("InputfieldRepeaterInit");var f=h.closest(".InputfieldRenderValueMode").length>0;var k=$("").css("display","block");var i=$("");var a=$("");var j=ProcessWire.config.InputfieldRepeater;var e=!b.hasClass("InputfieldRepeaterNoAjaxAdd");if(j){a.attr("title",j.labels.toggle);i.attr("title",j.labels.remove);k.attr("title",j.labels.clone)}$("input.InputfieldRepeaterDelete",l).parents(".InputfieldCheckbox").hide();function m(o){o.each(function(){var r=$(this);if(r.hasClass("InputfieldRepeaterHeaderInit")){return}var q="fa-arrows";var p=r.parent();if(p.hasClass("InputfieldRepeaterNewItem")){q="fa-plus";r.addClass("ui-priority-secondary")}r.addClass("ui-state-default InputfieldRepeaterHeaderInit");r.prepend("");if(!f){if(e){r.prepend(k.clone(true))}r.prepend(a.clone(true).addClass(r.parent().hasClass("InputfieldRepeaterOff")?"fa-toggle-off":"fa-toggle-on"));r.prepend(i.clone(true))}InputfieldRepeaterAdjustLabel(p,false)})}if(g){m(l.children(".InputfieldHeader"))}else{m($(".InputfieldRepeaterItem > .InputfieldHeader",l))}if(f){return}$(".InputfieldRepeaterDrag",l).hover(function(){$(this).parent("label").addClass("ui-state-focus")},function(){$(this).parent("label").removeClass("ui-state-focus")});$(".InputfieldRepeaterTrash",l).hover(function(){var o=$(this).parent("label");if(!o.parent().is(".InputfieldRepeaterDeletePending")){o.addClass("ui-state-error")}},function(){var o=$(this).parent("label");if(!o.parent().is(".InputfieldRepeaterDeletePending")){o.removeClass("ui-state-error")}});if(g){h=l.closest(".InputfieldRepeater").find(".Inputfields:eq(0)")}var d={items:"> li:not(.InputfieldRepeaterNewItem)",handle:".InputfieldRepeaterDrag",start:function(p,o){o.item.find(".InputfieldHeader").addClass("ui-state-highlight");o.item.find("textarea.InputfieldCKEditorNormal.InputfieldCKEditorLoaded").each(function(){$(this).removeClass("InputfieldCKEditorLoaded");var q=CKEDITOR.instances[$(this).attr("id")];q.destroy();CKEDITOR.remove($(this).attr("id"))});o.item.find(".InputfieldTinyMCE textarea").each(function(){tinyMCE.execCommand("mceRemoveControl",false,$(this).attr("id"))})},stop:function(p,o){o.item.find(".InputfieldHeader").removeClass("ui-state-highlight");$(this).children().each(function(q){$(this).find(".InputfieldRepeaterSort").slice(0,1).attr("value",q)});o.item.find("textarea.InputfieldCKEditorNormal:not(.InputfieldCKEditorLoaded)").each(function(){$(this).closest(".InputfieldCKEditor").trigger("reloaded",["InputfieldRepeaterSort"])});o.item.find(".InputfieldTinyMCE textarea").each(function(){tinyMCE.execCommand("mceAddControl",false,$(this).attr("id"))})}};var c=parseInt(b.attr("data-depth"));if(c>0){InputfieldRepeaterCheckDepths(b);d.grid=[InputfieldRepeaterDepthSize,1];d.beforeStop=function(o,p){var r=p.item.data("lastLeft");if(!r){r=0}var q=r+p.position.left;q-=InputfieldRepeaterDepthSize/2;if(q>10&&q" . + sprintf($this->_('Warning: only the first %d item(s) will be used.'), $this->repeaterMaxItems) . + "
"; + } + // cache $this->form = $form; @@ -403,23 +440,52 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { /** * Render a new item for ajax after 'add new' link clicked * + * @param int $cloneItemID * @return string * */ - public function renderAjaxNewItem() { + public function renderAjaxNewItem($cloneItemID = 0) { /** @var PageArray $value */ $value = $this->attr('value'); - $notIDs = $this->wire('sanitizer')->intArray(explode(',', trim($this->wire('input')->get('repeater_not'), ','))); - $readyPage = $this->getNextReadyPage($notIDs); - $readyPage->removeStatus(Page::statusHidden); + $clonePage = null; + $readyPage = null; + + if($cloneItemID) { + foreach($value as $item) { + if($item->id == $cloneItemID) { + $clonePage = $item; + break; + } + } + } - // ensure editing page doesn't get saved (just in case) since we're removing all items - $this->page->addStatus(Page::statusCorrupted); - - $value->add($readyPage); - - return $this->buildForm($readyPage->id)->render(); + if($clonePage && $clonePage->id) { + /** @var FieldtypeRepeater $fieldtype */ + $fieldtype = $this->field->type; + $readyPage = $this->wire('pages')->clone($clonePage, null, true, + array('set' => array( + 'name' => $fieldtype->getUniqueRepeaterPageName() . 'c', // trailing "c" indicates clone + 'sort' => count($value)+1, + 'status' => $clonePage->status | Page::statusUnpublished + ) + ) + ); + $readyPage->set('_repeater_clone', $clonePage->id); + } else if(!$cloneItemID) { + $notIDs = $this->wire('sanitizer')->intArray(explode(',', trim($this->wire('input')->get('repeater_not'), ','))); + $readyPage = $this->getNextReadyPage($notIDs); + $readyPage->removeStatus(Page::statusHidden); + } + + if($readyPage) { + // ensure editing page doesn't get saved (just in case) since we're removing all items + $this->page->addStatus(Page::statusCorrupted); + $value->add($readyPage); + return $this->buildForm($readyPage->id)->render(); + } else { + return ''; + } } /** @@ -450,8 +516,37 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { * */ public function renderReady(Inputfield $parent = null, $renderValueMode = false) { + $this->wire('modules')->get('JqueryCore')->use('cookie'); - $this->preloadInputfieldAssets(); + $this->wire('modules')->get('JqueryUI')->use('vex'); + $this->preloadInputfieldAssets(); + + if($this->field->get('repeaterLoading') == FieldtypeRepeater::loadingOff) { + $this->addClass('InputfieldRepeaterNoAjaxAdd', 'wrapClass'); + } + if($this->repeaterMaxItems > 0) { + $this->addClass('InputfieldRepeaterMax', 'wrapClass'); + } + if($this->repeaterDepth > 0) { + $this->addClass('InputfieldRepeaterDepth', 'wrapClass'); + } + $this->wrapAttr('data-page', $this->page->id); + $this->wrapAttr('data-max', (int) $this->repeaterMaxItems); + $this->wrapAttr('data-depth', (int) $this->repeaterDepth); + + list($editorUrl, $queryString) = explode('?', $this->page->editUrl()); + if($queryString) {} + + $this->wire('config')->js('InputfieldRepeater', array( + 'editorUrl' => $editorUrl, + 'labels' => array( + 'remove' => $this->_x('Delete this item', 'repeater-item-action'), + 'removeAll' => $this->_x('Delete all items?', 'repeater-item-action'), + 'toggle' => $this->_x('Toggle published/unpublished', 'repeater-item-action'), + 'clone' => $this->_x('Clone this item?', 'repeater-item-action') + ) + )); + return parent::renderReady($parent, $renderValueMode); } @@ -491,7 +586,10 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { if($ajax && $input->get('field') == $this->attr('name')) { $repeaterAdd = $input->get('repeater_add'); $repeaterEdit = (int) $input->get('repeater_edit'); - if($repeaterAdd !== null && !$noAjaxAdd) { + $repeaterClone = (int) $input->get('repeater_clone'); + if($repeaterClone) { + return $this->renderValueMode ? '' : $this->renderAjaxNewItem($repeaterClone); + } else if($repeaterAdd !== null && !$noAjaxAdd) { return $this->renderValueMode ? '' : $this->renderAjaxNewItem(); } else if($repeaterEdit) { if($this->renderValueMode) { @@ -508,17 +606,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $collapse = $this->field->get('repeaterCollapse'); $forIDs = null; if($loading == FieldtypeRepeater::loadingAll && $collapse != FieldtypeRepeater::collapseNone) $forIDs = array(); - list($editorUrl, $queryString) = explode('?', $this->page->editUrl()); - if($queryString) {} - - $this->wire('config')->js('InputfieldRepeater', array( - 'editorUrl' => $editorUrl, - 'labels' => array( - 'remove' => $this->_x('Delete this item', 'repeater-item-action'), - 'toggle' => $this->_x('Toggle published/unpublished', 'repeater-item-action') - ) - )); - + return $this->buildForm(0, $forIDs)->render() . $out; } @@ -569,7 +657,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { // existing items foreach($value as $key => $page) { - /** @var Page $page */ + /** @var RepeaterPage $page */ $pageIDs[] = $page->id; $isHidden = $page->isHidden(); @@ -579,6 +667,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $deleteName = "delete_repeater{$page->id}"; $sortName = "sort_repeater{$page->id}"; $publishName = "publish_repeater{$page->id}"; + $depthName = "depth_repeater{$page->id}"; if($input->$deleteName == $page->id) { $value->remove($page); @@ -596,6 +685,14 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { $sortChanged = true; } + if($this->repeaterDepth > 0) { + $depth = (int) $input->$depthName; + if($page->getDepth() != $depth) { + $page->setDepth($depth); + $numChanges++; + } + } + /** @var InputfieldWrapper $wrapper */ $wrapper = $this->wrappers[$page->id]; $wrapper->resetTrackChanges(true); @@ -640,7 +737,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { for($n = 0; $n < $numNewItems; $n++) { $page = $this->getNextReadyPage($pageIDs); $page->removeStatus(Page::statusHidden); - $page->sort = count($value); + $page->sort = count($value)+1; $value->add($page); $numChanges++; } @@ -722,7 +819,7 @@ class InputfieldRepeater extends Inputfield implements InputfieldItemList { * * @param string $key * @param mixed $value - * @return $this + * @return Inputfield|InputfieldRepeater * */ public function set($key, $value) { diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.scss b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.scss index bb52c949..74a1293c 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.scss +++ b/wire/modules/Fieldtype/FieldtypeRepeater/InputfieldRepeater.scss @@ -14,6 +14,7 @@ margin-right: 0.5em; } + & > .InputfieldRepeaterClone, & > .InputfieldRepeaterToggle, & > .InputfieldRepeaterTrash { cursor: pointer; @@ -25,6 +26,9 @@ & > .InputfieldRepeaterToggle { margin-right: 1em; } + & > .InputfieldRepeaterClone { + margin-right: 1em; + } } .InputfieldRepeaterItem:not(.InputfieldRepeaterDeletePending) { diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php index 728a3f86..30277ade 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterPage.php @@ -20,13 +20,21 @@ class RepeaterPage extends Page { * Field instance that contains this repeater item * */ - protected $forField = null; + protected $forField = null; + + /** + * Depth of this item + * + * @var int|null + * + */ + protected $depth = null; /** * Set the page that owns this repeater item * * @param Page $forPage - * @return this + * @return $this * */ public function setForPage(Page $forPage) { @@ -55,7 +63,10 @@ class RepeaterPage extends Page { // but if this repeater was loaded from somewhere else, that won't // have happened, so we have to determine it from it's location - $parentName = $this->parent()->name; + + /** @var Page $parent */ + $parent = $this->parent(); + $parentName = $parent->name; $prefix = FieldtypeRepeater::repeaterPageNamePrefix; // for-page- if(strpos($parentName, $prefix) === 0) { @@ -74,7 +85,7 @@ class RepeaterPage extends Page { * Set the field that owns this repeater item * * @param Field $forField - * @return this + * @return $this * */ public function setForField(Field $forField) { @@ -91,7 +102,8 @@ class RepeaterPage extends Page { public function getForField() { if(!is_null($this->forField)) return $this->forField; - $grandparentName = $this->parent()->parent()->name; + $grandparent = $this->parent()->parent(); + $grandparentName = $grandparent->name; $prefix = FieldtypeRepeater::fieldPageNamePrefix; // for-field- if(strpos($grandparentName, $prefix) === 0) { @@ -102,6 +114,31 @@ class RepeaterPage extends Page { return $this->forField; } + + public function get($key) { + $value = parent::get($key); + if($key === 'depth' && is_null($value)) { + $value = $this->getDepth(); + } + return $value; + } + + public function getDepth() { + if(is_null($this->depth)) { + $this->depth = 0; + $name = $this->name; + while($name[$this->depth] === '-') $this->depth++; + } + return $this->depth; + } + + public function setDepth($depth) { + $name = $this->name; + $_name = $name; + $name = ltrim($name, '-'); + if($depth > 0) $name = str_repeat('-', $depth) . $name; + if($name !== $_name) $this->name = $name; + } /** * Is this page public? diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/config.php b/wire/modules/Fieldtype/FieldtypeRepeater/config.php index 936c0bf6..6d40d333 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/config.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/config.php @@ -53,6 +53,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { $this->message($this->_('Please add fields to this repeater from the "details" tab.')); } + /** @var InputfieldHidden $f */ $f = $this->modules->get('InputfieldHidden'); $f->attr('name', 'template_id'); $f->label = 'Repeater Template ID'; @@ -80,6 +81,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { $select->setAsmSelectOption('hideDeleted', false); foreach($template->fieldgroup as $f) { + /** @var Field $f */ $f = $template->fieldgroup->getField($f->id, true); // get in context $columnWidth = (int) $f->get('columnWidth'); @@ -146,6 +148,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { // ------------------------------------------------- + /** @var InputfieldRadios $f */ $f = $this->wire('modules')->get('InputfieldRadios'); $f->attr('name', 'repeaterCollapse'); $f->label = $this->_('Repeater item visibility in editor'); @@ -191,8 +194,37 @@ class FieldtypeRepeaterConfigHelper extends Wire { $inputfields->add($f); // ------------------------------------------------- + + $value = (int) $field->get('repeaterMaxItems'); + $f = $this->wire('modules')->get('InputfieldInteger'); + $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'; + $inputfields->add($f); + + // ------------------------------------------------- + + $value = (int) $field->get('repeaterDepth'); + $f = $this->wire('modules')->get('InputfieldInteger'); + $f->attr('name', 'repeaterDepth'); + $f->attr('value', $value > 0 ? $value : ''); + $f->label = $this->_('Item depth'); + $f->collapsed = Inputfield::collapsedBlank; + $f->description = $this->_('To support items with depth, enter the max allowed depth, or leave blank to disable.'); + $f->description .= ' ' . $this->_('When editing a repeater, you can change item depth by clicking the repeater item drag arrows and dragging the item right or left.'); + $f->notes = $this->_('Depths are zero-based, meaning a depth of 3 allows depths 0, 1, 2 and 3.'); + $f->notes .= ' ' . $this->_('Depth can be accessed from a repeater page item via `$item->depth`.'); + $f->icon = 'indent'; + $inputfields->add($f); - $numOldReady = $this->field->type->countOldReadyPages($field); + // ------------------------------------------------- + + /** @var FieldtypeRepeater $fieldtype */ + $fieldtype = $this->field->type; + $numOldReady = $fieldtype->countOldReadyPages($field); if($numOldReady) { // @todo: should we just do this automatically? $f = $this->wire('modules')->get('InputfieldCheckbox'); @@ -206,42 +238,6 @@ class FieldtypeRepeaterConfigHelper extends Wire { // ------------------------------------------------- - /* - $f = $this->wire('modules')->get('InputfieldCheckbox'); - $f->attr('name', 'noAjaxAdd'); - $f->label = $this->_('Disable AJAX for adding items in editor'); - $f->description = $this->_('By default, clicking the "Add Item" link will load the new repeater item via AJAX, which provides a more seamless experience. But some fields may not be compatible with this. Check this box to disable AJAX for adding items.'); // Description for noAjaxAdd option - $f->icon = 'frown-o'; - $f->columnWidth = 50; - if($field->get('noAjaxAdd')) { - $f->attr('checked', 'checked'); - } else { - $f->collapsed = Inputfield::collapsedYes; - } - $inputfields->add($f); - */ - - // ------------------------------------------------- - - /* - if(is_null($field->repeaterReadyItems)) $field->repeaterReadyItems = self::defaultRepeaterReadyItems; - if($field->noAjaxAdd) $field->repeaterReadyItems = 0; - $input = $this->wire('modules')->get('InputfieldInteger'); - $input->attr('id+name', 'repeaterReadyItems'); - $input->attr('value', (int) abs($field->repeaterReadyItems)); - $input->label = $this->_('Ready-To-Edit New Repeater Items') . " ({$field->repeaterReadyItems})"; - $input->description = $this->_('The number of ready-to-edit (unpublished) items per page to keep rendered for use as new items.'); - $input->notes = - $this->_('If set to 0, new items will only be created as needed. This is the most efficient setting.') . " \n" . - $this->_('If set to 1 or above, that many new items will be ready to edit as soon as you click "add item". This makes for faster additions.'); - $input->collapsed = Inputfield::collapsedYes; - $input->showIf = 'noAjaxAdd=1'; - $inputfields->add($input); - */ - - // ------------------------------------------------- - - /** TBA if(is_null($field->repeaterMaxItems)) $field->repeaterMaxItems = self::defaultRepeaterMaxItems; $input = wire('modules')->get('InputfieldInteger'); @@ -303,7 +299,9 @@ class FieldtypeRepeaterConfigHelper extends Wire { $fieldgroup->save(); if($this->wire('input')->post('_deleteOldReady')) { - $cnt = $this->field->type->countOldReadyPages($field, true); + /** @var FieldtypeRepeater $fieldtype */ + $fieldtype = $this->field->type; + $cnt = $fieldtype->countOldReadyPages($field, true); $this->message(sprintf($this->_('Deleted %d old/unused repeater item(s)'), $cnt)); } diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js index 769ee18e..444a3fc2 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.js @@ -546,7 +546,7 @@ function InputfieldImage($) { var $button = $(this); var $list = $button.closest('.gridImages'); if(!$list.hasClass('gridImagesAlerted')) { - alert(ProcessWire.config.InputfieldImage.labels.changes); + ProcessWire.alert(ProcessWire.config.InputfieldImage.labels.changes); $list.addClass('gridImagesAlerted'); } setTimeout(function() { diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js index a5766602..eb5db758 100644 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.min.js @@ -1 +1 @@ -function InputfieldImage(u){var k=null;var b={file:"",item:null,edit:null};var E={type:"image",closeOnContentClick:true,closeBtnInside:true};var c=null;var q=[];function r(){var L=window.File&&window.FileList&&window.FileReader;var K=u(".InputfieldAllowAjaxUpload").length>0;var M=u("#PageIDIndicator").length>0;return(L&&(M||K))}function x(M,K,L){K||(K=250);var N,O;return function(){var R=L||this;var Q=+new Date(),P=arguments;if(N&&Q'+aG.dimensions+' | '+aG.na+" |
---|---|
"+aG.filesize+" | "+au+" |
"+aG.variations+" | 0 |
'+aG.dimensions+' | '+aG.na+" |
---|---|
"+aG.filesize+" | "+au+" |
"+aG.variations+" | 0 |