Completed
Pull Request — master (#3075)
by
unknown
02:57
created

Select::templateResult()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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