Passed
Push — fix-html ( 588533 )
by Alexander
12:56
created

Field::enclosedByContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form\Widget;
6
7
use InvalidArgumentException;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Factory\Exceptions\InvalidConfigException;
10
use Yiisoft\Html\Html;
11
use Yiisoft\Widget\Widget;
12
use Yiisoft\Form\FormModelInterface;
13
use Yiisoft\Form\Helper\HtmlForm;
14
15
use function array_merge;
16
use function strtr;
17
18
/**
19
 * Renders the field widget along with label, error tag and hint tag (if any) according to template.
20
 */
21
final class Field extends Widget
22
{
23
    private const DEFAULT_CONTAINER_OPTIONS = ['class' => 'form-group'];
24
    private const DEFAULT_ERROR_OPTIONS = ['class' => 'help-block'];
25
    private const DEFAULT_HINT_OPTIONS = ['class' => 'hint-block'];
26
    private const DEFAULT_LABEL_OPTIONS = ['class' => 'control-label'];
27
28
    private FormModelInterface $data;
29
    private string $attribute;
30
    private array $options = [];
31
    private array $inputOptions = [];
32
    private array $errorOptions = [];
33
    private array $hintOptions = [];
34
    private array $labelOptions = [];
35
    private bool $ariaAttribute = true;
36
    private string $errorCssClass = 'has-error';
37
    private string $errorSummaryCssClass = 'error-summary';
38
    private string $inputCssClass = 'form-control';
39
    private string $requiredCssClass = 'required';
40
    private string $successCssClass = 'has-success';
41
    private string $template = "{label}\n{input}\n{hint}\n{error}";
42
    private string $validatingCssClass = 'validating';
43
    private string $validationStateOn = 'input';
44
    private ?string $inputId = null;
45
    private array $parts = [];
46
    private bool $skipForInLabel = false;
47
    private bool $containerEnabled = true;
48
49
    /**
50
     * Renders the whole field.
51
     *
52
     * This method will generate the label, error tag, input tag and hint tag (if any), and assemble them into HTML
53
     * according to {@see template}.
54
     *
55
     * @param string|null $content the content within the field container.
56
     *
57
     * If `null` (not set), the default methods will be called to generate the label, error tag and input tag, and use
58
     * them as the content.
59
     *
60
     * @throws InvalidConfigException
61
     *
62
     * @return string the rendering result.
63
     */
64
    public function run(?string $content = null): string
65
    {
66
        if ($content === null) {
67
            if (!isset($this->parts['{input}'])) {
68
                $this->textInput();
69
            }
70
71
            if (!isset($this->parts['{label}'])) {
72
                $this->label();
73
            }
74
75
            if (!isset($this->parts['{error}'])) {
76
                $this->error();
77
            }
78
79
            if (!isset($this->parts['{hint}'])) {
80
                $this->hint();
81
            }
82
83
            $content = strtr($this->template, $this->parts);
84
        } else {
85
            $content = $content($this);
86
        }
87
88
        if ($this->containerEnabled) {
89
            return $this->renderBegin() . "\n" . $content . "\n" . $this->renderEnd();
90
        }
91
92
        return $content;
93
94
    }
95
96
    /**
97
     * Renders the opening tag of the field container.
98
     *
99
     * @throws InvalidArgumentException
100
     *
101
     * @return string the rendering result.
102
     */
103
    private function renderBegin(): string
104
    {
105
        $new = clone $this;
106
107
        $inputId = $new->addInputId();
108
109
        $class = [];
110
        $class[] = "field-$inputId";
111
        $class[] = $new->options['class'] ?? '';
112
113
        $new->options['class'] = trim(implode(' ', array_merge(self::DEFAULT_CONTAINER_OPTIONS, $class)));
114
115
        $new->addErrorCssClassToContainer();
116
117
        $tag = ArrayHelper::remove($new->options, 'tag', 'div');
118
119
        return Html::beginTag($tag, $new->options);
120
    }
121
122
    /**
123
     * Renders the closing tag of the field container.
124
     *
125
     * @return string the rendering result.
126
     */
127
    private function renderEnd(): string
128
    {
129
        return Html::endTag(ArrayHelper::keyExists($this->options, 'tag') ? $this->options['tag'] : 'div');
130
    }
131
132
    /**
133
     * Generates a label tag for {@see attribute}.
134
     *
135
     * @param bool $enabledLabel enabled/disable <label>.
136
     * @param array $options the tag options in terms of name-value pairs. It will be merged with {@see labelOptions}.
137
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
138
     * {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be rendered.
139
     * @param string|null $label the label to use.
140
     * If `null`, the label will be generated via {@see FormModel::getAttributeLabel()}.
141
     * Note that this will NOT be {@see \Yiisoft\Html\Html::encode()|encoded}.
142
     *
143
     * @throws InvalidConfigException
144
     *
145
     * @return self the field object itself.
146
     */
147
    public function label(bool $enabledLabel = true, array $options = [], ?string $label = null): self
148
    {
149
        if ($enabledLabel === false) {
150
            $this->parts['{label}'] = '';
151
152
            return $this;
153
        }
154
155
        $new = clone $this;
156
157
        if ($label !== null) {
158
            $new->labelOptions['label'] = $label;
159
        }
160
161
        $new->labelOptions($options);
162
        $new->skipForInLabel();
163
164
        unset($options['class']);
165
166
        $new->labelOptions = array_merge($new->labelOptions, $options);
167
168
        $this->parts['{label}'] = Label::widget()
169
            ->config($new->data, $new->attribute, $new->labelOptions)
0 ignored issues
show
Bug introduced by
The method config() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Widget\Tests\Stubs\TestWidget or Yiisoft\Widget\Tests\Stubs\TestWidgetA or Yiisoft\Widget\Tests\Stubs\TestInjectionWidget or Yiisoft\Form\Widget\Form or Yiisoft\Widget\Tests\Stubs\TestWidgetB. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
            ->/** @scrutinizer ignore-call */ config($new->data, $new->attribute, $new->labelOptions)
Loading history...
170
            ->run();
171
172
        return $this;
173
    }
174
175
    /**
176
     * Generates a tag that contains the first validation error of {@see attribute}.
177
     *
178
     * Note that even if there is no validation error, this method will still return an empty error tag.
179
     *
180
     * @param array $options the tag options in terms of name-value pairs. It will be merged with
181
     * {@see DEFAULT_ERROR_OPTIONS}.
182
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
183
     * {@see \Yiisoft\Html\Html::encode()}. If this parameter is `false`, no error tag will be rendered.
184
     *
185
     * The following options are specially handled:
186
     *
187
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
188
     *
189
     * If you set a custom `id` for the error element, you may need to adjust the {@see $selectors} accordingly.
190
     *
191
     * @return self the field object itself.
192
     *
193
     * {@see DEFAULT_ERROR_OPTIONS}
194
     *@throws InvalidConfigException
195
     *
196
     */
197
    public function error(array $options = []): self
198
    {
199
        $new = clone $this;
200
201
        $new->errorOptions($options);
202
203
        unset($options['class']);
204
205
        $new->errorOptions = array_merge($new->errorOptions, $options);
206
207
        $this->parts['{error}'] = Error::widget()
208
            ->config($new->data, $new->attribute, $new->errorOptions)
209
            ->run();
210
211
        return $this;
212
    }
213
214
    /**
215
     * Renders the hint tag.
216
     *
217
     * @param string|null $content the hint content. If `null`, the hint will be generated via
218
     * {@see FormModel::getAttributeHint()}.
219
     * @param bool $typeHint If `false`, the generated field will not contain the hint part. Note that this will NOT be
220
     * {@see \Yiisoft\Html\Html::encode()|encoded}.
221
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
222
     * the hint tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
223
     *
224
     * The following options are specially handled:
225
     *
226
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
227
     *
228
     * @throws InvalidConfigException
229
     *
230
     * @return self the field object itself.
231
     */
232
    public function hint(?string $content = null, bool $typeHint = true, array $options = []): self
233
    {
234
        if ($typeHint === false) {
235
            $this->parts['{hint}'] = '';
236
237
            return $this;
238
        }
239
240
        $new = clone $this;
241
242
        $new->hintOptions($options);
243
244
        unset($options['class']);
245
246
        if ($content !== null) {
247
            $new->hintOptions['hint'] = $content;
248
        }
249
250
        $new->hintOptions = array_merge($new->hintOptions, $options);
251
252
        $this->parts['{hint}'] = Hint::widget()
253
            ->config($new->data, $new->attribute, $new->hintOptions)
254
            ->run();
255
256
        return $this;
257
    }
258
259
    /**
260
     * Renders an input tag.
261
     *
262
     * @param string $type the input type (e.g. `text`, `password`).
263
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
264
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
265
     *
266
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
267
     *
268
     * @throws InvalidArgumentException
269
     * @throws InvalidConfigException
270
     *
271
     * @return self the field object itself.
272
     */
273
    public function input(string $type, array $options = []): self
274
    {
275
        $new = clone $this;
276
277
        $new->setAriaAttributes($options);
278
        $new->inputOptions($options);
279
        $new->addErrorCssClassToInput();
280
281
        unset($options['class']);
282
283
        $new->inputOptions = array_merge($options, $new->inputOptions);
284
285
        $this->parts['{input}'] = Input::widget()
286
            ->type($type)
0 ignored issues
show
Bug introduced by
The method type() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Form\Widget\ListBox or Yiisoft\Form\Widget\Input or Yiisoft\Form\Widget\ListInput or Yiisoft\Form\Widget\BooleanInput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

286
            ->/** @scrutinizer ignore-call */ type($type)
Loading history...
287
            ->config($new->data, $new->attribute, $new->inputOptions)
288
            ->run();
289
290
        return $this;
291
    }
292
293
    /**
294
     * Renders a text input.
295
     *
296
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
297
     * they are explicitly specified in `$options`.
298
     *
299
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes
300
     * of the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
301
     *
302
     * The following special options are recognized:
303
     *
304
     * Note that if you set a custom `id` for the input element, you may need to adjust the value of {@see selectors}
305
     * accordingly.
306
     *
307
     * @throws InvalidConfigException
308
     *
309
     * @return self the field object itself.
310
     */
311
    public function textInput(array $options = []): self
312
    {
313
        $new = clone $this;
314
315
        $new->setAriaAttributes($options);
316
        $new->inputOptions($options);
317
        $new->addErrorCssClassToInput();
318
319
        unset($options['class']);
320
321
        $new->inputOptions = array_merge($options, $new->inputOptions);
322
323
        $this->parts['{input}'] = TextInput::widget()
324
            ->config($new->data, $new->attribute, $new->inputOptions)
325
            ->run();
326
327
        return $this;
328
    }
329
330
    /**
331
     * Renders a hidden input.
332
     *
333
     * Note that this method is provided for completeness. In most cases because you do not need to validate a hidden
334
     * input, you should not need to use this method. Instead, you should use
335
     * {@see \Yiisoft\Html\Html::activeHiddenInput()}.
336
     *
337
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
338
     * they are explicitly specified in `$options`.
339
     *
340
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
341
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
342
     *
343
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
344
     *
345
     * @throws InvalidConfigException
346
     *
347
     * @return self the field object itself.
348
     */
349
    public function hiddenInput(array $options = []): self
350
    {
351
        $new = clone $this;
352
353
        $new->inputOptions($options);
354
355
        unset($options['class']);
356
357
        $new->inputOptions = array_merge($options, $new->inputOptions);
358
359
        $this->parts['{label}'] = '';
360
        $this->parts['{hint}'] = '';
361
        $this->parts['{error}'] = '';
362
        $this->parts['{input}'] = HiddenInput::widget()
363
            ->config($new->data, $new->attribute, $new->inputOptions)
364
            ->run();
365
366
        return $this;
367
    }
368
369
    /**
370
     * Renders a password input.
371
     *
372
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
373
     * they are explicitly specified in `$options`.
374
     *
375
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
376
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
377
     *
378
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
379
     *
380
     * @throws InvalidArgumentException
381
     * @throws InvalidConfigException
382
     *
383
     * @return self the field object itself.
384
     */
385
    public function passwordInput(array $options = []): self
386
    {
387
        $new = clone $this;
388
389
        $new->setAriaAttributes($options);
390
        $new->inputOptions($options);
391
        $new->addErrorCssClassToInput();
392
393
        unset($options['class']);
394
395
        $new->inputOptions = array_merge($options, $new->inputOptions);
396
397
        $this->parts['{input}'] = PasswordInput::widget()
398
            ->config($new->data, $new->attribute, $new->inputOptions)
399
            ->run();
400
401
        return $this;
402
    }
403
404
    /**
405
     * Renders a file input.
406
     *
407
     * This method will generate the `name` tag attribute automatically for the model attribute unless
408
     * they are explicitly specified in `$options`.
409
     *
410
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
411
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
412
     * @param bool $withoutHiddenInput enable/disable hidden input field.
413
     *
414
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
415
     *
416
     * @throws InvalidArgumentException
417
     * @throws InvalidConfigException
418
     *
419
     * @return self the field object itself.
420
     */
421
    public function fileInput(array $options = [], bool $withoutHiddenInput = false): self
422
    {
423
        $new = clone $this;
424
425
        $new->setAriaAttributes($options);
426
        $new->addErrorCssClassToInput();
427
        $new->inputOptions($options);
428
        $new->setForInLabel($options);
429
430
        unset($options['class']);
431
432
        $new->inputOptions = array_merge($options, $new->inputOptions);
433
434
        $this->parts['{input}'] = FileInput::widget()
435
            ->config($new->data, $new->attribute, $new->inputOptions)
436
            ->withoutHiddenInput($withoutHiddenInput)
437
            ->run();
438
439
        return $this;
440
    }
441
442
    /**
443
     * Renders a text area.
444
     *
445
     * The model attribute value will be used as the content in the textarea.
446
     *
447
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
448
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
449
     *
450
     * If you set a custom `id` for the textarea element, you may need to adjust the {@see $selectors} accordingly.
451
     *
452
     * @throws InvalidArgumentException
453
     * @throws InvalidConfigException
454
     *
455
     * @return self the field object itself.
456
     */
457
    public function textArea(array $options = []): self
458
    {
459
        $new = clone $this;
460
461
        $new->setAriaAttributes($options);
462
        $new->inputOptions($options);
463
        $new->addErrorCssClassToInput();
464
465
        unset($options['class']);
466
467
        $new->inputOptions = array_merge($options, $new->inputOptions);
468
469
        $this->parts['{input}'] = TextArea::widget()
470
            ->config($new->data, $new->attribute, $new->inputOptions)
471
            ->run();
472
473
        return $this;
474
    }
475
476
    /**
477
     * Renders a radio button.
478
     *
479
     * This method will generate the `checked` tag attribute according to the model attribute value.
480
     *
481
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
482
     *
483
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
484
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
485
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
486
     * want any hidden input, you should explicitly set this option as `null`.
487
     * - `label`: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can
488
     * pass in HTML code such as an image tag. If this is coming from end users, you should
489
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
490
     * When this option is specified, the radio button will be enclosed by a label tag. If you do not want any label,
491
     * you should explicitly set this option as `null`.
492
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
493
     * specified.
494
     *
495
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
496
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
497
     * rendered.
498
     *
499
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
500
     * @param bool $enclosedByLabel whether to enclose the radio within the label.
501
     * If `true`, the method will still use {@see template} to layout the radio button and the error message except
502
     * that the radio is enclosed by the label tag.
503
     *
504
     * @throws InvalidArgumentException
505
     * @throws InvalidConfigException
506
     *
507
     * @return self the field object itself.
508
     */
509
    public function radio(array $options = [], bool $enclosedByLabel = true): self
510
    {
511
        $new = clone $this;
512
513
        if ($enclosedByLabel) {
514
            $this->parts['{label}'] = '';
515
        }
516
517
        $this->parts['{input}'] = Radio::widget()
518
            ->config($new->data, $new->attribute, $options)
519
            ->enclosedByLabel($enclosedByLabel)
520
            ->run();
521
522
        $new->setAriaAttributes($options);
523
        $new->addErrorCssClassToInput();
524
525
        return $this;
526
    }
527
528
    /**
529
     * Renders a checkbox.
530
     *
531
     * This method will generate the `checked` tag attribute according to the model attribute value.
532
     *
533
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
534
     *
535
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
536
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
537
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
538
     * want any hidden input, you should explicitly set this option as `null`.
539
     * - `label`: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass
540
     * in HTML code such as an image tag. If this is coming from end users, you should
541
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
542
     * When this option is specified, the checkbox will be enclosed by a label tag. If you do not want any label, you
543
     * should explicitly set this option as `null`.
544
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
545
     * specified.
546
     *
547
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
548
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
549
     * rendered.
550
     *
551
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
552
     * @param bool $enclosedByLabel whether to enclose the checkbox within the label.
553
     * If `true`, the method will still use [[template]] to layout the checkbox and the error message except that the
554
     * checkbox is enclosed by the label tag.
555
     *
556
     * @throws InvalidArgumentException
557
     * @throws InvalidConfigException
558
     *
559
     * @return self the field object itself.
560
     */
561
    public function checkbox(array $options = [], bool $enclosedByLabel = true): self
562
    {
563
        $new = clone $this;
564
565
        if ($enclosedByLabel) {
566
            $this->parts['{label}'] = '';
567
        }
568
569
        $this->parts['{input}'] = CheckBox::widget()
570
            ->config($new->data, $new->attribute, $options)
571
            ->enclosedByLabel($enclosedByLabel)
572
            ->run();
573
574
        $new->addErrorCssClassToInput();
575
576
        return $this;
577
    }
578
579
    /**
580
     * Renders a drop-down list.
581
     *
582
     * The selection of the drop-down list is taken from the value of the model attribute.
583
     *
584
     * @param array $items the option data items. The array keys are option values, and the array values are the
585
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
586
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
587
     * If you have a list of data models, you may convert them into the format described above using
588
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
589
     *
590
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
591
     * labels will also be HTML-encoded.
592
     * @param array $options the tag options in terms of name-value pairs.
593
     *
594
     * For the list of available options please refer to the `$options` parameter of
595
     * {@see \Yiisoft\Html\Html::activeDropDownList()}.
596
     *
597
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
598
     *
599
     * @throws InvalidArgumentException
600
     * @throws InvalidConfigException
601
     *
602
     * @return self the field object itself.
603
     */
604
    public function dropDownList(array $items, array $options = []): self
605
    {
606
        $new = clone $this;
607
608
        $new->setAriaAttributes($options);
609
        $new->inputOptions($options);
610
        $new->addErrorCssClassToInput();
611
612
        unset($options['class']);
613
614
        $new->inputOptions = array_merge($options, $new->inputOptions);
615
616
        $this->parts['{input}'] = DropDownList::widget()
617
            ->config($new->data, $new->attribute, $new->inputOptions)
618
            ->items($items)
619
            ->run();
620
621
        return $this;
622
    }
623
624
    /**
625
     * Renders a list box.
626
     *
627
     * The selection of the list box is taken from the value of the model attribute.
628
     *
629
     * @param array $items the option data items. The array keys are option values, and the array values are the
630
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
631
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
632
     * If you have a list of data models, you may convert them into the format described above using
633
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
634
     *
635
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
636
     * labels will also be HTML-encoded.
637
     * @param array $options the tag options in terms of name-value pairs.
638
     *
639
     * For the list of available options please refer to the `$options` parameter of
640
     * {@see \Yiisoft\Html\Html::activeListBox()}.
641
     *
642
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
643
     *
644
     * @throws InvalidArgumentException
645
     * @throws InvalidConfigException
646
     *
647
     * @return self the field object itself.
648
     */
649
    public function listBox(array $items, array $options = []): self
650
    {
651
        $new = clone $this;
652
653
        $new->setForInLabel($options);
654
        $new->setAriaAttributes($options);
655
        $new->inputOptions = array_merge($options, $new->inputOptions);
656
657
        $this->parts['{input}'] = ListBox::widget()
658
            ->config($new->data, $new->attribute, $new->inputOptions)
659
            ->items($items)
660
            ->run();
661
662
        return $this;
663
    }
664
665
    /**
666
     * Renders a list of checkboxes.
667
     *
668
     * A checkbox list allows multiple selection, like {@see listBox()}.
669
     * As a result, the corresponding submitted value is an array.
670
     * The selection of the checkbox list is taken from the value of the model attribute.
671
     *
672
     * @param array $items the data item used to generate the checkboxes.
673
     * The array values are the labels, while the array keys are the corresponding checkbox values.
674
     * @param array $options options (name => config) for the checkbox list.
675
     * For the list of available options please refer to the `$options` parameter of
676
     * {@see \Yiisoft\Html\Html::activeCheckboxList()}.
677
     *
678
     * @throws InvalidArgumentException
679
     * @throws InvalidConfigException
680
     *
681
     * @return self the field object itself.
682
     */
683
    public function checkboxList(array $items, array $options = []): self
684
    {
685
        $new = clone $this;
686
687
        $new->setForInLabel($options);
688
        $new->setAriaAttributes($options);
689
        $new->inputOptions = array_merge($options, $new->inputOptions);
690
        $new->skipForInLabel = true;
691
692
        $this->parts['{input}'] = CheckBoxList::widget()
693
            ->config($new->data, $new->attribute, $new->inputOptions)
694
            ->items($items)
695
            ->run();
696
697
        return $this;
698
    }
699
700
    /**
701
     * Renders a list of radio buttons.
702
     *
703
     * A radio button list is like a checkbox list, except that it only allows single selection.
704
     * The selection of the radio buttons is taken from the value of the model attribute.
705
     *
706
     * @param array $items the data item used to generate the radio buttons.
707
     * The array values are the labels, while the array keys are the corresponding radio values.
708
     * @param array $options options (name => config) for the radio button list.
709
     * For the list of available options please refer to the `$options` parameter of
710
     * {@see \Yiisoft\Html\Html::activeRadioList()}.
711
     *
712
     * @throws InvalidArgumentException
713
     * @throws InvalidConfigException
714
     *
715
     * @return self the field object itself.
716
     */
717
    public function radioList(array $items, array $options = []): self
718
    {
719
        $new = clone $this;
720
721
        $new->setAriaAttributes($options);
722
        $new->setForInLabel($options);
723
        $new->inputOptions($options);
724
        $new->addErrorCssClassToInput();
725
        $new->setInputRole($options);
726
        $new->inputOptions = array_merge($options, $new->inputOptions);
727
        $new->skipForInLabel = true;
728
729
        $this->parts['{input}'] = RadioList::widget()
730
            ->config($new->data, $new->attribute, $new->inputOptions)
731
            ->items($items)
732
            ->run();
733
734
        return $this;
735
    }
736
737
    private function addInputId(): string
738
    {
739
        return $this->inputId ?: HtmlForm::getInputId($this->data, $this->attribute);
740
    }
741
742
    /**
743
     * Set form model and name for the widget.
744
     *
745
     * @param FormModelInterface $data Form model.
746
     * @param string $attribute Form model property this widget is rendered for.
747
     *
748
     * @return self
749
     */
750
    public function config(FormModelInterface $data, string $attribute): self
751
    {
752
        $new = clone $this;
753
        $new->data = $data;
754
        $new->attribute = $attribute;
755
        return $new;
756
    }
757
758
    /**
759
     * Generate a container tag for {@see attribute}.
760
     *
761
     * @param bool $containerEnabled enabled/disable container for the widget.
762
     * @param array $options The HTML attributes for the widget container tag if it is enabled.
763
     * See {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
764
     * @return self
765
     */
766
    public function enclosedByContainer(bool $containerEnabled, array $options = []): self
767
    {
768
        $new = clone $this;
769
        $new->options = $options;
770
        $new->containerEnabled = $containerEnabled;
771
        return $new;
772
    }
773
774
    public function ariaAttribute(bool $value): self
775
    {
776
        $new = clone $this;
777
        $new->ariaAttribute = $value;
778
        return $new;
779
    }
780
781
    public function errorCssClass(string $value): self
782
    {
783
        $new = clone $this;
784
        $new->errorCssClass = $value;
785
        return $new;
786
    }
787
788
    public function errorSummaryCssClass(string $value): self
789
    {
790
        $new = clone $this;
791
        $new->errorSummaryCssClass = $value;
792
        return $new;
793
    }
794
795
    public function inputCssClass(string $value): self
796
    {
797
        $new = clone $this;
798
        $new->inputCssClass = $value;
799
        return $new;
800
    }
801
802
    public function requiredCssClass(string $value): self
803
    {
804
        $new = clone $this;
805
        $new->requiredCssClass = $value;
806
        return $new;
807
    }
808
809
    public function successCssClass(string $value): self
810
    {
811
        $new = clone $this;
812
        $new->successCssClass = $value;
813
        return $new;
814
    }
815
816
    public function template(string $value): self
817
    {
818
        $new = clone $this;
819
        $new->template = $value;
820
        return $new;
821
    }
822
823
    public function validatingCssClass(string $value): self
824
    {
825
        $new = clone $this;
826
        $new->validatingCssClass = $value;
827
        return $new;
828
    }
829
830
    public function validationStateOn(string $value): self
831
    {
832
        $new = clone $this;
833
        $new->validationStateOn = $value;
834
        return $new;
835
    }
836
837
    public function errorOptions(array $options = []): void
838
    {
839
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_ERROR_OPTIONS['class']);
840
841
        if ($classFromOptions !== self::DEFAULT_ERROR_OPTIONS['class']) {
842
            $classFromOptions = self::DEFAULT_ERROR_OPTIONS['class'] . ' ' . $classFromOptions;
843
        }
844
845
        $this->errorOptions = array_merge($this->errorOptions, $options);
846
        Html::addCssClass($this->errorOptions, $classFromOptions);
847
    }
