yiisoft /
yii-bootstrap4
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | namespace Yiisoft\Yii\Bootstrap4; |
||||
| 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 is_array; |
||||
| 17 | use function is_int; |
||||
| 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 | * // equivalent to the above |
||||
| 31 | * [ |
||||
| 32 | * 'label' => 'Collapsible Group Item #1', |
||||
| 33 | * 'content' => 'Anim pariatur cliche...', |
||||
| 34 | * // open its content by default |
||||
| 35 | * 'contentOptions' => ['class' => 'in'], |
||||
| 36 | * ], |
||||
| 37 | * // another group item |
||||
| 38 | * [ |
||||
| 39 | * 'label' => 'Collapsible Group Item #1', |
||||
| 40 | * 'content' => 'Anim pariatur cliche...', |
||||
| 41 | * 'contentOptions' => [...], |
||||
| 42 | * 'options' => [...], |
||||
| 43 | * 'expand' => true, |
||||
| 44 | * ], |
||||
| 45 | * // if you want to swap out .card-block with .list-group, you may use the following |
||||
| 46 | * [ |
||||
| 47 | * 'label' => 'Collapsible Group Item #1', |
||||
| 48 | * 'content' => [ |
||||
| 49 | * 'Anim pariatur cliche...', |
||||
| 50 | * 'Anim pariatur cliche...', |
||||
| 51 | * ], |
||||
| 52 | * 'contentOptions' => [...], |
||||
| 53 | * 'options' => [...], |
||||
| 54 | * 'footer' => 'Footer' // the footer label in list-group, |
||||
| 55 | * ], |
||||
| 56 | * ]); |
||||
| 57 | * ``` |
||||
| 58 | */ |
||||
| 59 | class Accordion extends Widget |
||||
| 60 | { |
||||
| 61 | private array $items = []; |
||||
| 62 | private bool $encodeLabels = true; |
||||
| 63 | private bool $autoCloseItems = true; |
||||
| 64 | private array $itemToggleOptions = []; |
||||
| 65 | private array $options = []; |
||||
| 66 | |||||
| 67 | 7 | protected function run(): string |
|||
| 68 | { |
||||
| 69 | 7 | if (!isset($this->options['id'])) { |
|||
| 70 | 7 | $this->options['id'] = "{$this->getId()}-accordion"; |
|||
| 71 | } |
||||
| 72 | |||||
| 73 | 7 | $this->registerPlugin('collapse', $this->options); |
|||
| 74 | |||||
| 75 | 7 | Html::addCssClass($this->options, 'accordion'); |
|||
| 76 | |||||
| 77 | 7 | return implode("\n", [ |
|||
| 78 | 7 | Html::beginTag('div', $this->options), |
|||
| 79 | 7 | $this->renderItems(), |
|||
| 80 | 4 | Html::endTag('div'), |
|||
| 81 | 4 | ]) . "\n"; |
|||
| 82 | } |
||||
| 83 | |||||
| 84 | /** |
||||
| 85 | * Renders collapsible items as specified on {@see items}. |
||||
| 86 | * |
||||
| 87 | * @throws RuntimeException|JsonException| |
||||
| 88 | * |
||||
| 89 | * @return string the rendering result. |
||||
| 90 | */ |
||||
| 91 | 7 | public function renderItems(): string |
|||
| 92 | { |
||||
| 93 | 7 | $items = []; |
|||
| 94 | 7 | $index = 0; |
|||
| 95 | 7 | $expanded = array_search(true, array_column($this->items, 'expand'), true); |
|||
| 96 | |||||
| 97 | 7 | foreach ($this->items as $key => $item) { |
|||
| 98 | 7 | if (!is_array($item)) { |
|||
| 99 | 1 | $item = ['content' => $item]; |
|||
| 100 | } |
||||
| 101 | |||||
| 102 | 7 | if ($expanded === false && $index === 0) { |
|||
| 103 | 6 | $item['expand'] = true; |
|||
| 104 | } |
||||
| 105 | |||||
| 106 | 7 | if (!array_key_exists('label', $item)) { |
|||
| 107 | 3 | if (is_int($key)) { |
|||
| 108 | 3 | throw new RuntimeException('The "label" option is required.'); |
|||
| 109 | } |
||||
| 110 | |||||
| 111 | $item['label'] = $key; |
||||
| 112 | } |
||||
| 113 | |||||
| 114 | 4 | $header = ArrayHelper::remove($item, 'label'); |
|||
| 115 | 4 | $options = ArrayHelper::getValue($item, 'options', []); |
|||
| 116 | |||||
| 117 | 4 | Html::addCssClass($options, ['panel' => 'card']); |
|||
| 118 | |||||
| 119 | 4 | $items[] = Html::tag('div', $this->renderItem($header, $item, $index++), $options); |
|||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
It seems like
$item can also be of type object; however, parameter $item of Yiisoft\Yii\Bootstrap4\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
Loading history...
|
|||||
| 120 | } |
||||
| 121 | |||||
| 122 | 4 | return implode("\n", $items); |
|||
| 123 | } |
||||
| 124 | |||||
| 125 | /** |
||||
| 126 | * Renders a single collapsible item group. |
||||
| 127 | * |
||||
| 128 | * @param string $header a label of the item group {@see items} |
||||
| 129 | * @param array $item a single item from {@see items} |
||||
| 130 | * @param int $index the item index as each item group content must have an id. |
||||
| 131 | * |
||||
| 132 | * @throws JsonException|RuntimeException |
||||
| 133 | * |
||||
| 134 | * @return string the rendering result |
||||
| 135 | */ |
||||
| 136 | 4 | public function renderItem(string $header, array $item, int $index): string |
|||
| 137 | { |
||||
| 138 | 4 | if (array_key_exists('content', $item)) { |
|||
| 139 | 4 | $id = $this->options['id'] . '-collapse' . $index; |
|||
| 140 | 4 | $expand = ArrayHelper::remove($item, 'expand', false); |
|||
| 141 | 4 | $options = ArrayHelper::getValue($item, 'contentOptions', []); |
|||
| 142 | 4 | $options['id'] = $id; |
|||
| 143 | |||||
| 144 | 4 | Html::addCssClass($options, ['widget' => 'collapse']); |
|||
| 145 | |||||
| 146 | 4 | if ($expand) { |
|||
| 147 | 4 | Html::addCssClass($options, 'show'); |
|||
| 148 | } |
||||
| 149 | |||||
| 150 | 4 | if (!isset($options['aria-label'], $options['aria-labelledby'])) { |
|||
| 151 | 4 | $options['aria-labelledby'] = $options['id'] . '-heading'; |
|||
| 152 | } |
||||
| 153 | |||||
| 154 | 4 | $encodeLabel = $item['encode'] ?? $this->encodeLabels; |
|||
| 155 | |||||
| 156 | 4 | if ($encodeLabel) { |
|||
| 157 | 4 | $header = Html::encode($header); |
|||
| 158 | } |
||||
| 159 | |||||
| 160 | 4 | $itemToggleOptions = array_merge([ |
|||
| 161 | 4 | 'tag' => 'button', |
|||
| 162 | 4 | 'type' => 'button', |
|||
| 163 | 4 | 'data-toggle' => 'collapse', |
|||
| 164 | 4 | 'data-target' => '#' . $options['id'], |
|||
| 165 | 4 | 'aria-expanded' => $expand ? 'true' : 'false', |
|||
| 166 | 4 | 'aria-controls' => $options['id'], |
|||
| 167 | 4 | ], $this->itemToggleOptions); |
|||
| 168 | 4 | $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button'); |
|||
| 169 | |||||
| 170 | /** @psalm-suppress ConflictingReferenceConstraint */ |
||||
| 171 | 4 | if ($itemToggleTag === 'a') { |
|||
| 172 | 1 | ArrayHelper::remove($itemToggleOptions, 'data-target'); |
|||
| 173 | 1 | $headerToggle = Html::a($header, '#' . $id, $itemToggleOptions) . "\n"; |
|||
| 174 | } else { |
||||
| 175 | 3 | Html::addCssClass($itemToggleOptions, 'btn-link'); |
|||
| 176 | 3 | $headerToggle = Button::widget() |
|||
| 177 | 3 | ->label($header) |
|||
|
0 ignored issues
–
show
The method
label() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bootstrap4\Progress or Yiisoft\Yii\Bootstrap4\Button or Yiisoft\Yii\Bootstrap4\Nav or Yiisoft\Yii\Bootstrap4\ButtonDropdown.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 178 | 3 | ->encodeLabels(false) |
|||
| 179 | 3 | ->options($itemToggleOptions) |
|||
| 180 | 3 | ->render() . "\n"; |
|||
| 181 | } |
||||
| 182 | |||||
| 183 | 4 | $header = Html::tag('h5', $headerToggle, ['class' => 'mb-0']); |
|||
| 184 | |||||
| 185 | 4 | if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) { |
|||
| 186 | 4 | $content = Html::tag('div', $item['content'], ['class' => 'card-body']) . "\n"; |
|||
| 187 | 1 | } elseif (is_array($item['content'])) { |
|||
| 188 | 1 | $content = Html::ul($item['content'], [ |
|||
| 189 | 1 | 'class' => 'list-group', |
|||
| 190 | 'itemOptions' => [ |
||||
| 191 | 'class' => 'list-group-item', |
||||
| 192 | ], |
||||
| 193 | 'encode' => false, |
||||
| 194 | 1 | ]) . "\n"; |
|||
| 195 | } else { |
||||
| 196 | 4 | throw new RuntimeException('The "content" option should be a string, array or object.'); |
|||
| 197 | } |
||||
| 198 | } else { |
||||
| 199 | throw new RuntimeException('The "content" option is required.'); |
||||
| 200 | } |
||||
| 201 | |||||
| 202 | 4 | $group = []; |
|||
| 203 | |||||
| 204 | 4 | if ($this->autoCloseItems) { |
|||
| 205 | 4 | $options['data-parent'] = '#' . $this->options['id']; |
|||
| 206 | } |
||||
| 207 | |||||
| 208 | 4 | $group[] = Html::tag('div', $header, ['class' => 'card-header', 'id' => $options['id'] . '-heading']); |
|||
| 209 | 4 | $group[] = Html::beginTag('div', $options); |
|||
| 210 | 4 | $group[] = $content; |
|||
| 211 | |||||
| 212 | 4 | if (isset($item['footer'])) { |
|||
| 213 | 1 | $group[] = Html::tag('div', $item['footer'], ['class' => 'card-footer']); |
|||
| 214 | } |
||||
| 215 | |||||
| 216 | 4 | $group[] = Html::endTag('div'); |
|||
| 217 | |||||
| 218 | 4 | return implode("\n", $group); |
|||
| 219 | } |
||||
| 220 | |||||
| 221 | /** |
||||
| 222 | * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect. |
||||
| 223 | * |
||||
| 224 | * Set this to `false` to allow keeping multiple items open at once. |
||||
| 225 | * |
||||
| 226 | * @param bool $value |
||||
| 227 | * |
||||
| 228 | * @return $this |
||||
| 229 | */ |
||||
| 230 | 1 | public function autoCloseItems(bool $value): self |
|||
| 231 | { |
||||
| 232 | 1 | $this->autoCloseItems = $value; |
|||
| 233 | |||||
| 234 | 1 | return $this; |
|||
| 235 | } |
||||
| 236 | |||||
| 237 | /** |
||||
| 238 | * Whether the labels for header items should be HTML-encoded. |
||||
| 239 | * |
||||
| 240 | * @param bool $value |
||||
| 241 | * |
||||
| 242 | * @return $this |
||||
| 243 | */ |
||||
| 244 | public function encodeLabels(bool $value): self |
||||
| 245 | { |
||||
| 246 | $this->encodeLabels = $value; |
||||
| 247 | |||||
| 248 | return $this; |
||||
| 249 | } |
||||
| 250 | |||||
| 251 | /** |
||||
| 252 | * List of groups in the collapse widget. Each array element represents a single group with the following structure: |
||||
| 253 | * |
||||
| 254 | * - label: string, required, the group header label. |
||||
| 255 | * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global |
||||
| 256 | * `$this->encodeLabels` param. |
||||
| 257 | * - content: array|string|object, required, the content (HTML) of the group |
||||
| 258 | * - options: array, optional, the HTML attributes of the group |
||||
| 259 | * - contentOptions: optional, the HTML attributes of the group's content |
||||
| 260 | * |
||||
| 261 | * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers |
||||
| 262 | * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained |
||||
| 263 | * above. |
||||
| 264 | * |
||||
| 265 | * For example: |
||||
| 266 | * |
||||
| 267 | * ```php |
||||
| 268 | * echo Accordion::widget([ |
||||
| 269 | * 'items' => [ |
||||
| 270 | * 'Introduction' => 'This is the first collapsible menu', |
||||
| 271 | * 'Second panel' => [ |
||||
| 272 | * 'content' => 'This is the second collapsible menu', |
||||
| 273 | * ], |
||||
| 274 | * [ |
||||
| 275 | * 'label' => 'Third panel', |
||||
| 276 | * 'content' => 'This is the third collapsible menu', |
||||
| 277 | * ], |
||||
| 278 | * ] |
||||
| 279 | * ]) |
||||
| 280 | * ``` |
||||
| 281 | * |
||||
| 282 | * @param array $value |
||||
| 283 | * |
||||
| 284 | * @return $this |
||||
| 285 | */ |
||||
| 286 | 7 | public function items(array $value): self |
|||
| 287 | { |
||||
| 288 | 7 | $this->items = $value; |
|||
| 289 | |||||
| 290 | 7 | return $this; |
|||
| 291 | } |
||||
| 292 | |||||
| 293 | /** |
||||
| 294 | * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification. |
||||
| 295 | * |
||||
| 296 | * For example: |
||||
| 297 | * |
||||
| 298 | * ```php |
||||
| 299 | * [ |
||||
| 300 | * 'tag' => 'div', |
||||
| 301 | * 'class' => 'custom-toggle', |
||||
| 302 | * ] |
||||
| 303 | * ``` |
||||
| 304 | * |
||||
| 305 | * @param array $value |
||||
| 306 | * |
||||
| 307 | * @return $this |
||||
| 308 | */ |
||||
| 309 | 1 | public function itemToggleOptions(array $value): self |
|||
| 310 | { |
||||
| 311 | 1 | $this->itemToggleOptions = $value; |
|||
| 312 | |||||
| 313 | 1 | return $this; |
|||
| 314 | } |
||||
| 315 | |||||
| 316 | /** |
||||
| 317 | * The HTML attributes for the widget container tag. The following special options are recognized. |
||||
| 318 | * |
||||
| 319 | * @param array $value |
||||
| 320 | * |
||||
| 321 | * @return $this |
||||
| 322 | * |
||||
| 323 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered. |
||||
| 324 | */ |
||||
| 325 | public function options(array $value): self |
||||
| 326 | { |
||||
| 327 | $this->options = $value; |
||||
| 328 | |||||
| 329 | return $this; |
||||
| 330 | } |
||||
| 331 | } |
||||
| 332 |