Completed
Pull Request — master (#3039)
by
unknown
05:55
created

Select::buildJsJson()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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