Completed
Pull Request — master (#3039)
by
unknown
05:36 queued 02:13
created

Select::configs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Form\Field;
4
5
use Encore\Admin\Facades\Admin;
6
use Encore\Admin\Form\Field;
7
use Illuminate\Contracts\Support\Arrayable;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Support\Arr;
10
use Illuminate\Support\Str;
11
12
class Select extends Field
13
{
14
    /**
15
     * @var array
16
     */
17
    protected static $css = [
18
        '/vendor/laravel-admin/AdminLTE/plugins/select2/select2.min.css',
19
    ];
20
21
    /**
22
     * @var array
23
     */
24
    protected static $js = [
25
        '/vendor/laravel-admin/AdminLTE/plugins/select2/select2.full.min.js',
26
    ];
27
28
    /**
29
     * @var array
30
     */
31
    protected $groups = [];
32
33
    /**
34
     * @var array
35
     */
36
    protected $config = [];
37
38
    /**
39
     * Set options.
40
     *
41
     * @param array|callable|string $options
42
     *
43
     * @return $this|mixed
44
     */
45
    public function options($options = [])
46
    {
47
        // remote options
48
        if (is_string($options)) {
49
            // reload selected
50
            if (class_exists($options) && in_array(Model::class, class_parents($options))) {
51
                return $this->model(...func_get_args());
0 ignored issues
show
Documentation introduced by
func_get_args() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
52
            }
53
54
            return $this->loadRemoteOptions(...func_get_args());
0 ignored issues
show
Documentation introduced by
func_get_args() is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
55
        }
56
57
        if ($options instanceof Arrayable) {
58
            $options = $options->toArray();
59
        }
60
61
        if (is_callable($options)) {
62
            $this->options = $options;
0 ignored issues
show
Documentation Bug introduced by
It seems like $options of type callable is incompatible with the declared type array of property $options.

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...
63
        } else {
64
            $this->options = (array) $options;
65
        }
66
67
        return $this;
68
    }
69
70
    /**
71
     * @param array $groups
72
     */
73
74
    /**
75
     * Set option groups.
76
     *
77
     * eg: $group = [
78
     *        [
79
     *        'label' => 'xxxx',
80
     *        'options' => [
81
     *            1 => 'foo',
82
     *            2 => 'bar',
83
     *            ...
84
     *        ],
85
     *        ...
86
     *     ]
87
     *
88
     * @param array $groups
89
     *
90
     * @return $this
91
     */
92
    public function groups(array $groups)
93
    {
94
        $this->groups = $groups;
95
96
        return $this;
97
    }
98
99
    public function template(array $view)
100
    {
101
        $view = array_intersect_key($view, array_flip(['result', 'selection']));
102
        if ($view) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
103
            $this->config['escapeMarkup'] = 'function (markup) {return markup;}';
104
            foreach ($view as $key => $val) {
105
                $key = ucfirst(strtolower($key));
106
                $func_key = "template{$key}";
107
                $func_name = str_replace('.', '', "{$this->getElementClassSelector()}_{$key}");
108
                $this->config[$func_key] = $func_name;
109
                $script = implode("\n", [
110
                    "{$func_name} = function(data) {",
111
                    "\tif ( !data.id || data.loading) return data.text;",
112
                    $val,
113
                    '}',
114
                ]);
115
                Admin::script($script);
116
            }
117
        }
118
119
        return $this;
120
    }
121
122
    public function readonly()
123
    {
124
        $script = <<<EOT
125
        $("form select").on("select2:opening", function (e) {
126
            if($(this).attr('readonly') || $(this).is(':hidden')){
127
            e.preventDefault();
128
            }
129
        });
130
        $(document).ready(function(){
131
            $('{$this->getElementClassSelector()}').closest('.form-group').find('span.select2-selection__choice__remove').first().remove();
132
            $('{$this->getElementClassSelector()}').closest('.form-group').find('li.select2-search').first().remove();
133
        });
134
EOT;
135
        Admin::script($script);
136
        $this->config('allowClear', false);
137
        $this->attribute('readonly');
138
139
        return $this;
140
    }
141
142
    private function buildJsJson(array $options, array $functions = [])
143
    {
144
        $functions = array_merge([
145
            'ajax',
146
            'escapeMarkup',
147
            'templateResult',
148
            'templateSelection',
149
            'initSelection',
150
            'sorter',
151
            'tokenizer',
152
        ], $functions);
153
154
        return implode(
155
            ",\n",
156
            array_map(function ($u, $v) use ($functions) {
157
                if (is_string($v)) {
158
                    return  in_array($u, $functions) ? "{$u}: {$v}" : "{$u}: \"{$v}\"";
159
                }
160
161
                return "{$u}: ".json_encode($v);
162
            }, array_keys($options), $options)
163
        );
164
    }
165
166
    private function configs($default = [], $quoted = false)
167
    {
168
        $configs = array_merge(
169
            [
170
                'allowClear'  => true,
171
                'language'    => app()->getLocale(),
172
                'placeholder' => [
173
                    'id'   => '',
174
                    'text' => $this->label,
175
                ],
176
                'escapeMarkup' => 'function (markup) {return markup;}',
177
            ],
178
            $default,
179
            $this->config
180
        );
181
        $configs = $this->buildJsJson($configs);
182
183
        return $quoted ? '{'.$configs.'}' : $configs;
184
    }
185
186
    /**
187
     * Load options for other select on change.
188
     *
189
     * @param string $field
190
     * @param string $sourceUrl
191
     * @param string $idField
192
     * @param string $textField
193
     *
194
     * @return $this
195
     */
196
    public function load($field, $sourceUrl, $idField = 'id', $textField = 'text')
197
    {
198
        if (Str::contains($field, '.')) {
199
            $field = $this->formatName($field);
200
            $class = str_replace(['[', ']'], '_', $field);
201
        } else {
202
            $class = $field;
203
        }
204
205
        $script = <<<EOT
206
$(document).off('change', "{$this->getElementClassSelector()}");
207
$(document).on('change', "{$this->getElementClassSelector()}", function () {
208
    var target = $(this).closest('.fields-group').find(".$class");
209
    if(this.value)
210
    $.get("$sourceUrl?q="+this.value, function (data) {
211
        target.find("option").remove();
212
        config=window._config[".{$class}"];
213
        config.data=$.map(data, function (d) {
214
            d.id = d.$idField;
215
            d.text = d.$textField;
216
            return d;
217
        });
218
        $(target).select2(config).trigger('change');
219
220
    });
221
});
222
EOT;
223
224
        Admin::script($script);
225
226
        return $this;
227
    }
228
229
    /**
230
     * Load options for other selects on change.
231
     *
232
     * @param string $fields
233
     * @param string $sourceUrls
234
     * @param string $idField
235
     * @param string $textField
236
     *
237
     * @return $this
238
     */
239
    public function loads($fields = [], $sourceUrls = [], $idField = 'id', $textField = 'text')
240
    {
241
        $fieldsStr = implode('.', $fields);
242
        $urlsStr = implode('^', $sourceUrls);
243
        $script = <<<EOT
244
var fields = '$fieldsStr'.split('.');
245
var urls = '$urlsStr'.split('^');
246
247
var refreshOptions = function(url, target, name) {
248
    $.get(url).then(function(data) {
249
        target.find("option").remove();
250
        config=window._config[name];
251
        config.data=$.map(data, function (d) {
252
            d.id = d.$idField;
253
            d.text = d.$textField;
254
            return d;
255
        });
256
        $(target).select2(config).trigger('change');
257
258
    });
259
};
260
261
$(document).off('change', "{$this->getElementClassSelector()}");
262
$(document).on('change', "{$this->getElementClassSelector()}", function () {
263
    var _this = this;
264
    var promises = [];
265
266
    fields.forEach(function(field, index){
267
        var target = $(_this).closest('.fields-group').find('.' + fields[index]);
268
        promises.push(refreshOptions(urls[index] + "?q="+ _this.value, target, name));
269
    });
270
271
    $.when(promises).then(function() {
272
        console.log('开始更新其它select的选择options');
273
    });
274
});
275
EOT;
276
277
        Admin::script($script);
278
279
        return $this;
280
    }
281
282
    /**
283
     * Load options from current selected resource(s).
284
     *
285
     * @param string $model
286
     * @param string $idField
287
     * @param string $textField
288
     *
289
     * @return $this
290
     */
291 View Code Duplication
    public function model($model, $idField = 'id', $textField = 'name')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
    {
293
        if (
294
            !class_exists($model)
295
            || !in_array(Model::class, class_parents($model))
296
        ) {
297
            throw new \InvalidArgumentException("[$model] must be a valid model class");
298
        }
299
300
        $this->options = function ($value) use ($model, $idField, $textField) {
0 ignored issues
show
Documentation Bug introduced by
It seems like function ($value) use($m...$idField)->toArray(); } of type object<Closure> is incompatible with the declared type array of property $options.

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...
301
            if (empty($value)) {
302
                return [];
303
            }
304
305
            $resources = [];
306
307
            if (is_array($value)) {
308
                if (Arr::isAssoc($value)) {
309
                    $resources[] = array_get($value, $idField);
310
                } else {
311
                    $resources = array_column($value, $idField);
312
                }
313
            } else {
314
                $resources[] = $value;
315
            }
316
317
            return $model::find($resources)->pluck($textField, $idField)->toArray();
318
        };
319
320
        return $this;
321
    }
