1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\Yii\Bootstrap4; |
||
6 | |||
7 | use JsonException; |
||
8 | use RuntimeException; |
||
9 | use Yiisoft\Arrays\ArrayHelper; |
||
10 | use Yiisoft\Html\Html; |
||
11 | |||
12 | use function array_key_exists; |
||
13 | use function array_merge; |
||
14 | use function array_merge_recursive; |
||
15 | use function is_string; |
||
16 | |||
17 | /** |
||
18 | * Dropdown renders a Bootstrap dropdown menu component. |
||
19 | * |
||
20 | * For example, |
||
21 | * |
||
22 | * ```php |
||
23 | * <div class="dropdown"> |
||
24 | * <?php |
||
25 | * echo Dropdown::widget() |
||
26 | * ->items([ |
||
27 | * ['label' => 'DropdownA', 'url' => '/'], |
||
28 | * ['label' => 'DropdownB', 'url' => '#'], |
||
29 | * ]); |
||
30 | * ?> |
||
31 | * </div> |
||
32 | * ``` |
||
33 | */ |
||
34 | class Dropdown extends Widget |
||
35 | { |
||
36 | private array $items = []; |
||
37 | private bool $encodeLabels = true; |
||
38 | private array $submenuOptions = []; |
||
39 | private array $options = []; |
||
40 | |||
41 | 16 | protected function run(): string |
|
42 | { |
||
43 | 16 | if (!isset($this->options['id'])) { |
|
44 | 15 | $this->options['id'] = "{$this->getId()}-dropdown"; |
|
45 | } |
||
46 | |||
47 | 16 | Html::addCssClass($this->options, ['widget' => 'dropdown-menu']); |
|
48 | |||
49 | 16 | $this->registerClientEvents($this->options['id']); |
|
50 | |||
51 | 16 | return $this->renderItems($this->items, $this->options); |
|
52 | } |
||
53 | |||
54 | /** |
||
55 | * Renders menu items. |
||
56 | * |
||
57 | * @param array $items the menu items to be rendered |
||
58 | * @param array $options the container HTML attributes |
||
59 | * |
||
60 | * @throws JsonException|RuntimeException if the label option is not specified in one of the items. |
||
61 | * |
||
62 | * @return string the rendering result. |
||
63 | */ |
||
64 | 16 | protected function renderItems(array $items, array $options = []): string |
|
65 | { |
||
66 | 16 | $lines = []; |
|
67 | |||
68 | 16 | foreach ($items as $item) { |
|
69 | 16 | if (is_string($item)) { |
|
70 | 3 | $item = ['label' => $item, 'encode' => false, 'enclose' => false]; |
|
71 | } |
||
72 | |||
73 | 16 | if (isset($item['visible']) && !$item['visible']) { |
|
74 | 3 | continue; |
|
75 | } |
||
76 | |||
77 | 16 | if (!array_key_exists('label', $item)) { |
|
78 | throw new RuntimeException('The "label" option is required.'); |
||
79 | } |
||
80 | |||
81 | 16 | $encodeLabel = $item['encode'] ?? $this->encodeLabels; |
|
82 | 16 | $label = $encodeLabel ? Html::encode($item['label']) : $item['label']; |
|
83 | 16 | $itemOptions = ArrayHelper::getValue($item, 'options', []); |
|
84 | 16 | $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); |
|
85 | 16 | $active = ArrayHelper::getValue($item, 'active', false); |
|
86 | 16 | $disabled = ArrayHelper::getValue($item, 'disabled', false); |
|
87 | 16 | $enclose = ArrayHelper::getValue($item, 'enclose', true); |
|
88 | |||
89 | 16 | Html::addCssClass($linkOptions, 'dropdown-item'); |
|
90 | |||
91 | 16 | if ($disabled) { |
|
92 | 1 | ArrayHelper::setValue($linkOptions, 'tabindex', '-1'); |
|
93 | 1 | ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true'); |
|
94 | 1 | Html::addCssClass($linkOptions, 'disabled'); |
|
95 | 16 | } elseif ($active) { |
|
96 | 3 | Html::addCssClass($linkOptions, 'active'); |
|
97 | } |
||
98 | |||
99 | 16 | $url = $item['url'] ?? null; |
|
100 | |||
101 | 16 | if (empty($item['items'])) { |
|
102 | 16 | if ($label === '-') { |
|
103 | 3 | $content = Html::div('', ['class' => 'dropdown-divider']); |
|
104 | 16 | } elseif ($enclose === false) { |
|
105 | 1 | $content = $label; |
|
106 | 16 | } elseif ($url === null) { |
|
107 | 8 | $content = Html::tag('h6', $label, ['class' => 'dropdown-header']); |
|
108 | } else { |
||
109 | 10 | $content = Html::a($label, $url, $linkOptions); |
|
110 | } |
||
111 | |||
112 | 16 | $lines[] = $content; |
|
113 | } else { |
||
114 | 3 | $submenuOptions = $this->submenuOptions; |
|
115 | |||
116 | 3 | if (isset($item['submenuOptions'])) { |
|
117 | 1 | $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']); |
|
118 | } |
||
119 | |||
120 | 3 | Html::addCssClass($submenuOptions, ['dropdown-submenu']); |
|
121 | 3 | Html::addCssClass($linkOptions, ['dropdown-toggle']); |
|
122 | |||
123 | 3 | $lines[] = Html::beginTag( |
|
124 | 3 | 'div', |
|
125 | 3 | array_merge_recursive(['class' => ['dropdown'], 'aria-expanded' => 'false'], $itemOptions) |
|
126 | ); |
||
127 | |||
128 | 3 | $lines[] = Html::a($label, $url, array_merge([ |
|
129 | 3 | 'data-toggle' => 'dropdown', |
|
130 | 'aria-haspopup' => 'true', |
||
131 | 'aria-expanded' => 'false', |
||
132 | 'role' => 'button', |
||
133 | 3 | ], $linkOptions)); |
|
134 | |||
135 | 3 | $lines[] = self::widget() |
|
136 | 3 | ->items($item['items']) |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
137 | 3 | ->options($submenuOptions) |
|
138 | 3 | ->submenuOptions($submenuOptions) |
|
139 | 3 | ->encodeLabels($this->encodeLabels) |
|
140 | 3 | ->run(); |
|
141 | 3 | $lines[] = Html::endTag('div'); |
|
142 | } |
||
143 | } |
||
144 | |||
145 | 16 | return Html::tag('div', implode("\n", $lines), $options); |
|
146 | } |
||
147 | |||
148 | /** |
||
149 | * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a |
||
150 | * single menu with the following structure: |
||
151 | * |
||
152 | * - label: string, required, the label of the item link. |
||
153 | * - encode: bool, optional, whether to HTML-encode item label. |
||
154 | * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}. |
||
155 | * If not set, the item will be treated as a menu header when the item has no sub-menu. |
||
156 | * - visible: bool, optional, whether this menu item is visible. Defaults to true. |
||
157 | * - linkOptions: array, optional, the HTML attributes of the item link. |
||
158 | * - options: array, optional, the HTML attributes of the item. |
||
159 | * - items: array, optional, the submenu items. The structure is the same as this property. |
||
160 | * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. |
||
161 | * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be |
||
162 | * merged with {@see submenuOptions}. |
||
163 | * |
||
164 | * To insert divider use `-`. |
||
165 | * |
||
166 | * @param array $value |
||
167 | * |
||
168 | * @return $this |
||
169 | */ |
||
170 | 16 | public function items(array $value): self |
|
171 | { |
||
172 | 16 | $this->items = $value; |
|
173 | |||
174 | 16 | return $this; |
|
175 | } |
||
176 | |||
177 | /** |
||
178 | * Whether the labels for header items should be HTML-encoded. |
||
179 | * |
||
180 | * @param bool $value |
||
181 | * |
||
182 | * @return $this |
||
183 | */ |
||
184 | 12 | public function encodeLabels(bool $value): self |
|
185 | { |
||
186 | 12 | $this->encodeLabels = $value; |
|
187 | |||
188 | 12 | return $this; |
|
189 | } |
||
190 | |||
191 | /** |
||
192 | * The HTML attributes for sub-menu container tags. |
||
193 | * |
||
194 | * @param array $value |
||
195 | * |
||
196 | * @return $this |
||
197 | */ |
||
198 | 3 | public function submenuOptions(array $value): self |
|
199 | { |
||
200 | 3 | $this->submenuOptions = $value; |
|
201 | |||
202 | 3 | return $this; |
|
203 | } |
||
204 | |||
205 | /** |
||
206 | * @param array $value the HTML attributes for the widget container tag. The following special options are |
||
207 | * recognized. |
||
208 | * |
||
209 | * @return $this |
||
210 | * |
||
211 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered. |
||
212 | */ |
||
213 | 12 | public function options(array $value): self |
|
214 | { |
||
215 | 12 | $this->options = $value; |
|
216 | |||
217 | 12 | return $this; |
|
218 | } |
||
219 | } |
||
220 |