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

Accordion::encodeLabels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
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_int;
19
use function is_numeric;
20
use function is_object;
21
use function is_string;
22
23
/**
24
 * Accordion renders an accordion bootstrap javascript component.
25
 *
26
 * For example:
27
 *
28
 * ```php
29
 * echo Accordion::widget()
30
 *     ->witItems([
31
 *         [
32
 *             'label' => 'Collapsible Group Item #1',
33
 *             'content' => 'Anim pariatur cliche...',
34
 *             // open its content by default
35
 *             'contentOptions' => ['class' => 'show'],
36
 *         ],
37
 *         // another group item
38
 *         [
39
 *             'label' => 'Collapsible Group Item #2',
40
 *             'content' => 'Anim pariatur cliche...',
41
 *             'contentOptions' => [...],
42
 *             'options' => [...],
43
 *             'expand' => true,
44
 *         ],
45
 *         // if you want to swap out .accordion-body with .list-group, you may provide an array
46
 *         [
47
 *             'label' => 'Collapsible Group Item #3',
48
 *             'content' => [
49
 *                 'Anim pariatur cliche...',
50
 *                 'Anim pariatur cliche...',
51
 *             ],
52
 *             'contentOptions' => [...],
53
 *             'options' => [...],
54
 *         ],
55
 *     ]);
56
 * ```
57
 */
58
final class Accordion extends Widget
59
{
60
    private array $items = [];
61
    private bool $encodeLabels = true;
62
    private bool $encodeTags = false;
63
    private bool $autoCloseItems = true;
64
    private array $itemToggleOptions = [];
65
    private array $options = [];
66 7
67
    protected function run(): string
68 7
    {
69 7
        if (!isset($this->options['id'])) {
70
            $this->options['id'] = "{$this->getId()}-accordion";
71
        }
72 7
73
        $this->registerPlugin('collapse', $this->options);
74
75 7
        /** @psalm-suppress InvalidArgument */
76
        Html::addCssClass($this->options, 'accordion');
77 7
78
        if ($this->encodeTags === false) {
79
            $this->options = array_merge($this->options, ['encode' => false]);
80
        }
81
82
        return Html::div($this->renderItems(), $this->options);
83
    }
84
85
    /**
86
     * Renders collapsible items as specified on {@see items}.
87 7
     *
88
     * @throws JsonException|RuntimeException
89 7
     *
90 7
     * @return string the rendering result
91 7
     */
92
    public function renderItems(): string
93 7
    {
94 7
        $items = [];
95 1
        $index = 0;
96
        $expanded = array_search(true, array_column($this->items, 'expand'), true);
97
98 7
        foreach ($this->items as $key => $item) {
99 6
            if (!is_array($item)) {
100
                $item = ['content' => $item];
101
            }
102 7
103 3
            if ($expanded === false && $index === 0) {
104 3
                $item['expand'] = true;
105
            }
106
107
            if (!array_key_exists('label', $item)) {
108
                throw new RuntimeException('The "label" option is required.');
109
            }
110 4
111 4
            $header = ArrayHelper::remove($item, 'label');
112
            $options = ArrayHelper::getValue($item, 'options', []);
113 4
114
            if ($this->encodeTags === false) {
115 4
                $options = array_merge($options, ['encode' => false]);
116
            }
117
118 4
            Html::addCssClass($options, ['panel' => 'accordion-item']);
119
120
            $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

120
            $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

120
            $items[] = Html::div($this->renderItem($header, /** @scrutinizer ignore-type */ $item, $index++), $options);
Loading history...
121
        }
122
123
        return implode("\n", $items);
124
    }
125
126
    /**
127
     * Renders a single collapsible item group.
128
     *
129
     * @param string $header a label of the item group {@see items}
130
     * @param array $item a single item from {@see items}
131
     * @param int $index the item index as each item group content must have an id
132 4
     *
133
     * @throws JsonException|RuntimeException
134 4
     *
135 4
     * @return string the rendering result
136 4
     */
137 4
    public function renderItem(string $header, array $item, int $index): string
138 4
    {
139
        if (array_key_exists('content', $item)) {
140 4
            $id = $this->options['id'] . '-collapse' . $index;
141
            $expand = ArrayHelper::remove($item, 'expand', false);
142 4
            $options = ArrayHelper::getValue($item, 'contentOptions', []);
143 4
            $options['id'] = $id;
144
145
            Html::addCssClass($options, ['accordion-body', 'collapse']);
146 4
147 4
            if ($expand) {
148
                Html::addCssClass($options, 'show');
149
            }
150 4
151
            if (!isset($options['aria-label'], $options['aria-labelledby'])) {
152 4
                $options['aria-labelledby'] = $options['id'] . '-heading';
153 4
            }
154
155
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
156 4
157 4
            if ($encodeLabel) {
158 4
                $header = Html::encode($header);
159 4
            }
160 4
161 4
            $itemToggleOptions = array_merge([
162 4
                'tag' => 'button',
163 4
                'type' => 'button',
164
                'data-bs-toggle' => 'collapse',
165
                'data-bs-target' => '#' . $options['id'],
166 4
                'aria-expanded' => $expand ? 'true' : 'false',
167 1
            ], $this->itemToggleOptions);
168 1
169
            if ($this->encodeTags === false) {
170 3
                $itemToggleOptions = array_merge($itemToggleOptions, ['encode' => false]);
171 3
            }
172 3
173
            $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
174 3
175
            /** @psalm-suppress ConflictingReferenceConstraint */
176
            if ($itemToggleTag === 'a') {
177 4
                ArrayHelper::remove($itemToggleOptions, 'data-bs-target');
178 4
                $header = Html::a($header, '#' . $id, $itemToggleOptions) . "\n";
179 1
            } else {
180 1
                Html::addCssClass($itemToggleOptions, 'accordion-button');
181 1
                if (!$expand) {
182
                    Html::addCssClass($itemToggleOptions, 'collapsed');
183
                }
184
                $header = Html::button($header, $itemToggleOptions);
185
            }
186 1
187
            if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
188 4
                $content = $item['content'];
189
            } elseif (is_array($item['content'])) {
190
                $ulOptions = ['class' => 'list-group'];
191
                $ulItemOptions = ['itemOptions' => ['class' => 'list-group-item']];
192
193
                if ($this->encodeTags === false) {
194 4
                    $ulOptions = array_merge($ulOptions, ['encode' => false]);
195
                    $ulItemOptions['itemOptions'] = array_merge($ulItemOptions['itemOptions'], ['encode' => false]);
196 4
                }
197 4
198
                $content = Html::ul($item['content'], array_merge($ulOptions, $ulItemOptions)) . "\n";
199
            } else {
200
                throw new RuntimeException('The "content" option should be a string, array or object.');
201 4
            }
202 4
        } else {
203
            throw new RuntimeException('The "content" option is required.');
204 4
        }
205
206
        $group = [];
207
208
        if ($this->autoCloseItems) {
209
            $options['data-bs-parent'] = '#' . $this->options['id'];
210
        }
211
212
        $groupOptions = ['class' => 'accordion-header', 'id' => $options['id'] . '-heading'];
213
214
        if ($this->encodeTags === false) {
215
            $options = array_merge($options, ['encode' => false]);
216 1
            $groupOptions = array_merge($groupOptions, ['encode' => false]);
217
        }
218 1
219
        $group[] = Html::tag('h2', $header, $groupOptions);
220 1
        $group[] = Html::div($content, $options);
221
222
        return implode("\n", $group);
223
    }
