Passed
Push — master ( 6d0312...290831 )
by Alexander
04:13 queued 01:47
created

Nav   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 99
c 2
b 0
f 0
dl 0
loc 280
ccs 102
cts 102
cp 1
rs 9.44
wmc 37

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isItemActive() 0 11 5
B isChildActive() 0 23 8
A renderIcon() 0 10 2
A activateParents() 0 5 1
A renderDropdown() 0 15 2
A deactivateItems() 0 5 1
B renderItem() 0 65 11
A currentPath() 0 5 1
A run() 0 11 4
A items() 0 5 1
A withoutEncodeLabels() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use InvalidArgumentException;
8
use JsonException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function array_key_exists;
13
use function implode;
14
use function is_array;
15
16
final class Nav extends Widget
17
{
18
    private bool $activateItems = true;
19
    private bool $activateParents = false;
20
    private string $currentPath = '';
21
    private bool $encodeLabels = true;
22
    private array $items = [];
23
24
    /**
25
     * Disables active items according to their current path and returns a new instance.
26
     *
27
     * @return self
28
     *
29
     * {@see isItemActive}
30
     */
31 3
    public function deactivateItems(): self
32
    {
33 3
        $new = clone $this;
34 3
        $new->activateItems = false;
35 3
        return $new;
36
    }
37
38
    /**
39
     * Returns a new instance with the activated parent items.
40
     *
41
     * Activates parent menu items when one of the corresponding child menu items is active.
42
     *
43
     * @return self
44
     */
45 2
    public function activateParents(): self
46
    {
47 2
        $new = clone $this;
48 2
        $new->activateParents = true;
49 2
        return $new;
50
    }
51
52
    /**
53
     * Returns a new instance with the specified current path.
54
     *
55
     * @param string $value The current path.
56
     *
57
     * @return self
58
     */
59 3
    public function currentPath(string $value): self
60
    {
61 3
        $new = clone $this;
62 3
        $new->currentPath = $value;
63 3
        return $new;
64
    }
65
66
    /**
67
     * Disables encoding for labels and returns a new instance.
68
     *
69
     * @return self
70
     */
71 2
    public function withoutEncodeLabels(): self
72
    {
73 2
        $new = clone $this;
74 2
        $new->encodeLabels = false;
75 2
        return $new;
76
    }
77
78
    /**
79
     * Returns a new instance with the specified items.
80
     *
81
     * @param array $value List of items in the nav widget. Each array element represents a single menu item
82
     * which can be either a string or an array with the following structure:
83
     *
84
     * - label: string, required, the nav item label.
85
     * - url: optional, the item's URL. Defaults to "#".
86
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
87
     * - linkOptions: array, optional, the HTML attributes of the item's link.
88
     * - options: array, optional, the HTML attributes of the item container (LI).
89
     * - active: bool, optional, whether the item should be on active state or not.
90
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
91
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
92
     *   representing the dropdown menu.
93
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
94
     *   only this item.
95
     *
96
     * If a menu item is a string, it will be rendered directly without HTML encoding.
97
     *
98
     * @return self
99
     */
100 19
    public function items(array $value): self
101
    {
102 19
        $new = clone $this;
103 19
        $new->items = $value;
104 19
        return $new;
105
    }
106
107 18
    protected function run(): string
108
    {
109 18
        $items = [];
110
111 18
        foreach ($this->items as $item) {
112 18
            if (!isset($item['visible']) || $item['visible']) {
113 18
                $items[] = $this->renderItem($item);
114
            }
115
        }
116
117 16
        return implode("\n", $items);
118
    }
119
120
    /**
121
     * Renders the given items as a dropdown.
122
     *
123
     * This method is called to create sub-menus.
124
     *
125
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
126
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
127
     * array.
128
     *
129
     * @throws InvalidArgumentException
130
     *
131
     * @return string The rendering result.
132
     */
133 11
    private function renderDropdown(array $items, array $parentItem): string
134
    {
135 11
        $dropdown = Dropdown::widget()
136 11
            ->dividerClass('navbar-divider')
0 ignored issues
show
Bug introduced by
The method dividerClass() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bulma\Dropdown. ( Ignorable by Annotation )

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

136
            ->/** @scrutinizer ignore-call */ dividerClass('navbar-divider')
Loading history...
137 11
            ->itemClass('navbar-item')
138 11
            ->itemsClass('navbar-dropdown')
139 11
            ->withoutEncloseByContainer()
140 11
            ->items($items)
141 11
            ->itemsOptions(ArrayHelper::getValue($parentItem, 'dropdownOptions', []));
142
143 11
        if ($this->encodeLabels === false) {
144 2
            $dropdown = $dropdown->withoutEncodeLabels();
145
        }
146
147 11
        return $dropdown->render() . "\n";
148
    }
149
150
    /**
151
     * Checks to see if a child item is active optionally activating the parent.
152
     *
153
     * @param array $items
154
     * @param bool $active should the parent be active too
155
     *
156
     * @return array
157
     *
158
     * {@see items}
159
     */
160 11
    private function isChildActive(array $items, bool &$active): array
161
    {
162 11
        foreach ($items as $i => $child) {
163 10
            if ($this->isItemActive($child)) {
164 3
                ArrayHelper::setValue($items[$i], 'active', true);
165 3
                if ($this->activateParents) {
166 1
                    $active = $this->activateParents;
167
                }
168
            }
169
170 10
            if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
171 1
                $activeParent = false;
172 1
                $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
173
174 1
                if ($activeParent) {
175 1
                    $items[$i]['options'] ??= ['class' => ''];
176 1
                    Html::addCssClass($items[$i]['options'], ['active' => 'is-active']);
177 1
                    $active = $activeParent;
178
                }
179
            }
180
        }
181
182 11
        return $items;
183
    }