848
849
    public function hintOptions(array $options = []): void
850
    {
851
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_HINT_OPTIONS['class']);
852
853
        if ($classFromOptions !== self::DEFAULT_HINT_OPTIONS['class']) {
854
            $classFromOptions = self::DEFAULT_HINT_OPTIONS['class'] . ' ' . $classFromOptions;
855
        }
856
857
        $this->hintOptions = array_merge($this->hintOptions, $options);
858
        Html::addCssClass($this->hintOptions, $classFromOptions);
859
    }
860
861
    public function inputOptions(array $options = []): void
862
    {
863
        $classFromOptions = ArrayHelper::remove($options, 'class', $this->inputCssClass);
864
865
        if ($classFromOptions !== $this->inputCssClass) {
866
            $classFromOptions = $this->inputCssClass . ' ' . $classFromOptions;
867
        }
868
869
        $this->inputOptions = array_merge($this->inputOptions, $options);
870
        Html::addCssClass($this->inputOptions, $classFromOptions);
871
    }
872
873
    public function labelOptions(array $options = []): void
874
    {
875
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_LABEL_OPTIONS['class']);
876
877
        if ($classFromOptions !== self::DEFAULT_LABEL_OPTIONS['class']) {
878
            $classFromOptions = self::DEFAULT_LABEL_OPTIONS['class'] . ' ' . $classFromOptions;
879
        }
