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

Select::buildJsJson()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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