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