Test Failed
Pull Request — master (#35)
by Wilmer
02:20
created

Dropdown::withSubmenuOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 3
cts 3
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\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
 *             ->withItems([
27
 *                 ['label' => 'DropdownA', 'url' => '/'],
28
 *                 ['label' => 'DropdownB', 'url' => '#'],
29
 *             ]);
30
 *     ?>
31
 * </div>
32
 * ```
33
 */
34
final class Dropdown extends Widget
35
{
36
    private array $items = [];
37
    private bool $encodeLabels = true;
38
    private bool $encodeTags = false;
39
    private array $submenuOptions = [];
40
    private array $options = [];
41 16
42
    public function run(): string
43 16
    {
44 15
        if (!isset($this->options['id'])) {
45
            $this->options['id'] = "{$this->getId()}-dropdown";
46
        }
47
48 16
        /** @psalm-suppress InvalidArgument */
49
        Html::addCssClass($this->options, 'dropdown-menu');
50 16
51
        $this->registerClientEvents($this->options['id']);
0 ignored issues
show
Bug introduced by
The method registerClientEvents() does not exist on Yiisoft\Yii\Bootstrap5\Dropdown. ( Ignorable by Annotation )

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

51
        $this->/** @scrutinizer ignore-call */ 
52
               registerClientEvents($this->options['id']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
52 16
53
        if ($this->encodeTags === false) {
54
            $this->options = array_merge($this->options, ['itemOptions' => ['encode' =>false], 'encode' => false]);
55
        }
56
57
        return $this->renderItems($this->items, $this->options);
58
    }
59
60
    /**
61
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
62
     * single menu with the following structure:
63
     *
64
     * - label: string, required, the label of the item link.
65 16
     * - encode: bool, optional, whether to HTML-encode item label.
66
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
67 16
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
68
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
69 16
     * - linkOptions: array, optional, the HTML attributes of the item link.
70 16
     * - options: array, optional, the HTML attributes of the item.
71 3
     * - items: array, optional, the submenu items. The structure is the same as this property.
72
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
73
     * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
74 16
     *   merged with {@see submenuOptions}.
75 3
     *
76
     * To insert divider use `-`.
77
     *
78 16
     * @param array $value
79
     *
80
     * @return $this
81
     */
82 16
    public function withItems(array $value): self
83 16
    {
84 16
        $new = clone $this;
85 16
        $new->items = $value;
86 16
87 16
        return $new;
88 16
    }
89
90 16
    /**
91
     * Whether the labels for header items should be HTML-encoded.
92 16
     *
93 1
     * @param bool $value
94 1
     *
95 1
     * @return $this
96 16
     */
97 3
    public function withoutEncodeLabels(bool $value = false): self
98
    {
99
        $new = clone $this;
100 16
        $new->encodeLabels = $value;
101
102 16
        return $new;
103 16
    }
104 3
105 16
    /**
106 1
     * The HTML attributes for sub-menu container tags.
107 16
     *
108 8
     * @param array $value
109
     *
110 10
     * @return $this
111
     */
112
    public function withSubmenuOptions(array $value): self
113 16
    {
114
        $new = clone $this;
115 3
        $new->submenuOptions = $value;
116
117 3
        return $new;
118 1
    }
119
120
    /**
121 3
     * @param array $value the HTML attributes for the widget container tag. The following special options are
122 3
     * recognized.
123
     *
124 3
     * @return $this
125 3
     *
126
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
127
     */
128
    public function withOptions(array $value): self
129 3
    {
130
        $new = clone $this;
131 3
        $new->options = $value;
132 3
133 3
        return $new;
134 3
    }
135 3
136 3
    /**
137 3
     * Allows you to enable or disable the encoding tags html.
138 3
     *
139 3
     * @param bool $value
140 3
     *
141 3
     * @return self
142
     */
143
    public function withEncodeTags(bool $value = true): self
144
    {
145
        $new = clone $this;
146
        $new->encodeTags = $value;
147 16
148 16
        return $new;
149 16
    }
150
151
    /**
152
     * Renders menu items.
153
     *
154
     * @param array $items the menu items to be rendered
155
     * @param array $options the container HTML attributes
156
     *
157
     * @throws JsonException|RuntimeException if the label option is not specified in one of the items.
158
     *
159
     * @return string the rendering result.
160
     */
161
    private function renderItems(array $items, array $options = []): string
162
    {
163
        $lines = [];
164
165
        foreach ($items as $item) {
166
            if (is_string($item)) {
167
                $item = ['label' => $item, 'encode' => false, 'enclose' => false];
168
            }
169
170
            if (isset($item['visible']) && !$item['visible']) {
171
                continue;
172
            }
173
174
            if (!array_key_exists('label', $item)) {
175 16
                throw new RuntimeException("The 'label' option is required.");
176
            }
177 16
178
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
179 16
            $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
180
            $itemOptions = ArrayHelper::getValue($item, 'options', []);
181
            $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
182
            $active = ArrayHelper::getValue($item, 'active', false);
183
            $disabled = ArrayHelper::getValue($item, 'disabled', false);
184
            $enclose = ArrayHelper::getValue($item, 'enclose', true);
185
186
            Html::addCssClass($linkOptions, 'dropdown-item');
187
188
            if ($this->encodeTags === false) {
189 12
                $linkOptions = array_merge($linkOptions, ['encode' => false]);
190
                $itemOptions = array_merge($itemOptions, ['encode' => false]);
191 12
            }
192
193 12
            if ($disabled) {
194
                ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
195
                ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
196
                Html::addCssClass($linkOptions, 'disabled');
197
            } elseif ($active) {
198
                Html::addCssClass($linkOptions, 'active');
199
            }
200
201
            $url = $item['url'] ?? null;
202
203 3
            if (empty($item['items'])) {
204
                if ($label === '-') {
205 3
                    $content = Html::div('', ['class' => 'dropdown-divider']);
206
                } elseif ($enclose === false) {
207 3
                    $content = $label;
208
                } elseif ($url === null) {
209
                    $content = Html::tag('h6', $label, ['class' => 'dropdown-header']);
210
                } else {
211
                    $content = Html::a($label, $url, $linkOptions);
212
                }
213
214
                $lines[] = $content;
215
            } else {
216
                $submenuOptions = $this->submenuOptions;
217
218 12
                if (isset($item['submenuOptions'])) {
219
                    $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']);
220 12
                }
221
222 12
                Html::addCssClass($submenuOptions, ['dropdown-menu']);
223
                Html::addCssClass($linkOptions, ['dropdown-toggle']);
224
225
                $lines[] = Html::a($label, $url, array_merge([
226
                    'data-bs-toggle' => 'dropdown',
227
                    'aria-haspopup' => 'true',
228
                    'aria-expanded' => 'false',
229
                    'role' => 'button',
230
                ], $linkOptions))
231
232
                . Html::tag(
233
                    'ul',
234
                    self::widget()
235
                        ->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\Carousel or Yiisoft\Yii\Bootstrap5\Dropdown 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

235
                        ->/** @scrutinizer ignore-call */ withItems($item['items'])
Loading history...
236
                        ->withOptions($submenuOptions)
237
                        ->withSubmenuOptions($submenuOptions)
238
                        ->withoutEncodeLabels($this->encodeLabels)
239
                        ->render(),
240
                    array_merge(
241
                        ['aria-expanded' => 'false', 'class' => ['dropdown']],
242
                        $itemOptions
243
                    )
244
                );
245
            }
246
        }
247
248
        $options = array_merge(['aria-expanded' => 'false'], $options);
249
250
        return Html::ul($lines, $options);
251
    }
252
}
253