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

Select::buildJsJson()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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