Completed
Pull Request — master (#3039)
by
unknown
02:33
created

Select::readonly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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