Passed
Push — master ( 438f38...27fec6 )
by Wilmer
02:48
created

ButtonDropdown::renderLabel()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 13
ccs 9
cts 9
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use Stringable;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Definitions\Exception\InvalidConfigException;
10
use Yiisoft\Html\Html;
11
12
/**
13
 * ButtonDropdown renders a group or split button dropdown bootstrap component.
14
 *
15
 * For example,
16
 *
17
 * ```php
18
 * // a button group using Dropdown widget
19
 * echo ButtonDropdown::widget()
20
 *     ->label('Action')
21
 *     ->items([
22
*             ['label' => 'DropdownA', 'url' => '/'],
23
*             ['label' => 'DropdownB', 'url' => '#'],
24
 *     ]);
25
 * ```
26
 */
27
final class ButtonDropdown extends Widget
28
{
29
    /**
30
     * The css class part of dropdown
31
     */
32
    public const DIRECTION_DOWN = 'down';
33
34
    /**
35
     * The css class part of dropleft
36
     */
37
    public const DIRECTION_LEFT = 'left';
38
39
    /**
40
     * The css class part of dropright
41
     */
42
    public const DIRECTION_RIGHT = 'right';
43
44
    /**
45
     * The css class part of dropup
46
     */
47
    public const DIRECTION_UP = 'up';
48
49
    private string $label = 'Button';
50
    private ?array $labelOptions = null;
51
    private array $options = [];
52
    private array $buttonOptions = [];
53
    private array|string|Stringable $items = [];
54
    private string $direction = self::DIRECTION_DOWN;
55
    private bool $split = false;
56
    /** @psalm-var non-empty-string */
57
    private string $tagName = 'button';
58
    private bool $encodeLabels = true;
59
    private bool $encodeTags = false;
60
    /** @psalm-var class-string|Dropdown */
61
    private string|Dropdown $dropdownClass = Dropdown::class;
62
    private bool $renderContainer = true;
63
64 11
    public function getId(?string $suffix = '-button-dropdown'): ?string
65
    {
66 11
        return $this->options['id'] ?? parent::getId($suffix);
67
    }
68
69 13
    public function render(): string
70
    {
71 13
        if (empty($this->items)) {
72 1
            return '';
73
        }
74
75 12
        if ($this->renderContainer) {
76 11
            $options = $this->options;
77 11
            $options['id'] = $this->getId();
78
79
            /** @psalm-suppress InvalidArgument */
80 11
            Html::addCssClass($options, ['widget' => 'drop' . $this->direction, 'btn-group']);
81
82 11
            if ($this->theme) {
83 1
                $options['data-bs-theme'] = $this->theme;
84
            }
85
86 11
            $tag = ArrayHelper::remove($options, 'tag', 'div');
87 11
            return Html::tag($tag, $this->renderButton() . "\n" . $this->renderDropdown(), $options)
88 11
                ->encode($this->encodeTags)
89 11
                ->render();
90
        }
91
92 2
        return $this->renderButton() . "\n" . $this->renderDropdown();
93
    }
94
95
    /**
96
     * The HTML attributes of the button.
97
     *
98
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
99
     */
100 1
    public function buttonOptions(array $value): self
101
    {
102 1
        $new = clone $this;
103 1
        $new->buttonOptions = $value;
104
105 1
        return $new;
106
    }
107
108
    /**
109
     * The drop-direction of the widget.
110
     *
111
     * Possible values are 'left', 'right', 'up', or 'down' (default)
112
     */
113 2
    public function direction(string $value): self
114
    {
115 2
        $new = clone $this;
116 2
        $new->direction = $value;
117
118 2
        return $new;
119
    }
120
121
    /**
122
     * The configuration array for example:
123
     *
124
     * ```php
125
     *    [
126
     *            ['label' => 'DropdownA', 'url' => '/'],
127
     *            ['label' => 'DropdownB', 'url' => '#'],
128
     *    ]
129
     * ```
130
     *
131
     * {@see Dropdown}
132
     */
133 12
    public function items(array|string|Stringable $value): self
134
    {
135 12
        $new = clone $this;
136 12
        $new->items = $value;
137
138 12
        return $new;
139
    }
140
141
    /**
142
     * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
143
     *
144
     * @psalm-param class-string $value
145
     */
146 2
    public function dropdownClass(string|Dropdown $value): self
147
    {
148 2
        $new = clone $this;
149 2
        $new->dropdownClass = $value;
150
151 2
        return $new;
152
    }
153
154
    /**
155
     * When tags Labels HTML should not be encoded.
156
     */
157 2
    public function withoutEncodeLabels(): self
158
    {
159 2
        $new = clone $this;
160 2
        $new->encodeLabels = false;
161
162 2
        return $new;
163
    }
164
165
    /**
166
     * The button label.
167
     */
168 3
    public function label(string $value): self
169
    {
170 3
        $new = clone $this;
171 3
        $new->label = $value;
172
173 3
        return $new;
174
    }
175
176 1
    public function withLabelOptions(?array $options): self
177
    {
178 1
        $new = clone $this;
179 1
        $new->labelOptions = $options;
180
181 1
        return $new;
182
    }
183
184
    /**
185
     * The HTML attributes for the container tag. The following special options are recognized.
186
     *
187
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
188
     */
189 1
    public function options(array $value): self
190
    {
191 1
        $new = clone $this;
192 1
        $new->options = $value;
193
194 1
        return $new;
195
    }
196
197
    /**
198
     * Whether to render the container using the {@see options} as HTML attributes. If set to `false`, the container
199
     * element enclosing the button and dropdown will NOT be rendered.
200
     */
201 2
    public function withoutRenderContainer(): self
202
    {
203 2
        $new = clone $this;
204 2
        $new->renderContainer = false;
205
206 2
        return $new;
207
    }
208
209
    /**
210
     * Whether to display a group of split-styled button group.
211
     */
212 1
    public function split(): self
213
    {
214 1
        $new = clone $this;
215 1
        $new->split = true;
216
217 1
        return $new;
218
    }
219
220
    /**
221
     * The tag to use to render the button.
222
     *
223
     * @psalm-param non-empty-string $value
224
     */
225 1
    public function tagName(string $value): self
226
    {
227 1
        $new = clone $this;
228 1
        $new->tagName = $value;
229
230 1
        return $new;
231
    }
232
233 12
    private function prepareButtonOptions(bool $toggle): array
234
    {
235 12
        $options = $this->buttonOptions;
236 12
        $classNames = ['button' => 'btn'];
237
238 12
        if ($toggle) {
239 12
            $options['data-bs-toggle'] = 'dropdown';
240 12
            $options['aria-haspopup'] = 'true';
241 12
            $options['aria-expanded'] = 'false';
242 12
            $classNames['toggle'] = 'dropdown-toggle';
243
        }
244
245 12
        Html::addCssClass($options, $classNames);
246
247 12
        if ($this->tagName !== 'button') {
248 1
            $options['role'] = 'button';
249
250 1
            if ($this->tagName === 'a' && !isset($options['href'])) {
251 1
                $options['href'] = '#';
252
            }
253
        }
254
255 12
        return $options;
256
    }
257
258
    /**
259
     * Generates the button dropdown.
260
     *
261
     * @throws InvalidConfigException
262
     *
263
     * @return string the rendering result.
264
     */
265 12
    private function renderButton(): string
266
    {
267 12
        $splitButton = $this->renderSplitButton();
268 12
        $options = $this->prepareButtonOptions($splitButton === null);
269 12
        $button = Button::widget()
270 12
            ->options($options)
0 ignored issues
show
Bug introduced by
The method options() 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\Yii\Bootstrap5\Widget. 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

270
            ->/** @scrutinizer ignore-call */ options($options)
Loading history...
271 12
            ->label($this->renderLabel())
272 12
            ->withTheme($this->renderContainer ? null : $this->theme)
273 12
            ->tagName($this->tagName);
274
275 12
        if ($this->encodeLabels === false) {
276 2
            $button = $button->withoutEncodeLabels();
277
        }
278
279 12
        return $button->render() . "\n" . $splitButton;
280
    }
281
282 12
    private function renderSplitButton(): ?string
283
    {
284 12
        if ($this->split === false) {
285 11
            return null;
286
        }
287
288 1
        $options = $this->prepareButtonOptions(true);
289 1
        Html::addCssClass($options, 'dropdown-toggle-split');
290
291 1
        return Button::widget()
292 1
            ->options($options)
293 1
            ->label('<span class="visually-hidden">Toggle Dropdown</span>')
294 1
            ->tagName($this->tagName)
295 1
            ->withoutEncodeLabels()
296 1
            ->withTheme($this->renderContainer ? null : $this->theme)
297 1
            ->render();
298
    }
299
300 12
    private function renderLabel(): string
301
    {
302 12
        if ($this->labelOptions === null) {
303 11
            return $this->encodeLabels ? Html::encode($this->label) : $this->label;
304
        }
305
306 1
        $options = $this->labelOptions;
307 1
        $tag = ArrayHelper::remove($options, 'tag', 'span');
308 1
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeLabels);
309
310 1
        return Html::tag($tag, $this->label, $options)
311 1
            ->encode($encode)
312 1
            ->render();
313
    }
