Completed
Pull Request — master (#3039)
by
unknown
04:48
created

Select::loads()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 4
dl 0
loc 42
rs 9.248
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
            $('select').each(function(){
132
                if($(this).is('[readonly]')){
133
                    $(this).closest('.form-group').find('span.select2-selection__choice__remove').first().remove();
134
                    $(this).closest('.form-group').find('li.select2-search').first().remove();
135
                    $(this).closest('.form-group').find('span.select2-selection__clear').first().remove();
136
                }
137
            });
138
        });
139
EOT;
140
        Admin::script($script);
141
        // $this->config('allowClear', false);
142
        $this->attribute('readonly');
143
        return $this;
144
    }
145
146
    private function buildJsJson(array $options, array $functions = [])
147
    {
148
        $functions = array_merge([
149
            'ajax',
150
            'escapeMarkup',
151
            'templateResult',
152
            'templateSelection',
153
            'initSelection',
154
            'sorter',
155
            'tokenizer',
156
        ], $functions);
157
158
        return implode(
159
            ",\n",
160
            array_map(function ($u, $v) use ($functions) {
161
                if (is_string($v)) {
162
                    return  in_array($u, $functions) ? "{$u}: {$v}" : "{$u}: \"{$v}\"";
163
                }
164
165
                return "{$u}: " . json_encode($v);
166
            }, array_keys($options), $options)
167
        );
168
    }
169
170
    private function configs($default = [], $quoted = false)
171
    {
172
        $configs = array_merge(
173
            [
174
                'allowClear'  => true,
175
                'language'    => app()->getLocale(),
176
                'placeholder' => [
177
                    'id'   => '',
178
                    'text' => $this->label,
179
                ],
180
                'escapeMarkup' => 'function (markup) {return markup;}',
181
            ],
182
            $default,
183
            $this->config
184
        );
185
        $configs = $this->buildJsJson($configs);
186
187
        return $quoted ? '{' . $configs . '}' : $configs;
188
    }
189
190
    /**
191
     * Load options for other select on change.
192
     *
193
     * @param string $field
194
     * @param string $sourceUrl
195
     * @param string $idField
196
     * @param string $textField
197
     *
198
     * @return $this
199
     */
200
    public function load($field, $sourceUrl, $idField = 'id', $textField = 'text')
201
    {
202
        if (Str::contains($field, '.')) {
203
            $field = $this->formatName($field);
204
            $class = str_replace(['[', ']'], '_', $field);
205
        } else {
206
            $class = $field;
207
        }
208
209
        $script = <<<EOT
210
$(document).off('change', "{$this->getElementClassSelector()}");
211
$(document).on('change', "{$this->getElementClassSelector()}", function () {
212
    var target = $(this).closest('.fields-group').find(".$class");
213
    if(this.value)
214
    $.get("$sourceUrl?q="+this.value, function (data) {
215
        target.find("option").remove();
216
        config=window._config[".{$class}"];
217
        config.data=$.map(data, function (d) {
218
            d.id = d.$idField;
219
            d.text = d.$textField;
220
            return d;
221
        });
222
        $(target).select2(config).trigger('change');
223
224
    });
225
});
226
EOT;
227
228
        Admin::script($script);
229
230
        return $this;
231
    }
232
233
    /**
234
     * Load options for other selects on change.
235
     *
236
     * @param string $fields
237
     * @param string $sourceUrls
238
     * @param string $idField
239
     * @param string $textField
240
     *
241
     * @return $this
242
     */
243
    public function loads($fields = [], $sourceUrls = [], $idField = 'id', $textField = 'text')
244
    {
245
        $fieldsStr = implode('.', $fields);
246
        $urlsStr = implode('^', $sourceUrls);
247
        $script = <<<EOT
248
var fields = '$fieldsStr'.split('.');
249
var urls = '$urlsStr'.split('^');
250
251
var refreshOptions = function(url, target, name) {
252
    $.get(url).then(function(data) {
253
        target.find("option").remove();
254
        config=window._config[name];
255
        config.data=$.map(data, function (d) {
256
            d.id = d.$idField;
257
            d.text = d.$textField;
258
            return d;
259
        });
260
        $(target).select2(config).trigger('change');
261
262
    });
263
};
264
265
$(document).off('change', "{$this->getElementClassSelector()}");
266
$(document).on('change', "{$this->getElementClassSelector()}", function () {
267
    var _this = this;
268
    var promises = [];
269
270
    fields.forEach(function(field, index){
271
        var target = $(_this).closest('.fields-group').find('.' + fields[index]);
272
        promises.push(refreshOptions(urls[index] + "?q="+ _this.value, target, name));
273
    });
274
275
    $.when(promises).then(function() {
276
        console.log('开始更新其它select的选择options');
277
    });
278
});
279
EOT;
280
281
        Admin::script($script);
282
283
        return $this;
284
    }
285
286
    /**
287
     * Load options from current selected resource(s).
288
     *
289
     * @param string $model
290
     * @param string $idField
291
     * @param string $textField
292
     *
293
     * @return $this
294
     */
