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

Select::buildJsJson()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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