880
881
        $this->labelOptions = array_merge($this->labelOptions, $options);
882
        Html::addCssClass($this->labelOptions, $classFromOptions);
883
    }
884
885
    private function addErrorCssClassToContainer(): void
886
    {
887
        if ($this->validationStateOn === 'container') {
888
            Html::addCssClass($this->options, $this->errorCssClass);
889
        }
890
    }
891
892
    private function addErrorCssClassToInput(): void
893
    {
894
        if ($this->validationStateOn === 'input') {
895
            $attributeName = Html::getAttributeName($this->attribute);
896
897
            if ($this->data->hasErrors($attributeName)) {
898
                Html::addCssClass($this->inputOptions, $this->errorCssClass);
899
            }
900
        }
901
    }
902
903
    private function setInputRole(array $options = []): void
904
    {
905
        $this->inputOptions['role'] = $options['role'] ?? 'radiogroup';
906
    }
907
908
    private function skipForInLabel(): void
909
    {
910
        if ($this->skipForInLabel) {
911
            $this->labelOptions['for'] = null;
912
        }
913
    }
914
915
    private function setForInLabel(array $options = []): void
916
    {
917
        if (isset($options['id'])) {
918
            $this->inputId = $options['id'];
919
920
            if (!isset($this->labelOptions['for'])) {
921
                $this->labelOptions['for'] = $options['id'];
922
            }
923
        }
924
    }
925
926
    private function setAriaAttributes(array $options = []): void
927
    {
928
        if ($this->ariaAttribute) {
929
            if (!isset($options['aria-required']) && $this->data->isAttributeRequired($this->attribute)) {
930
                $this->inputOptions['aria-required'] = 'true';
931
            }
932
933
            if (!isset($options['aria-invalid']) && $this->data->hasErrors($this->attribute)) {
934
                $this->inputOptions['aria-invalid'] = 'true';
935
            }
936
        }
937
    }
938
}
939