Passed
Push — master ( e80b4e...741461 )
by Alexander
02:19
created

Nav::withoutActivateItems()   A

Complexity

Conditions 1
Paths 1

Size

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

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

199
            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...
200
        }
201
202
        return
203 17
            isset($item['url']) &&
204 17
            $this->currentPath !== '/' &&
205 17
            $item['url'] === $this->currentPath &&
206 17
            $this->activateItems;
207
    }
208
209 17
    private function renderIcon(string $label, string $icon, array $iconOptions): string
210
    {
211 17
        if ($icon !== '') {
212 1
            $label = Html::beginTag('span', $iconOptions) .
213 1
                Html::tag('i', '', ['class' => $icon]) .
214 1
                Html::endTag('span') .
215 1
                Html::tag('span', $label);
216
        }
217
218 17
        return $label;
219
    }
220
221
    /**
222
     * Renders a widget's item.
223
     *
224
     * @param array $item the item to render.
225
     *
226
     * @throws InvalidArgumentException|JsonException
227
     *
228
     * @return string the rendering result.
229
     */
230 18
    private function renderItem(array $item): string
231
    {
232 18
        if (!isset($item['label'])) {
233 1
            throw new InvalidArgumentException('The "label" option is required.');
234
        }
235
236 17
        $dropdown = false;
237 17
        $this->encodeLabels = $item['encode'] ?? $this->encodeLabels;
238
239 17
        if ($this->encodeLabels) {
240 15
            $label = Html::encode($item['label']);
241
        } else {
242 3
            $label = $item['label'];
243
        }
244
245 17
        $iconOptions = [];
246
247 17
        $icon = $item['icon'] ?? '';
248
249 17
        if (array_key_exists('iconOptions', $item) && is_array($item['iconOptions'])) {
250 1
            $iconOptions = $this->addOptions($iconOptions, 'icon');
251
        }
252
253 17
        $label = $this->renderIcon($label, $icon, $iconOptions);
254
255 17
        $options = ArrayHelper::getValue($item, 'options', []);
256 17
        $items = ArrayHelper::getValue($item, 'items');
257 17
        $url = ArrayHelper::getValue($item, 'url', '#');
258 17
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
259 17
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
260
261 17
        $active = $this->isItemActive($item);
262
263 17
        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 16
        Html::addCssClass($linkOptions, 'navbar-item');
275
276 16
        if ($disabled) {
277 1
            Html::addCssStyle($linkOptions, 'opacity:.65; pointer-events:none;');
278
        }
279
280
        /** @psalm-suppress ConflictingReferenceConstraint */
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', 'encode' => false];
287
288
            return
289 10
                Html::beginTag('div', $options) . "\n" .
290 10
                Html::a($label, $url, $dropdownOptions) . "\n" .
291 10
                $items .
292 10
                Html::endTag('div');
293
        }
294
295 12
        if ($this->encodeTags === false) {
296 12
            $linkOptions['encode'] = false;
297
        }
298
299 12
        return Html::a($label, $url, $linkOptions);
300
    }
301
}
302