From 5400ec7d2da83c17a8a805e20e4cc7c42f1fa36c Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 10 May 2016 06:02:35 +1000 Subject: [PATCH] Complete taglist form widget --- modules/backend/formwidgets/TagList.php | 118 ++++++++++++++++-- .../formwidgets/taglist/partials/_taglist.htm | 11 +- modules/backend/widgets/Form.php | 14 ++- modules/system/assets/ui/js/select.js | 19 ++- modules/system/assets/ui/less/select.less | 8 +- modules/system/assets/ui/storm-min.js | 13 +- modules/system/assets/ui/storm.css | 1 + 7 files changed, 156 insertions(+), 28 deletions(-) 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}