From e416ee505b40e96b905dbef95e18eaf0ac92da18 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 29 Apr 2021 09:13:16 -0400 Subject: [PATCH] Update InputfieldTextTags with support for single-select mode, option to specify max allowed items, and support for a placeholder attribute --- .../InputfieldTextTags/InputfieldTextTags.css | 2 +- .../InputfieldTextTags/InputfieldTextTags.js | 14 ++-- .../InputfieldTextTags.min.js | 2 +- .../InputfieldTextTags.module | 71 +++++++++++++++++-- .../InputfieldTextTags.scss | 36 +++++++--- 5 files changed, 103 insertions(+), 22 deletions(-) diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css index e557059d..a47ebc50 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.css @@ -1 +1 @@ -.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-control .selectize-input.has-items>div{background:#f0f3f7;white-space:nowrap;border:1px solid #cbd7e3;border-radius:3px}.Inputfield .selectize-control .selectize-input.has-items>div a.remove{color:#555}.Inputfield .selectize-input:not(.has-items){background:#f0f3f7} +.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} diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js index 1178fa79..1fc7b7c4 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.js @@ -1,9 +1,11 @@ function InputfieldTextTags($parent) { - if(typeof $parent === "undefined") $parent = $('.InputfieldForm'); + if(typeof $parent === "undefined") $parent = jQuery('.InputfieldForm'); + + var pluginsMulti = [ 'remove_button', 'drag_drop' ]; + var pluginsSingle = []; var defaults = { - plugins: [ 'remove_button', 'drag_drop' ], delimiter: ' ', persist: true, // If false, items created by the user will not show up as available options once they are unselected. submitOnReturn: false, @@ -29,6 +31,8 @@ function InputfieldTextTags($parent) { 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); $input.selectize(options); } @@ -51,6 +55,8 @@ function InputfieldTextTags($parent) { 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', @@ -126,14 +132,14 @@ function InputfieldTextTags($parent) { if($inputs.length) { $inputs.each(function() { - $input = $(this); + $input = jQuery(this); initInput($input); }); } if($selects.length) { $selects.each(function() { - var $select = $(this); + var $select = jQuery(this); initSelect($select); }); } diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.min.js index f46802bf..78ad4052 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=$(".InputfieldForm");var defaults={plugins:["remove_button","drag_drop"],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 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;$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,persist:true,valueField:"value",labelField:"label",searchField:["value","label"],options:tagsList,createFilter:function(input){if(o.allowUserTags)return true;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)+"
"}}});if(o.tagsUrl.length){options.load=function(query,callback){if(!query.length)return callback();var tagsUrl=o.tagsUrl.replace("{q}",encodeURIComponent(query));Inputfields.startSpinner($select);jQuery.ajax({url:tagsUrl,type:"GET",error:function(){Inputfields.stopSpinner($select);callback()},success:function(items){for(var n=0;n0?o.maxItems:null;options.plugins=o.maxItems===1?pluginsSingle:pluginsMulti;$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;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)+"
"}}});if(o.tagsUrl.length){options.load=function(query,callback){if(!query.length)return callback();var tagsUrl=o.tagsUrl.replace("{q}",encodeURIComponent(query));Inputfields.startSpinner($select);jQuery.ajax({url:tagsUrl,type:"GET",error:function(){Inputfields.stopSpinner($select);callback()},success:function(items){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' => 3, + 'version' => 4, 'icon' => 'tags', ); } @@ -68,7 +70,9 @@ class InputfieldTextTags extends Inputfield implements $this->set('allowUserTags', 0); $this->set('closeAfterSelect', 1); $this->set('delimiter', 's'); + $this->set('maxItems', 0); parent::set('useAjax', false); // parent and boolean intentional + $this->setAttribute('placeholder', ''); parent::__construct(); } @@ -83,7 +87,9 @@ class InputfieldTextTags extends Inputfield implements $languages = $this->wire()->languages; if($languages) { foreach($languages as $language) { - if(!$language->isDefault()) $this->set("tagsList$language->id", array()); + if($language->isDefault()) continue; + $this->set("tagsList$language->id", array()); + parent::set("placeholder$language->id", ''); } } } @@ -117,11 +123,30 @@ class InputfieldTextTags extends Inputfield implements if($key === 'tagsList') return $this->setTagsList($value); list(,$languageId) = explode('tagsList', $key, 2); return $this->setTagsList($value, (int) $languageId); - } else if($key === 'allowUserTags' || $key === 'closeAfterSelect' || $key === 'useAjax') { + } else if($key === 'allowUserTags' || $key === 'closeAfterSelect' || $key === 'useAjax' || $key === 'maxItems') { $value = (int) $value; } return parent::set($key, $value); } + + /** + * Get all attributes in an associative array + * + * @return array + * + */ + public function getAttributes() { + $attrs = parent::getAttributes(); + if(!empty($attrs['placeholder'])) { + // placeholder attribute, languages support + list($languages, $language) = array($this->wire()->languages, $this->wire()->user->language); + if($languages && "$language" && !$language->isDefault()) { + $placeholder = parent::get("placeholder$language->id"); + if(strlen($placeholder)) $attrs['placeholder'] = $placeholder; + } + } + return $attrs; + } /** * Set attribute @@ -531,6 +556,7 @@ class InputfieldTextTags extends Inputfield implements 'closeAfterSelect' => $this->closeAfterSelect, 'createOnBlur' => $this->allowUserTags() && $this->isTextField(), 'delimiter' => $this->delimiter(), + 'maxItems' => $this->maxItems, 'tagsUrl' => $tagsUrl, ); @@ -625,8 +651,13 @@ class InputfieldTextTags extends Inputfield implements $isPageField = $this->isPageField(); $validTags = $this->getTagsList(); $delimiter = $this->delimiter(); + $maxItems = (int) $this->maxItems; if(!is_array($tags)) $tags = $this->tagStringToArray($tags); + + if($maxItems > 0) { + while(count($tags) > $maxItems) array_pop($tags); + } foreach(array_keys($tags) as $tag) { if(isset($validTags[$tag])) { @@ -646,7 +677,7 @@ class InputfieldTextTags extends Inputfield implements if($isPageField) unset($tags[$tag]); // stuffed into addedTags which is handled by processInput() } } - + return trim(implode($delimiter, $tags)); } @@ -906,7 +937,8 @@ class InputfieldTextTags extends Inputfield implements $f->description .= ' ' . $this->_('To define separate labels per-language, re-enter each tag `value=label` for each language, where the `value` is the same for each while the `label` differs.'); $f->useLanguages = true; foreach($languages as $language) { - if(!$language->isDefault()) $f->set("value$language", $this->tagsListArrayToString($this->get("tagsList$language"))); + if($language->isDefault()) continue; + $f->set("value$language", $this->tagsListArrayToString($this->get("tagsList$language"))); } } $fieldset->add($f); @@ -997,7 +1029,7 @@ class InputfieldTextTags extends Inputfield implements $f->val('0'); $fieldset->add($f); } - + /** @var InputfieldToggle $f */ $f = $modules->get('InputfieldToggle'); $f->attr('name', 'closeAfterSelect'); @@ -1019,7 +1051,34 @@ class InputfieldTextTags extends Inputfield implements $f->val($this->delimiter(true)); $fieldset->add($f); } + + /** @var InputfieldInteger $f */ + $f = $modules->get('InputfieldInteger'); + $f->attr('name', 'maxItems'); + $f->label = $this->_('Max tags/options'); + $f->description = + $this->_('Use 0 for no limit or enter the max number of options/tags the user may select or input.') . ' ' . + $this->_('Note that entering 1 makes the input appear more like a select than a text input.'); + $f->val($this->maxItems); + $fieldset->add($f); + /** @var InputfieldText $f */ + $f = $modules->get('InputfieldText'); + $f->attr('name', 'placeholder'); + $f->label = $this->_('Placeholder text'); + $f->val($this->attr('placeholder')); + $f->description = $this->_('Optional placeholder text that appears in the field when blank.'); + $f->collapsed = Inputfield::collapsedBlank; + if($languages) { + $f->useLanguages = true; + foreach($languages as $language) { + if($language->isDefault()) continue; + $value = $this->getSetting("placeholder$language"); + if($value !== null) $f->set("value$language", $value); + } + } + $fieldset->add($f); + return $inputfields; } diff --git a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss index 7b401537..38cf6da7 100644 --- a/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss +++ b/wire/modules/Inputfield/InputfieldTextTags/InputfieldTextTags.scss @@ -16,21 +16,37 @@ $tag-border-colors: #b1c3d4 #cbd7e3 #cbd7e3 #cbd7e3; } .Inputfield { + .selectize-input { border: 1px solid $tag-border-colors; border-color: $tag-border-colors; box-shadow: none; - } - .selectize-control .selectize-input.has-items > div { - background: $tag-background-color; - white-space: nowrap; - border: 1px solid $tag-border-color; - border-radius: 3px; - a.remove { - color: #555; + &:not(.has-items) { + background: $tag-background-color; + } + &.dropdown-active { + padding-bottom: 7px; // rather than 8px, to offset 1 px in dropdown } } - .selectize-input:not(.has-items) { - background: $tag-background-color; + + .selectize-control.multi { + // in multi-selection mode + .selectize-input.has-items > div { + background: $tag-background-color; + white-space: nowrap; + border: 1px solid $tag-border-color; + border-radius: 3px; + a.remove { + color: #555; + } + } + } + + .selectize-control.single { + // in single selection mode + .selectize-input { + background: $tag-background-color; + font-size: 14px; + } } }