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) |
|
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 | * @throws InvalidConfigException |
||
319 | * @throws \Yiisoft\Definitions\Exception\CircularReferenceException |
||
320 | * @throws \Yiisoft\Definitions\Exception\NotInstantiableException |
||
321 | * @throws \Yiisoft\Factory\NotFoundException |
||
322 | * @return string |
||
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
![]() |
|||
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 |