Test Failed
Pull Request — master (#41)
by Sergei
06:46
created

Accordion::renderItems()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 17
c 1
b 0
f 0
nc 9
nop 0
dl 0
loc 30
ccs 16
cts 16
cp 1
crap 6
rs 9.0777
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_column;
13
use function array_key_exists;
14
use function array_merge;
15
use function array_search;
16
use function implode;
17
use function is_array;
18
use function is_numeric;
19
use function is_object;
20
use function is_string;
21
22
/**
23
 * Accordion renders an accordion bootstrap javascript component.
24
 *
25
 * For example:
26
 *
27
 * ```php
28
 * echo Accordion::widget()
29
 *     ->items([
30
 *         [
31
 *             'label' => 'Collapsible Group Item #1',
32
 *             'content' => 'Anim pariatur cliche...',
33
 *             // open its content by default
34
 *             'contentOptions' => ['class' => 'show'],
35
 *         ],
36
 *         // another group item
37
 *         [
38
 *             'label' => 'Collapsible Group Item #2',
39
 *             'content' => 'Anim pariatur cliche...',
40
 *             'contentOptions' => [...],
41
 *             'options' => [...],
42
 *             'expand' => true,
43
 *         ],
44
 *         // if you want to swap out .accordion-body with .list-group, you may provide an array
45
 *         [
46
 *             'label' => 'Collapsible Group Item #3',
47
 *             'content' => [
48
 *                 'Anim pariatur cliche...',
49
 *                 'Anim pariatur cliche...',
50
 *             ],
51
 *             'contentOptions' => [...],
52
 *             'options' => [...],
53
 *         ],
54
 *     ]);
55
 * ```
56
 */
57
final class Accordion extends Widget
58
{
59
    private array $items = [];
60
    private bool $encodeLabels = true;
61
    private bool $encodeTags = false;
62
    private bool $autoCloseItems = true;
63
    private array $itemToggleOptions = [];
64
    private array $options = [];
65
66 10
    protected function run(): string
67
    {
68 10
        if (!isset($this->options['id'])) {
69 10
            $this->options['id'] = "{$this->getId()}-accordion";
70
        }
71
72
        /** @psalm-suppress InvalidArgument */
73 10
        Html::addCssClass($this->options, ['widget' => 'accordion']);
74
75 10
        return Html::div($this->renderItems(), $this->options)
76 10
            ->encode($this->encodeTags)
77
            ->render();
78
    }
79 10
80
    /**
81
     * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect.
82
     *
83
     * Set this to `false` to allow keeping multiple items open at once.
84
     *
85
     * @return $this
86
     */
87
    public function allowMultipleOpenedItems(): self
88
    {
89 1
        $new = clone $this;
90
        $new->autoCloseItems = false;
91 1
92 1
        return $new;
93
    }
94 1
95
    /**
96
     * When tags Labels HTML should not be encoded.
97
     *
98
     * @return $this
99
     */
100
    public function withoutEncodeLabels(): self
101
    {
102 1
        $new = clone $this;
103
        $new->encodeLabels = false;
104 1
105 1
        return $new;
106
    }
107 1
108
    /**
109
     * List of groups in the collapse widget. Each array element represents a single group with the following structure:
110
     *
111
     * - label: string, required, the group header label.
112
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global
113
     *   `$this->encodeLabels` param.
114
     * - content: array|string|object, required, the content (HTML) of the group
115
     * - options: array, optional, the HTML attributes of the group
116
     * - contentOptions: optional, the HTML attributes of the group's content
117
     *
118
     * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers
119
     * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained
120
     * above.
121
     *
122
     * For example:
123
     *
124
     * ```php
125
     * echo Accordion::widget()
126
     *     ->items(
127
     *         [
128
     *             [
129
     *                 'Introduction' => 'This is the first collapsible menu',
130
     *                 'Second panel' => [
131
     *                     'content' => 'This is the second collapsible menu',
132
     *                 ],
133
     *             ],
134
     *             [
135
     *                 'label' => 'Third panel',
136
     *                 'content' => 'This is the third collapsible menu',
137
     *             ],
138
     *         ],
139
     *     );
140
     * ```
141
     *
142
     * @param array $value
143
     *
144
     * @return $this
145
     */
146
    public function items(array $value): self
147
    {
148 10
        $new = clone $this;
149
        $new->items = $value;
150 10
151 10
        return $new;
152
    }
153 10
154
    /**
155
     * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
156
     *
157
     * For example:
158
     *
159
     * ```php
160
     * [
161
     *     'tag' => 'div',
162
     *     'class' => 'custom-toggle',
163
     * ]
164
     * ```
165
     *
166
     * @param array $value
167
     *
168
     * @return $this
169
     */
170
    public function itemToggleOptions(array $value): self
171
    {
172 1
        $new = clone $this;
173
        $new->itemToggleOptions = $value;
174 1
175 1
        return $new;
176
    }
177 1
178
    /**
179
     * The HTML attributes for the widget container tag. The following special options are recognized.
180
     *
181
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
182
     *
183
     * @param array $value
184
     *
185
     * @return $this
186
     */
187
    public function options(array $value): self
188
    {
189 1
        $new = clone $this;
190
        $new->options = $value;
191 1
192 1
        return $new;
193
    }
194 1
195
    /**
196
     * Renders collapsible items as specified on {@see items}.
197
     *
198
     * @throws JsonException|RuntimeException
199
     *
200
     * @return string the rendering result
201
     */
202
    private function renderItems(): string
203
    {
204 10
        $items = [];
205
        $index = 0;
206 10
        $expanded = array_search(true, array_column($this->items, 'expand'), true);
207 10
208 10
        foreach ($this->items as $key => $item) {
209
            if (!is_array($item)) {
210 10
                $item = ['content' => $item];
211 10
            }
212 1
213
            if ($expanded === false && $index === 0) {
214
                $item['expand'] = true;
215 10
            }
216 9
217
            if (!array_key_exists('label', $item)) {
218
                throw new RuntimeException('The "label" option is required.');
219 10
            }
220 3
221
            $header = ArrayHelper::remove($item, 'label');
222
            $options = ArrayHelper::getValue($item, 'options', []);
223 7
224 7
            Html::addCssClass($options, ['panel' => 'accordion-item']);
225
226 7
            $items[] = Html::div($this->renderItem($header, $item, $index++), $options)
0 ignored issues
show
Bug introduced by
It seems like $header can also be of type null; however, parameter $header of Yiisoft\Yii\Bootstrap5\Accordion::renderItem() does only seem to accept string, 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

226
            $items[] = Html::div($this->renderItem(/** @scrutinizer ignore-type */ $header, $item, $index++), $options)
Loading history...
Bug introduced by
It seems like $item can also be of type object; however, parameter $item of Yiisoft\Yii\Bootstrap5\Accordion::renderItem() does only seem to accept array, 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

226
            $items[] = Html::div($this->renderItem($header, /** @scrutinizer ignore-type */ $item, $index++), $options)
Loading history...
227 7
                ->encode($this->encodeTags)
228
                ->render();
229
        }
230 7
231
        return implode("\n", $items);
232 7
    }
233
234
    /**
235 6
     * Renders a single collapsible item group.
236
     *
237
     * @param string $header a label of the item group {@see items}
238
     * @param array $item a single item from {@see items}
239
     * @param int $index the item index as each item group content must have an id
240
     *
241
     * @throws JsonException|RuntimeException
242
     *
243
     * @return string the rendering result
244
     */
245
    private function renderItem(string $header, array $item, int $index): string
246
    {
247
        if (array_key_exists('content', $item)) {
248
            $id = $this->options['id'] . '-collapse' . $index;
249 7
            $expand = ArrayHelper::remove($item, 'expand', false);
250
            $options = ArrayHelper::getValue($item, 'contentOptions', []);
251 7
            $options['id'] = $id;
252 6
253 6
            Html::addCssClass($options, ['widget' => 'accordion-body collapse']);
254 6
255 6
            if ($expand) {
256
                Html::addCssClass($options, ['visibility' => 'show']);
257 6
            }
258
259 6
            if (!isset($options['aria-label'], $options['aria-labelledby'])) {
260 6
                $options['aria-labelledby'] = $options['id'] . '-heading';
261
            }
262
263 6
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
264 6
265
            if ($encodeLabel) {
266
                $header = Html::encode($header);
267 6
            }
268
269 6
            $itemToggleOptions = array_merge([
270 6
                'tag' => 'button',
271
                'type' => 'button',
272
                'data-bs-toggle' => 'collapse',
273 6
                'data-bs-target' => '#' . $options['id'],
274 6
                'aria-expanded' => $expand ? 'true' : 'false',
275 6
            ], $this->itemToggleOptions);
276 6
277 6
            $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
278 6
279 6
            /** @psalm-suppress ConflictingReferenceConstraint */
280
            if ($itemToggleTag === 'a') {
281 6
                ArrayHelper::remove($itemToggleOptions, 'data-bs-target');
282 6
                $header = Html::a($header, '#' . $id, $itemToggleOptions)->encode($this->encodeTags) . "\n";
283
            } else {
284
                Html::addCssClass($itemToggleOptions, ['widget' => 'accordion-button']);
285 6
                if (!$expand) {
286
                    Html::addCssClass($itemToggleOptions, ['expand' => 'collapsed']);
287
                }
288 6
                $header = Html::button($header, $itemToggleOptions)->encode($this->encodeTags)->render();
289 1
            }
290 1
291
            if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
292 5
                $content = $item['content'];
293 5
            } elseif (is_array($item['content'])) {
294 5
                $ulOptions = ['class' => 'list-group'];
295
                $ulItemOptions = ['class' => 'list-group-item'];
296 5
297
                $items = [];
298
                foreach ($item['content'] as $content) {
299 6
                    $items[] = Html::li($content)
0 ignored issues
show
Bug introduced by
The method li() does not exist on Yiisoft\Html\Html. ( Ignorable by Annotation )

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

299
                    $items[] = Html::/** @scrutinizer ignore-call */ li($content)

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...
300 6
                        ->attributes($ulItemOptions)
301 1
                        ->encode($this->encodeTags);
302 1
                }
303 1
304
                $content = Html::ul()
0 ignored issues
show
Bug introduced by
The call to Yiisoft\Html\Html::ul() has too few arguments starting with items. ( Ignorable by Annotation )

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

304
                $content = Html::/** @scrutinizer ignore-call */ ul()

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
305 1
                        ->items(...$items)
306 1
                        ->attributes($ulOptions) . "\n";
307 1
            } else {
308
                throw new RuntimeException('The "content" option should be a string, array or object.');
309
            }
310 1
        } else {
311
            throw new RuntimeException('The "content" option is required.');
312 6
        }
313
314
        $group = [];
315 1
316
        if ($this->autoCloseItems) {
317
            $options['data-bs-parent'] = '#' . $this->options['id'];
318 6
        }
319
320 6
        $groupOptions = ['class' => 'accordion-header', 'id' => $options['id'] . '-heading'];
321 6
322
        $group[] = Html::tag('h2', $header, $groupOptions)->encode($this->encodeTags);
323
        $group[] = Html::div($content, $options)->encode($this->encodeTags);
324 6
325
        return implode("\n", $group);
326 6
    }
327
}
328