Completed
Pull Request — master (#3075)
by
unknown
03:14
created

Select::readonly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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