224
225
    /**
226
     * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect.
227
     *
228
     * Set this to `false` to allow keeping multiple items open at once.
229
     *
230
     * @param bool $value
231
     *
232
     * @return $this
233
     */
234
    public function withAutoCloseItems(bool $value): self
235
    {
236
        $new = clone $this;
237
        $new->autoCloseItems = $value;
238
239
        return $new;
240
    }
241
242
    /**
243
     * Whether the labels for header items should be HTML-encoded.
244
     *
245
     * @param bool $value
246
     *
247
     * @return $this
248
     */
249
    public function withEncodeLabels(bool $value): self
250
    {
251
        $new = clone $this;
252
        $new->encodeLabels = $value;
253
254
        return $new;
255
    }
256
257
    /**
258
     * List of groups in the collapse widget. Each array element represents a single group with the following structure:
259
     *
260
     * - label: string, required, the group header label.
261
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global
262
     *   `$this->encodeLabels` param.
263
     * - content: array|string|object, required, the content (HTML) of the group
264
     * - options: array, optional, the HTML attributes of the group
265
     * - contentOptions: optional, the HTML attributes of the group's content
266
     *
267
     * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers
268
     * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained
269
     * above.
270
     *
271
     * For example:
272 7
     *
273
     * ```php
274 7
     * echo Accordion::widget([
275
     *     'withItems' => [
276 7
     *       'Introduction' => 'This is the first collapsible menu',
277
     *       'Second panel' => [
278
     *           'content' => 'This is the second collapsible menu',
279
     *       ],
280
     *       [
281
     *           'label' => 'Third panel',
282
     *           'content' => 'This is the third collapsible menu',
283
     *       ],
284
     *   ]
285
     * ])
286
     * ```
287
     *
288
     * @param array $value
289
     *
290
     * @return $this
291
     */
292
    public function withItems(array $value): self
293
    {
294
        $new = clone $this;
295 1
        $new->items = $value;
296
297 1
        return $new;
298
    }
299 1
300
    /**
301
     * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
302
     *
303
     * For example:
304
     *
305
     * ```php
306
     * [
307
     *     'tag' => 'div',
308
     *     'class' => 'custom-toggle',
309
     * ]
310
     * ```
311
     *
312
     * @param array $value
313
     *
314
     * @return $this
315
     */
316
    public function withItemToggleOptions(array $value): self
317
    {
318
        $new = clone $this;
319
        $new->itemToggleOptions = $value;
320
321
        return $new;
322
    }
323
324
    /**
325
     * The HTML attributes for the widget container tag. The following special options are recognized.
326
     *
327
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
328
     *
329
     * @param array $value
330
     *
331
     * @return $this
332
     */
333
    public function withOptions(array $value): self
334
    {
335
        $new = clone $this;
336
        $new->options = $value;
337
338
        return $new;
339
    }
340
341
    /**
342
     * Allows you to enable or disable the encoding tags html.
343
     *
344
     * @param bool $value
345
     *
346
     * @return self
347
     */
348
    public function withencodeTags(bool $value): self
349
    {
350
        $new = clone $this;
351
        $new->encodeTags = $value;
352
353
        return $new;
354
    }
355
}
356