Combo   A
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 392
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 23.39%

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 10
dl 0
loc 392
ccs 29
cts 124
cp 0.2339
rs 9.28
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A run() 0 6 1
A registerClientConfig() 0 9 1
A registerClientScript() 0 7 1
A getReturn() 0 4 1
A getRename() 0 4 1
A getFilter() 0 4 1
A setFilter() 0 4 1
A setRename() 0 4 1
A setReturn() 0 4 1
A getPrimaryFilter() 0 4 2
A setPrimaryFilter() 0 4 1
A getFormIsBulk() 0 4 1
A setPluginOptions() 0 4 1
A getHasId() 0 4 2
A setHasId() 0 4 1
B getCurrentOptions() 0 27 7
C init() 0 33 10
A renderInput() 0 15 2
A getPluginOptions() 0 39 3
1
<?php
2
/**
3
 * Combo widget for Yii2
4
 *
5
 * @link      https://github.com/hiqdev/yii2-combo
6
 * @package   yii2-combo
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\combo;
12
13
use hiqdev\yii2\assets\select2\Select2SelectAllAsset;
14
use Yii;
15
use yii\helpers\ArrayHelper;
16
use yii\helpers\Html;
17
use yii\helpers\Json;
18
use yii\helpers\Url;
19
use yii\web\JsExpression;
20
use yii\web\View;
21
use yii\widgets\InputWidget;
22
23
/**
24
 * Widget Combo.
25
 *
26
 * @property mixed $return see [[_return]]
27
 * @property mixed $rename see [[_rename]]
28
 * @property mixed $filter see [[_filter]]
29
 * @property mixed $pluginOptions see [[_pluginOptions]]
30
 * @property mixed $primaryFilter see [[_primaryFilter]]
31
 * @property mixed hasId
32
 */
