Passed
Pull Request — master (#35)
by Wilmer
02:31
created

Dropdown::withOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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

245
                        ->/** @scrutinizer ignore-call */ withItems($item['items'])
Loading history...
246 4
                        ->withOptions($submenuOptions)
247 4
                        ->withSubmenuOptions($submenuOptions)
248 4
                        ->withoutEncodeLabels($this->encodeLabels)
249 4
                        ->render(),
250 4
                    $itemOptions,
251
                );
252
            }
253
        }
254
255 24
        $options = array_merge(['aria-expanded' => 'false'], $options);
256
257 24
        return Html::ul($lines, $options);
258
    }
259
}
260