diff --git a/modules/backend/formwidgets/TagList.php b/modules/backend/formwidgets/TagList.php
index 349ee198c..ca4460cb9 100644
--- a/modules/backend/formwidgets/TagList.php
+++ b/modules/backend/formwidgets/TagList.php
@@ -7,6 +7,12 @@ use Backend\Classes\FormWidgetBase;
*/
class TagList extends FormWidgetBase
{
+ use \Backend\Traits\FormModelWidget;
+
+ const MODE_ARRAY = 'array';
+ const MODE_STRING = 'string';
+ const MODE_RELATION = 'relation';
+
//
// Configurable properties
//
@@ -14,12 +20,27 @@ class TagList extends FormWidgetBase
/**
* @var string Tag separator: space, comma.
*/
- public $separator = 'space';
+ public $separator = 'comma';
/**
* @var bool Allows custom tags to be entered manually by the user.
*/
- public $useCustom = true;
+ public $customTags = true;
+
+ /**
+ * @var mixed Predefined options settings. Set to true to get from model.
+ */
+ public $options = null;
+
+ /**
+ * @var string Mode for the return value. Values: string, array, relation.
+ */
+ public $mode = 'string';
+
+ /**
+ * @var string If mode is relation, model column to use for the name reference.
+ */
+ public $nameFrom = 'name';
//
// Object properties
@@ -37,7 +58,9 @@ class TagList extends FormWidgetBase
{
$this->fillFromConfig([
'separator',
- 'useCustom',
+ 'customTags',
+ 'options',
+ 'mode',
]);
}
@@ -55,11 +78,10 @@ class TagList extends FormWidgetBase
*/
public function prepareVars()
{
- $this->vars['customSeparators'] = $this->getCustomSeparators();
$this->vars['field'] = $this->formField;
- $this->vars['name'] = $this->formField->getName();
- $this->vars['value'] = $this->getLoadValue();
- $this->vars['model'] = $this->model;
+ $this->vars['fieldOptions'] = $this->getFieldOptions();
+ $this->vars['selectedValues'] = $this->getLoadValue();
+ $this->vars['customSeparators'] = $this->getCustomSeparators();
}
/**
@@ -67,24 +89,102 @@ class TagList extends FormWidgetBase
*/
public function getSaveValue($value)
{
+ if ($this->mode === static::MODE_RELATION) {
+ return $this->hydrateRelationSaveValue($value);
+ }
+
+ if (is_array($value) && $this->mode === static::MODE_STRING) {
+ return implode($this->getSeparatorCharacter(), $value);
+ }
+
return $value;
}
+ /**
+ * Returns an array suitable for saving against a relation (array of keys).
+ * This method also creates non-existent tags.
+ * @return array
+ */
+ protected function hydrateRelationSaveValue($names)
+ {
+ if (!$names) {
+ return $names;
+ }
+
+ $relationModel = $this->getRelationModel();
+ $existingTags = $relationModel
+ ->whereIn($this->nameFrom, $names)
+ ->lists($this->nameFrom, $relationModel->getKeyName())
+ ;
+
+ $newTags = $this->customTags ? array_diff($names, $existingTags) : [];
+
+ foreach ($newTags as $newTag) {
+ $newModel = $relationModel::create([$this->nameFrom => $newTag]);
+ $existingTags[$newModel->id] = $newTag;
+ }
+
+ return array_keys($existingTags);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getLoadValue()
+ {
+ $value = parent::getLoadValue();
+
+ if ($this->mode === static::MODE_RELATION) {
+ return $this->getRelationObject()->lists($this->nameFrom);
+ }
+
+ return $this->mode === static::MODE_STRING
+ ? explode($this->getSeparatorCharacter(), $value)
+ : $value;
+ }
+
+ /**
+ * Returns defined field options, or from the relation if available.
+ * @return array
+ */
+ public function getFieldOptions()
+ {
+ $options = $this->formField->options();
+
+ if (!$options && $this->mode === static::MODE_RELATION) {
+ $options = $this->getRelationModel()->lists($this->nameFrom);
+ }
+
+ return $options;
+ }
+
/**
* Returns character(s) to use for separating keywords.
* @return mixed
*/
protected function getCustomSeparators()
{
- if (!$this->useCustom) {
+ if (!$this->customTags) {
return false;
}
$separators = [];
- $separators[] = $this->separator == 'comma' ? ',' : ' ';
+ $separators[] = $this->getSeparatorCharacter();
return implode('|', $separators);
}
+ /**
+ * Convert the character word to the singular character.
+ * @return string
+ */
+ protected function getSeparatorCharacter()
+ {
+ switch (strtolower($this->separator)) {
+ case 'comma': return ',';
+ case 'space': return ' ';
+ }
+ }
+
}
diff --git a/modules/backend/formwidgets/taglist/partials/_taglist.htm b/modules/backend/formwidgets/taglist/partials/_taglist.htm
index f8c6cdb90..38c37652a 100644
--- a/modules/backend/formwidgets/taglist/partials/_taglist.htm
+++ b/modules/backend/formwidgets/taglist/partials/_taglist.htm
@@ -1,15 +1,16 @@
options();
+ $availableOptions = array_unique(array_merge($selectedValues, $fieldOptions));
?>
diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php
index d800f18ca..0867fd6aa 100644
--- a/modules/backend/widgets/Form.php
+++ b/modules/backend/widgets/Form.php
@@ -690,7 +690,7 @@ class Form extends WidgetBase
/*
* Get field options from model
*/
- $optionModelTypes = ['dropdown', 'radio', 'checkboxlist', 'taglist', 'balloon-selector'];
+ $optionModelTypes = ['dropdown', 'radio', 'checkboxlist', 'balloon-selector'];
if (in_array($field->type, $optionModelTypes, false)) {
/*
@@ -770,6 +770,18 @@ class Form extends WidgetBase
$widget = $this->makeFormWidget($widgetClass, $field, $widgetConfig);
+ /*
+ * If options config is defined, request options from the model.
+ */
+ if (isset($field->config['options'])) {
+ $field->options(function () use ($field) {
+ $fieldOptions = $field->config['options'];
+ if ($fieldOptions === true) $fieldOptions = null;
+ $fieldOptions = $this->getOptionsFromModel($field, $fieldOptions);
+ return $fieldOptions;
+ });
+ }
+
return $this->formWidgets[$field->fieldName] = $widget;
}
diff --git a/modules/system/assets/ui/js/select.js b/modules/system/assets/ui/js/select.js
index 3897d94d1..3d2ccc901 100644
--- a/modules/system/assets/ui/js/select.js
+++ b/modules/system/assets/ui/js/select.js
@@ -44,7 +44,10 @@
*/
$('select.custom-select').each(function(){
var $element = $(this),
- extraOptions = {}
+ extraOptions = {
+ dropdownCssClass: '',
+ containerCssClass: ''
+ }
// Prevent duplicate loading
if ($element.data('select2') != null) {
@@ -61,23 +64,27 @@
if ($element.hasClass('select-no-search')) {
extraOptions.minimumResultsForSearch = Infinity
}
-
if ($element.hasClass('select-no-dropdown')) {
- extraOptions.dropdownCssClass = 'select-no-dropdown'
- extraOptions.containerCssClass = 'select-no-dropdown'
+ extraOptions.dropdownCssClass += ' select-no-dropdown'
+ extraOptions.containerCssClass += ' select-no-dropdown'
+ }
+
+ if ($element.hasClass('select-hide-selected')) {
+ extraOptions.dropdownCssClass += ' select-hide-selected'
}
var separators = $element.data('token-separators')
if (separators) {
extraOptions.tags = true
- extraOptions.selectOnClose = true
- extraOptions.closeOnSelect = false
extraOptions.tokenSeparators = separators.split('|')
/*
* When the dropdown is hidden, force the first option to be selected always.
*/
if ($element.hasClass('select-no-dropdown')) {
+ extraOptions.selectOnClose = true
+ extraOptions.closeOnSelect = false
+
$element.on('select2:closing', function() {
$('.select2-dropdown.select-no-dropdown:first .select2-results__option--highlighted').removeClass('select2-results__option--highlighted')
$('.select2-dropdown.select-no-dropdown:first .select2-results__option:first').addClass('select2-results__option--highlighted')
diff --git a/modules/system/assets/ui/less/select.less b/modules/system/assets/ui/less/select.less
index 44d70459b..d63ec2561 100644
--- a/modules/system/assets/ui/less/select.less
+++ b/modules/system/assets/ui/less/select.less
@@ -237,11 +237,17 @@
// No Dropdown
//------------------------------------
-
+
.select2-dropdown.select-no-dropdown {
display: none !important;
}
+ .select2-dropdown.select-hide-selected {
+ li[aria-selected=true] {
+ display: none !important;
+ }
+ }
+
// Single select
//------------------------------------
diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js
index 1af298a1c..ad8d62201 100644
--- a/modules/system/assets/ui/storm-min.js
+++ b/modules/system/assets/ui/storm-min.js
@@ -2234,19 +2234,20 @@ if(imageSrc)
return'
'+state.text
return state.text}
var selectOptions={templateResult:formatSelectOption,templateSelection:formatSelectOption,escapeMarkup:function(m){return m},width:'style'}
-$('select.custom-select').each(function(){var $element=$(this),extraOptions={}
+$('select.custom-select').each(function(){var $element=$(this),extraOptions={dropdownCssClass:'',containerCssClass:''}
if($element.data('select2')!=null){return true;}
$element.attr('data-disposable','data-disposable')
$element.one('dispose-control',function(){if($element.data('select2')){$element.select2('destroy')}})
if($element.hasClass('select-no-search')){extraOptions.minimumResultsForSearch=Infinity}
-if($element.hasClass('select-no-dropdown')){extraOptions.dropdownCssClass='select-no-dropdown'
-extraOptions.containerCssClass='select-no-dropdown'}
+if($element.hasClass('select-no-dropdown')){extraOptions.dropdownCssClass+=' select-no-dropdown'
+extraOptions.containerCssClass+=' select-no-dropdown'}
+if($element.hasClass('select-hide-selected')){extraOptions.dropdownCssClass+=' select-hide-selected'}
var separators=$element.data('token-separators')
if(separators){extraOptions.tags=true
-extraOptions.selectOnClose=true
-extraOptions.closeOnSelect=false
extraOptions.tokenSeparators=separators.split('|')
-if($element.hasClass('select-no-dropdown')){$element.on('select2:closing',function(){$('.select2-dropdown.select-no-dropdown:first .select2-results__option--highlighted').removeClass('select2-results__option--highlighted')
+if($element.hasClass('select-no-dropdown')){extraOptions.selectOnClose=true
+extraOptions.closeOnSelect=false
+$element.on('select2:closing',function(){$('.select2-dropdown.select-no-dropdown:first .select2-results__option--highlighted').removeClass('select2-results__option--highlighted')
$('.select2-dropdown.select-no-dropdown:first .select2-results__option:first').addClass('select2-results__option--highlighted')})}}
$element.select2($.extend({},selectOptions,extraOptions))})})
$(document).on('disable','select.custom-select',function(event,status){$(this).select2('enable',!status)})
diff --git a/modules/system/assets/ui/storm.css b/modules/system/assets/ui/storm.css
index 983bd73cc..77890614b 100644
--- a/modules/system/assets/ui/storm.css
+++ b/modules/system/assets/ui/storm.css
@@ -2283,6 +2283,7 @@ html.cssanimations .cursor-loading-indicator.hide{display:none}
.select2-container--default .select2-dropdown--above{margin-top:1px;-webkit-box-shadow:0 -3px 6px rgba(0,0,0,0.075);box-shadow:0 -3px 6px rgba(0,0,0,0.075)}
.select2-container--default .select2-results > .select2-results__options{font-size:14px;max-height:200px;overflow-y:auto}
.select2-container--default .select2-dropdown.select-no-dropdown{display:none !important}
+.select2-container--default .select2-dropdown.select-hide-selected li[aria-selected=true]{display:none !important}
.select2-container--default .select2-selection--single{height:38px;line-height:1.42857143;padding:8px 25px 8px 13px}
.select2-container--default .select2-selection--single .select2-selection__arrow{position:absolute;bottom:0;right:13px;top:0;width:4px}
.select2-container--default .select2-selection--single .select2-selection__arrow b{position:absolute;top:50%;height:9px;width:8px;right:3px;margin-top:-5px;line-height:9px}