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