322
323
    /**
324
     * Load options from remote.
325
     *
326
     * @param string $url
327
     * @param array  $parameters
328
     * @param array  $options
329
     *
330
     * @return $this
331
     */
332
    protected function loadRemoteOptions($url, $parameters = [], $options = [])
333
    {
334
        $ajaxOptions = [
335
            'url' => $url.'?'.http_build_query($parameters),
336
        ];
337
338
        $configs = $this->configs([
339
            'allowClear'         => true,
340
            'placeholder'        => [
341
                'id'        => '',
342
                'text'      => trans('admin.choose'),
343
            ],
344
        ]);
345
346
        $ajaxOptions = json_encode(array_merge($ajaxOptions, $options));
347
348
        $this->script = <<<EOT
349
350
$.ajax($ajaxOptions).done(function(data) {
351
352
  var select = $("{$this->getElementClassSelector()}");
353
354
  select.select2({
355
    data: data,
356
    $configs
357
  });
358
  
359
  var value = select.data('value') + '';
360
  
361
  if (value) {
362
    value = value.split(',');
363
    select.select2('val', value);
364
  }
365
});
366
367
EOT;
368
369
        return $this;
370
    }
371
372
    /**
373
     * Load options from ajax results.
374
     *
375
     * @param string $url
376
     * @param $idField
377
     * @param $textField
378
     *
379
     * @return $this
380
     */
