From 6d225f3c9992171d16fcc21e3ccb090dc6a3d786 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 20 Jun 2024 09:41:40 -0400 Subject: [PATCH] Update InputfieldTextTags to support page selection for pages having digit-only titles (such as "2024"). Plus add support for single-page selection mode (previously it only supported multi-page selection mode). --- .../InputfieldPage/InputfieldPage.module | 25 +++++- .../InputfieldTextTags/InputfieldTextTags.css | 47 ++++++++++- .../InputfieldTextTags/InputfieldTextTags.js | 14 +++- .../InputfieldTextTags.min.js | 2 +- .../InputfieldTextTags.module | 83 +++++++++++++++---- .../InputfieldTextTags.scss | 15 +++- 6 files changed, 165 insertions(+), 21 deletions(-) diff --git a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module index 8834f66d..bbec9cd0 100644 --- a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module +++ b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module @@ -85,7 +85,18 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { 'derefAsPage' => 0, 'addable' => 0, 'allowUnpub' => 0, // This option configured by FieldtypePage:Advanced - ); + ); + + /** + * Core inputfield classes that support both single and multi-selection mode + * + * @var string[] + * + */ + protected $singleOrMultiInputfields = array( + 'InputfieldPageAutocomplete', + 'InputfieldTextTags', + ); /** * Contains true when this module is in configuration state (via it's getConfigInputfields function) @@ -1505,7 +1516,13 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $fieldset->collapsed = Inputfield::collapsedYes; $inClass = $inputfield->className(); $fieldset->showIf = 'inputfield=' . $inClass; - if($inClass == 'InputfieldPageAutocomplete') $fieldset->showIf .= "|_$inClass"; + if(in_array($inClass, $this->singleOrMultiInputfields)) { + $fieldset->showIf .= "|_$inClass"; + if($inClass == 'InputfieldTextTags' && $this->derefAsPage > 0) { + $f = $fieldset->getChildByName('maxItems'); + if($f) $f->val(1); + } + } $inputfields->insertAfter($fieldset, $inputfieldSelection); } } @@ -1547,7 +1564,9 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { } else { $singles[$class] = $label; } - if($class == 'InputfieldPageAutocomplete') $singles["_$class"] = $label; + if(in_array($class, $this->singleOrMultiInputfields)) { + $singles["_$class"] = $label . ' ' . $this->_('(single)'); + } } return array( diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css index a47ebc50..daf0944a 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css @@ -1 +1,46 @@ -.InputfieldTextTags label.pw-hidden{display:none}.InputfieldTextTags input.InputfieldTextTagsSelect:not(.selectized),.InputfieldTextTags input.InputfieldTextTagsInput:not(.selectized){color:#f0f3f7}.InputfieldTextTags .InputfieldPageAdd{display:none}.Inputfield .selectize-input{border:1px solid #b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3;border-color:#b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3;box-shadow:none}.Inputfield .selectize-input:not(.has-items){background:#f0f3f7}.Inputfield .selectize-input.dropdown-active{padding-bottom:7px}.Inputfield .selectize-control.multi .selectize-input.has-items>div{background:#f0f3f7;white-space:nowrap;border:1px solid #cbd7e3;border-radius:3px}.Inputfield .selectize-control.multi .selectize-input.has-items>div a.remove{color:#555}.Inputfield .selectize-control.single .selectize-input{background:#f0f3f7;font-size:14px} +.InputfieldTextTags label.pw-hidden { + display: none; +} +.InputfieldTextTags input.InputfieldTextTagsSelect:not(.selectized), +.InputfieldTextTags input.InputfieldTextTagsInput:not(.selectized) { + color: #f0f3f7; +} +.InputfieldTextTags .InputfieldPageAdd { + display: none; +} + +.Inputfield .selectize-input { + border: 1px solid #b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3; + border-color: #b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3; + box-shadow: none; +} +.Inputfield .selectize-input:not(.has-items) { + background: #f0f3f7; +} +.Inputfield .selectize-input.dropdown-active { + padding-bottom: 7px; +} +.Inputfield .selectize-control.multi .selectize-input.has-items > div { + background: #f0f3f7; + white-space: nowrap; + border: 1px solid #cbd7e3; + border-radius: 3px; +} +.Inputfield .selectize-control.multi .selectize-input.has-items > div a.remove { + color: #555; +} +.Inputfield .selectize-control.single .selectize-input { + background: #f0f3f7; + font-size: inherit; +} + +.AdminThemeUikit .Inputfield .selectize-control.single .selectize-input:not(.uk-form-small) { + padding-top: 10px; + padding-bottom: 8px; + height: 40px; +} +.AdminThemeUikit .Inputfield .selectize-control.single:has(.selectize-input:not(.uk-form-small)) { + height: 40px; +} + +/*# sourceMappingURL=InputfieldTextTags.css.map */ diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js index f238a2de..e32d6d93 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js @@ -61,6 +61,7 @@ function InputfieldTextTags($parent) { var tags = cfgName.length ? ProcessWire.config[cfgName] : o.tags; var tagsList = []; var n = 0; + var isPageField = $select.closest('.InputfieldPage').length > 0; for(var tag in tags) { var label = tags[tag]; @@ -118,12 +119,23 @@ function InputfieldTextTags($parent) { for(var n = 0; n < items.length; n++) { var item = items[n]; if(typeof item === "object") { + if(typeof item.value === 'number' && isPageField) { + item.value = '_' + item.value.toString() + } if(typeof item.label === "undefined") { item.label = item.value; items[n] = item; } } else { - items[n] = { value: item, label: item }; + var value, label; + if(isPageField && (typeof item === 'number' || item.match(/^\d+$/))) { + value = '_' + item; + label = '' + item + } else { + value = item; + label = item; + } + items[n] = { value: value, label: label }; } } Inputfields.stopSpinner($select); diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js index 680b331e..4376fa95 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js @@ -1 +1 @@ -function InputfieldTextTags($parent){if(typeof $parent==="undefined")$parent=jQuery(".InputfieldForm");var pluginsMulti=["remove_button","drag_drop"];var pluginsSingle=[];var defaults={delimiter:" ",persist:true,submitOnReturn:false,openOnFocus:true,closeAfterSelect:true,copyClassesToDropdown:false,createOnBlur:false,selectOnTab:true,maxItems:null,create:function(input){return{value:input,text:input}}};function getRenderOptions(addLabel){return{item:function(item,escape){if(typeof item.label==="undefined"||!item.label.length)item.label=item.value;return"
"+escape(item.label)+"
"},option:function(item,escape){if(typeof item.label==="undefined"||!item.label.length)item.label=item.value;return"
"+escape(item.label)+"
"},option_create:function(data,escape){return'
'+addLabel+" "+escape(data.input)+"
"}}}function initInput($input){var o=JSON.parse($input.attr("data-opts"));var options=defaults;options.delimiter=o.delimiter;options.closeAfterSelect=o.closeAfterSelect;options.createOnBlur=o.createOnBlur;options.persist=false;options.maxItems=o.maxItems>0?o.maxItems:null;options.plugins=o.maxItems===1?pluginsSingle:pluginsMulti;options.render=getRenderOptions(o.addLabel);$input.selectize(options)}function initSelect($select){var o=JSON.parse($select.attr("data-opts"));var cfgName=typeof o.cfgName==="undefined"?"":o.cfgName;var tags=cfgName.length?ProcessWire.config[cfgName]:o.tags;var tagsList=[];var n=0;for(var tag in tags){var label=tags[tag];tagsList[n]={value:tag,label:label};n++}var options=jQuery.extend(defaults,{allowUserTags:o.allowUserTags,delimiter:o.delimiter,closeAfterSelect:o.closeAfterSelect,createOnBlur:o.createOnBlur,maxItems:o.maxItems>0?o.maxItems:null,plugins:o.maxItems===1?pluginsSingle:pluginsMulti,persist:true,valueField:"value",labelField:"label",searchField:["value","label"],options:tagsList,createFilter:function(input){if(o.allowUserTags)return true;var allow=false;for(var n=0;n"+escape(item.label)+""},option:function(item,escape){if(typeof item.label==="undefined"||!item.label.length)item.label=item.value;return"
"+escape(item.label)+"
"},option_create:function(data,escape){return'
'+addLabel+" "+escape(data.input)+"
"}}}function initInput($input){var o=JSON.parse($input.attr("data-opts"));var options=defaults;options.delimiter=o.delimiter;options.closeAfterSelect=o.closeAfterSelect;options.createOnBlur=o.createOnBlur;options.persist=false;options.maxItems=o.maxItems>0?o.maxItems:null;options.plugins=o.maxItems===1?pluginsSingle:pluginsMulti;options.render=getRenderOptions(o.addLabel);$input.selectize(options)}function initSelect($select){var o=JSON.parse($select.attr("data-opts"));var cfgName=typeof o.cfgName==="undefined"?"":o.cfgName;var tags=cfgName.length?ProcessWire.config[cfgName]:o.tags;var tagsList=[];var n=0;var isPageField=$select.closest(".InputfieldPage").length>0;for(var tag in tags){var label=tags[tag];tagsList[n]={value:tag,label:label};n++}var options=jQuery.extend(defaults,{allowUserTags:o.allowUserTags,delimiter:o.delimiter,closeAfterSelect:o.closeAfterSelect,createOnBlur:o.createOnBlur,maxItems:o.maxItems>0?o.maxItems:null,plugins:o.maxItems===1?pluginsSingle:pluginsMulti,persist:true,valueField:"value",labelField:"label",searchField:["value","label"],options:tagsList,createFilter:function(input){if(o.allowUserTags)return true;var allow=false;for(var n=0;n __('Text Tags', __FILE__), // Module Title 'summary' => __('Enables input of user entered tags or selection of predefined tags.', __FILE__), // Module Summary - 'version' => 6, + 'version' => 7, 'icon' => 'tags', ); } @@ -133,6 +134,8 @@ class InputfieldTextTags extends Inputfield implements return $this->setTagsList($value, (int) $languageId); } else if($key === 'allowUserTags' || $key === 'closeAfterSelect' || $key === 'useAjax' || $key === 'maxItems') { $value = (int) $value; + } else if($key === 'maxSelectedItems') { + $key = 'maxItems'; } return parent::set($key, $value); } @@ -170,9 +173,13 @@ class InputfieldTextTags extends Inputfield implements if($key === 'value') { if($value instanceof WireArray) { $value = explode('|', (string) $value); - } + } else if($value instanceof Page) { + $value = $value->id ? array("$value->id") : array(); + } if(is_array($value)) { $value = $this->tagArrayToString($value); + } else if("$value" === "0") { + $value = ''; } } return parent::setAttribute($key, $value); @@ -352,6 +359,9 @@ class InputfieldTextTags extends Inputfield implements } } } + foreach($tags as $key => $tag) { + if($key === 0 && "$tag" === "0") unset($tags[$key]); + } if(!$getArray) return $this->tagsListArrayToString($tags); return $tags; } @@ -584,6 +594,19 @@ class InputfieldTextTags extends Inputfield implements $out = ""; + if($config->ajax && !empty($opts['cfgName'])) { + // when renderReady was called during non-ajax, it may not have included the current value + // as would be the case when there are TextTags fields in repeaters, PageEditChildren, etc. + // so we add them here to the JS ProcessWire.config as part of the output + $script = 'script'; + $cfgName = $opts['cfgName']; + $out .= "<$script>"; + foreach($tags as $val => $label) { + $out .= "ProcessWire.config." . $cfgName . "['$val'] = '$label';"; + } + $out .= ""; + } + return $out; } @@ -653,6 +676,7 @@ class InputfieldTextTags extends Inputfield implements * */ public function ___processInput(WireInputData $input) { + if($this->isPageField()) $this->prepareInputPage($input); parent::___processInput($input); $val = $this->val(); $value = $this->validateValue($val); @@ -665,6 +689,39 @@ class InputfieldTextTags extends Inputfield implements return $this; } + /** + * Prepare input for processing when values are Page IDs + * + * This enables it to differenate between page IDs and numeric page titles. + * + * @param WireInputData $input + * + */ + protected function prepareInputPage(WireInputData $input) { + + $val = $input->get($this->name); + + if(is_string($val) && strpos($val, '|')) { + $val = explode('|', $val); + } + + // prepend "+" to indicate it is a newly-added numeric page title + // like a year (i.e. "2024") and not an existing page ID + + if(is_string($val)) { + if(ctype_digit($val)) $val = "+$val"; + } else if(is_array($val)) { + foreach($val as $k => $v) { + if(ctype_digit($v)) $val[$k] = "+$v"; + } + $val = implode('|', $val); + } + + // stuff back in post vars for processing by parent::processInput + $input->set($this->name, $val); + $_POST[$this->name] = $val; + } + /** * Encode numeric tags (like page IDs) so they aren’t lost by JSON encoding * @@ -707,10 +764,10 @@ class InputfieldTextTags extends Inputfield implements while(count($tags) > $maxItems) array_pop($tags); } - foreach(array_keys($tags) as $tag) { + foreach($tags as $tag => $label) { if(isset($validTags[$tag])) { // tag is known/valid - } else if($isPageField && $this->tagsUrl && ctype_digit(ltrim($tag, '_'))) { + } else if($isPageField && $this->tagsUrl && ctype_digit(ltrim("$tag", '_'))) { // tag is page ID from ajax: will be validated by InputfieldPage } else if(!$allowUserTags && ($isPageField || !$this->tagsUrl)) { // user tags not allowed @@ -718,6 +775,7 @@ class InputfieldTextTags extends Inputfield implements $this->error(sprintf($this->_('Removed invalid tag value: %s'), $tag)); } else { // newly added tag + if($isPageField && strpos($tag, "+") === 0) $tag = ltrim($tag, "+"); $tag = $sanitizer->text($tag); $label = $tag; if(strpos($tag, $delimiter) !== false) $tag = str_replace($delimiter, '-', $tag); @@ -1004,15 +1062,14 @@ class InputfieldTextTags extends Inputfield implements $f->set("value$language", $this->tagsListArrayToString($this->get("tagsList$language"))); } } - $fieldset->add($f); } else { /** @var InputfieldHidden $f */ $f = $modules->get('InputfieldHidden'); $f->attr('name', 'tagsList'); $f->val(''); - $fieldset->add($f); } - + $fieldset->add($f); + if($isTextField || $isPageField) { /** @var InputfieldText $f */ $inputName = $this->name; @@ -1071,30 +1128,28 @@ class InputfieldTextTags extends Inputfield implements $f->error($this->_('The placeholder “{q}” is required somewhere in your Ajax URL.')); } $f->showIf = 'useAjax=1'; - $fieldset->add($f); } else { /** @var InputfieldHidden $f */ $f = $modules->get('InputfieldHidden'); $f->attr('name', 'tagsUrl'); $f->val(''); - $fieldset->add($f); } - + $fieldset->add($f); + if($isTextField) { /** @var InputfieldToggle $f */ $f = $modules->get('InputfieldToggle'); $f->attr('name', 'allowUserTags'); $f->label = $this->_('Allow user to enter their own tags?'); $f->val($this->allowUserTags); - $fieldset->add($f); } else { /** @var InputfieldHidden $f */ $f = $modules->get('InputfieldHidden'); $f->attr('name', 'allowUserTags'); $f->val('0'); - $fieldset->add($f); } - + $fieldset->add($f); + /** @var InputfieldToggle $f */ $f = $modules->get('InputfieldToggle'); $f->attr('name', 'closeAfterSelect'); diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss index 38cf6da7..1806bc40 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss @@ -46,7 +46,20 @@ $tag-border-colors: #b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3; // in single selection mode .selectize-input { background: $tag-background-color; - font-size: 14px; + font-size: inherit; } } } + +.AdminThemeUikit .Inputfield { + .selectize-control.single { + .selectize-input:not(.uk-form-small) { + padding-top: 10px; + padding-bottom: 8px; + height: 40px; + } + } + .selectize-control.single:has(.selectize-input:not(.uk-form-small)) { + height: 40px; + } +}