184
185
    /**
186
     * Checks whether a menu item is active.
187
     *
188
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
189
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
190
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
191
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
192
     *
193
     * @param array|object|string $item the menu item to be checked
194
     *
195
     * @return bool Whether the menu item is active
196
     */
197 17
    private function isItemActive($item): bool
198
    {
199 17
        if (isset($item['active'])) {
200 5
            return ArrayHelper::getValue($item, 'active');
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type string; however, parameter $array of Yiisoft\Arrays\ArrayHelper::getValue() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

200
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active');
Loading history...
Bug Best Practice introduced by
The expression return Yiisoft\Arrays\Ar...tValue($item, 'active') could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
201
        }
202
203
        return
204 17
            isset($item['url']) &&
205 17
            $this->currentPath !== '/' &&
206 17
            $item['url'] === $this->currentPath &&
207 17
            $this->activateItems;
208
    }
209
210 17
    private function renderIcon(string $label, string $icon, array $iconOptions): string
211
    {
212 17
        if ($icon !== '') {
213 1
            $label = Html::openTag('span', $iconOptions) .
214 1
                Html::tag('i', '', ['class' => $icon]) .
215 1
                Html::closeTag('span') .
216 1
                Html::tag('span', $label);
217
        }
218
219 17
        return $label;
220
    }
221
222
    /**
223
     * Renders a widget's item.
224
     *
225
     * @param array $item the item to render.
226
     *
227
     * @throws InvalidArgumentException|JsonException
228
     *
229
     * @return string The rendering result.
230
     */
231 18
    private function renderItem(array $item): string
232
    {
233 18
        if (!isset($item['label'])) {
234 1
            throw new InvalidArgumentException('The "label" option is required.');
235
        }
236
237 17
        $dropdown = false;
238 17
        $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
239
240 17
        if ($this->encodeLabels) {
241 15
            $label = Html::encode($item['label']);
242
        } else {
243 3
            $label = $item['label'];
244
        }
245
246 17
        $iconOptions = [];
247
248 17
        $icon = $item['icon'] ?? '';
249
250 17
        if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
251 1
            $iconOptions = $this->addOptions($iconOptions, 'icon');
252
        }
253
254 17
        $label = $this->renderIcon($label, $icon, $iconOptions);
255
256 17
        $options = ArrayHelper::getValue($item, 'options', []);
257 17
        $items = ArrayHelper::getValue($item, 'items');
258 17
        $url = ArrayHelper::getValue($item, 'url', '#');
259 17
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
260 17
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
261
262 17
        $active = $this->isItemActive($item);
263
264 17
        if (isset($items)) {
265 11
            $dropdown = true;
266
267 11
            Html::addCssClass($options, 'navbar-item has-dropdown is-hoverable');
268
269 11
            if (is_array($items)) {
270 11
                $items = $this->isChildActive($items, $active);
271 11
                $items = $this->renderDropdown($items, $item);
272
            }
273
        }
274
275 16
        Html::addCssClass($linkOptions, 'navbar-item');
276
277 16
        if ($disabled) {
278 1
            Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
279
        }
280
281 16
        if ($this->activateItems && $active) {
282 2
            Html::addCssClass($linkOptions, ['active' => 'is-active']);
283
        }
284
285 16
        if ($dropdown) {
286 10
            $dropdownOptions = ['class' => 'navbar-link'];
287
288
            return
289 10
                Html::openTag('div', $options) . "\n" .
290 10
                Html::a($label, $url, $dropdownOptions)->encode(false) . "\n" .
291 10
                $items .
292 10
                Html::closeTag('div');
293
        }
294
295 12
        return Html::a($label, $url, $linkOptions)->encode(false)->render();
296
    }
297
}
298