381
    public function ajax($url, $idField = 'id', $textField = 'text')
382
    {
383
        $configs = $this->configs([
384
            'allowClear'         => true,
385
            'placeholder'        => $this->label,
386
            'minimumInputLength' => 1,
387
        ]);
388
389
        $this->script = <<<EOT
390
391
$("{$this->getElementClassSelector()}").select2({
392
  ajax: {
393
    url: "$url",
394
    dataType: 'json',
395
    delay: 250,
396
    data: function (params) {
397
      return {
398
        q: params.term,
399
        page: params.page
400
      };
401
    },
402
    processResults: function (data, params) {
403
      params.page = params.page || 1;
404
405
      return {
406
        results: $.map(data.data, function (d) {
407
                   d.id = d.$idField;
408
                   d.text = d.$textField;
409
                   return d;
410
                }),
411
        pagination: {
412
          more: data.next_page_url
413
        }
414
      };
415
    },
416
    cache: true
417
  },
418
  $configs,
419
  escapeMarkup: function (markup) {
420
      return markup;
421
  }
422
});
423
424
EOT;
425
426
        return $this;
427
    }
428
429
    /**
430
     * Set config for select2.
431
     *
432
     * all configurations see https://select2.org/configuration/options-api
433
     *
434
     * @param string $key
435
     * @param mixed  $val
436
     *
437
     * @return $this
438
     */
439
    public function config($key, $val)
440
    {
441
        $this->config[$key] = $val;
442
443
        return $this;
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     */
449
    public function render()
450
    {
451
        Admin::js('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/'.app()->getLocale().'.js');
452
        $configs = str_replace("\n", '', $this->configs(
453
            [
454
                'allowClear'  => true,
455
                'placeholder' => [
456
                    'id'   => '',
457
                    'text' => $this->label,
458
                ],
459
            ],
460
            true
461
        ));
462
        Admin::script("if(!window.hasOwnProperty('_config')) window._config=new Object();");
463
        Admin::script("window._config['{$this->getElementClassSelector()}']=eval('({$configs})');\n");
464
465
        if (empty($this->script)) {
466
            $this->script = "$(\"{$this->getElementClassSelector()}\").select2({$configs});";
467
        }
468
469
        if ($this->options instanceof \Closure) {
470
            if ($this->form) {
471
                $this->options = $this->options->bindTo($this->form->model());
472
            }
473
474
            $this->options(call_user_func($this->options, $this->value, $this));
475
        }
476
477
        $this->options = array_filter($this->options, 'strlen');
478
479
        $this->addVariables([
480
            'options' => $this->options,
481
            'groups'  => $this->groups,
482
        ]);
483
484
        $this->attribute('data-value', implode(',', (array) $this->value()));
485
486
        return parent::render();
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::render(); of type string|Illuminate\View\V...\Contracts\View\Factory adds the type Illuminate\Contracts\View\Factory to the return on line 486 which is incompatible with the return type declared by the interface Illuminate\Contracts\Support\Renderable::render of type string.
Loading history...
487
    }
488
}
489