From 6667caa1d0cbbfb9b2b197f1d0b6380121cdc714 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 27 May 2022 10:40:58 -0400 Subject: [PATCH] Add new page editor Inputfield visibility mode 'Tab' which makes any Inputfield display as a page editor tab. Options included for 'Tab', 'Tab (AJAX)', and 'Tab (locked)'. --- wire/core/Inputfield.php | 29 ++++++++++++++ wire/core/InputfieldWrapper.php | 26 ++++++++++--- .../Fieldtype/FieldtypeFieldsetTabOpen.module | 28 +++++++++----- .../Process/ProcessField/ProcessField.module | 24 +++++++++++- .../ProcessPageEdit/ProcessPageEdit.module | 38 +++++++++++++++++++ 5 files changed, 129 insertions(+), 16 deletions(-) diff --git a/wire/core/Inputfield.php b/wire/core/Inputfield.php index 239db39e..672a03c0 100644 --- a/wire/core/Inputfield.php +++ b/wire/core/Inputfield.php @@ -59,6 +59,7 @@ * @property string $icon Optional font-awesome icon name to accompany label (excluding the "fa-") part). #pw-group-labels * @property string $requiredLabel Optional custom label to display when missing required value. @since 3.0.98 #pw-group-labels * @property string $head Optional text that appears below label but above description (only used by some Inputfields). #pw-internal + * @property string $tabLabel Label for tab if Inputfield rendered in its own tab via Inputfield::collapsedTab* setting. @since 3.0.201 #pw-group-labels * @property string|null $prependMarkup Optional markup to prepend to the Inputfield content container. #pw-group-other * @property string|null $appendMarkup Optional markup to append to the Inputfield content container. #pw-group-other * @@ -218,6 +219,30 @@ abstract class Inputfield extends WireData implements Module { */ const collapsedBlankAjax = 11; + /** + * Collapsed into a separate tab + * #pw-group-collapsed-constants + * @since 3.0.201 + * + */ + const collapsedTab = 20; + + /** + * Collapsed into a separate tab and AJAX loaded + * #pw-group-collapsed-constants + * @since 3.0.201 + * + */ + const collapsedTabAjax = 21; + + /** + * Collapsed into a separate tab and locked (not editable) + * #pw-group-collapsed-constants + * @since 3.0.201 + * + */ + const collapsedTabLocked = 22; + /** * Don't skip the label (default) * #pw-group-skipLabel-constants @@ -366,6 +391,7 @@ abstract class Inputfield extends WireData implements Module { $this->set('notes', ''); // highlighted descriptive copy, below output of input field $this->set('detail', ''); // text details that appear below notes $this->set('head', ''); // below label, above description + $this->set('tabLabel', ''); // alternate label for tab when Inputfield::collapsedTab* in use $this->set('required', 0); // set to 1 to make value required for this field $this->set('requiredIf', ''); // optional conditions to make it required $this->set('collapsed', ''); // see the collapsed* constants at top of class (use blank string for unset value) @@ -1467,6 +1493,9 @@ abstract class Inputfield extends WireData implements Module { if($this->hasFieldtype !== false) { $field->addOption(self::collapsedYesAjax, $this->_('Closed + Load only when opened (AJAX)') . " †"); $field->notes = sprintf($this->_('Options indicated with %s may not work with all input types or placements, test to ensure compatibility.'), '†'); + $field->addOption(self::collapsedTab, $this->_('Tab')); + $field->addOption(self::collapsedTabAjax, $this->_('Tab + Load only when clicked (AJAX)') . " †"); + $field->addOption(self::collapsedTabLocked, $this->_('Tab + Locked (not editable)')); } $field->addOption(self::collapsedHidden, $this->_('Hidden (not shown in the editor)')); $field->attr('value', (int) $this->collapsed); diff --git a/wire/core/InputfieldWrapper.php b/wire/core/InputfieldWrapper.php index 938dde23..9dcdfef2 100644 --- a/wire/core/InputfieldWrapper.php +++ b/wire/core/InputfieldWrapper.php @@ -697,7 +697,13 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre $classes = array(); $useColumnWidth = $this->useColumnWidth; $renderAjaxInputfield = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : null; - $lockedStates = array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedBlankLocked); + + $lockedStates = array( + Inputfield::collapsedNoLocked, + Inputfield::collapsedYesLocked, + Inputfield::collapsedBlankLocked, + Inputfield::collapsedTabLocked + ); if($useColumnWidth === true && isset($_classes['form']) && strpos($_classes['form'], 'InputfieldFormNoWidths') !== false) { $useColumnWidth = false; @@ -988,8 +994,9 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre public function ___renderInputfield(Inputfield $inputfield, $renderValueMode = false) { $inputfieldID = $inputfield->attr('id'); - $collapsed = $inputfield->getSetting('collapsed'); - $ajaxInputfield = $collapsed == Inputfield::collapsedYesAjax || ($collapsed == Inputfield::collapsedBlankAjax && $inputfield->isEmpty()); + $collapsed = (int) $inputfield->getSetting('collapsed'); + $ajaxInputfield = $collapsed == Inputfield::collapsedYesAjax || $collapsed === Inputfield::collapsedTabAjax + || ($collapsed == Inputfield::collapsedBlankAjax && $inputfield->isEmpty()); $ajaxHiddenInput = ""; $ajaxID = $this->wire()->config->ajax ? $this->wire()->input->get('renderInputfieldAjax') : ''; $required = $inputfield->getSetting('required'); @@ -1000,6 +1007,7 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre $ajaxInputfield = false; if($collapsed == Inputfield::collapsedYesAjax) $inputfield->collapsed = Inputfield::collapsedYes; if($collapsed == Inputfield::collapsedBlankAjax) $inputfield->collapsed = Inputfield::collapsedBlank; + if($collapsed == Inputfield::collapsedTabAjax) $inputfield->collapsed = Inputfield::collapsedTab; // indicate to next processInput that this field can be processed $inputfield->appendMarkup .= $ajaxHiddenInput; } @@ -1172,12 +1180,20 @@ class InputfieldWrapper extends Inputfield implements \Countable, \IteratorAggre Inputfield::collapsedLocked, Inputfield::collapsedNoLocked, Inputfield::collapsedBlankLocked, - Inputfield::collapsedYesLocked + Inputfield::collapsedYesLocked, + Inputfield::collapsedTabLocked, ); + + $ajaxTypes = array( + Inputfield::collapsedYesAjax, + Inputfield::collapsedBlankAjax, + Inputfield::collapsedTabAjax, + ); + $collapsed = (int) $inputfield->getSetting('collapsed'); if(in_array($collapsed, $skipTypes)) return false; - if(in_array($collapsed, array(Inputfield::collapsedYesAjax, Inputfield::collapsedBlankAjax))) { + if(in_array($collapsed, $ajaxTypes)) { $processAjax = $this->wire()->input->post('processInputfieldAjax'); if(is_array($processAjax) && in_array($inputfield->attr('id'), $processAjax)) { // field can be processed (convention used by InputfieldWrapper) diff --git a/wire/modules/Fieldtype/FieldtypeFieldsetTabOpen.module b/wire/modules/Fieldtype/FieldtypeFieldsetTabOpen.module index aafb2e29..d156011c 100644 --- a/wire/modules/Fieldtype/FieldtypeFieldsetTabOpen.module +++ b/wire/modules/Fieldtype/FieldtypeFieldsetTabOpen.module @@ -8,7 +8,7 @@ * For documentation about the fields used in this class, please see: * /wire/core/Fieldtype.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2022 by Ryan Cramer * https://processwire.com * * @@ -25,12 +25,19 @@ class InputfieldFieldsetTabOpen extends InputfieldFieldsetOpen { public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); - $inputfields->remove($inputfields->getChildByName('columnWidth')); + + $in = $inputfields->getChildByName('columnWidth'); + if($in) $inputfields->remove($in); + $in = $inputfields->getChildByName('collapsed'); //if($in->parent) $in->parent->set('collapsed', Inputfield::collapsedYes); - foreach($in->getOptions() as $key => $value) { - // tabs may not be collapsed - if($key != Inputfield::collapsedNo && $key != Inputfield::collapsedYesAjax) $in->removeOption($key); + if($in) { + foreach($in->getOptions() as $key => $value) { + // tabs may not be collapsed + if($key != Inputfield::collapsedNo && $key != Inputfield::collapsedYesAjax) { + $in->removeOption($key); + } + } } // tabs don't support showIf $in = $inputfields->getChildByName('showIf'); @@ -49,15 +56,16 @@ class FieldtypeFieldsetTabOpen extends FieldtypeFieldsetOpen { 'version' => 100, 'summary' => 'Open a fieldset to group fields. Same as Fieldset (Open) except that it displays in a tab instead.', 'permanent' => true, - ); + ); } public function getInputfield(Page $page, Field $field) { + /** @var InputfieldFieldsetTabOpen $inputfield */ $inputfield = $this->wire(new InputfieldFieldsetTabOpen()); $inputfield->class = $this->className(); - if($field->modal) { - $inputfield->modal = true; - } else if($field->collapsed == Inputfield::collapsedYesAjax) { + if($field->get('modal')) { + $inputfield->set('modal', true); + } else if($field->collapsed == Inputfield::collapsedYesAjax || $field->collapsed == Inputfield::collapsedTabAjax) { $inputfield->collapsed = $field->collapsed; } return $inputfield; @@ -70,7 +78,7 @@ class FieldtypeFieldsetTabOpen extends FieldtypeFieldsetOpen { $in->label = $this->_('Open in modal window?'); $in->description = $this->_('Check the box to make this tab open in its own modal window. This can improve performance with large forms.'); $in->notes = $this->_('To solve a similar need, you might instead consider the AJAX option, available at: Input (tab) > Visibility > Presentation.'); - if($field->modal) $in->attr('checked', 'checked'); + if($field->get('modal')) $in->attr('checked', 'checked'); $inputfields->add($in); return $inputfields; } diff --git a/wire/modules/Process/ProcessField/ProcessField.module b/wire/modules/Process/ProcessField/ProcessField.module index ae13f4b6..1bfcdc26 100644 --- a/wire/modules/Process/ProcessField/ProcessField.module +++ b/wire/modules/Process/ProcessField/ProcessField.module @@ -1175,6 +1175,15 @@ class ProcessField extends Process implements ConfigurableModule { $field->attr('value', ''); $form->append($field); } + + // move the 'tabLabel' input right below the 'collapsed' input + $f1 = $form->getChildByName('tabLabel'); + $fs = $form->getChildByName('visibility'); + $f2 = $fs ? $fs->getChildByName('collapsed') : null; + if($f1 && $f2) { + $f1->getParent()->remove($f1); + $fs->insertAfter($f1, $f2); + } $focus = $input->get('focus'); if($focus) { @@ -1649,6 +1658,19 @@ class ProcessField extends Process implements ConfigurableModule { $field->collapsed = Inputfield::collapsedBlank; $form->add($field); $languageFields[] = $field; + + /** @var InputfieldText $field */ + $field = $this->modules->get('InputfieldText'); + $field->label = $this->_('Label for tab'); + $field->attr('name', 'tabLabel'); + $field->attr('value', (string) $this->field->get('tabLabel')); + $field->icon = 'tag'; + $field->description = $this->_('If field is displayed in its own tab, optionally specify an alternate tab label if different from the field label.'); + $field->notes = $this->_('The tab label should ideally be very short, like just one word.'); + $field->collapsed = Inputfield::collapsedBlank; + $field->showIf = 'collapsed=' . Inputfield::collapsedTab . '|' . Inputfield::collapsedTabAjax . '|' . Inputfield::collapsedTabLocked; + $form->add($field); + $languageFields[] = $field; if($languages) foreach($languageFields as $field) { $field->useLanguages = true; @@ -2286,7 +2308,7 @@ class ProcessField extends Process implements ConfigurableModule { } if($name === 'tags') { - $value = $sanitizer->words($value); + $value = $sanitizer->getTextTools()->strtolower($sanitizer->words($value)); } $this->field->set($name, $value); diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index 34905569..01ff4a63 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -914,6 +914,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod Inputfield::collapsedNoLocked, Inputfield::collapsedBlankLocked, Inputfield::collapsedYesLocked, + Inputfield::collapsedTabLocked, ); $collapsed = $inputfield->getSetting('collapsed'); if($collapsed > 0 && !in_array($collapsed, $skipCollapsed)) { @@ -932,6 +933,42 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $tabWrap = null; $tabOpen = null; $tabViewable = null; + $fieldsetTab = null; + $collapsedTabTypes = array( + Inputfield::collapsedTab => 1, + Inputfield::collapsedTabAjax => 1, + Inputfield::collapsedTabLocked => 1, + ); + + // identify fields displayed as tabs and add fieldset open/close around them + foreach($contentTab as $inputfield) { + /** @var Inputfield $inputfield */ + if(!isset($collapsedTabTypes[$inputfield->collapsed])) continue; + /** @var InputfieldFieldsetTabOpen $tab */ + if(!$fieldsetTab) { + $fieldsetTab = $this->modules->get('FieldtypeFieldsetTabOpen'); + $this->modules->get('FieldtypeFieldsetClose'); + } + $tab = new InputfieldFieldsetTabOpen(); + $this->wire($tab); + $tab->attr('name', '_tab_' . $inputfield->attr('name')); + $tab->attr('id', '_tab_' . $inputfield->attr('id')); + $tab->label = $inputfield->getSetting('tabLabel|label'); + if($inputfield->collapsed === Inputfield::collapsedTabAjax) { + $tab->collapsed = Inputfield::collapsedYesAjax; + $inputfield->collapsed = Inputfield::collapsedNo; + if($this->isPost && !$contentTab->isProcessable($tab)) { + $contentTab->remove($inputfield); + continue; + } + } + $contentTab->insertBefore($tab, $inputfield); + /** @var InputfieldFieldsetClose $tabClose */ + $tabClose = new InputfieldFieldsetClose(); + $this->wire($tabClose); + $tabClose->attr('id+name', $tab->attr('name') . '_END'); + $contentTab->insertAfter($tabClose, $inputfield); + } foreach($contentTab as $inputfield) { if(!$tabOpen && $inputfield->className() === 'InputfieldFieldsetTabOpen') { @@ -945,6 +982,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod continue; } $tabOpen = $inputfield; + /** @var InputfieldWrapper $tabWrap */ $tabWrap = $this->wire(new InputfieldWrapper()); $tabWrap->attr('title', $tabOpen->getSetting('label')); $tabWrap->id = $tabOpen->attr('id');