314
315
    /**
316
     * Generates the dropdown menu.
317
     *
318
     * @return string
319
     * @throws InvalidConfigException
320
     * @throws \Yiisoft\Definitions\Exception\CircularReferenceException
321
     * @throws \Yiisoft\Definitions\Exception\NotInstantiableException
322
     * @throws \Yiisoft\Factory\NotFoundException
323
     */
324 12
    private function renderDropdown(): string
325
    {
326 12
        if (is_string($this->dropdownClass)) {
327 11
            $dropdownClass = $this->dropdownClass;
328
            /** @var Dropdown $dropdownClass */
329 11
            $dropdown = $dropdownClass::widget()->items($this->items);
0 ignored issues
show
Bug introduced by
The method items() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bootstrap5\Nav or Yiisoft\Yii\Bootstrap5\Carousel or Yiisoft\Yii\Bootstrap5\Dropdown or Yiisoft\Yii\Bootstrap5\ButtonDropdown or Yiisoft\Yii\Bootstrap5\Tabs or Yiisoft\Yii\Bootstrap5\Accordion. ( Ignorable by Annotation )

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

329
            $dropdown = $dropdownClass::widget()->/** @scrutinizer ignore-call */ items($this->items);
Loading history...
330
        } else {
331 1
            $dropdown = $this->dropdownClass->items($this->items);
332
        }
333
334 12
        if ($this->theme && !$this->renderContainer) {
335 1
            $dropdown = $dropdown->withTheme($this->theme);
336
        }
337
338 12
        if ($this->encodeLabels === false) {
339 2
            $dropdown = $dropdown->withoutEncodeLabels();
340
        }
341
342 12
        return $dropdown->render();
343
    }
344
}
345