Passed
Push — master ( d73cb5...7c2ef6 )
by Alexander
02:26
created

Nav::renderItems()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 4
rs 10
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 20
    protected function run(): string
25
    {
26 20
        $items = [];
27
28 20
        foreach ($this->items as $item) {
29 20
            if (!isset($item['visible']) || $item['visible']) {
30 20
                $items[] = $this->renderItem($item);
31
            }
32
        }
33
34 18
        return implode("\n", $items);
35
    }
36
37
    /**
38
     * Whether to automatically activate items according to whether their currentPath matches the currently requested.
39
     *
40
     * @param bool $value
41
     *
42
     * @return self
43
     *
44
     * {@see isItemActive}
45
     */
46 2
    public function activateItems(bool $value): self
47
    {
48 2
        $new = clone $this;
49 2
        $new->activateItems = $value;
50 2
        return $new;
51
    }
52
53
    /**
54
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
55
     *
56
     * @param bool $value
57
     *
58
     * @return self
59
     */
60 1
    public function activateParents(bool $value): self
61
    {
62 1
        $new = clone $this;
63 1
        $new->activateParents = $value;
64 1
        return $new;
65
    }
66
67
    /**
68
     * Allows you to assign the current path of the url from request controller.
69
     *
70
     * @param string $value
71
     *
72
     * @return self
73
     */
74 2
    public function currentPath(string $value): self
75
    {
76 2
        $new = clone $this;
77 2
        $new->currentPath = $value;
78 2
        return $new;
79
    }
80
81
    /**
82
     * Whether the nav items labels should be HTML-encoded.
83
     *
84
     * @param bool $value
85
     *
86
     * @return self
87
     */
88 8
    public function encodeLabels(bool $value): self
89
    {
90 8
        $new = clone $this;
91 8
        $new->encodeLabels = $value;
92 8
        return $new;
93
    }
94
95
    /**
96
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
97
     * or an array with the following structure:
98
     *
99
     * - label: string, required, the nav item label.
100
     * - url: optional, the item's URL. Defaults to "#".
101
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
102
     * - linkOptions: array, optional, the HTML attributes of the item's link.
103
     * - options: array, optional, the HTML attributes of the item container (LI).
104
     * - active: bool, optional, whether the item should be on active state or not.
105
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
106
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
107
     *   representing the dropdown menu.
108
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
109
     *   only this item.
110
     *
111
     * If a menu item is a string, it will be rendered directly without HTML encoding.
112
     *
113
     * @param array $value
114
     *
115
     * @return self
116
     */
117 20
    public function items(array $value): self
118
    {
119 20
        $new = clone $this;
120 20
        $new->items = $value;
121 20
        return $new;
122
    }
123
124
    /**
125
     * Renders the given items as a dropdown.
126
     *
127
     * This method is called to create sub-menus.
128
     *
129
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
130
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
131
     * array.
132
     *
133
     * @throws InvalidArgumentException
134
     *
135
     * @return string the rendering result.
136
     */
137 11
    private function renderDropdown(array $items, array $parentItem): string
138
    {
139 11
        return Dropdown::widget()
140 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

140
                ->/** @scrutinizer ignore-call */ dividerClass('navbar-divider')
Loading history...
141 11
                ->itemClass('navbar-item')
142 11
                ->itemsClass('navbar-dropdown')
143 11
                ->encloseByContainer(false)
144 11
                ->encodeLabels($this->encodeLabels)
145 11
                ->items($items)
146 11
                ->itemsOptions(ArrayHelper::getValue($parentItem, 'dropdownOptions', []))
147 11
                ->render() . "\n";
148
    }
149
150
    /**
151
     * Check 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');
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 19
    private function isItemActive($item): bool
198
    {
199 19
        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 19
            isset($item['url']) &&
205 19
            $this->currentPath !== '/' &&
206 19
            $item['url'] === $this->currentPath &&
207 19
            $this->activateItems;
208
    }
209
210 19
    private function renderIcon(string $label, string $icon, array $iconOptions): string
211
    {
212 19
        if ($icon !== '') {
213 1
            $label = Html::beginTag('span', $iconOptions) .
214 1
                Html::tag('i', '', ['class' => $icon]) .
215 1
                Html::endTag('span') .
216 1
                Html::tag('span', $label);
217
        }
218
219 19
        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 20
    private function renderItem(array $item): string
232
    {
233 20
        if (!isset($item['label'])) {
234 1
            throw new InvalidArgumentException('The "label" option is required.');
235
        }
236 19
        $dropdown = false;
237 19
        $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
238
239 19
        if ($this->encodeLabels) {
240 17
            $label = Html::encode($item['label']);
241
        } else {
242 3
            $label = $item['label'];
243
        }
244
245 19
        $iconOptions = [];
246
247 19
        $icon = $item['icon'] ?? '';
248
249 19
        if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
250 1
            $iconOptions = $this->addOptions($iconOptions, 'icon');
251
        }
252
253 19
        $label = $this->renderIcon($label, $icon, $iconOptions);
254
255 19
        $options = ArrayHelper::getValue($item, 'options', []);
256 19
        $items = ArrayHelper::getValue($item, 'items');
257 19
        $url = ArrayHelper::getValue($item, 'url', '#');
258 19
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
259 19
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
260
261 19
        $active = $this->isItemActive($item);
262
263 19
        if (isset($items)) {
264 11
            $dropdown = true;
265
266 11
            Html::addCssClass($options, 'navbar-item has-dropdown is-hoverable');
267
268 11
            if (is_array($items)) {
269 11
                $items = $this->isChildActive($items, $active);
270 11
                $items = $this->renderDropdown($items, $item);
271
            }
272
        }
273
274 18
        Html::addCssClass($linkOptions, 'navbar-item');
275
276 18
        if ($disabled) {
277 1
            Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
278
        }
279
280
        /** @psalm-suppress ConflictingReferenceConstraint */
281 18
        if ($this->activateItems && $active) {
282 2
            Html::addCssClass($linkOptions, 'is-active');
283
        }
284
285 18
        if ($dropdown) {
286
            return
287 10
                Html::beginTag('div', $options) . "\n" .
288 10
                Html::a($label, $url, ['class' => 'navbar-link']) . "\n" .
289 10
                $items .
290 10
                Html::endTag('div');
291
        }
292
293 14
        return Html::a($label, $url, $linkOptions);
294
    }
295
}
296