Completed
Pull Request — master (#3039)
by
unknown
02:26
created

Select::template()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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