Completed
Pull Request — master (#3039)
by
unknown
03:46 queued 01:17
created

Select::ajax()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

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