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