33
class Combo extends InputWidget
34
{
35
    /**
36
     * @var array the url that will be passed to [[Url::to()]] method to create the request URL
37
     */
38
    public $url;
39
40
    /**
41
     * @var string the type of the field.
42
     * Usual should be module/comboName.
43
     * For example: client/client, hosting/account, domain/domain.
44
     * In case of the combo overriding with some specific filters,
45
     * the type should represent the filter.
46
     * For example: if the hosting/service combo is extended with filter
47
     * to show only DB services, the type should be hosting/service/db or hosting/dbService.
48
     * The decision of the style depends on overall code style and readability
49
     */
50
    public $type;
51
52
    /**
53
     * @var string the name of the representative field in the model.
54
     * Used by [[getPrimaryFilter]] to create the name of the filtering field
55
     * @see getPrimaryFilter()
56
     */
57
    public $name;
58
59
    /**
60
     * @var string md5 of the configuration.
61
     * Appears only after the combo registration in [[register]]
62
     * @see register()
63
     */
64
    public $configId;
65
66
    /**
67
     * @var array the HTML options for the input element
68
     */
69
    public $inputOptions = [];
70
71
    /**
72
     * @var string the outer element selector, that holds all of related Combos
73
     */
74
    public $formElementSelector = 'form';
75
76
    /**
77
     * @var string the language. Default is application language
78
     */
79
    public $language;
80
81
    /**
82
     * @var bool allow multiple selection
83
     */
84
    public $multiple;
85
86
    /**
87
     * @var bool allow `Select All` button. Only if multiple is true
88
     */
89
    public $selectAllButton = true;
90
91
    /**
92
     * @var array
93
     */
94
    public $current;
95
96
    /**
97
     * @var mixed returning arguments
98
     * Example:
99
     *
100
     * ```
101
     *  ['id', 'password', 'another_column']
102
     * ```
103
     *
104
     * @see getReturn()
105
     * @see setReturn()
106
     */
107
    protected $_return;
108
109
    /**
110
     * @var array renamed arguments
111
     * Example:
112
     *
113
     * ```
114
     *  [
115
     *      'new_col_name' => 'old_col_name',
116
     *      'text' => 'login',
117
     *      'deep' => 'array.subarray.value' // can extract some value from an array
118
     *  ]
119
     * ```
120
     *
121
     * @see getName()
122
     * @see setName()
123
     */
124
    protected $_rename;
125
126
    /**
127
     * @var array the static filters
128
     * Example:
129
     *
130
     * ```
131
     * [
132
     *      'someStaticValue' => ['format' => 'the_value'],
133
     *      'type'            => ['format' => 'seller'],
134
     *      'is_active'       => [
135
     *          'field'  => 'server',
136
     *          'format' => new JsExpression('function (id, text, field) {
137
     *              if (field.isSet()) {
138
     *                  return 1;
139
     *              }
140
     *          }'),
141
     *      ]
142
     * ]
143
     * @see setFilter()
144
     * @see getFilter()
145
     */
146
    protected $_filter = [];
147
148
    /**
149
     * @var string the name of the primary filter. Default: [[name]]_like
150
     * @see getPrimaryFilter
151
     * @see setPrimaryFilter
152
     */
153
    protected $_primaryFilter;
154
155
    /**
156
     * @var boolean|string whether the combo has a primary key
157
     *   null (default) - decision will be taken automatically.
158
     *                    In case when [[attribute]] has the `_id` postfix, this property will be treated as `true`
159
     *            false - the combo does not have an id. Meaning the value of the attribute will be used as the ID
160
     *           string - the explicit name of the ID attribute
161
     */
162
    public $_hasId;
163
164
    /**
165
     * Options that will be passed to the plugin.
166
     *
167
     * @var array
168
     * @see getPluginOptions()
169
     */
170
    public $_pluginOptions = [];
171
172
    /** {@inheritdoc} */
173 2
    public function init()
174
    {
175 2
        parent::init();
176
177
        // Set language
178 2
        if ($this->language === null && ($language = Yii::$app->language) !== 'en-US') {
179 2
            $this->language = substr($language, 0, 2);
180 2
        }
181 2
        if (!$this->_return) {
182 2
            $this->return = ['id'];
183 2
        }
184 2
        if (!$this->rename) {
185 2
            $this->rename = ['text' => $this->name];
186 2
        }
187 2
        if (empty($this->inputOptions['id'])) {
188
            $this->inputOptions['id'] = Html::getInputId($this->model, $this->attribute);
189
        }
190 2
        if ($this->multiple) {
191
            $this->inputOptions['multiple'] = true;
192
        }
193 2
        if (!empty($this->inputOptions['readonly'])) {
194
            // According to the HTML specification, the `select` element does not support
195
            // property `readonly`. Solution: render `readonly` field as disabled and prepend hidden
196
            // input to submit the attribute value.
197
            $this->inputOptions['disabled'] = true;
198
        }
199 2
        if (empty($this->inputOptions['data-combo-field'])) {
200 2
            $this->inputOptions['data-combo-field'] = $this->name;
201 2
        }
202 2
        if (!isset($this->inputOptions['unselect'])) {
203 2
            $this->inputOptions['unselect'] = null;
204 2
        }
205 2
    }
206
207
    public function run()
208
    {
209
        $this->registerClientConfig();
210
        $this->registerClientScript();
211
        return $this->renderInput();
212
    }
213
214
    /**
215
     * Renders text input that will be used by the plugin.
216
     * Must apply [[inputOptions]] to the HTML element.
217
     *
218
     * @param string $type for compatible reasons. Should not be used
219
     * @return string
220
     */
221
    protected function renderInput($type = null)
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
222
    {
223
        $html = [];
224
        if (!empty($this->inputOptions['readonly'])) {
225
            // As it was said in comments of `init` method, the `select` element does not support property `readonly`.
226
            // However, disabled select will not be submitted.
227
            // Solution: render hidden input to submit the attribue value.
228
            $html[] = Html::activeHiddenInput($this->model, $this->attribute, [
229
                'id' => $this->inputOptions['id'] . '-hidden',
230
            ]);
231
        }
232
        $html[] = Html::activeDropDownList($this->model, $this->attribute, $this->getCurrentOptions(), $this->inputOptions);
233
234
        return implode('', $html);
235
    }
236
237
    public function registerClientConfig()
238
    {
239
        $view = $this->view;
240
        ComboAsset::register($view);
241
242
        $pluginOptions = Json::encode($this->pluginOptions);
243
        $this->configId = md5($this->type . $pluginOptions);
244
        $view->registerJs("$.comboConfig().add('{$this->configId}', $pluginOptions);", View::POS_READY, 'combo_' . $this->configId);
245
    }
246
247
    public function registerClientScript()
248
    {
249
        $selector = $this->inputOptions['id'];
250
        $js = "if ($('#$selector').length > 0) $('#$selector').closest('{$this->formElementSelector}').combo().register('#$selector', '$this->configId');";
251
252
        $this->view->registerJs($js);
253
    }
254
255
    public function getReturn()
256
    {
257
        return $this->_return;
258
    }
259
260
    /**
261
     * @return mixed
262
     */
263
    public function getRename()
264 2
    {
265
        return $this->_rename;
266 2
    }
267
268
    /**
269
     * @return mixed
270
     */
271
    public function getFilter()
272
    {
273
        return $this->_filter;
274
    }
275
276
    /**
277
     * @param mixed $filter
278
     */
279
    public function setFilter($filter)
280
    {
281
        $this->_filter = $filter;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filter of type * is incompatible with the declared type array of property $_filter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
282
    }
283
284
    /**
285
     * @param mixed $rename
286
     */
287
    public function setRename($rename)
288 2
    {
289
        $this->_rename = $rename;
0 ignored issues
show
Documentation Bug introduced by
It seems like $rename of type * is incompatible with the declared type array of property $_rename.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
290 2
    }
291 2
292
    /**
293
     * @param mixed $return
294
     */
295
    public function setReturn($return)
296 2
    {
297
        $this->_return = $return;
298 2
    }
299 2
300
    /**
301
     * @return string
302
     * @see _primaryFilter
303
     */
304
    public function getPrimaryFilter()
305
    {
306
        return $this->_primaryFilter ?: $this->name . '_like';
307
    }
308
309
    /**
310
     * @param $primaryFilter
311
     * @see _primaryFilter
312
     */
313
    public function setPrimaryFilter($primaryFilter)
314
    {
315
        $this->_primaryFilter = $primaryFilter;
316
    }
317
318
    /**
319
     * Returns the config of the Combo, merges with the passed $config.
320
     *
321
     * @param array $options
322
     * @return array
323
     */
324
    public function getPluginOptions($options = [])
325
    {
326
        $defaultOptions = [
327
            'name' => $this->name,
328
            'type' => $this->type,
329
            'hasId' => $this->hasId,
330
            'select2Options' => [
331
                'width'       => '100%',
332
                'placeholder' => '----------',
333
                'minimumInputLength' => '0',
334
                'ajax'        => [
335
                    'url'    => Url::toRoute($this->url ?? ''),
336
                    'type'   => 'post',
337
                    'return' => $this->return,
338
                    'rename' => $this->rename,
339
                    'filter' => $this->filter,
340
                    'data' => new JsExpression("
341
                        function (event) {
342
                            return $(this).data('field').createFilter($.extend(true, {
343
                                '{$this->primaryFilter}': {format: event.term}
344
                            }, event.filters || {}));
345
                        }
346
                    "),
347
                ],
348
            ],
349
        ];
350
        if ($this->multiple && $this->selectAllButton) {
351
            Select2SelectAllAsset::register($this->view);
352
            $defaultOptions = ArrayHelper::merge($defaultOptions, [
353
                'select2Options' => [
354
                    'tags' => false,
355
                    'tokenSeparators' => [',', ', ', ' '],
356
                    'dropdownAdapter' => new JsExpression('$.fn.select2.amd.require("select2/custom/dropdown-adapter/select-all")'),
357
                ],
358
            ]);
359
        }
360
361
        return ArrayHelper::merge($defaultOptions, $this->_pluginOptions, $options);
362
    }
363
364
    public function getFormIsBulk()
365
    {
366
        return preg_match("/^\[.*\].+$/", $this->attribute);
367
    }
368
369
    /**
370
     * @param array $pluginOptions
371
     */
372
    public function setPluginOptions($pluginOptions)
373
    {
374
        $this->_pluginOptions = $pluginOptions;
375
    }
376
377
    /**
378
     * @return bool|string
379
     */
380
    public function getHasId()
381
    {
382
        return $this->_hasId === null ? (substr($this->attribute, -3) === '_id') : $this->_hasId;
383
    }
384
385
    /**
386
     * @param bool|string $hasId
387
     */
388
    public function setHasId($hasId)
389
    {
390
        $this->_hasId = $hasId;
391
    }
392
393
    /**
394
     * Method collects list of options that will be rendered inside the `select` tag.
395
     * @return array
396
     */
397
    protected function getCurrentOptions()
398
    {
399
        $value = Html::getAttributeValue($this->model, $this->attribute);
400
401
        if (!isset($value) || empty($value)) {
402
            return [];
403
        }
404
405
        if (!empty($this->current)) {
406
            return $this->current;
407
        }
408
409
        if ($this->getHasId()) {
410
            if (!is_scalar($value)) {
411
                Yii::error('When Combo has ID, property $current must be set manually, or attribute value must be a scalar. Value ' . var_export($value, true) . ' is not a scalar.', __METHOD__);
412
                return [];
413
            }
414
415
            return [$value => $value];
416
        } else {
417
            if (is_array($value)) {
418
                return array_combine(array_values($value), array_values($value));
419
            }
420
421
            return [$value => $value];
422
        }
423
    }
424
}
425