295 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...
296
    {
297
        if (
298
            !class_exists($model)
299
            || !in_array(Model::class, class_parents($model))
300
        ) {
301
            throw new \InvalidArgumentException("[$model] must be a valid model class");
302
        }
303
304
        $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...
305
            if (empty($value)) {
306
                return [];
307
            }
308
309
            $resources = [];
310
311
            if (is_array($value)) {
312
                if (Arr::isAssoc($value)) {
313
                    $resources[] = array_get($value, $idField);
314
                } else {
315
                    $resources = array_column($value, $idField);
316
                }
317
            } else {
318
                $resources[] = $value;
319
            }
320
321
            return $model::find($resources)->pluck($textField, $idField)->toArray();
322
        };
323
324
        return $this;
325
    }
326
327
    /**
328
     * Load options from remote.
329
     *
330
     * @param string $url
331
     * @param array  $parameters
332
     * @param array  $options
333
     *
334
     * @return $this
335
     */
336
    protected function loadRemoteOptions($url, $parameters = [], $options = [])
337
    {
338
        $ajaxOptions = [
339
            'url' => $url . '?' . http_build_query($parameters),
340
        ];
341
342
        $configs = $this->configs([
343
            'allowClear'         => true,
344
            'placeholder'        => [
345
                'id'        => '',
346
                'text'      => trans('admin.choose'),
347
            ],
348
        ]);
349
350
        $ajaxOptions = json_encode(array_merge($ajaxOptions, $options));
351
352
        $this->script = <<<EOT
353
354
$.ajax($ajaxOptions).done(function(data) {
355
356
  var select = $("{$this->getElementClassSelector()}");
357
358
  select.select2({
359
    data: data,
360
    $configs
361
  });
362
  
363
  var value = select.data('value') + '';
364
  
365
  if (value) {
366
    value = value.split(',');
367
    select.select2('val', value);
368
  }
369
});
370
371
EOT;
372
373
        return $this;
374
    }
375
376
    /**
377
     * Load options from ajax results.
378
     *
379
     * @param string $url
380
     * @param $idField
381
     * @param $textField
382
     *
383
     * @return $this
384
     */
385
    public function ajax($url, $idField = 'id', $textField = 'text')
386
    {
387
        $configs = $this->configs([
388
            'allowClear'         => true,
389
            'placeholder'        => $this->label,
390
            'minimumInputLength' => 1,
391
        ]);
392
393
        $this->script = <<<EOT
394
395
$("{$this->getElementClassSelector()}").select2({
396
  ajax: {
397
    url: "$url",
398
    dataType: 'json',
399
    delay: 250,
400
    data: function (params) {
401
      return {
402
        q: params.term,
403
        page: params.page
404
      };
405
    },
406
    processResults: function (data, params) {
407
      params.page = params.page || 1;
408
409
      return {
410
        results: $.map(data.data, function (d) {
411
                   d.id = d.$idField;
412
                   d.text = d.$textField;
413
                   return d;
414
                }),
415
        pagination: {
416
          more: data.next_page_url
417
        }
418
      };
419
    },
420
    cache: true
421
  },
422
  $configs,
423
  escapeMarkup: function (markup) {
424
      return markup;
425
  }
426
});
427
428
EOT;
429
430
        return $this;
431
    }
432
433
    /**
434
     * Set config for select2.
435
     *
436
     * all configurations see https://select2.org/configuration/options-api
437
     *
438
     * @param string $key
439
     * @param mixed  $val
440
     *
441
     * @return $this
442
     */
443
    public function config($key, $val)
444
    {
445
        $this->config[$key] = $val;
446
447
        return $this;
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453
    public function render()
454
    {
455
        Admin::js('https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/' . app()->getLocale() . '.js');
456
        $configs = str_replace("\n", "", $this->configs(
457
            [
458
                'allowClear'  => true,
459
                'placeholder' => [
460
                    'id'   => '',
461
                    'text' => $this->label,
462
                ],
463
            ],
464
            true
465
        ));
466
        Admin::script("if(!window.hasOwnProperty('_config')) window._config=new Object();");
467
        Admin::script("window._config['{$this->getElementClassSelector()}']=eval('({$configs})');\n");
468
469
        if (empty($this->script)) {
470
            $this->script = "$(\"{$this->getElementClassSelector()}\").select2({$configs});";
471
        }
472
473
        if ($this->options instanceof \Closure) {
474
            if ($this->form) {
475
                $this->options = $this->options->bindTo($this->form->model());
476
            }
477
478
            $this->options(call_user_func($this->options, $this->value, $this));
479
        }
480
481
        $this->options = array_filter($this->options, 'strlen');
482
483
        $this->addVariables([
484
            'options' => $this->options,
485
            'groups'  => $this->groups,
486
        ]);
487
488
        $this->attribute('data-value', implode(',', (array)$this->value()));
489
490
        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 490 which is incompatible with the return type declared by the interface Illuminate\Contracts\Support\Renderable::render of type string.
Loading history...
491
    }
492
}
493