From 1ea694a5493f846046b82b9e3552faf63e016634 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula Date: Fri, 10 Nov 2023 21:40:34 -0300 Subject: [PATCH] initial commit. --- composer.json | 13 +- phpunit.xml.dist | 4 +- src/Accordion.php | 263 +++++++++ src/ActiveField.php | 628 ++++++++++++++++++++++ src/ActiveForm.php | 144 +++++ src/Alert.php | 144 +++++ src/BaseHtml.php | 205 +++++++ src/BootstrapAsset.php | 30 ++ src/BootstrapIconAsset.php | 30 ++ src/BootstrapPluginAsset.php | 37 ++ src/BootstrapWidgetTrait.php | 121 +++++ src/Breadcrumbs.php | 237 ++++++++ src/Button.php | 69 +++ src/ButtonDropdown.php | 214 ++++++++ src/ButtonGroup.php | 118 ++++ src/ButtonToolbar.php | 118 ++++ src/Carousel.php | 224 ++++++++ src/Dropdown.php | 168 ++++++ src/Example.php | 13 - src/Html.php | 17 + src/InputWidget.php | 18 + src/LinkPager.php | 322 +++++++++++ src/Modal.php | 315 +++++++++++ src/Nav.php | 307 +++++++++++ src/NavBar.php | 235 ++++++++ src/Offcanvas.php | 260 +++++++++ src/Popover.php | 198 +++++++ src/Progress.php | 190 +++++++ src/Tabs.php | 276 ++++++++++ src/Toast.php | 226 ++++++++ src/ToggleButtonGroup.php | 139 +++++ src/Widget.php | 24 + src/i18n/TranslationBootstrap.php | 34 ++ src/messages/af/messages.mo | Bin 0 -> 28 bytes src/messages/af/messages.po | 32 ++ src/messages/ar/messages.mo | Bin 0 -> 28 bytes src/messages/ar/messages.po | 32 ++ src/messages/az/messages.mo | Bin 0 -> 28 bytes src/messages/az/messages.po | 32 ++ src/messages/be/messages.mo | Bin 0 -> 28 bytes src/messages/be/messages.po | 32 ++ src/messages/bg/messages.mo | Bin 0 -> 28 bytes src/messages/bg/messages.po | 32 ++ src/messages/bs/messages.mo | Bin 0 -> 28 bytes src/messages/bs/messages.po | 32 ++ src/messages/ca/messages.mo | Bin 0 -> 28 bytes src/messages/ca/messages.po | 32 ++ src/messages/config.php | 62 +++ src/messages/cs/messages.mo | Bin 0 -> 28 bytes src/messages/cs/messages.po | 32 ++ src/messages/da/messages.mo | Bin 0 -> 28 bytes src/messages/da/messages.po | 32 ++ src/messages/de-CH/messages.mo | Bin 0 -> 326 bytes src/messages/de-CH/messages.po | 32 ++ src/messages/de/messages.mo | Bin 0 -> 326 bytes src/messages/de/messages.po | 32 ++ src/messages/el/messages.mo | Bin 0 -> 28 bytes src/messages/el/messages.po | 32 ++ src/messages/es/messages.mo | Bin 0 -> 28 bytes src/messages/es/messages.po | 32 ++ src/messages/et/messages.mo | Bin 0 -> 28 bytes src/messages/et/messages.po | 32 ++ src/messages/fa/messages.mo | Bin 0 -> 28 bytes src/messages/fa/messages.po | 32 ++ src/messages/fi/messages.mo | Bin 0 -> 28 bytes src/messages/fi/messages.po | 32 ++ src/messages/fr/messages.mo | Bin 0 -> 309 bytes src/messages/fr/messages.po | 32 ++ src/messages/he/messages.mo | Bin 0 -> 28 bytes src/messages/he/messages.po | 32 ++ src/messages/hi/messages.mo | Bin 0 -> 28 bytes src/messages/hi/messages.po | 32 ++ src/messages/hr/messages.mo | Bin 0 -> 28 bytes src/messages/hr/messages.po | 32 ++ src/messages/hu/messages.mo | Bin 0 -> 28 bytes src/messages/hu/messages.po | 32 ++ src/messages/hy/messages.mo | Bin 0 -> 28 bytes src/messages/hy/messages.po | 32 ++ src/messages/id/messages.mo | Bin 0 -> 28 bytes src/messages/id/messages.po | 32 ++ src/messages/it/messages.mo | Bin 0 -> 321 bytes src/messages/it/messages.po | 32 ++ src/messages/ja/messages.mo | Bin 0 -> 247 bytes src/messages/ja/messages.po | 32 ++ src/messages/ka/messages.mo | Bin 0 -> 28 bytes src/messages/ka/messages.po | 32 ++ src/messages/kk/messages.mo | Bin 0 -> 28 bytes src/messages/kk/messages.po | 32 ++ src/messages/ko/messages.mo | Bin 0 -> 28 bytes src/messages/ko/messages.po | 32 ++ src/messages/kz/messages.mo | Bin 0 -> 28 bytes src/messages/kz/messages.po | 32 ++ src/messages/lt/messages.mo | Bin 0 -> 28 bytes src/messages/lt/messages.po | 32 ++ src/messages/lv/messages.mo | Bin 0 -> 28 bytes src/messages/lv/messages.po | 32 ++ src/messages/ms/messages.mo | Bin 0 -> 28 bytes src/messages/ms/messages.po | 32 ++ src/messages/nb-NO/messages.mo | Bin 0 -> 28 bytes src/messages/nb-NO/messages.po | 32 ++ src/messages/nl/messages.mo | Bin 0 -> 28 bytes src/messages/nl/messages.po | 32 ++ src/messages/pl/messages.mo | Bin 0 -> 28 bytes src/messages/pl/messages.po | 32 ++ src/messages/pt-BR/messages.mo | Bin 0 -> 28 bytes src/messages/pt-BR/messages.po | 32 ++ src/messages/pt/messages.mo | Bin 0 -> 28 bytes src/messages/pt/messages.po | 32 ++ src/messages/ro/messages.mo | Bin 0 -> 28 bytes src/messages/ro/messages.po | 32 ++ src/messages/ru/messages.mo | Bin 0 -> 364 bytes src/messages/ru/messages.po | 31 ++ src/messages/sk/messages.mo | Bin 0 -> 28 bytes src/messages/sk/messages.po | 32 ++ src/messages/sl/messages.mo | Bin 0 -> 28 bytes src/messages/sl/messages.po | 32 ++ src/messages/sr-Latn/messages.mo | Bin 0 -> 28 bytes src/messages/sr-Latn/messages.po | 32 ++ src/messages/sr/messages.mo | Bin 0 -> 28 bytes src/messages/sr/messages.po | 32 ++ src/messages/sv/messages.mo | Bin 0 -> 28 bytes src/messages/sv/messages.po | 32 ++ src/messages/tg/messages.mo | Bin 0 -> 28 bytes src/messages/tg/messages.po | 32 ++ src/messages/th/messages.mo | Bin 0 -> 28 bytes src/messages/th/messages.po | 32 ++ src/messages/tr/messages.mo | Bin 0 -> 28 bytes src/messages/tr/messages.po | 32 ++ src/messages/uk/messages.mo | Bin 0 -> 28 bytes src/messages/uk/messages.po | 32 ++ src/messages/uz/messages.mo | Bin 0 -> 28 bytes src/messages/uz/messages.po | 32 ++ src/messages/vi/messages.mo | Bin 0 -> 28 bytes src/messages/vi/messages.po | 32 ++ src/messages/zh-CN/messages.mo | Bin 0 -> 28 bytes src/messages/zh-CN/messages.po | 32 ++ src/messages/zh-TW/messages.mo | Bin 0 -> 28 bytes src/messages/zh-TW/messages.po | 32 ++ tests/AccordionTest.php | 328 +++++++++++ tests/ActiveFieldDefaultFormCheckTest.php | 240 +++++++++ tests/ActiveFieldTest.php | 326 +++++++++++ tests/ActiveFormTest.php | 397 ++++++++++++++ tests/AlertTest.php | 90 ++++ tests/BreadcrumbsTest.php | 56 ++ tests/ButtonDropdownTest.php | 87 +++ tests/ButtonGroupTest.php | 36 ++ tests/ButtonToolbarTest.php | 111 ++++ tests/CarouselTest.php | 100 ++++ tests/DropdownTest.php | 249 +++++++++ tests/ExampleTest.php | 18 - tests/HtmlTest.php | 148 +++++ tests/LinkPagerTest.php | 199 +++++++ tests/ModalTest.php | 203 +++++++ tests/NavBarTest.php | 251 +++++++++ tests/NavTest.php | 385 +++++++++++++ tests/OffcanvasTest.php | 103 ++++ tests/PopoverTest.php | 67 +++ tests/ProgressTest.php | 104 ++++ tests/TabsTest.php | 423 +++++++++++++++ tests/TestCase.php | 123 +++++ tests/ToastTest.php | 203 +++++++ tests/ToggleButtonGroupTest.php | 109 ++++ tests/TranslationTest.php | 80 +++ tests/assets/.gitignore | 2 + tests/bootstrap.php | 14 + tests/data/ExtendedActiveField.php | 23 + tests/data/Singer.php | 27 + tests/data/User.php | 44 ++ tests/providers/Data.php | 33 ++ 169 files changed, 11606 insertions(+), 39 deletions(-) create mode 100644 src/Accordion.php create mode 100644 src/ActiveField.php create mode 100644 src/ActiveForm.php create mode 100644 src/Alert.php create mode 100644 src/BaseHtml.php create mode 100644 src/BootstrapAsset.php create mode 100644 src/BootstrapIconAsset.php create mode 100644 src/BootstrapPluginAsset.php create mode 100644 src/BootstrapWidgetTrait.php create mode 100644 src/Breadcrumbs.php create mode 100644 src/Button.php create mode 100644 src/ButtonDropdown.php create mode 100644 src/ButtonGroup.php create mode 100644 src/ButtonToolbar.php create mode 100644 src/Carousel.php create mode 100644 src/Dropdown.php delete mode 100644 src/Example.php create mode 100644 src/Html.php create mode 100644 src/InputWidget.php create mode 100644 src/LinkPager.php create mode 100644 src/Modal.php create mode 100644 src/Nav.php create mode 100644 src/NavBar.php create mode 100644 src/Offcanvas.php create mode 100644 src/Popover.php create mode 100644 src/Progress.php create mode 100644 src/Tabs.php create mode 100644 src/Toast.php create mode 100644 src/ToggleButtonGroup.php create mode 100644 src/Widget.php create mode 100644 src/i18n/TranslationBootstrap.php create mode 100644 src/messages/af/messages.mo create mode 100644 src/messages/af/messages.po create mode 100644 src/messages/ar/messages.mo create mode 100644 src/messages/ar/messages.po create mode 100644 src/messages/az/messages.mo create mode 100644 src/messages/az/messages.po create mode 100644 src/messages/be/messages.mo create mode 100644 src/messages/be/messages.po create mode 100644 src/messages/bg/messages.mo create mode 100644 src/messages/bg/messages.po create mode 100644 src/messages/bs/messages.mo create mode 100644 src/messages/bs/messages.po create mode 100644 src/messages/ca/messages.mo create mode 100644 src/messages/ca/messages.po create mode 100644 src/messages/config.php create mode 100644 src/messages/cs/messages.mo create mode 100644 src/messages/cs/messages.po create mode 100644 src/messages/da/messages.mo create mode 100644 src/messages/da/messages.po create mode 100644 src/messages/de-CH/messages.mo create mode 100644 src/messages/de-CH/messages.po create mode 100644 src/messages/de/messages.mo create mode 100644 src/messages/de/messages.po create mode 100644 src/messages/el/messages.mo create mode 100644 src/messages/el/messages.po create mode 100644 src/messages/es/messages.mo create mode 100644 src/messages/es/messages.po create mode 100644 src/messages/et/messages.mo create mode 100644 src/messages/et/messages.po create mode 100644 src/messages/fa/messages.mo create mode 100644 src/messages/fa/messages.po create mode 100644 src/messages/fi/messages.mo create mode 100644 src/messages/fi/messages.po create mode 100644 src/messages/fr/messages.mo create mode 100644 src/messages/fr/messages.po create mode 100644 src/messages/he/messages.mo create mode 100644 src/messages/he/messages.po create mode 100644 src/messages/hi/messages.mo create mode 100644 src/messages/hi/messages.po create mode 100644 src/messages/hr/messages.mo create mode 100644 src/messages/hr/messages.po create mode 100644 src/messages/hu/messages.mo create mode 100644 src/messages/hu/messages.po create mode 100644 src/messages/hy/messages.mo create mode 100644 src/messages/hy/messages.po create mode 100644 src/messages/id/messages.mo create mode 100644 src/messages/id/messages.po create mode 100644 src/messages/it/messages.mo create mode 100644 src/messages/it/messages.po create mode 100644 src/messages/ja/messages.mo create mode 100644 src/messages/ja/messages.po create mode 100644 src/messages/ka/messages.mo create mode 100644 src/messages/ka/messages.po create mode 100644 src/messages/kk/messages.mo create mode 100644 src/messages/kk/messages.po create mode 100644 src/messages/ko/messages.mo create mode 100644 src/messages/ko/messages.po create mode 100644 src/messages/kz/messages.mo create mode 100644 src/messages/kz/messages.po create mode 100644 src/messages/lt/messages.mo create mode 100644 src/messages/lt/messages.po create mode 100644 src/messages/lv/messages.mo create mode 100644 src/messages/lv/messages.po create mode 100644 src/messages/ms/messages.mo create mode 100644 src/messages/ms/messages.po create mode 100644 src/messages/nb-NO/messages.mo create mode 100644 src/messages/nb-NO/messages.po create mode 100644 src/messages/nl/messages.mo create mode 100644 src/messages/nl/messages.po create mode 100644 src/messages/pl/messages.mo create mode 100644 src/messages/pl/messages.po create mode 100644 src/messages/pt-BR/messages.mo create mode 100644 src/messages/pt-BR/messages.po create mode 100644 src/messages/pt/messages.mo create mode 100644 src/messages/pt/messages.po create mode 100644 src/messages/ro/messages.mo create mode 100644 src/messages/ro/messages.po create mode 100644 src/messages/ru/messages.mo create mode 100644 src/messages/ru/messages.po create mode 100644 src/messages/sk/messages.mo create mode 100644 src/messages/sk/messages.po create mode 100644 src/messages/sl/messages.mo create mode 100644 src/messages/sl/messages.po create mode 100644 src/messages/sr-Latn/messages.mo create mode 100644 src/messages/sr-Latn/messages.po create mode 100644 src/messages/sr/messages.mo create mode 100644 src/messages/sr/messages.po create mode 100644 src/messages/sv/messages.mo create mode 100644 src/messages/sv/messages.po create mode 100644 src/messages/tg/messages.mo create mode 100644 src/messages/tg/messages.po create mode 100644 src/messages/th/messages.mo create mode 100644 src/messages/th/messages.po create mode 100644 src/messages/tr/messages.mo create mode 100644 src/messages/tr/messages.po create mode 100644 src/messages/uk/messages.mo create mode 100644 src/messages/uk/messages.po create mode 100644 src/messages/uz/messages.mo create mode 100644 src/messages/uz/messages.po create mode 100644 src/messages/vi/messages.mo create mode 100644 src/messages/vi/messages.po create mode 100644 src/messages/zh-CN/messages.mo create mode 100644 src/messages/zh-CN/messages.po create mode 100644 src/messages/zh-TW/messages.mo create mode 100644 src/messages/zh-TW/messages.po create mode 100644 tests/AccordionTest.php create mode 100644 tests/ActiveFieldDefaultFormCheckTest.php create mode 100644 tests/ActiveFieldTest.php create mode 100644 tests/ActiveFormTest.php create mode 100644 tests/AlertTest.php create mode 100644 tests/BreadcrumbsTest.php create mode 100644 tests/ButtonDropdownTest.php create mode 100644 tests/ButtonGroupTest.php create mode 100644 tests/ButtonToolbarTest.php create mode 100644 tests/CarouselTest.php create mode 100644 tests/DropdownTest.php delete mode 100644 tests/ExampleTest.php create mode 100644 tests/HtmlTest.php create mode 100644 tests/LinkPagerTest.php create mode 100644 tests/ModalTest.php create mode 100644 tests/NavBarTest.php create mode 100644 tests/NavTest.php create mode 100644 tests/OffcanvasTest.php create mode 100644 tests/PopoverTest.php create mode 100644 tests/ProgressTest.php create mode 100644 tests/TabsTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/ToastTest.php create mode 100644 tests/ToggleButtonGroupTest.php create mode 100644 tests/TranslationTest.php create mode 100644 tests/assets/.gitignore create mode 100644 tests/bootstrap.php create mode 100644 tests/data/ExtendedActiveField.php create mode 100644 tests/data/Singer.php create mode 100644 tests/data/User.php create mode 100644 tests/providers/Data.php diff --git a/composer.json b/composer.json index cd5155d..d6a3b70 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,10 @@ { - "name": "yii2/template", - "type": "library", - "description": "_____", + "name": "yii2-extensions/bootstrap5", + "type": "yii2-extension", + "description": "The Twitter Bootstrap5 extension for the Yii framework.", "keywords": [ - "_____" + "yii2-extensions", + "bootstrap5" ], "license": "mit", "minimum-stability": "dev", @@ -19,12 +20,12 @@ }, "autoload": { "psr-4": { - "yii\\template\\": "src" + "yii\\bootstrap5\\": "src" } }, "autoload-dev": { "psr-4": { - "yii\\template\\tests\\": "tests" + "yiiunit\\extensions\\bootstrap5\\": "tests" } }, "extra": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f29a28d..e2c8e6d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ - + tests diff --git a/src/Accordion.php b/src/Accordion.php new file mode 100644 index 0000000..607711d --- /dev/null +++ b/src/Accordion.php @@ -0,0 +1,263 @@ + [ + * // equivalent to the above + * [ + * 'label' => 'Collapsible Group Item #1', + * 'content' => 'Anim pariatur cliche...', + * // open its content by default + * 'contentOptions' => ['class' => 'in'] + * ], + * // another group item + * [ + * 'label' => 'Collapsible Group Item #1', + * 'content' => 'Anim pariatur cliche...', + * 'contentOptions' => [...], + * 'options' => [...], + * 'expand' => true, + * ], + * // if you want to swap out .card-block with .list-group, you may use the following + * [ + * 'label' => 'Collapsible Group Item #1', + * 'content' => [ + * 'Anim pariatur cliche...', + * 'Anim pariatur cliche...' + * ], + * 'contentOptions' => [...], + * 'options' => [...], + * 'footer' => 'Footer' // the footer label in list-group + * ], + * ] + * ]); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/collapse/#accordion-example + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Accordion extends Widget +{ + /** + * @var array list of groups in the collapse widget. Each array element represents a single + * group with the following structure: + * + * - label: string, required, the group header label. + * - encode: bool, optional, whether this label should be HTML-encoded. This param will override + * global `$this->encodeLabels` param. + * - content: array|string|object, required, the content (HTML) of the group + * - options: array, optional, the HTML attributes of the group + * - contentOptions: optional, the HTML attributes of the group's content + * + * Since version 2.0.7 you may also specify this property as key-value pairs, where the key refers to the + * `label` and the value refers to `content`. If value is a string it is interpreted as label. If it is + * an array, it is interpreted as explained above. + * + * For example: + * + * ```php + * echo Accordion::widget([ + * 'items' => [ + * 'Introduction' => 'This is the first collapsable menu', + * 'Second panel' => [ + * 'content' => 'This is the second collapsable menu', + * ], + * [ + * 'label' => 'Third panel', + * 'content' => 'This is the third collapsable menu', + * ], + * ] + * ]) + * ``` + */ + public $items = []; + /** + * @var bool whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var bool whether to close other items if an item is opened. Defaults to `true` which causes an + * accordion effect. Set this to `false` to allow keeping multiple items open at once. + */ + public $autoCloseItems = true; + /** + * @var array the HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification. + * For example: + * + * ```php + * [ + * 'tag' => 'div', + * 'class' => 'custom-toggle', + * ] + * ``` + * + */ + public $itemToggleOptions = []; + + + /** + * @return string + * @throws InvalidConfigException + */ + public function run(): string + { + $this->registerPlugin('collapse'); + Html::addCssClass($this->options, ['widget' => 'accordion']); + + return implode("\n", [ + Html::beginTag('div', $this->options), + $this->renderItems(), + Html::endTag('div'), + ]) . "\n"; + } + + /** + * Renders collapsible items as specified on [[items]]. + * @throws InvalidConfigException if label isn't specified + * @return string the rendering result + */ + public function renderItems(): string + { + $items = []; + $index = 0; + $expanded = array_search(true, ArrayHelper::getColumn(ArrayHelper::toArray($this->items), 'expand', true)); + foreach ($this->items as $key => $item) { + if (!is_array($item)) { + $item = ['content' => $item]; + } + // BC compatibility: expand first item if none is expanded + if ($expanded === false && $index === 0) { + $item['expand'] = true; + } + if (!array_key_exists('label', $item)) { + if (is_int($key)) { + throw new InvalidConfigException("The 'label' option is required."); + } else { + $item['label'] = $key; + } + } + $header = ArrayHelper::remove($item, 'label'); + $options = ArrayHelper::getValue($item, 'options', []); + Html::addCssClass($options, ['panel' => 'accordion-item']); + $items[] = Html::tag('div', $this->renderItem($header, $item, $index++), $options); + } + + return implode("\n", $items); + } + + /** + * Renders a single collapsible item group + * @param string $header a label of the item group [[items]] + * @param array $item a single item from [[items]] + * @param int $index the item index as each item group content must have an id + * @return string the rendering result + * @throws InvalidConfigException + * @throws Exception + */ + public function renderItem(string $header, array $item, int $index): string + { + if (array_key_exists('content', $item)) { + $id = $this->options['id'] . '-collapse' . $index; + $expand = ArrayHelper::remove($item, 'expand', false); + $options = ArrayHelper::getValue($item, 'contentOptions', []); + $options['id'] = $id; + Html::addCssClass($options, ['widget' => 'collapse']); + + // check if accordion expanded, if true add show class + if ($expand) { + Html::addCssClass($options, ['visibility' => 'show']); + } + + if (!isset($options['aria']['label'], $options['aria']['labelledby'])) { + $options['aria']['labelledby'] = $options['id'] . '-heading'; + } + + $encodeLabel = $item['encode'] ?? $this->encodeLabels; + if ($encodeLabel) { + $header = Html::encode($header); + } + + $itemToggleOptions = array_merge([ + 'tag' => 'button', + 'type' => 'button', + 'data' => [ + 'bs-toggle' => 'collapse', + 'bs-target' => '#' . $options['id'] + ], + 'aria' => [ + 'expanded' => $expand ? 'true' : 'false', + 'controls' => $options['id'] + ] + ], $this->itemToggleOptions); + + $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button'); + if ($itemToggleTag === 'a') { + ArrayHelper::remove($itemToggleOptions, 'data.bs-target'); + $headerToggle = Html::a($header, '#' . $id, $itemToggleOptions) . "\n"; + } else { + if (!$expand) { + Html::addCssClass($itemToggleOptions, ['widget' => 'accordion-button collapsed']); + } else { + Html::addCssClass($itemToggleOptions, ['widget' => 'accordion-button']); + } + $headerToggle = Button::widget([ + 'label' => $header, + 'encodeLabel' => false, + 'options' => $itemToggleOptions, + ]) . "\n"; + } + + $header = Html::tag('h5', $headerToggle, ['class' => 'mb-0']); + + if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) { + $content = Html::tag('div', $item['content'], ['class' => 'accordion-body']) . "\n"; + } elseif (is_array($item['content'])) { + $content = Html::ul($item['content'], [ + 'class' => 'list-group', + 'itemOptions' => [ + 'class' => 'list-group-item', + ], + 'encode' => false, + ]) . "\n"; + } else { + throw new InvalidConfigException('The "content" option should be a string, array or object.'); + } + } else { + throw new InvalidConfigException('The "content" option is required.'); + } + $group = []; + + if ($this->autoCloseItems) { + $options['data']['bs-parent'] = '#' . $this->options['id']; + } + + $group[] = Html::tag('div', $header, ['class' => 'accordion-header', 'id' => $options['id'] . '-heading']); + $group[] = Html::beginTag('div', $options); + $group[] = $content; + if (isset($item['footer'])) { + $group[] = Html::tag('div', $item['footer'], ['class' => 'accordion-footer']); + } + $group[] = Html::endTag('div'); + + return implode("\n", $group); + } +} diff --git a/src/ActiveField.php b/src/ActiveField.php new file mode 100644 index 0000000..eda6f89 --- /dev/null +++ b/src/ActiveField.php @@ -0,0 +1,628 @@ + 'horizontal']); + * + * // Form field without label + * echo $form->field($model, 'demo', [ + * 'inputOptions' => [ + * 'placeholder' => $model->getAttributeLabel('demo'), + * ], + * ])->label(false); + * + * // Inline radio list + * echo $form->field($model, 'demo')->inline()->radioList($items); + * + * // Control sizing in horizontal mode + * echo $form->field($model, 'demo', [ + * 'horizontalCssClasses' => [ + * 'wrapper' => 'col-sm-2', + * ] + * ]); + * + * // With 'default' layout you would use 'template' to size a specific field: + * echo $form->field($model, 'demo', [ + * 'template' => '{label}
{input}{error}{hint}
' + * ]); + * + * // Input group + * echo $form->field($model, 'demo', [ + * 'inputTemplate' => '
+ * @ + *
{input}
', + * ]); + * + * ActiveForm::end(); + * ``` + * + * @see ActiveForm + * @see https://getbootstrap.com/docs/5.1/components/forms/ + * + * @author Michael Härtl + * @author Simon Karlen + */ +class ActiveField extends \yii\widgets\ActiveField +{ + /** + * @var bool whether to render [[checkboxList()]] and [[radioList()]] inline. + */ + public $inline = false; + /** + * @var string|null optional template to render the `{input}` placeholder content + */ + public $inputTemplate = null; + /** + * @var array options for the wrapper tag, used in the `{beginWrapper}` placeholder + */ + public $wrapperOptions = []; + /** + * {@inheritdoc} + */ + public $options = ['class' => ['widget' => 'mb-3']]; + /** + * {@inheritdoc} + */ + public $inputOptions = ['class' => ['widget' => 'form-control']]; + /** + * @var array the default options for the input checkboxes. The parameter passed to individual + * input methods (e.g. [[checkbox()]]) will be merged with this property when rendering the input tag. + * + * If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $checkOptions = [ + 'class' => ['widget' => 'form-check-input'], + 'labelOptions' => [ + 'class' => ['widget' => 'form-check-label'], + ], + ]; + /** + * @var array the default options for the input radios. The parameter passed to individual + * input methods (e.g. [[radio()]]) will be merged with this property when rendering the input tag. + * + * If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $radioOptions = [ + 'class' => ['widget' => 'form-check-input'], + 'labelOptions' => [ + 'class' => ['widget' => 'form-check-label'], + ], + ]; + /** + * {@inheritdoc} + */ + public $errorOptions = ['class' => 'invalid-feedback']; + /** + * {@inheritdoc} + */ + public $labelOptions = ['class' => ['widget' => 'form-label']]; + /** + * {@inheritdoc} + */ + public $hintOptions = ['class' => ['widget' => 'form-text', 'text-muted'], 'tag' => 'div']; + /** + * @var null|array CSS grid classes for horizontal layout. This must be an array with these keys: + * - 'offset' the offset grid class to append to the wrapper if no label is rendered + * - 'label' the label grid class + * - 'wrapper' the wrapper grid class + * - 'error' the error grid class + * - 'hint' the hint grid class + */ + public $horizontalCssClasses = []; + /** + * @var string the template for checkboxes in default layout + */ + public $checkTemplate = "
\n{input}\n{label}\n{error}\n{hint}\n
"; + /** + * @var string the template forswitches (custom checkboxes) in default layout + */ + public $switchTemplate = "
\n{input}\n{label}\n{error}\n{hint}\n
"; + /** + * @var string the template for radios in default layout + */ + public $radioTemplate = "
\n{input}\n{label}\n{error}\n{hint}\n
"; + /** + * @var string the template for checkboxes and radios in horizontal layout + */ + public $checkHorizontalTemplate = "{beginWrapper}\n
\n{input}\n{label}\n{error}\n{hint}\n
\n{endWrapper}"; + /** + * @var string the template for switches (custom checkboxes) in horizontal layout + */ + public $switchHorizontalTemplate = "{beginWrapper}\n
\n{input}\n{label}\n{error}\n{hint}\n
\n{endWrapper}"; + /** + * @var string the template for checkboxes and radios in horizontal layout + */ + public $radioHorizontalTemplate = "{beginWrapper}\n
\n{input}\n{label}\n{error}\n{hint}\n
\n{endWrapper}"; + /** + * @var string the `enclosed by label` template for checkboxes and radios in default layout + */ + public $checkEnclosedTemplate = "
\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n
"; + /** + * @var string tthe `enclosed by label` template for switches(custom checkboxes) in default layout + */ + public $switchEnclosedTemplate = "
\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n
"; + /** + * @var bool whether to render the error. Default is `true` except for layout `inline`. + */ + public $enableError = true; + /** + * @var bool whether to render the label. Default is `true`. + */ + public $enableLabel = true; + + + /** + * {@inheritdoc} + */ + public function __construct($config = []) + { + $layoutConfig = $this->createLayoutConfig($config); + $config = ArrayHelper::merge($layoutConfig, $config); + parent::__construct($config); + } + + /** + * {@inheritdoc} + */ + public function render($content = null): string + { + if ($content === null) { + if (!isset($this->parts['{beginWrapper}'])) { + $options = $this->wrapperOptions; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $this->parts['{beginWrapper}'] = Html::beginTag($tag, $options); + $this->parts['{endWrapper}'] = Html::endTag($tag); + } + if ($this->enableLabel === false) { + $this->parts['{label}'] = ''; + $this->parts['{beginLabel}'] = ''; + $this->parts['{labelTitle}'] = ''; + $this->parts['{endLabel}'] = ''; + } elseif (!isset($this->parts['{beginLabel}'])) { + $this->renderLabelParts(); + } + if ($this->enableError === false) { + $this->parts['{error}'] = ''; + } + if ($this->inputTemplate) { + $options = $this->inputOptions; + + if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) { + $this->addErrorClassIfNeeded($options); + } + $this->addAriaAttributes($options); + + $input = $this->parts['{input}'] ?? Html::activeTextInput($this->model, $this->attribute, $options); + $this->parts['{input}'] = strtr($this->inputTemplate, ['{input}' => $input]); + } + } + + return parent::render($content); + } + + /** + * {@inheritdoc} + * Enable option `switch` to render as toggle switch. + * @see https://getbootstrap.com/docs/5.1/forms/checks-radios/#switches + */ + public function checkbox($options = [], $enclosedByLabel = false) + { + $checkOptions = $this->checkOptions; + $options = ArrayHelper::merge($checkOptions, $options); + $labelOptions = ArrayHelper::remove($options, 'labelOptions', []); + $wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', []); + Html::removeCssClass($options, 'form-control'); + $this->labelOptions = ArrayHelper::merge($this->labelOptions, $labelOptions); + $this->wrapperOptions = ArrayHelper::merge($this->wrapperOptions, $wrapperOptions); + $switch = isset($options['switch']) && $options['switch']; + + if ($switch) { + $this->addRoleAttributes($options, 'switch'); + } + if (!isset($options['template'])) { + if ($switch) { + $this->template = $enclosedByLabel ? $this->switchEnclosedTemplate : $this->switchTemplate; + } else { + $this->template = $enclosedByLabel ? $this->checkEnclosedTemplate : $this->checkTemplate; + } + } else { + $this->template = $options['template']; + } + if ($this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) { + if (!isset($options['template'])) { + $this->template = ($switch) + ? $this->switchHorizontalTemplate + : $this->checkHorizontalTemplate; + } + Html::removeCssClass($this->labelOptions, $this->horizontalCssClasses['label']); + Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); + } + Html::removeCssClass($this->labelOptions, 'form-label'); + unset($options['template'], $options['switch']); + + if ($enclosedByLabel) { + if (isset($options['label'])) { + $this->parts['{labelTitle}'] = $options['label']; + } + } + + return parent::checkbox($options, false); + } + + /** + * {@inheritdoc} + */ + public function radio($options = [], $enclosedByLabel = false) + { + $checkOptions = $this->radioOptions; + $options = ArrayHelper::merge($checkOptions, $options); + $labelOptions = ArrayHelper::remove($options, 'labelOptions', []); + $wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', []); + Html::removeCssClass($options, 'form-control'); + $this->labelOptions = ArrayHelper::merge($this->labelOptions, $labelOptions); + $this->wrapperOptions = ArrayHelper::merge($this->wrapperOptions, $wrapperOptions); + + if (!isset($options['template'])) { + $this->template = $enclosedByLabel ? $this->checkEnclosedTemplate : $this->radioTemplate; + } else { + $this->template = $options['template']; + } + if ($this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) { + if (!isset($options['template'])) { + $this->template = $this->radioHorizontalTemplate; + } + Html::removeCssClass($this->labelOptions, $this->horizontalCssClasses['label']); + Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); + } + Html::removeCssClass($this->labelOptions, 'form-label'); + unset($options['template']); + + if ($enclosedByLabel && isset($options['label'])) { + $this->parts['{labelTitle}'] = $options['label']; + } + + return parent::radio($options, false); + } + + /** + * {@inheritdoc} + */ + public function checkboxList($items, $options = []) + { + if (!isset($options['item'])) { + $this->template = str_replace("\n{error}", '', $this->template); + $itemOptions = $options['itemOptions'] ?? []; + $encode = ArrayHelper::getValue($options, 'encode', true); + $itemCount = count($items) - 1; + $error = $this->error()->parts['{error}']; + $options['item'] = function ($i, $label, $name, $checked, $value) use ( + $itemOptions, + $encode, + $itemCount, + $error + ) { + $options = array_merge($this->checkOptions, [ + 'label' => $encode ? Html::encode($label) : $label, + 'value' => $value, + ], $itemOptions); + $wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', ['class' => ['widget' => 'form-check']]); + if ($this->inline) { + Html::addCssClass($wrapperOptions, ['inline' => 'form-check-inline']); + } + + $html = Html::beginTag('div', $wrapperOptions) . "\n" . + Html::checkbox($name, $checked, $options) . "\n"; + if ($itemCount === $i) { + $html .= $error . "\n"; + } + $html .= Html::endTag('div') . "\n"; + + return $html; + }; + } + + parent::checkboxList($items, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function radioList($items, $options = []) + { + if (!isset($options['item'])) { + $this->template = str_replace("\n{error}", '', $this->template); + $itemOptions = $options['itemOptions'] ?? []; + $encode = ArrayHelper::getValue($options, 'encode', true); + $itemCount = count($items) - 1; + $error = $this->error()->parts['{error}']; + $options['item'] = function ($i, $label, $name, $checked, $value) use ( + $itemOptions, + $encode, + $itemCount, + $error + ) { + $options = array_merge($this->radioOptions, [ + 'label' => $encode ? Html::encode($label) : $label, + 'value' => $value, + ], $itemOptions); + $wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', ['class' => ['widget' => 'form-check']]); + if ($this->inline) { + Html::addCssClass($wrapperOptions, ['inline' => 'form-check-inline']); + } + + $html = Html::beginTag('div', $wrapperOptions) . "\n" . + Html::radio($name, $checked, $options) . "\n"; + if ($itemCount === $i) { + $html .= $error . "\n"; + } + $html .= Html::endTag('div') . "\n"; + + return $html; + }; + } + + parent::radioList($items, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function listBox($items, $options = []) + { + if ($this->form->layout === ActiveForm::LAYOUT_INLINE) { + Html::removeCssClass($this->labelOptions, 'visually-hidden'); + } + Html::addCssClass($options, ['widget' => 'form-select']); + + return parent::listBox($items, $options); + } + + /** + * {@inheritdoc} + */ + public function dropDownList($items, $options = []) + { + if ($this->form->layout === ActiveForm::LAYOUT_INLINE) { + Html::removeCssClass($this->labelOptions, 'visually-hidden'); + } + Html::addCssClass($options, ['widget' => 'form-select']); + + return parent::dropDownList($items, $options); + } + + /** + * Renders Bootstrap static form control. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. There are also a special options: + * + * - encode: bool, whether value should be HTML-encoded or not. + * + * @return $this the field object itself + * @see https://getbootstrap.com/docs/5.1/components/forms/#readonly-plain-text + */ + public function staticControl(array $options = []): self + { + $this->adjustLabelFor($options); + $this->parts['{input}'] = Html::activeStaticControl($this->model, $this->attribute, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function label($label = null, $options = []) + { + if (is_bool($label)) { + $this->enableLabel = $label; + if ($label === false && $this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) { + Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']); + } + } else { + $this->enableLabel = true; + $this->renderLabelParts($label, $options); + parent::label($label, $options); + } + + return $this; + } + + /** + * @param bool $value whether to render a inline list + * @return $this the field object itself + * Make sure you call this method before [[checkboxList()]] or [[radioList()]] to have any effect. + */ + public function inline($value = true): self + { + $this->inline = (bool)$value; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function fileInput($options = []) + { + Html::addCssClass($options, ['widget' => 'form-control']); + + return parent::fileInput($options); + } + + /** + * Renders a range (custom input). + * + * @param array $options the tag options in terms of name-value pairs: + * + * - 'min': min. value + * - 'max': max. value + * - 'step': range step, by default, 1 + * + * @return $this + * @see https://getbootstrap.com/docs/5.1/forms/range/ + */ + public function rangeInput(array $options = []) + { + Html::addCssClass($options, ['widget' => 'form-range']); + + return $this->input('range', $options); + } + + /** + * Renders a color picker (custom input). + * + * @param array $options the tag options in terms of name-value pairs + * @return $this + * @see https://getbootstrap.com/docs/5.1/forms/form-control/#color + */ + public function colorInput(array $options = []) + { + Html::removeCssClass($options, 'form-control'); + Html::addCssClass($options, ['widget' => 'form-control form-control-color']); + + return $this->input('color', $options); + } + + /** + * @param array $instanceConfig the configuration passed to this instance's constructor + * @return array the layout specific default configuration for this instance + */ + protected function createLayoutConfig(array $instanceConfig): array + { + $config = [ + 'hintOptions' => [ + 'tag' => 'div', + 'class' => ['form-text', 'text-muted'], + ], + 'errorOptions' => [ + 'tag' => 'div', + 'class' => 'invalid-feedback', + ], + 'inputOptions' => [ + 'class' => 'form-control', + ], + 'labelOptions' => [ + 'class' => ['form-label'], + ], + ]; + + $layout = $instanceConfig['form']->layout; + + if ($layout === ActiveForm::LAYOUT_HORIZONTAL) { + $config['template'] = "{label}\n{beginWrapper}\n{input}\n{error}\n{hint}\n{endWrapper}"; + $config['wrapperOptions'] = []; + $config['labelOptions'] = []; + $config['options'] = []; + $cssClasses = [ + 'offset' => ['col-sm-10', 'offset-sm-2'], + 'label' => ['col-sm-2', 'col-form-label'], + 'wrapper' => 'col-sm-10', + 'error' => '', + 'hint' => '', + 'field' => 'mb-3 row', + ]; + if (isset($instanceConfig['horizontalCssClasses'])) { + $cssClasses = ArrayHelper::merge($cssClasses, $instanceConfig['horizontalCssClasses']); + } + $config['horizontalCssClasses'] = $cssClasses; + + Html::addCssClass($config['wrapperOptions'], $cssClasses['wrapper']); + Html::addCssClass($config['labelOptions'], $cssClasses['label']); + Html::addCssClass($config['errorOptions'], $cssClasses['error']); + Html::addCssClass($config['hintOptions'], $cssClasses['hint']); + Html::addCssClass($config['options'], $cssClasses['field']); + } elseif ($layout === ActiveForm::LAYOUT_INLINE) { + $config['inputOptions']['placeholder'] = true; + $config['enableError'] = false; + + Html::addCssClass($config['labelOptions'], ['screenreader' => 'visually-hidden']); + } elseif ($layout === ActiveForm::LAYOUT_FLOATING) { + $config['inputOptions']['placeholder'] = true; + $config['template'] = "{input}\n{label}\n{error}\n{hint}"; + Html::addCssClass($config['options'], ['layout' => 'form-floating mt-3']); + } + + return $config; + } + + /** + * @param string|null $label the label or null to use model label + * @param array $options the tag options + */ + protected function renderLabelParts(string $label = null, array $options = []) + { + $options = array_merge($this->labelOptions, $options); + if ($label === null) { + if (isset($options['label'])) { + $label = $options['label']; + unset($options['label']); + } else { + $attribute = Html::getAttributeName($this->attribute); + $label = Html::encode($this->model->getAttributeLabel($attribute)); + } + } + if (!isset($options['for'])) { + $options['for'] = Html::getInputId($this->model, $this->attribute); + } + $this->parts['{beginLabel}'] = Html::beginTag('label', $options); + $this->parts['{endLabel}'] = Html::endTag('label'); + if (!isset($this->parts['{labelTitle}'])) { + $this->parts['{labelTitle}'] = $label; + } + } +} diff --git a/src/ActiveForm.php b/src/ActiveForm.php new file mode 100644 index 0000000..a155c0f --- /dev/null +++ b/src/ActiveForm.php @@ -0,0 +1,144 @@ + 'horizontal']) + * ``` + * + * This will set default values for the [[ActiveField]] + * to render horizontal form fields. In particular the [[ActiveField::template|template]] + * is set to `{label} {beginWrapper} {input} {error} {endWrapper} {hint}` and the + * [[ActiveField::horizontalCssClasses|horizontalCssClasses]] are set to: + * + * ```php + * [ + * 'offset' => 'offset-sm-3', + * 'label' => 'col-sm-3', + * 'wrapper' => 'col-sm-6', + * 'error' => '', + * 'hint' => 'col-sm-3', + * ] + * ``` + * + * To get a different column layout in horizontal mode you can modify those options + * through [[fieldConfig]]: + * + * ```php + * $form = ActiveForm::begin([ + * 'layout' => 'horizontal', + * 'fieldConfig' => [ + * 'template' => "{label}\n{beginWrapper}\n{input}\n{hint}\n{error}\n{endWrapper}", + * 'horizontalCssClasses' => [ + * 'label' => 'col-sm-4', + * 'offset' => 'offset-sm-4', + * 'wrapper' => 'col-sm-8', + * 'error' => '', + * 'hint' => '', + * ], + * ], + * ]); + * ``` + * + * @see ActiveField for details on the [[fieldConfig]] options + * @see https://getbootstrap.com/docs/5.1/components/forms/ + * + * @author Michael Härtl + * @author Simon Karlen + */ +class ActiveForm extends \yii\widgets\ActiveForm +{ + /** + * Default form layout + */ + const LAYOUT_DEFAULT = 'default'; + /** + * Horizontal form layout + */ + const LAYOUT_HORIZONTAL = 'horizontal'; + /** + * Inline form layout + */ + const LAYOUT_INLINE = 'inline'; + /** + * Floating labels form layout + */ + const LAYOUT_FLOATING = 'floating'; + + /** + * @var string the default field class name when calling [[field()]] to create a new field. + * @see fieldConfig + */ + public $fieldClass = ActiveField::class; + /** + * @var array HTML attributes for the form tag. Default is `[]`. + */ + public $options = []; + /** + * @var string the form layout. Either [[LAYOUT_DEFAULT]], [[LAYOUT_HORIZONTAL]] or [[LAYOUT_INLINE]]. + * By choosing a layout, an appropriate default field configuration is applied. This will + * render the form fields with slightly different markup for each layout. You can + * override these defaults through [[fieldConfig]]. + * @see ActiveField for details on Bootstrap 5 field configuration + */ + public $layout = self::LAYOUT_DEFAULT; + /** + * @var string the CSS class that is added to a field container when the associated attribute has validation error. + */ + public $errorCssClass = 'is-invalid'; + /** + * {@inheritdoc} + */ + public $successCssClass = 'is-valid'; + /** + * {@inheritdoc} + */ + public $errorSummaryCssClass = 'alert alert-danger'; + /** + * {@inheritdoc} + */ + public $validationStateOn = self::VALIDATION_STATE_ON_INPUT; + + + /** + * {@inheritdoc} + * @throws InvalidConfigException + */ + public function init() + { + if (!in_array($this->layout, [self::LAYOUT_DEFAULT, self::LAYOUT_HORIZONTAL, self::LAYOUT_INLINE, self::LAYOUT_FLOATING])) { + throw new InvalidConfigException('Invalid layout type: ' . $this->layout); + } + + if ($this->layout === self::LAYOUT_INLINE) { + Html::addCssClass($this->options, ['widget' => 'form-inline']); + } + parent::init(); + } + + /** + * {@inheritDoc} + * @return ActiveField|\yii\widgets\ActiveField + */ + public function field($model, $attribute, $options = []): ActiveField + { + return parent::field($model, $attribute, $options); + } +} diff --git a/src/Alert.php b/src/Alert.php new file mode 100644 index 0000000..36db2ec --- /dev/null +++ b/src/Alert.php @@ -0,0 +1,144 @@ + [ + * 'class' => 'alert-info', + * ], + * 'body' => 'Say hello...', + * ]); + * ``` + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the alert box: + * + * ```php + * Alert::begin([ + * 'options' => [ + * 'class' => 'alert-warning', + * ], + * ]); + * + * echo 'Say hello...'; + * + * Alert::end(); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/alerts/ + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Alert extends Widget +{ + /** + * @var string the body content in the alert component. Note that anything between + * the [[begin()]] and [[end()]] calls of the Alert widget will also be treated + * as the body content, and will be rendered before this. + */ + public $body; + /** + * @var array|false the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is false, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Alert documentation](https://getbootstrap.com/docs/5.1/components/alerts/) + * for the supported HTML attributes. + */ + public $closeButton = []; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * {@inheritdoc} + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('alert'); + } + + /** + * Renders the alert body and the close button (if any). + * @return string the rendering result + */ + protected function renderBodyEnd(): string + { + return $this->body . "\n" . $this->renderCloseButton() . "\n"; + } + + /** + * Renders the close button. + * @return string|null the rendering result + */ + protected function renderCloseButton() + { + if (($closeButton = $this->closeButton) !== false) { + $tag = ArrayHelper::remove($closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($closeButton, 'label', ''); + if ($tag === 'button' && !isset($closeButton['type'])) { + $closeButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + Html::addCssClass($this->options, ['widget' => 'alert']); + + if ($this->closeButton !== false) { + $this->closeButton = array_merge([ + 'class' => ['widget' => 'btn-close'], + 'data' => ['bs-dismiss' => 'alert'], + 'aria' => ['label' => Yii::t('yii/bootstrap5', 'Close')] + ], $this->closeButton); + + Html::addCssClass($this->options, ['toggle' => 'alert-dismissible']); + } + if (!isset($this->options['role'])) { + $this->options['role'] = 'alert'; + } + } +} diff --git a/src/BaseHtml.php b/src/BaseHtml.php new file mode 100644 index 0000000..a3eb3b2 --- /dev/null +++ b/src/BaseHtml.php @@ -0,0 +1,205 @@ + 'form-check-input']); + if (!isset($itemOptions['labelOptions'])) { + $itemOptions['labelOptions'] = ['class' => 'form-check-label']; + } else { + static::addCssClass($itemOptions['labelOptions'], ['bootstrap' => 'form-check-label']); + } + + $wrapperOptions = $inline ? ['class' => 'form-check form-check-inline'] : ['class' => 'form-check']; + + $encode = ArrayHelper::getValue($options, 'encode', true); + + $options['item'] = function ($index, $label, $name, $checked, $value) use ($itemOptions, $wrapperOptions, $encode) { + $itemOptions['value'] = $value; + if (!isset($itemOptions['label'])) { + $itemOptions['label'] = $encode ? static::encode($label) : $label; + } + + return static::tag('div', static::radio($name, $checked, $itemOptions), $wrapperOptions); + }; + } + + return parent::radioList($name, $selection, $items, $options); + } + + /** + * {@inheritdoc} + * Pass `true` in `$options['inline']` to generate [inline list](https://getbootstrap.com/docs/5.1/forms/checks-radios/#inline). + */ + public static function checkboxList($name, $selection = null, $items = [], $options = []): string + { + $inline = ArrayHelper::remove($options, 'inline', false); + + if (!isset($options['item'])) { + $itemOptions = ArrayHelper::remove($options, 'itemOptions', []); + static::addCssClass($itemOptions, 'form-check-input'); + if (!isset($itemOptions['labelOptions'])) { + $itemOptions['labelOptions'] = ['class' => 'form-check-label']; + } else { + static::addCssClass($itemOptions['labelOptions'], 'form-check-label'); + } + + $wrapperOptions = $inline ? ['class' => 'form-check form-check-inline'] : ['class' => 'form-check']; + + $encode = ArrayHelper::getValue($options, 'encode', true); + + $options['item'] = function ($index, $label, $name, $checked, $value) use ($itemOptions, $wrapperOptions, $encode) { + $itemOptions['value'] = $value; + if (!isset($itemOptions['label'])) { + $itemOptions['label'] = $encode ? static::encode($label) : $label; + } + + return static::tag('div', static::checkbox($name, $checked, $itemOptions), $wrapperOptions); + }; + } + + return parent::checkboxList($name, $selection, $items, $options); + } + + /** + * {@inheritdoc} + */ + public static function error($model, $attribute, $options = []): string + { + if (!array_key_exists('class', $options)) { + $options['class'] = ['invalid-feedback']; + } + + return parent::error($model, $attribute, $options); + } + + /** + * {@inheritdoc} + */ + protected static function booleanInput($type, $name, $checked = false, $options = []): string + { + $options['checked'] = (bool)$checked; + $value = array_key_exists('value', $options) ? $options['value'] : '1'; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hiddenOptions = []; + if (isset($options['form'])) { + $hiddenOptions['form'] = $options['form']; + } + $hidden = static::hiddenInput($name, $options['uncheck'], $hiddenOptions); + unset($options['uncheck']); + } else { + $hidden = ''; + } + if (isset($options['label'])) { + $label = $options['label']; + $labelOptions = $options['labelOptions'] ?? []; + unset($options['label'], $options['labelOptions']); + + if (!isset($options['id'])) { + $options['id'] = static::getId(); + } + + $input = static::input($type, $name, $value, $options); + + if (isset($labelOptions['wrapInput']) && $labelOptions['wrapInput']) { + unset($labelOptions['wrapInput']); + $content = static::label($input . $label, $options['id'], $labelOptions); + } else { + $content = $input . "\n" . static::label($label, $options['id'], $labelOptions); + } + + return $hidden . $content; + } + + return $hidden . static::input($type, $name, $value, $options); + } + + /** + * Returns an autogenerated ID + * @return string Autogenerated ID + */ + protected static function getId(): string + { + return static::$autoIdPrefix . static::$counter++; + } +} diff --git a/src/BootstrapAsset.php b/src/BootstrapAsset.php new file mode 100644 index 0000000..2790a09 --- /dev/null +++ b/src/BootstrapAsset.php @@ -0,0 +1,30 @@ + + * @author Qiang Xue + * @author Paul Klimov + */ +trait BootstrapWidgetTrait +{ + /** + * @var array|false the options for the underlying Bootstrap JS plugin/component. + * Please refer to the corresponding Bootstrap plugin/component Web page for possible options. + * For example, [this page](https://getbootstrap.com/docs/5.1/components/modal/#options) shows + * how to use the "Modal" component and the supported options (e.g. "backdrop"). + * If this property is false, `registerJs()` will not be called on the view to initialize the module. + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying Bootstrap JS plugin. + * Please refer to the corresponding Bootstrap plugin Web page for possible events. + * For example, [this page](https://getbootstrap.com/docs/5.1/components/modal/#events) shows + * how to use the "Modal" plugin and the supported events (e.g. "shown.bs.modal"). + */ + public $clientEvents = []; + + /** + * Initializes the widget. + * This method will register the bootstrap asset bundle. If you override this method, + * make sure you call the parent implementation first. + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Registers a specific Bootstrap plugin/component and the related events. + * + * @param string $name the name of the Bootstrap plugin + */ + protected function registerPlugin(string $name) + { + /** + * @see https://github.com/twbs/bootstrap/blob/v5.2.0/js/index.esm.js + */ + $jsPlugins = [ + 'alert', + 'button', + 'carousel', + 'collapse', + 'dropdown', + 'modal', + 'offcanvas', + 'popover', + 'scrollspy', + 'tab', + 'toast', + 'tooltip' + ]; + if (in_array($name, $jsPlugins, true)) { + $view = $this->getView(); + BootstrapPluginAsset::register($view); + // 'popover', 'toast' and 'tooltip' plugins not activates via data attributes + if ($this->clientOptions !== false || in_array($name, ['popover', 'toast', 'tooltip'], true)) { + $name = ucfirst($name); + $id = $this->options['id']; + $options = empty($this->clientOptions) ? '{}' : Json::htmlEncode($this->clientOptions); + $view->registerJs("(new bootstrap.$name('#$id', $options));"); + } + + $this->registerClientEvents($name); + } + } + + /** + * Registers JS event handlers that are listed in [[clientEvents]]. + */ + protected function registerClientEvents(string $name = null) + { + if (!empty($this->clientEvents)) { + $id = $this->options['id']; + $js = []; + $appendix = ($name === 'dropdown') ? '.parentElement' : ''; + foreach ($this->clientEvents as $event => $handler) { + $js[] = "document.getElementById('$id')$appendix.addEventListener('$event', $handler);"; + } + $this->getView()->registerJs(implode("\n", $js)); + } + } +} diff --git a/src/Breadcrumbs.php b/src/Breadcrumbs.php new file mode 100644 index 0000000..70254a1 --- /dev/null +++ b/src/Breadcrumbs.php @@ -0,0 +1,237 @@ + [ + * [ + * 'label' => 'the item label', // required + * 'url' => 'the item URL', // optional, will be processed by `Url::to()` + * 'template' => 'own template of the item', // optional + * ], + * ['label' => 'the label of the active item'] + * ], + * 'options' => [...], + * ]); + * ``` + * or + * ```php + * echo Breadcrumbs::widget([ + * 'links' => [ + * 'the item URL' => 'the item label', + * 0 => 'the label of the active item', + * ], + * 'options' => [...], + * ]); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/breadcrumb/ + * @author Alexandr Kozhevnikov + * @author Simon Karlen + */ +class Breadcrumbs extends \yii\widgets\Breadcrumbs +{ + use BootstrapWidgetTrait; + + /** + * {@inheritDoc} + */ + public $tag = 'ol'; + /** + * @var array|false the first hyperlink in the breadcrumbs (called home link). + * Please refer to [[links]] on the format of the link. + * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] + * with the label 'Home'. If this property is false, the home link will not be rendered. + */ + public $homeLink = []; + /** + * {@inheritDoc} + */ + public $itemTemplate = "
  • {link}
  • \n"; + /** + * {@inheritDoc} + */ + public $activeItemTemplate = "
  • {link}
  • \n"; + /** + * @var array the HTML attributes for the widgets nav container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $navOptions = ['aria' => ['label' => 'breadcrumb']]; + + /** + * {@inheritDoc} + */ + public function run(): string + { + if (empty($this->links)) { + return ''; + } + + // Normalize links + $links = []; + foreach ($this->links as $key => $value) { + if (is_array($value)) { + $links[] = $value; + } else { + $links[] = ['label' => $value, 'url' => is_string($key) ? $key : null]; + } + } + $this->links = $links; + unset($links); + + if ($this->homeLink === []) { + $this->homeLink = null; + } + + if (!isset($this->options['id'])) { + $this->options['id'] = "{$this->getId()}-breadcrumb"; + } + Html::addCssClass($this->options, ['widget' => 'breadcrumb']); + + // parent method not return result + ob_start(); + parent::run(); + $content = ob_get_clean(); + + return Html::tag('nav', $content, $this->navOptions); + } + + /** + * The template used to render each active item in the breadcrumbs. The token `{link}` will be replaced with the + * actual HTML link for each active item. + * + * @param string $value + * + * @return $this + */ + public function activeItemTemplate(string $value): self + { + $this->activeItemTemplate = $value; + + return $this; + } + + /** + * Whether to HTML-encode the link labels. + * + * @param bool $value + * + * @return $this + */ + public function encodeLabels(bool $value): self + { + $this->encodeLabels = $value; + + return $this; + } + + /** + * The first hyperlink in the breadcrumbs (called home link). + * + * Please refer to {@see links} on the format of the link. + * + * If this property is not set, it will default to a link pointing with the label 'Home'. If this property is false, + * the home link will not be rendered. + * + * @param array|false $value + * + * @return $this + */ + public function homeLink($value): self + { + $this->homeLink = $value; + + return $this; + } + + /** + * The template used to render each inactive item in the breadcrumbs. The token `{link}` will be replaced with the + * actual HTML link for each inactive item. + * + * @param string $value + * + * @return $this + */ + public function itemTemplate(string $value): self + { + $this->itemTemplate = $value; + + return $this; + } + + /** + * List of links to appear in the breadcrumbs. If this property is empty, the widget will not render anything. + * Each array element represents a single item in the breadcrumbs with the following structure. + * + * @param array $value + * + * @return $this + */ + public function links(array $value): self + { + $this->links = $value; + + return $this; + } + + /** + * The HTML attributes for the widgets nav container tag. + * + * {@see \yii\helpers\Html::renderTagAttributes()} for details on how attributes are being rendered. + * + * @param array $value + * + * @return $this + */ + public function navOptions(array $value): self + { + $this->navOptions = $value; + + return $this; + } + + /** + * The HTML attributes for the widget container tag. The following special options are recognized. + * + * {@see \yii\helpers\Html::renderTagAttributes()} for details on how attributes are being rendered. + * + * @param array $value + * + * @return $this + */ + public function options(array $value): self + { + $this->options = $value; + + return $this; + } + + /** + * The name of the breadcrumb container tag. + * + * @param string $value + * + * @return $this + */ + public function tag(string $value): self + { + $this->tag = $value; + + return $this; + } +} diff --git a/src/Button.php b/src/Button.php new file mode 100644 index 0000000..3b12796 --- /dev/null +++ b/src/Button.php @@ -0,0 +1,69 @@ + 'Action', + * 'options' => ['class' => 'btn-lg'], + * ]); + * ``` + * @see https://getbootstrap.com/docs/5.1/components/buttons/ + * @author Antonio Ramirez + */ +class Button extends Widget +{ + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var string the button label + */ + public $label = 'Button'; + /** + * @var bool whether the label should be HTML-encoded. + */ + public $encodeLabel = true; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + $this->clientOptions = []; + Html::addCssClass($this->options, ['widget' => 'btn']); + } + + /** + * {@inheritdoc} + * @return string + */ + public function run(): string + { + $this->registerPlugin('button'); + return Html::tag( + $this->tagName, + $this->encodeLabel ? Html::encode($this->label) : $this->label, + $this->options + ); + } +} diff --git a/src/ButtonDropdown.php b/src/ButtonDropdown.php new file mode 100644 index 0000000..0407a73 --- /dev/null +++ b/src/ButtonDropdown.php @@ -0,0 +1,214 @@ + 'Action', + * 'dropdown' => [ + * 'items' => [ + * ['label' => 'DropdownA', 'url' => '/'], + * ['label' => 'DropdownB', 'url' => '#'], + * ], + * ], + * ]); + * ``` + * @see https://getbootstrap.com/docs/5.1/components/buttons/ + * @see https://getbootstrap.com/docs/5.1/components/dropdowns/ + * @author Antonio Ramirez + */ +class ButtonDropdown extends Widget +{ + /** + * The css class part of dropdown + */ + const DIRECTION_DOWN = 'down'; + /** + * The css class part of dropleft + */ + const DIRECTION_LEFT = 'left'; + /** + * The css class part of dropright + */ + const DIRECTION_RIGHT = 'right'; + /** + * The css class part of dropup + */ + const DIRECTION_UP = 'up'; + + /** + * @var string|null the button label + */ + public $label = null; + /** + * @var array the HTML attributes for the container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array the HTML attributes of the button. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $buttonOptions = []; + /** + * @var array the configuration array for [[Dropdown]]. + */ + public $dropdown = []; + /** + * @var string the drop-direction of the widget + * + * Possible values are 'left', 'right', 'up', or 'down' (default) + */ + public $direction = self::DIRECTION_DOWN; + /** + * @var bool whether to display a group of split-styled button group. + */ + public $split = false; + /** + * @var string the tag to use to render the button + */ + public $tagName = 'button'; + /** + * @var bool whether the label should be HTML-encoded. + */ + public $encodeLabel = true; + /** + * @var string name of a class to use for rendering dropdowns withing this widget. Defaults to [[Dropdown]]. + */ + public $dropdownClass = Dropdown::class; + /** + * @var bool whether to render the container using the [[options]] as HTML attributes. If set to `false`, + * the container element enclosing the button and dropdown will NOT be rendered. + */ + public $renderContainer = true; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + + if (!isset($this->buttonOptions['id'])) { + $this->buttonOptions['id'] = $this->options['id'] . '-button'; + } + if ($this->label === null) { + $this->label = Yii::t('yii/bootstrap5', 'Button'); + } + } + + /** + * {@inheritdoc} + * @return string + * @throws Throwable + */ + public function run(): string + { + $html = $this->renderButton() . "\n" . $this->renderDropdown(); + + if ($this->renderContainer) { + Html::addCssClass($this->options, ['widget' => 'drop' . $this->direction, 'btn-group']); + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $html = Html::tag($tag, $html, $options); + } + + // Set options id to button options id to ensure correct css selector in plugin initialisation + $this->options['id'] = $this->buttonOptions['id']; + + $this->registerPlugin('dropdown'); + + return $html; + } + + /** + * Generates the button dropdown. + * @return string the rendering result. + * @throws Throwable + */ + protected function renderButton(): string + { + Html::addCssClass($this->buttonOptions, ['widget' => 'btn']); + $label = $this->label; + if ($this->encodeLabel) { + $label = Html::encode($label); + } + + if ($this->split) { + $buttonOptions = $this->buttonOptions; + $this->buttonOptions['data'] = ['bs-toggle' => 'dropdown']; + $this->buttonOptions['aria'] = ['expanded' => 'false']; + Html::addCssClass($this->buttonOptions, ['toggle' => 'dropdown-toggle dropdown-toggle-split']); + unset($buttonOptions['id']); + $splitButton = Button::widget([ + 'label' => '' . Yii::t('yii/bootstrap5', 'Toggle Dropdown') . '', + 'encodeLabel' => false, + 'options' => $this->buttonOptions, + 'view' => $this->getView(), + ]); + } else { + $buttonOptions = $this->buttonOptions; + Html::addCssClass($buttonOptions, ['toggle' => 'dropdown-toggle']); + $buttonOptions['data'] = ['bs-toggle' => 'dropdown']; + $buttonOptions['aria'] = ['expanded' => 'false']; + $splitButton = ''; + } + + if (isset($buttonOptions['href'])) { + if (is_array($buttonOptions['href'])) { + $buttonOptions['href'] = Url::to($buttonOptions['href']); + } + } else { + if ($this->tagName === 'a') { + $buttonOptions['href'] = '#'; + $buttonOptions['role'] = 'button'; + } + } + + return Button::widget([ + 'tagName' => $this->tagName, + 'label' => $label, + 'options' => $buttonOptions, + 'encodeLabel' => false, + 'view' => $this->getView(), + ]) . "\n" . $splitButton; + } + + /** + * Generates the dropdown menu. + * @return string the rendering result. + * @throws Throwable + */ + protected function renderDropdown(): string + { + $config = $this->dropdown; + $config['clientOptions'] = []; + $config['view'] = $this->getView(); + /** @var Widget $dropdownClass */ + $dropdownClass = $this->dropdownClass; + + return $dropdownClass::widget($config); + } +} diff --git a/src/ButtonGroup.php b/src/ButtonGroup.php new file mode 100644 index 0000000..d709f79 --- /dev/null +++ b/src/ButtonGroup.php @@ -0,0 +1,118 @@ + [ + * ['label' => 'A'], + * ['label' => 'B'], + * ['label' => 'C', 'visible' => false], + * ] + * ]); + * + * // button group with an item as a string + * echo ButtonGroup::widget([ + * 'buttons' => [ + * Button::widget(['label' => 'A']), + * ['label' => 'B'], + * ] + * ]); + * ``` + * + * Pressing on the button should be handled via JavaScript. See the following for details: + * + * @see https://getbootstrap.com/docs/5.1/components/buttons/ + * @see https://getbootstrap.com/docs/5.1/components/button-group/ + * + * @author Antonio Ramirez + * @author Simon Karlen + */ +class ButtonGroup extends Widget +{ + /** + * @var array list of buttons. Each array element represents a single button + * which can be specified as a string or an array of the following structure: + * + * - label: string, required, the button label. + * - options: array, optional, the HTML attributes of the button. + * - visible: bool, optional, whether this button is visible. Defaults to true. + */ + public $buttons = []; + /** + * @var bool whether to HTML-encode the button labels. + */ + public $encodeLabels = true; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, ['widget' => 'btn-group']); + if (!isset($this->options['role'])) { + $this->options['role'] = 'group'; + } + } + + /** + * {@inheritdoc} + * @return string + * @throws Throwable + */ + public function run(): string + { + BootstrapAsset::register($this->getView()); + + return Html::tag('div', $this->renderButtons(), $this->options); + } + + /** + * Generates the buttons that compound the group as specified on [[buttons]]. + * @return string the rendering result. + * @throws Throwable + */ + protected function renderButtons(): string + { + $buttons = []; + foreach ($this->buttons as $button) { + if (is_array($button)) { + $visible = ArrayHelper::remove($button, 'visible', true); + if ($visible === false) { + continue; + } + + $button['view'] = $this->getView(); + if (!isset($button['encodeLabel'])) { + $button['encodeLabel'] = $this->encodeLabels; + } + if (!isset($button['options'], $button['options']['type'])) { + ArrayHelper::setValue($button, 'options.type', 'button'); + } + $buttons[] = Button::widget($button); + } else { + $buttons[] = $button; + } + } + + return implode("\n", $buttons); + } +} diff --git a/src/ButtonToolbar.php b/src/ButtonToolbar.php new file mode 100644 index 0000000..1c880ae --- /dev/null +++ b/src/ButtonToolbar.php @@ -0,0 +1,118 @@ + [ + * [ + * 'buttons' => [ + * ['label' => '1', 'options' => ['class' => ['btn-secondary']]], + * ['label' => '2', 'options' => ['class' => ['btn-secondary']]], + * ['label' => '3', 'options' => ['class' => ['btn-secondary']]], + * ['label' => '4', 'options' => ['class' => ['btn-secondary']]] + * ], + * 'class' => ['mr-2'] + * ], + * [ + * 'buttons' => [ + * ['label' => '5', 'options' => ['class' => ['btn-secondary']]], + * ['label' => '6', 'options' => ['class' => ['btn-secondary']]], + * ['label' => '7', 'options' => ['class' => ['btn-secondary']]] + * ], + * 'class' => ['mr-2'] + * ], + * [ + * 'buttons' => [ + * ['label' => '8', 'options' => ['class' => ['btn-secondary']]] + * ] + * ] + * ] + * ]); + * ``` + * + * Pressing on the button should be handled via JavaScript. See the following for details: + * + * @see https://getbootstrap.com/docs/5.1/components/buttons/ + * @see https://getbootstrap.com/docs/5.1/components/button-group/#button-toolbar + * + * @author Simon Karlen + */ +class ButtonToolbar extends Widget +{ + /** + * @var array list of buttons groups. Each array element represents a single group + * which can be specified as a string or an array of the following structure: + * + * - buttons: array list of buttons. Either as array or string representation + * - options: array optional, the HTML attributes of the button group. + * - encodeLabels: bool whether to HTML-encode the button labels. + */ + public $buttonGroups = []; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, ['widget' => 'btn-toolbar']); + if (!isset($this->options['role'])) { + $this->options['role'] = 'toolbar'; + } + } + + /** + * {@inheritdoc} + * @return string + * @throws Throwable + */ + public function run(): string + { + BootstrapAsset::register($this->getView()); + + return Html::tag('div', $this->renderButtonGroups(), $this->options); + } + + /** + * Generates the button groups that compound the toolbar as specified on [[buttonGroups]]. + * @return string the rendering result. + * @throws Throwable + */ + protected function renderButtonGroups(): string + { + $buttonGroups = []; + foreach ($this->buttonGroups as $group) { + if (is_array($group)) { + $group['view'] = $this->getView(); + + if (!isset($group['buttons'])) { + continue; + } + + $buttonGroups[] = ButtonGroup::widget($group); + } else { + $buttonGroups[] = $group; + } + } + + return implode("\n", $buttonGroups); + } +} diff --git a/src/Carousel.php b/src/Carousel.php new file mode 100644 index 0000000..849c834 --- /dev/null +++ b/src/Carousel.php @@ -0,0 +1,224 @@ + [ + * // the item contains only the image + * '', + * // equivalent to the above + * ['content' => ''], + * // the item contains both the image and the caption + * [ + * 'content' => '', + * 'caption' => '

    This is title

    This is the caption text

    ', + * 'captionOptions' => ['class' => ['d-none', 'd-md-block']] + * 'options' => [...], + * ], + * ] + * ]); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/carousel/ + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Carousel extends Widget +{ + /** + * @var array|null the labels for the previous and the next control buttons. + * If null, it means the previous and the next control buttons should not be displayed. + */ + public $controls = [ + 'Previous', + 'Next', + ]; + /** + * @var bool whether carousel indicators (
      tag with anchors to items) should be displayed or not. + */ + public $showIndicators = true; + /** + * @var array list of slides in the carousel. Each array element represents a single + * slide with the following structure: + * + * ```php + * [ + * // required, slide content (HTML), such as an image tag + * 'content' => '', + * // optional, the caption (HTML) of the slide + * 'caption' => '

      This is title

      This is the caption text

      ', + * // optional the HTML attributes of the slide container + * 'options' => [], + * ] + * ``` + */ + public $items = []; + /** + * @var bool Animate slides with a fade transition instead of a slide. Defaults to `false` + */ + public $crossfade = false; + /** + * {@inheritdoc} + */ + public $options = ['data' => ['bs-ride' => 'carousel']]; + + + /** + * {@inheritDoc} + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, ['widget' => 'carousel slide']); + if ($this->crossfade) { + Html::addCssClass($this->options, ['animation' => 'carousel-fade']); + } + } + + /** + * {@inheritdoc} + * @throws InvalidConfigException + */ + public function run(): string + { + $this->registerPlugin('carousel'); + + return implode("\n", [ + Html::beginTag('div', $this->options), + $this->renderIndicators(), + $this->renderItems(), + $this->renderControls(), + Html::endTag('div'), + ]) . "\n"; + } + + /** + * Renders carousel indicators. + * @return string the rendering result + */ + public function renderIndicators(): string + { + if ($this->showIndicators === false) { + return ''; + } + $indicators = []; + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $options = [ + 'data' => [ + 'bs-target' => '#' . $this->options['id'], + 'bs-slide-to' => $i + ], + 'type' => 'button' + ]; + if ($i === 0) { + Html::addCssClass($options, ['activate' => 'active']); + $options['aria']['current'] = 'true'; + } + $indicators[] = Html::tag('button', '', $options); + } + + return Html::tag('div', implode("\n", $indicators), ['class' => ['carousel-indicators']]); + } + + /** + * Renders carousel items as specified on [[items]]. + * @return string the rendering result + * @throws InvalidConfigException + */ + public function renderItems(): string + { + $items = []; + for ($i = 0, $count = count($this->items); $i < $count; $i++) { + $items[] = $this->renderItem($this->items[$i], $i); + } + + return Html::tag('div', implode("\n", $items), ['class' => 'carousel-inner']); + } + + /** + * Renders a single carousel item + * @param string|array $item a single item from [[items]] + * @param int $index the item index as the first item should be set to `active` + * @return string the rendering result + * @throws InvalidConfigException if the item is invalid + * @throws Exception + */ + public function renderItem($item, int $index): string + { + if (is_string($item)) { + $content = $item; + $caption = null; + $options = []; + } elseif (isset($item['content'])) { + $content = $item['content']; + $caption = ArrayHelper::getValue($item, 'caption'); + if ($caption !== null) { + $captionOptions = ArrayHelper::remove($item, 'captionOptions', []); + Html::addCssClass($captionOptions, ['widget' => 'carousel-caption']); + + $caption = Html::tag('div', $caption, $captionOptions); + } + $options = ArrayHelper::getValue($item, 'options', []); + } else { + throw new InvalidConfigException('The "content" option is required.'); + } + + Html::addCssClass($options, ['widget' => 'carousel-item']); + if ($index === 0) { + Html::addCssClass($options, ['activate' => 'active']); + } + + return Html::tag('div', $content . "\n" . $caption, $options); + } + + /** + * Renders previous and next control buttons. + * + * @return string The rendered controls + * + * @throws InvalidConfigException if [[controls]] is invalid. + */ + public function renderControls(): string + { + if (isset($this->controls[0], $this->controls[1])) { + return Html::button($this->controls[0], [ + 'class' => 'carousel-control-prev', + 'data' => [ + 'bs-target' => '#' . $this->options['id'], + 'bs-slide' => 'prev' + ], + 'type' => 'button', + ]) . "\n" + . Html::button($this->controls[1], [ + 'class' => 'carousel-control-next', + 'data' => [ + 'bs-target' => '#' . $this->options['id'], + 'bs-slide' => 'next' + ], + 'type' => 'button', + ]); + } elseif ($this->controls === false) { + return ''; + } else { + throw new InvalidConfigException('The "controls" property must be either false or an array of two elements.'); + } + } +} diff --git a/src/Dropdown.php b/src/Dropdown.php new file mode 100644 index 0000000..4afca38 --- /dev/null +++ b/src/Dropdown.php @@ -0,0 +1,168 @@ + + * Label + * [ + * ['label' => 'DropdownA', 'url' => '/'], + * ['label' => 'DropdownB', 'url' => '#'], + * ], + * ]); + * ?> + * + * ``` + * @see https://getbootstrap.com/docs/5.1/components/dropdowns/ + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Dropdown extends Widget +{ + /** + * @var array list of menu items in the dropdown. Each array element can be either an HTML string, + * or an array representing a single menu with the following structure: + * + * - label: string, required, the label of the item link. + * - encode: bool, optional, whether to HTML-encode item label. + * - url: string|array, optional, the URL of the item link. This will be processed by [[\yii\helpers\Url::to()]]. + * If not set, the item will be treated as a menu header when the item has no sub-menu. + * - visible: bool, optional, whether this menu item is visible. Defaults to true. + * - disabled: bool, optional, whether this menu item is disabled. Defaults to false. + * - linkOptions: array, optional, the HTML attributes of the item link. + * - options: array, optional, the HTML attributes of the item. + * - active: bool, optional, whether the item should be on active state or not. + * - items: array, optional, the submenu items. The structure is the same as this property. + * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. + * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be + * merged with [[submenuOptions]]. + * + * To insert divider use `-`. + */ + public $items = []; + /** + * @var bool whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var array|null the HTML attributes for sub-menu container tags. + */ + public $submenuOptions = []; + + + /** + * {@inheritDoc} + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, ['widget' => 'dropdown-menu']); + } + + /** + * Renders the widget. + * @return string + * @throws InvalidConfigException + */ + public function run(): string + { + BootstrapPluginAsset::register($this->getView()); + $this->registerClientEvents('dropdown'); + + return $this->renderItems($this->items, $this->options); + } + + /** + * Renders menu items. + * @param array $items the menu items to be rendered + * @param array $options the container HTML attributes + * @return string the rendering result. + * @throws InvalidConfigException if the label option is not specified in one of the items. + * @throws Exception + */ + protected function renderItems(array $items, array $options = []): string + { + $lines = []; + foreach ($items as $item) { + if (is_string($item)) { + $lines[] = ($item === '-') + ? Html::tag('hr', '', ['class' => 'dropdown-divider']) + : $item; + continue; + } + if (isset($item['visible']) && !$item['visible']) { + continue; + } + if (!array_key_exists('label', $item)) { + throw new InvalidConfigException("The 'label' option is required."); + } + $encodeLabel = $item['encode'] ?? $this->encodeLabels; + $label = $encodeLabel ? Html::encode($item['label']) : $item['label']; + $itemOptions = ArrayHelper::getValue($item, 'options', []); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); + $active = ArrayHelper::getValue($item, 'active', false); + $disabled = ArrayHelper::getValue($item, 'disabled', false); + + Html::addCssClass($linkOptions, ['widget' => 'dropdown-item']); + if ($disabled) { + ArrayHelper::setValue($linkOptions, 'tabindex', '-1'); + ArrayHelper::setValue($linkOptions, 'aria.disabled', 'true'); + Html::addCssClass($linkOptions, ['disable' => 'disabled']); + } elseif ($active) { + ArrayHelper::setValue($linkOptions, 'aria.current', 'true'); + Html::addCssClass($linkOptions, ['activate' => 'active']); + } + + $url = array_key_exists('url', $item) ? $item['url'] : null; + if (empty($item['items'])) { + if ($url === null) { + $content = Html::tag('h6', $label, ['class' => 'dropdown-header']); + } else { + $content = Html::a($label, $url, $linkOptions); + } + $lines[] = $content; + } else { + $submenuOptions = $this->submenuOptions; + if (isset($item['submenuOptions'])) { + $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']); + } + Html::addCssClass($submenuOptions, ['widget' => 'dropdown-submenu dropdown-menu']); + Html::addCssClass($linkOptions, ['toggle' => 'dropdown-toggle']); + + $lines[] = Html::beginTag('div', array_merge_recursive(['class' => ['dropdown'], 'aria' => ['expanded' => 'false']], $itemOptions)); + $lines[] = Html::a($label, $url, array_merge_recursive([ + 'data' => ['bs-toggle' => 'dropdown'], + 'aria' => ['expanded' => 'false'], + 'role' => 'button', + ], $linkOptions)); + $lines[] = static::widget([ + 'items' => $item['items'], + 'options' => $submenuOptions, + 'submenuOptions' => $submenuOptions, + 'encodeLabels' => $this->encodeLabels, + ]); + $lines[] = Html::endTag('div'); + } + } + + return Html::tag('div', implode("\n", $lines), $options); + } +} diff --git a/src/Example.php b/src/Example.php deleted file mode 100644 index 067eeb8..0000000 --- a/src/Example.php +++ /dev/null @@ -1,13 +0,0 @@ - [ + * 'definitions' => [ + * \yii\widgets\LinkPager::class => \yii\bootstrap5\LinkPager::class, + * ], + * ], + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/pagination/ + * @author Simon Karlen + * + * @property-read array $pageRange + */ +class LinkPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the pager container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array HTML attributes for the pager list tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $listOptions = ['class' => ['pagination']]; + /** + * @var array HTML attributes which will be applied to all link containers + */ + public $linkContainerOptions = ['class' => ['page-item']]; + /** + * @var array HTML attributes for the link in a pager container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $linkOptions = ['class' => ['page-link']]; + /** + * @var string the CSS class for the each page button. + */ + public $pageCssClass = 'page-item'; + /** + * @var string the CSS class for the "first" page button. + */ + public $firstPageCssClass = 'first'; + /** + * @var string the CSS class for the "last" page button. + */ + public $lastPageCssClass = 'last'; + /** + * @var string the CSS class for the "previous" page button. + */ + public $prevPageCssClass = 'prev'; + /** + * @var string the CSS class for the "next" page button. + */ + public $nextPageCssClass = 'next'; + /** + * @var string the CSS class for the active (currently selected) page button. + */ + public $activePageCssClass = 'active'; + /** + * @var string the CSS class for the disabled page buttons. + */ + public $disabledPageCssClass = 'disabled'; + /** + * @var array the options for the disabled tag to be generated inside the disabled list element. + * In order to customize the html tag, please use the tag key. + * + * ```php + * $disabledListItemSubTagOptions = ['class' => 'disabled-link']; + * ``` + */ + public $disabledListItemSubTagOptions = []; + /** + * @var int maximum number of page buttons that can be displayed. Defaults to 10. + */ + public $maxButtonCount = 10; + /** + * @var string|bool the label for the "next" page button. Note that this will NOT be HTML-encoded. + * If this property is false, the "next" page button will not be displayed. + */ + public $nextPageLabel = ''; + /** + * @var string|bool the text label for the "previous" page button. Note that this will NOT be HTML-encoded. + * If this property is false, the "previous" page button will not be displayed. + */ + public $prevPageLabel = ''; + /** + * @var string|bool the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If it's specified as true, page number will be used as label. + * Default is false that means the "first" page button will not be displayed. + */ + public $firstPageLabel = false; + /** + * @var string|bool the text label for the "last" page button. Note that this will NOT be HTML-encoded. + * If it's specified as true, page number will be used as label. + * Default is false that means the "last" page button will not be displayed. + */ + public $lastPageLabel = false; + /** + * @var bool whether to register link tags in the HTML header for prev, next, first and last page. + * Defaults to `false` to avoid conflicts when multiple pagers are used on one page. + * @see https://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + * @see registerLinkTags() + */ + public $registerLinkTags = false; + /** + * @var bool Hide widget when only one page exist. + */ + public $hideOnSinglePage = true; + /** + * @var bool whether to render current page button as disabled. + */ + public $disableCurrentPageButton = false; + + + /** + * Initializes the pager. + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + * @return string + */ + public function run(): string + { + if ($this->registerLinkTags) { + $this->registerLinkTags(); + } + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'nav'); + $html = Html::beginTag($tag, $options); + $html .= $this->renderPageButtons(); + $html .= Html::endTag($tag); + + return $html; + } + + /** + * Registers relational link tags in the html header for prev, next, first and last page. + * These links are generated using [[\yii\data\Pagination::getLinks()]]. + * @see https://www.w3.org/TR/html401/struct/links.html#h-12.1.2 + */ + protected function registerLinkTags() + { + $view = $this->getView(); + foreach ($this->pagination->getLinks() as $rel => $href) { + $view->registerLinkTag(['rel' => $rel, 'href' => $href], $rel); + } + } + + /** + * Renders the page buttons. + * @return string the rendering result + */ + protected function renderPageButtons(): string + { + $pageCount = $this->pagination->getPageCount(); + if ($pageCount < 2 && $this->hideOnSinglePage) { + return ''; + } + + $buttons = []; + $currentPage = $this->pagination->getPage(); + + // first page + $firstPageLabel = $this->firstPageLabel === true ? '1' : $this->firstPageLabel; + if ($firstPageLabel !== false) { + $buttons[] = $this->renderPageButton( + $firstPageLabel, + 0, + $this->firstPageCssClass, + $currentPage <= 0, + false + ); + } + + // prev page + if ($this->prevPageLabel !== false) { + if (($page = $currentPage - 1) < 0) { + $page = 0; + } + $buttons[] = $this->renderPageButton( + $this->prevPageLabel, + $page, + $this->prevPageCssClass, + $currentPage <= 0, + false + ); + } + + // internal pages + list($beginPage, $endPage) = $this->getPageRange(); + for ($i = $beginPage; $i <= $endPage; ++$i) { + $buttons[] = $this->renderPageButton( + (string)($i + 1), + $i, + '', + $this->disableCurrentPageButton && $i == $currentPage, + $i == $currentPage + ); + } + + // next page + if ($this->nextPageLabel !== false) { + if (($page = $currentPage + 1) >= $pageCount - 1) { + $page = $pageCount - 1; + } + $buttons[] = $this->renderPageButton( + $this->nextPageLabel, + $page, + $this->nextPageCssClass, + $currentPage >= $pageCount - 1, + false + ); + } + + // last page + $lastPageLabel = $this->lastPageLabel === true ? $pageCount : $this->lastPageLabel; + if ($lastPageLabel !== false) { + $buttons[] = $this->renderPageButton( + (string)$lastPageLabel, + $pageCount - 1, + $this->lastPageCssClass, + $currentPage >= $pageCount - 1, + false + ); + } + + $options = $this->listOptions; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + + return Html::tag($tag, implode("\n", $buttons), $options); + } + + /** + * Renders a page button. + * You may override this method to customize the generation of page buttons. + * @param string $label the text label for the button + * @param int $page the page number + * @param string $class the CSS class for the page button. + * @param bool $disabled whether this page button is disabled + * @param bool $active whether this page button is active + * @return string the rendering result + */ + protected function renderPageButton(string $label, int $page, string $class, bool $disabled, bool $active): string + { + $options = $this->linkContainerOptions; + $linkWrapTag = ArrayHelper::remove($options, 'tag', 'li'); + Html::addCssClass($options, $class ?: $this->pageCssClass); + + $linkOptions = $this->linkOptions; + $linkOptions['data']['page'] = $page; + + if ($active) { + $options['aria'] = ['current' => 'page']; + Html::addCssClass($options, $this->activePageCssClass); + } + if ($disabled) { + Html::addCssClass($options, $this->disabledPageCssClass); + $disabledItemOptions = $this->disabledListItemSubTagOptions; + $linkOptions = ArrayHelper::merge($linkOptions, $disabledItemOptions); + $linkOptions['tabindex'] = '-1'; + } + + return Html::tag($linkWrapTag, Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options); + } + + /** + * @return array the begin and end pages that need to be displayed. + */ + protected function getPageRange(): array + { + $currentPage = $this->pagination->getPage(); + $pageCount = $this->pagination->getPageCount(); + + $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); + if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { + $endPage = $pageCount - 1; + $beginPage = max(0, $endPage - $this->maxButtonCount + 1); + } + + return [$beginPage, $endPage]; + } +} diff --git a/src/Modal.php b/src/Modal.php new file mode 100644 index 0000000..ca8ea58 --- /dev/null +++ b/src/Modal.php @@ -0,0 +1,315 @@ + 'Hello world', + * 'toggleButton' => ['label' => 'click me'], + * ]); + * + * echo 'Say hello...'; + * + * Modal::end(); + * ~~~ + * + * @see https://getbootstrap.com/docs/5.1/components/modal/ + * @author Antonio Ramirez + * @author Qiang Xue + * @author Simon Karlen + */ +class Modal extends Widget +{ + /** + * The additional css class of extra large modal + */ + const SIZE_EXTRA_LARGE = 'modal-xl'; + /** + * The additional css class of large modal + */ + const SIZE_LARGE = 'modal-lg'; + /** + * The additional css class of small modal + */ + const SIZE_SMALL = 'modal-sm'; + /** + * The additional css class of default modal + */ + const SIZE_DEFAULT = ''; + + /** + * @var string the title content in the modal window. + */ + public $title; + /** + * @var array additional title options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $titleOptions = []; + /** + * @var array additional header options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array body options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $bodyOptions = []; + /** + * @var string|null the footer content in the modal window. + */ + public $footer; + /** + * @var array additional footer options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $footerOptions = []; + /** + * @var string|null the modal size. Can be [[SIZE_LARGE]] or [[SIZE_SMALL]], or empty for default. + */ + public $size; + /** + * @var array|false the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is false, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](https://getbootstrap.com/javascript/#modals) + * for the supported HTML attributes. + */ + public $closeButton = []; + /** + * @var array|false the options for rendering the toggle button tag. + * The toggle button is used to toggle the visibility of the modal window. + * If this property is false, no toggle button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to 'Show'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](https://getbootstrap.com/javascript/#modals) + * for the supported HTML attributes. + */ + public $toggleButton = false; + /** + * @var boolean whether to center the modal vertically + * + * When true the modal-dialog-centered class will be added to the modal-dialog + */ + public $centerVertical = false; + /** + * @var boolean whether to make the modal body scrollable + * + * When true the modal-dialog-scrollable class will be added to the modal-dialog + */ + public $scrollable = false; + /** + * @var array modal dialog options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $dialogOptions = []; + + + /** + * {@inheritDoc} + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo $this->renderToggleButton() . "\n"; + echo Html::beginTag('div', $this->options) . "\n"; + echo Html::beginTag('div', $this->dialogOptions) . "\n"; + echo Html::beginTag('div', ['class' => 'modal-content']) . "\n"; + echo $this->renderHeader() . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . $this->renderFooter(); + echo "\n" . Html::endTag('div'); // modal-content + echo "\n" . Html::endTag('div'); // modal-dialog + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('modal'); + } + + /** + * Renders the header HTML markup of the modal + * @return string the rendering result + */ + protected function renderHeader(): string + { + $button = $this->renderCloseButton(); + if (isset($this->title)) { + Html::addCssClass($this->titleOptions, ['widget' => 'modal-title']); + $header = Html::tag('h5', $this->title, $this->titleOptions); + } else { + $header = ''; + } + + if ($button !== null) { + $header .= "\n" . $button; + } elseif ($header === '') { + return ''; + } + Html::addCssClass($this->headerOptions, ['widget' => 'modal-header']); + + return Html::tag('div', "\n" . $header . "\n", $this->headerOptions); + } + + /** + * Renders the opening tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyBegin(): string + { + Html::addCssClass($this->bodyOptions, ['widget' => 'modal-body']); + + return Html::beginTag('div', $this->bodyOptions); + } + + /** + * Renders the closing tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyEnd(): string + { + return Html::endTag('div'); + } + + /** + * Renders the HTML markup for the footer of the modal + * @return string|null the rendering result + */ + protected function renderFooter() + { + if (isset($this->footer)) { + Html::addCssClass($this->footerOptions, ['widget' => 'modal-footer']); + + return Html::tag('div', "\n" . $this->footer . "\n", $this->footerOptions); + } else { + return null; + } + } + + /** + * Renders the toggle button. + * @return string|null the rendering result + */ + protected function renderToggleButton() + { + if (($toggleButton = $this->toggleButton) !== false) { + $tag = ArrayHelper::remove($toggleButton, 'tag', 'button'); + $label = ArrayHelper::remove($toggleButton, 'label', 'Show'); + + return Html::tag($tag, $label, $toggleButton); + } else { + return null; + } + } + + /** + * Renders the close button. + * @return string|null the rendering result + */ + protected function renderCloseButton() + { + if (($closeButton = $this->closeButton) !== false) { + $tag = ArrayHelper::remove($closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($closeButton, 'label', ''); + if ($tag === 'button' && !isset($closeButton['type'])) { + $closeButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + $this->options = array_merge([ + 'tabindex' => -1, + 'aria-hidden' => 'true', + ], $this->options); + Html::addCssClass($this->options, ['widget' => 'modal fade']); + + if (!empty($this->clientOptions)) { + $this->clientOptions = array_merge(['show' => false], $this->clientOptions); + } + + $this->titleOptions = array_merge([ + 'id' => $this->options['id'] . '-label', + ], $this->titleOptions); + if (!isset($this->options['aria']['label'], $this->options['aria']['labelledby']) && isset($this->title)) { + $this->options['aria']['labelledby'] = $this->titleOptions['id']; + } + + if ($this->closeButton !== false) { + $this->closeButton = array_merge([ + 'class' => ['widget' => 'btn-close'], + 'data' => ['bs-dismiss' => 'modal'], + 'aria' => ['label' => Yii::t('yii/bootstrap5', 'Close')] + ], $this->closeButton); + } + + if ($this->toggleButton !== false) { + $this->toggleButton = array_merge([ + 'data' => ['bs-toggle' => 'modal'], + 'type' => 'button', + ], $this->toggleButton); + if (!isset($this->toggleButton['data']['bs-target']) && !isset($this->toggleButton['href'])) { + $this->toggleButton['data']['bs-target'] = '#' . $this->options['id']; + } + } + + Html::addCssClass($this->dialogOptions, ['widget' => 'modal-dialog']); + if (isset($this->size)) { + Html::addCssClass($this->dialogOptions, ['size' => $this->size]); + } + if ($this->centerVertical) { + Html::addCssClass($this->dialogOptions, ['align' => 'modal-dialog-centered']); + } + if ($this->scrollable) { + Html::addCssClass($this->dialogOptions, ['scroll' => 'modal-dialog-scrollable']); + } + } +} diff --git a/src/Nav.php b/src/Nav.php new file mode 100644 index 0000000..88d9063 --- /dev/null +++ b/src/Nav.php @@ -0,0 +1,307 @@ + [ + * [ + * 'label' => 'Home', + * 'url' => ['site/index'], + * 'linkOptions' => [...], + * ], + * [ + * 'label' => 'Dropdown', + * 'items' => [ + * ['label' => 'Level 1 - Dropdown A', 'url' => '#'], + * '', + * '', + * ['label' => 'Level 1 - Dropdown B', 'url' => '#'], + * ], + * ], + * [ + * 'label' => 'Login', + * 'url' => ['site/login'], + * 'visible' => Yii::$app->user->isGuest + * ], + * ], + * 'options' => ['class' =>'nav-pills'], // set this to nav-tabs to get tab-styled navigation + * ]); + * ``` + * + * Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 5. + * + * @see https://getbootstrap.com/docs/5.1/components/navs/ + * @see https://getbootstrap.com/docs/5.1/components/dropdowns/ + * + * @author Antonio Ramirez + */ +class Nav extends Widget +{ + /** + * @var array list of items in the nav widget. Each array element represents a single + * menu item which can be either a string or an array with the following structure: + * + * - label: string, required, the nav item label. + * - url: optional, the item's URL. Defaults to "#". + * - visible: bool, optional, whether this menu item is visible. Defaults to true. + * - disabled: bool, optional, whether this menu item is disabled. Defaults to false. + * - linkOptions: array, optional, the HTML attributes of the item's link. + * - options: array, optional, the HTML attributes of the item container (LI). + * - active: bool, optional, whether the item should be on active state or not. + * - dropdownOptions: array, optional, the HTML options that will passed to the [[Dropdown]] widget. + * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, + * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. + * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for only this item. + * + * If a menu item is a string, it will be rendered directly without HTML encoding. + */ + public $items = []; + /** + * @var bool whether the nav items labels should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var bool whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive + */ + public $activateItems = true; + /** + * @var bool whether to activate parent menu items when one of the corresponding child menu items is active. + */ + public $activateParents = false; + /** + * @var string|null the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive + */ + public $route = null; + /** + * @var array|null the parameters used to determine if a menu item is active or not. + * If not set, it will use `$_GET`. + * @see route + * @see isItemActive + */ + public $params = null; + /** + * @var string name of a class to use for rendering dropdowns within this widget. Defaults to [[Dropdown]]. + */ + public $dropdownClass = Dropdown::class; + + + /** + * {@inheritDoc} + */ + public function init() + { + parent::init(); + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = Yii::$app->request->getQueryParams(); + } + Html::addCssClass($this->options, ['widget' => 'nav']); + } + + /** + * Renders the widget. + * @return string + * @throws InvalidConfigException|Throwable + */ + public function run(): string + { + BootstrapAsset::register($this->getView()); + + return $this->renderItems(); + } + + /** + * Renders widget items. + * @return string + * @throws InvalidConfigException|Throwable + */ + public function renderItems(): string + { + $items = []; + foreach ($this->items as $item) { + if (isset($item['visible']) && !$item['visible']) { + continue; + } + $items[] = $this->renderItem($item); + } + + return Html::tag('ul', implode("\n", $items), $this->options); + } + + /** + * Renders a widget's item. + * @param string|array $item the item to render. + * @return string the rendering result. + * @throws InvalidConfigException + * @throws Throwable + */ + public function renderItem($item): string + { + if (is_string($item)) { + return $item; + } + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + $encodeLabel = $item['encode'] ?? $this->encodeLabels; + $label = $encodeLabel ? Html::encode($item['label']) : $item['label']; + $options = ArrayHelper::getValue($item, 'options', []); + $items = ArrayHelper::getValue($item, 'items'); + $url = ArrayHelper::getValue($item, 'url', '#'); + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); + $disabled = ArrayHelper::getValue($item, 'disabled', false); + $active = $this->isItemActive($item); + + if (empty($items)) { + $items = ''; + Html::addCssClass($options, ['widget' => 'nav-item']); + Html::addCssClass($linkOptions, ['widget' => 'nav-link']); + } else { + $linkOptions['data']['bs-toggle'] = 'dropdown'; + $linkOptions['role'] = 'button'; + $linkOptions['aria']['expanded'] = 'false'; + Html::addCssClass($options, ['widget' => 'dropdown nav-item']); + Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle nav-link']); + if (is_array($items)) { + $items = $this->isChildActive($items, $active); + $items = $this->renderDropdown($items, $item); + } + } + + if ($disabled) { + ArrayHelper::setValue($linkOptions, 'tabindex', '-1'); + ArrayHelper::setValue($linkOptions, 'aria.disabled', 'true'); + Html::addCssClass($linkOptions, ['disable' => 'disabled']); + } elseif ($this->activateItems && $active) { + Html::addCssClass($linkOptions, ['activate' => 'active']); + } + + return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options); + } + + /** + * Renders the given items as a dropdown. + * This method is called to create sub-menus. + * @param array $items the given items. Please refer to [[Dropdown::items]] for the array structure. + * @param array $parentItem the parent item information. Please refer to [[items]] for the structure of this array. + * @return string the rendering result. + * @throws Throwable + */ + protected function renderDropdown(array $items, array $parentItem): string + { + /** @var Widget $dropdownClass */ + $dropdownClass = $this->dropdownClass; + + return $dropdownClass::widget([ + 'options' => ArrayHelper::getValue($parentItem, 'dropdownOptions', []), + 'items' => $items, + 'encodeLabels' => $this->encodeLabels, + 'clientOptions' => [], + 'view' => $this->getView(), + ]); + } + + /** + * Check to see if a child item is active optionally activating the parent. + * @param array $items @see items + * @param bool $active should the parent be active too + * @return array + * @throws Exception + * @see items + */ + protected function isChildActive(array $items, bool &$active): array + { + foreach ($items as $i => $child) { + if (is_array($child) && !ArrayHelper::getValue($child, 'visible', true)) { + continue; + } + if (is_array($child) && $this->isItemActive($child)) { + ArrayHelper::setValue($items[$i], 'active', true); + if ($this->activateParents) { + $active = true; + } + } + $childItems = ArrayHelper::getValue($child, 'items'); + if (is_array($childItems)) { + $activeParent = false; + $items[$i]['items'] = $this->isChildActive($childItems, $activeParent); + if ($activeParent) { + Html::addCssClass($items[$i]['options'], ['activate' => 'active']); + $active = true; + } + } + } + + return $items; + } + + /** + * Checks whether a menu item is active. + * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. + * When the `url` option of a menu item is specified in terms of an array, its first element is treated + * as the route for the item and the rest of the elements are the associated parameters. + * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item + * be considered active. + * @param array $item the menu item to be checked + * @return bool whether the menu item is active + * @throws Exception + */ + protected function isItemActive(array $item): bool + { + if (!$this->activateItems) { + return false; + } + if (isset($item['active'])) { + return (bool)ArrayHelper::getValue($item, 'active', false); + } + if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) { + $route = $item['url'][0]; + if ($route[0] !== '/' && Yii::$app->controller) { + $route = Yii::$app->controller->module->getUniqueId() . '/' . $route; + } + if (ltrim($route, '/') !== $this->route) { + return false; + } + unset($item['url']['#']); + if (count($item['url']) > 1) { + $params = $item['url']; + unset($params[0]); + foreach ($params as $name => $value) { + if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) { + return false; + } + } + } + + return true; + } + + return false; + } +} diff --git a/src/NavBar.php b/src/NavBar.php new file mode 100644 index 0000000..9f82408 --- /dev/null +++ b/src/NavBar.php @@ -0,0 +1,235 @@ + 'NavBar Test']); + * echo Nav::widget([ + * 'items' => [ + * ['label' => 'Home', 'url' => ['/site/index']], + * ['label' => 'About', 'url' => ['/site/about']], + * ], + * 'options' => ['class' => 'navbar-nav'], + * ]); + * NavBar::end(); + * ``` + * + * @property-write array $containerOptions + * + * @see https://getbootstrap.com/docs/5.1/components/navbar/ + * @author Antonio Ramirez + * @author Alexander Kochetov + */ +class NavBar extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "nav", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $options = []; + /** + * @var array|bool the HTML attributes for the collapse container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $collapseOptions = []; + /** + * @var array|bool the HTML attributes for the offcanvas container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $offcanvasOptions = false; + /** + * @var string|bool the text of the brand or false if it's not used. Note that this is not HTML-encoded. + * @see https://getbootstrap.com/docs/5.1/components/navbar/ + */ + public $brandLabel = false; + /** + * @var string|bool src of the brand image or false if it's not used. Note that this param will override `$this->brandLabel` param. + * @see https://getbootstrap.com/docs/5.1/components/navbar/ + */ + public $brandImage = false; + + /** + * @var array the HTML attributes of the brand image. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $brandImageOptions = []; + /** + * @var array|string|bool $url the URL for the brand's hyperlink tag. This parameter will be processed by [[\yii\helpers\Url::to()]] + * and will be used for the "href" attribute of the brand link. Default value is false that means + * [[\yii\web\Application::homeUrl]] will be used. + * You may set it to `null` if you want to have no link at all. + */ + public $brandUrl = false; + /** + * @var array the HTML attributes of the brand link. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $brandOptions = []; + /** + * @var string text to show for screen readers for the button to toggle the navbar. + */ + public $screenReaderToggleText; + /** + * @var string the toggle button content. Defaults to bootstrap 5 default `` + */ + public $togglerContent = ''; + /** + * @var array the HTML attributes of the navbar toggler button. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $togglerOptions = []; + /** + * @var bool whether the navbar content should be included in an inner div container which by default + * adds left and right padding. Set this to false for a 100% width navbar. + */ + public $renderInnerContainer = true; + /** + * @var array the HTML attributes of the inner container. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $innerContainerOptions = []; + /** + * {@inheritdoc} + */ + public $clientOptions = []; + + + /** + * {@inheritDoc} + */ + public function init() + { + parent::init(); + if (!isset($this->options['class']) || empty($this->options['class'])) { + Html::addCssClass($this->options, [ + 'widget' => 'navbar', + 'toggle' => 'navbar-expand-lg', + 'navbar-light', + 'bg-light' + ]); + } else { + Html::addCssClass($this->options, ['widget' => 'navbar']); + } + $navOptions = $this->options; + $navTag = ArrayHelper::remove($navOptions, 'tag', 'nav'); + $brand = ''; + if (!isset($this->innerContainerOptions['class'])) { + Html::addCssClass($this->innerContainerOptions, ['panel' => 'container']); + } + if ($this->collapseOptions !== false && !isset($this->collapseOptions['id'])) { + $this->collapseOptions['id'] = "{$this->options['id']}-collapse"; + } elseif ($this->offcanvasOptions !== false && !isset($this->offcanvasOptions['id'])) { + $this->offcanvasOptions['id'] = "{$this->options['id']}-offcanvas"; + } + if ($this->brandImage !== false) { + $this->brandLabel = Html::img($this->brandImage, $this->brandImageOptions); + } + if ($this->brandLabel !== false) { + Html::addCssClass($this->brandOptions, ['widget' => 'navbar-brand']); + if ($this->brandUrl === null) { + $brand = Html::tag('span', $this->brandLabel, $this->brandOptions); + } else { + $brand = Html::a( + $this->brandLabel, + $this->brandUrl === false ? Yii::$app->homeUrl : $this->brandUrl, + $this->brandOptions + ); + } + } + + echo Html::beginTag($navTag, $navOptions) . "\n"; + if ($this->renderInnerContainer) { + echo Html::beginTag('div', $this->innerContainerOptions) . "\n"; + } + echo $brand . "\n"; + echo $this->renderToggleButton() . "\n"; + if ($this->collapseOptions !== false) { + Html::addCssClass($this->collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']); + $collapseOptions = $this->collapseOptions; + $collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div'); + echo Html::beginTag($collapseTag, $collapseOptions) . "\n"; + } elseif ($this->offcanvasOptions !== false) { + Offcanvas::begin($this->offcanvasOptions); + } + } + + /** + * Renders the widget. + */ + public function run() + { + if ($this->collapseOptions !== false) { + $tag = ArrayHelper::remove($this->collapseOptions, 'tag', 'div'); + echo Html::endTag($tag) . "\n"; + } elseif ($this->offcanvasOptions !== false) { + Offcanvas::end(); + } + if ($this->renderInnerContainer) { + echo Html::endTag('div') . "\n"; + } + $tag = ArrayHelper::remove($this->options, 'tag', 'nav'); + echo Html::endTag($tag); + BootstrapPluginAsset::register($this->getView()); + } + + /** + * Renders collapsible toggle button. + * @return string the rendering toggle button. + */ + protected function renderToggleButton(): string + { + if ($this->collapseOptions === false && $this->offcanvasOptions === false) { + return ''; + } + + $options = $this->togglerOptions; + Html::addCssClass($options, ['widget' => 'navbar-toggler']); + if ($this->offcanvasOptions !== false) { + $bsData = ['bs-toggle' => 'offcanvas', 'bs-target' => '#' . $this->offcanvasOptions['id']]; + $aria = $this->offcanvasOptions['id']; + } elseif ($this->collapseOptions !== false) { + $bsData = ['bs-toggle' => 'collapse', 'bs-target' => '#' . $this->collapseOptions['id']]; + $aria = $this->collapseOptions['id']; + } + + return Html::button( + $this->togglerContent, + ArrayHelper::merge($options, [ + 'type' => 'button', + 'data' => $bsData, + 'aria' => [ + 'controls' => $aria, + 'expanded' => 'false', + 'label' => $this->screenReaderToggleText ?: Yii::t('yii/bootstrap5', 'Toggle navigation'), + ] + ]) + ); + } +} diff --git a/src/Offcanvas.php b/src/Offcanvas.php new file mode 100644 index 0000000..69fae51 --- /dev/null +++ b/src/Offcanvas.php @@ -0,0 +1,260 @@ + Offcanvas::PLACEMENT_END, + * 'backdrop' => true, + * 'scrolling' => true + * ]); + * + * Nav::widget([...]); + * + * Offcanvas::end(); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/offcanvas/ + * @author Simon Karlen + */ +class Offcanvas extends Widget +{ + const PLACEMENT_START = 'start'; + const PLACEMENT_END = 'end'; + const PLACEMENT_TOP = 'top'; + const PLACEMENT_BOTTOM = 'bottom'; + + /** + * @var string Where to place the offcanvas. Can be of of the [[PLACEMENT_*]] constants. + */ + public $placement = self::PLACEMENT_START; + /** + * @var boolean Whether to enable backdrop or not. Defaults to `true`. + */ + public $backdrop = true; + /** + * @var boolean Whether to enable body scrolling or not. Defaults to `false`. + */ + public $scrolling = false; + /** + * @var string The title content in the offcanvas container. + */ + public $title; + /** + * @var array|false the options for rendering the close button tag. + * The close button is displayed in the header of the offcanvas container. Clicking + * on the button will hide the offcanvas container. If this is false, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Offcanvas plugin help](https://getbootstrap.com/docs/5.1/components/offcanvas/) + * for the supported HTML attributes. + */ + public $closeButton = []; + /** + * @var array|false the options for rendering the toggle button tag. + * The toggle button is used to toggle the visibility of the modal window. + * If this property is false, no toggle button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to 'Show'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Modal plugin help](https://getbootstrap.com/javascript/#modals) + * for the supported HTML attributes. + */ + public $toggleButton = false; + /** + * @var array Additional header options. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array Additional title options. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'h5'. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $titleOptions = []; + /** + * @var array Additional body options. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $bodyOptions = []; + + + /** + * {@inheritDoc} + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo $this->renderToggleButton() . "\n"; + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderHeader() . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + + /** + * Renders the widget. + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('offcanvas'); + } + + /** + * Renders the header HTML markup of the modal + * @return string the rendering result + */ + protected function renderHeader(): string + { + $button = $this->renderCloseButton(); + if (isset($this->title)) { + $tag = ArrayHelper::remove($this->titleOptions, 'tag', 'h5'); + Html::addCssClass($this->titleOptions, ['widget' => 'offcanvas-title']); + $header = Html::tag($tag, $this->title, $this->titleOptions); + } else { + $header = ''; + } + + if ($button !== null) { + $header .= "\n" . $button; + } elseif ($header === '') { + return ''; + } + Html::addCssClass($this->headerOptions, ['widget' => 'offcanvas-header']); + + return Html::tag('div', "\n" . $header . "\n", $this->headerOptions); + } + + /** + * Renders the opening tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyBegin(): string + { + Html::addCssClass($this->bodyOptions, ['widget' => 'offcanvas-body']); + + return Html::beginTag('div', $this->bodyOptions); + } + + /** + * Renders the closing tag of the modal body. + * @return string the rendering result + */ + protected function renderBodyEnd(): string + { + return Html::endTag('div'); + } + + /** + * Renders the toggle button. + * @return string|null the rendering result + */ + protected function renderToggleButton() + { + if (($toggleButton = $this->toggleButton) !== false) { + $tag = ArrayHelper::remove($toggleButton, 'tag', 'button'); + $label = ArrayHelper::remove($toggleButton, 'label', Yii::t('yii/bootstrap5', 'Show')); + + return Html::tag($tag, $label, $toggleButton); + } else { + return null; + } + } + + /** + * Renders the close button. + * @return string|null the rendering result + */ + protected function renderCloseButton() + { + if (($closeButton = $this->closeButton) !== false) { + $tag = ArrayHelper::remove($closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($closeButton, 'label', ''); + if ($tag === 'button' && !isset($closeButton['type'])) { + $closeButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + $this->options = array_merge([ + 'tabindex' => -1, + 'data' =>[ + 'bs-backdrop' => $this->backdrop ? 'true' : 'false', + 'bs-scroll' => $this->scrolling ? 'true' : 'false' + ] + ], $this->options); + Html::addCssClass($this->options, ['widget' => 'offcanvas offcanvas-' . $this->placement]); + + $this->titleOptions = array_merge([ + 'id' => $this->options['id'] . '-label', + ], $this->titleOptions); + if (!isset($this->options['aria']['label'], $this->options['aria']['labelledby']) && isset($this->title)) { + $this->options['aria']['labelledby'] = $this->titleOptions['id']; + } + + if ($this->closeButton !== false) { + $this->closeButton = array_merge([ + 'class' => ['widget' => 'btn-close text-reset'], + 'data' => ['bs-dismiss' => 'offcanvas'], + 'aria' => ['label' => Yii::t('yii/bootstrap5', 'Close')] + ], $this->closeButton); + } + + if ($this->toggleButton !== false) { + $this->toggleButton = array_merge([ + 'data' => ['bs-toggle' => 'offcanvas'], + 'type' => 'button', + 'aria' => ['controls' => $this->options['id']] + ], $this->toggleButton); + if (!isset($this->toggleButton['data']['bs-target']) && !isset($this->toggleButton['href'])) { + $this->toggleButton['data']['bs-target'] = '#' . $this->options['id']; + } + } + } +} diff --git a/src/Popover.php b/src/Popover.php new file mode 100644 index 0000000..12e128a --- /dev/null +++ b/src/Popover.php @@ -0,0 +1,198 @@ + 'Hello world', + * 'toggleButton' => ['label' => 'click me'], + * ]); + * + * echo 'Say hello...'; + * + * Popover::end(); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/popovers/ + * @author Simon Karlen + */ +class Popover extends Widget +{ + const PLACEMENT_AUTO = 'auto'; + const PLACEMENT_TOP = 'top'; + const PLACEMENT_BOTTOM = 'bottom'; + const PLACEMENT_LEFT = 'left'; + const PLACEMENT_RIGHT = 'right'; + const TRIGGER_CLICK = 'click'; + const TRIGGER_HOVER = 'hover'; + const TRIGGER_FOCUS = 'focus'; + const TRIGGER_MANUAL = 'manual'; + + /** + * @var string|null the tile content in the popover. + */ + public $title = null; + /** + * @var array additional header options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array body options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $bodyOptions = []; + /** + * @var array arrow options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $arrowOptions = []; + /** + * @var string How to position the popover - [[PLACEMENT_AUTO]] | [[PLACEMENT_TOP]] | [[PLACEMENT_BOTTOM]] | + * [[PLACEMENT_LEFT]] | [[PLACEMENT_RIGHT]]. When auto is specified, it will dynamically reorient the popover. + */ + public $placement = self::PLACEMENT_AUTO; + /** + * @var array|false the options for rendering the toggle button tag. + * The toggle button is used to toggle the visibility of the popover. + * If this property is false, no toggle button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to 'Show'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Popover plugin help](https://getbootstrap.com/docs/5.1/components/popovers/) + * for the supported HTML attributes. + */ + public $toggleButton = false; + + + /** + * {@inheritDoc} + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + ob_start(); + } + + /** + * {@inheritDoc} + */ + public function run() + { + $content = ob_get_clean(); + + if (!empty($content)) { + $this->clientOptions['content'] = $content; + } + $html = $this->renderToggleButton(); + + $this->registerPlugin('popover'); + + return $html; + } + + /** + * Renders the arrow HTML markup of the popover + * @return string the rendering result + */ + protected function renderArrow(): string + { + Html::addCssClass($this->arrowOptions, ['widget' => 'popover-arrow']); + + return Html::tag('div', '', $this->arrowOptions); + } + + /** + * Renders the header HTML markup of the popover + * @return string the rendering result + */ + protected function renderHeader(): string + { + Html::addCssClass($this->headerOptions, ['widget' => 'popover-header']); + + return Html::tag('h3', '', $this->headerOptions); + } + + /** + * Renders the opening tag of the modal body. + * @return string the rendering result + */ + protected function renderBody(): string + { + Html::addCssClass($this->bodyOptions, ['widget' => 'popover-body']); + + return Html::tag('div', '', $this->bodyOptions); + } + + /** + * Renders the toggle button. + * @return string|null the rendering result + */ + protected function renderToggleButton() + { + if (($toggleButton = $this->toggleButton) !== false) { + $tag = ArrayHelper::remove($toggleButton, 'tag', 'button'); + $label = ArrayHelper::remove($toggleButton, 'label', Yii::t('yii/bootstrap5', 'Show')); + $toggleButton['id'] = $this->options['id']; + + return Html::tag($tag, $label, $toggleButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + + $options = array_merge([ + 'role' => 'tooltip', + ], $this->options, ['id' => $this->options['id'] . '-popover']); + Html::addCssClass($options, ['widget' => 'popover']); + $template = Html::beginTag('div', $options); + $template .= $this->renderArrow(); + $template .= $this->renderHeader(); + $template .= $this->renderBody(); + $template .= Html::endTag('div'); + + $this->clientOptions = array_merge([ + 'template' => $template, + 'placement' => $this->placement, + 'title' => $this->title, + 'sanitize' => false, + 'html' => true + ], $this->clientOptions); + + if ($this->toggleButton !== false) { + $this->toggleButton = array_merge([ + 'type' => 'button', + ], $this->toggleButton); + } + } +} diff --git a/src/Progress.php b/src/Progress.php new file mode 100644 index 0000000..77eb374 --- /dev/null +++ b/src/Progress.php @@ -0,0 +1,190 @@ + 60, + * 'label' => 'test' + * ]); + * // or + * echo Progress::widget([ + * 'bars' => [ + * ['percent' => 60, 'label' => 'test'] + * ] + * ]); + * + * // styled + * echo Progress::widget([ + * 'percent' => 65, + * 'barOptions' => ['class' => 'bg-danger'] + * ]); + * // or + * echo Progress::widget([ + * 'bars' => [ + * ['percent' => 65, 'options' => ['class' => 'bg-danger']] + * ] + * ]); + * + * // striped + * echo Progress::widget([ + * 'percent' => 70, + * 'barOptions' => ['class' => ['bg-warning', 'progress-bar-striped']] + * ]); + * // or + * echo Progress::widget([ + * 'bars' => [ + * ['percent' => 70, 'options' => ['class' => ['bg-warning', 'progress-bar-striped']]] + * ] + * ]); + * + * // striped animated + * echo Progress::widget([ + * 'percent' => 70, + * 'barOptions' => ['class' => ['bg-success', 'progress-bar-animated', 'progress-bar-striped']] + * ]); + * // or + * echo Progress::widget([ + * 'bars' => [ + * ['percent' => 70, 'options' => ['class' => ['bg-success', 'progress-bar-animated', 'progress-bar-striped']]] + * ] + * ]); + * + * // stacked bars + * echo Progress::widget([ + * 'bars' => [ + * ['percent' => 30, 'options' => ['class' => 'bg-danger']], + * ['percent' => 30, 'label' => 'test', 'options' => ['class' => 'bg-success']], + * ['percent' => 35, 'options' => ['class' => 'bg-warning']], + * ] + * ]); + * ``` + * @see https://getbootstrap.com/docs/5.1/components/progress/ + * @author Antonio Ramirez + * @author Alexander Makarov + * @author Simon Karlen + */ +class Progress extends Widget +{ + /** + * @var string the button label. This property will only be considered if [[bars]] is empty + */ + public $label; + /** + * @var int the amount of progress as a percentage. This property will only be considered if [[bars]] is empty + */ + public $percent = 0; + /** + * @var array the HTML attributes of the bar. This property will only be considered if [[bars]] is empty + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $barOptions = []; + /** + * @var array a set of bars that are stacked together to form a single progress bar. + * Each bar is an array of the following structure: + * + * ```php + * [ + * // required, the amount of progress as a percentage. + * 'percent' => 30, + * // optional, the label to be displayed on the bar + * 'label' => '30%', + * // optional, array, additional HTML attributes for the bar tag + * 'options' => [], + * ] + * ``` + */ + public $bars; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + + Html::addCssClass($this->options, ['widget' => 'progress']); + } + + /** + * {@inheritdoc} + * @throws InvalidConfigException + */ + public function run(): string + { + BootstrapAsset::register($this->getView()); + + return $this->renderProgress(); + } + + /** + * Renders the progress. + * @return string the rendering result. + * @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar. + * @throws Exception + */ + protected function renderProgress(): string + { + $out = Html::beginTag('div', $this->options) . "\n"; + if (empty($this->bars)) { + $this->bars = [ + ['label' => $this->label, 'percent' => $this->percent, 'options' => $this->barOptions], + ]; + } + $bars = []; + foreach ($this->bars as $bar) { + $label = ArrayHelper::getValue($bar, 'label', ''); + if (!isset($bar['percent'])) { + throw new InvalidConfigException("The 'percent' option is required."); + } + $options = ArrayHelper::getValue($bar, 'options', []); + $bars[] = $this->renderBar($bar['percent'], $label, $options); + } + $out .= implode("\n", $bars) . "\n"; + $out .= Html::endTag('div'); + + return $out; + } + + /** + * Generates a progress bar. + * @param int $percent the percentage of the bar + * @param string $label optional, the label to display at the bar + * @param array $options the HTML attributes of the bar + * @return string the rendering result. + */ + protected function renderBar(int $percent, string $label = '', array $options = []): string + { + + $options = array_merge($options, [ + 'role' => 'progressbar', + 'aria' => [ + 'valuenow' => $percent, + 'valuemin' => 0, + 'valuemax' => 100, + ] + ]); + Html::addCssClass($options, ['widget' => 'progress-bar']); + Html::addCssStyle($options, ['width' => $percent . '%'], true); + + return Html::tag('div', $label, $options); + } +} diff --git a/src/Tabs.php b/src/Tabs.php new file mode 100644 index 0000000..1f68d95 --- /dev/null +++ b/src/Tabs.php @@ -0,0 +1,276 @@ + [ + * [ + * 'label' => 'One', + * 'content' => 'Anim pariatur cliche...', + * 'active' => true + * ], + * [ + * 'label' => 'Two', + * 'content' => 'Anim pariatur cliche...', + * 'headerOptions' => [...], + * 'options' => ['id' => 'myveryownID'], + * ], + * [ + * 'label' => 'Example', + * 'url' => 'http://www.example.com', + * ], + * [ + * 'label' => 'Dropdown', + * 'items' => [ + * [ + * 'label' => 'DropdownA', + * 'content' => 'DropdownA, Anim pariatur cliche...', + * ], + * [ + * 'label' => 'DropdownB', + * 'content' => 'DropdownB, Anim pariatur cliche...', + * ], + * [ + * 'label' => 'External Link', + * 'url' => 'http://www.example.com', + * ], + * ], + * ], + * ], + * ]); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/navs/#tabs + * @see https://getbootstrap.com/docs/5.1/components/card/#navigation + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Tabs extends Widget +{ + /** + * @var array list of tabs in the tabs widget. Each array element represents a single + * tab with the following structure: + * + * - label: string, required, the tab header label. + * - encode: bool, optional, whether this label should be HTML-encoded. This param will override + * global `$this->encodeLabels` param. + * - headerOptions: array, optional, the HTML attributes of the tab header. + * - linkOptions: array, optional, the HTML attributes of the tab header link tags. + * - content: string, optional, the content (HTML) of the tab pane. + * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring + * the browser to this URL. This option is available since version 2.0.4. + * - options: array, optional, the HTML attributes of the tab pane container. + * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as + * 'active' explicitly - the first one will be activated. + * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true. + * - disabled: bool, optional, whether the item tab header and pane should be disabled or not. Defaults to false. + * - items: array, optional, can be used instead of `content` to specify a dropdown items + * configuration array. Each item can hold three extra keys, besides the above ones: + * * active: bool, optional, whether the item tab header and pane should be visible or not. + * * content: string, required if `items` is not set. The content (HTML) of the tab pane. + * * options: optional, array, the HTML attributes of the tab content container. + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array list of HTML attributes for the tab header link tags. This will be overwritten + * by the "linkOptions" set in individual [[items]]. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $linkOptions = []; + /** + * @var bool whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string specifies the Bootstrap tab styling. + */ + public $navType = 'nav-tabs'; + /** + * @var bool whether to render the `tab-content` container and its content. You may set this property + * to be false so that you can manually render `tab-content` yourself in case your tab contents are complex. + */ + public $renderTabContent = true; + /** + * @var array list of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $tabContentOptions = []; + /** + * @var string name of a class to use for rendering dropdowns withing this widget. Defaults to [[Dropdown]]. + */ + public $dropdownClass = Dropdown::class; + + /** + * @var array Tab panes (contents) + */ + protected $panes = []; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]); + Html::addCssClass($this->tabContentOptions, ['panel' => 'tab-content']); + } + + /** + * {@inheritdoc} + * @throws InvalidConfigException + * @throws Throwable + */ + public function run(): string + { + $this->registerPlugin('tab'); + $this->prepareItems($this->items); + + return Nav::widget([ + 'dropdownClass' => $this->dropdownClass, + 'options' => ArrayHelper::merge(['role' => 'tablist'], $this->options), + 'items' => $this->items, + 'encodeLabels' => $this->encodeLabels, + ]) . $this->renderPanes($this->panes); + } + + /** + * Renders tab panes. + * + * @param array $panes + * @return string the rendering result. + */ + public function renderPanes(array $panes): string + { + return $this->renderTabContent ? "\n" . Html::tag('div', implode("\n", $panes), $this->tabContentOptions) : ''; + } + + /** + * Renders tab items as specified on [[items]]. + * + * @param array $items + * @param string $prefix + * @throws InvalidConfigException + * @throws Exception + */ + protected function prepareItems(array &$items, string $prefix = '') + { + if (!$this->hasActiveTab()) { + $this->activateFirstVisibleTab(); + } + + foreach ($items as $n => $item) { + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n); + unset($items[$n]['options']['id']); // @see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339 + + if (!ArrayHelper::remove($item, 'visible', true)) { + continue; + } + if (!array_key_exists('label', $item)) { + throw new InvalidConfigException("The 'label' option is required."); + } + + $selected = ArrayHelper::getValue($item, 'active', false); + $disabled = ArrayHelper::getValue($item, 'disabled', false); + $headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions); + if (isset($item['items'])) { + $this->prepareItems($items[$n]['items'], '-dd' . $n); + continue; + } else { + ArrayHelper::setValue($items[$n], 'options', $headerOptions); + if (!isset($item['url'])) { + ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']); + ArrayHelper::setValue($items[$n], 'linkOptions.data.bs-toggle', 'tab'); + ArrayHelper::setValue($items[$n], 'linkOptions.role', 'tab'); + ArrayHelper::setValue($items[$n], 'linkOptions.aria.controls', $options['id']); + if (!$disabled) { + ArrayHelper::setValue($items[$n], 'linkOptions.aria.selected', $selected ? 'true' : 'false'); + } + } else { + continue; + } + } + + Html::addCssClass($options, ['widget' => 'tab-pane']); + if ($selected) { + Html::addCssClass($options, ['activate' => 'active']); + } + + if ($this->renderTabContent) { + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options); + } + } + } + + /** + * Check for active tab + * + * @return bool if there's active tab defined + */ + protected function hasActiveTab(): bool + { + foreach ($this->items as $item) { + if (isset($item['active']) && $item['active'] === true) { + return true; + } + } + + return false; + } + + /** + * Sets the first visible tab as active. + * + * This method activates the first tab that is visible and + * not explicitly set to inactive (`'active' => false`). + * @throws Exception + */ + protected function activateFirstVisibleTab() + { + foreach ($this->items as $i => $item) { + $active = ArrayHelper::getValue($item, 'active', null); + $visible = ArrayHelper::getValue($item, 'visible', true); + $disabled = ArrayHelper::getValue($item, 'disabled', false); + if ($visible && $active !== false && $disabled !== true) { + $this->items[$i]['active'] = true; + + return; + } + } + } +} diff --git a/src/Toast.php b/src/Toast.php new file mode 100644 index 0000000..c5ce11c --- /dev/null +++ b/src/Toast.php @@ -0,0 +1,226 @@ + 'Hello world!', + * 'dateTime' => 'now', + * 'body' => 'Say hello...', + * ]); + * ``` + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the toast box: + * + * ```php + * Toast::begin([ + * 'title' => 'Hello world!', + * 'dateTime' => 'now' + * ]); + * + * echo 'Say hello...'; + * + * Toast::end(); + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/toasts/ + * @author Simon Karlen + */ +class Toast extends Widget +{ + /** + * @var string|null the body content in the alert component. Note that anything between + * the [[begin()]] and [[end()]] calls of the Toast widget will also be treated + * as the body content, and will be rendered before this. + */ + public $body = null; + /** + * @var string|null The title content in the toast. + */ + public $title = null; + /** + * @var int|string|DateTime|DateTimeInterface|DateInterval|false The date time the toast message references to. + * This will be formatted as relative time (via formatter component). It will be omitted if + * set to `false` (default). + */ + public $dateTime = false; + /** + * @var array|false the options for rendering the close button tag. + * The close button is displayed in the header of the toast. Clicking on the button will hide the toast. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [Toast documentation](https://getbootstrap.com/docs/5.1/components/toasts/) + * for the supported HTML attributes. + */ + public $closeButton = []; + /** + * @var array additional title options + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'strong'. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $titleOptions = []; + /** + * @var array additional date time part options + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'small'. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $dateTimeOptions = []; + /** + * @var array additional header options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $headerOptions = []; + /** + * @var array body options + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public $bodyOptions = []; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + + $this->initOptions(); + + echo Html::beginTag('div', $this->options) . "\n"; + echo $this->renderHeader() . "\n"; + echo $this->renderBodyBegin() . "\n"; + } + + /** + * {@inheritdoc} + */ + public function run() + { + echo "\n" . $this->renderBodyEnd(); + echo "\n" . Html::endTag('div'); + + $this->registerPlugin('toast'); + } + + /** + * Renders the header HTML markup of the modal + * @return string the rendering result + */ + protected function renderHeader(): string + { + $button = $this->renderCloseButton(); + $tag = ArrayHelper::remove($this->titleOptions, 'tag', 'strong'); + Html::addCssClass($this->titleOptions, ['widget' => 'me-auto']); + $title = Html::tag($tag, $this->title === null ? '' : $this->title, $this->titleOptions); + + if ($this->dateTime !== false) { + $tag = ArrayHelper::remove($this->dateTimeOptions, 'tag', 'small'); + Html::addCssClass($this->dateTimeOptions, ['widget' => 'text-muted']); + $title .= "\n" . Html::tag($tag, Yii::$app->formatter->asRelativeTime($this->dateTime), $this->dateTimeOptions); + } + + $title .= "\n" . $button; + + Html::addCssClass($this->headerOptions, ['widget' => 'toast-header']); + + return Html::tag('div', "\n" . $title . "\n", $this->headerOptions); + } + + /** + * Renders the opening tag of the toast body. + * @return string the rendering result + */ + protected function renderBodyBegin(): string + { + Html::addCssClass($this->bodyOptions, ['widget' => 'toast-body']); + + return Html::beginTag('div', $this->bodyOptions); + } + + /** + * Renders the toast body and the close button (if any). + * @return string the rendering result + */ + protected function renderBodyEnd(): string + { + return $this->body . "\n" . Html::endTag('div'); + } + + /** + * Renders the close button. + * @return string|null the rendering result + */ + protected function renderCloseButton() + { + if (($closeButton = $this->closeButton) !== false) { + $tag = ArrayHelper::remove($closeButton, 'tag', 'button'); + $label = ArrayHelper::remove($closeButton, 'label', ''); + if ($tag === 'button' && !isset($closeButton['type'])) { + $closeButton['type'] = 'button'; + } + + return Html::tag($tag, $label, $closeButton); + } else { + return null; + } + } + + /** + * Initializes the widget options. + * This method sets the default values for various options. + */ + protected function initOptions() + { + Html::addCssClass($this->options, ['widget' => 'toast']); + + if ($this->closeButton !== false) { + $this->closeButton = array_merge([ + 'class' => ['widget' => 'btn-close'], + 'data' => ['bs-dismiss' => 'toast'], + 'aria' => ['label' => Yii::t('yii/bootstrap5', 'Close')] + ], $this->closeButton); + } + + if (!isset($this->options['role'])) { + $this->options['role'] = 'alert'; + } + if (!isset($this->options['aria']['live'])) { + $this->options['aria'] = [ + 'live' => 'assertive', + 'atomic' => 'true', + ]; + } + } +} diff --git a/src/ToggleButtonGroup.php b/src/ToggleButtonGroup.php new file mode 100644 index 0000000..c1e5aeb --- /dev/null +++ b/src/ToggleButtonGroup.php @@ -0,0 +1,139 @@ +field($model, 'item_id')->widget(\yii\bootstrap5\ToggleButtonGroup::class, [ + * 'type' => \yii\bootstrap5\ToggleButtonGroup::TYPE_CHECKBOX + * 'items' => [ + * 'fooValue' => 'BarLabel', + * 'barValue' => 'BazLabel' + * ] + * ]) ?> + * ``` + * + * @see https://getbootstrap.com/docs/5.1/components/buttons/#checkbox-and-radio-buttons + * + * @author Paul Klimov + * @author Simon Karlen + */ +class ToggleButtonGroup extends InputWidget +{ + /** + * Checkbox type + */ + const TYPE_CHECKBOX = 'checkbox'; + /** + * Radio type + */ + const TYPE_RADIO = 'radio'; + + /** + * @var string input type, can be [[TYPE_CHECKBOX]] or [[TYPE_RADIO]] + */ + public $type; + /** + * @var array the data item used to generate the checkboxes. + * The array values are the labels, while the array keys are the corresponding checkbox or radio values. + */ + public $items = []; + /** + * @var array, the HTML attributes for the label (button) tag. + * @see Html::checkbox() + * @see Html::radio() + */ + public $labelOptions = [ + 'class' => ['btn', 'btn-outline-secondary'], + ]; + /** + * @var bool whether the items labels should be HTML-encoded. + */ + public $encodeLabels = true; + + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + $this->registerPlugin('button'); + Html::addCssClass($this->options, ['widget' => 'btn-group']); + $this->options['role'] = 'group'; + } + + /** + * {@inheritdoc} + * @return string + * @throws InvalidConfigException + */ + public function run(): string + { + if (!isset($this->options['item'])) { + $this->options['item'] = [$this, 'renderItem']; + } + switch ($this->type) { + case 'checkbox': + if ($this->hasModel()) { + return Html::activeCheckboxList($this->model, $this->attribute, $this->items, $this->options); + } else { + return Html::checkboxList($this->name, $this->value, $this->items, $this->options); + } + case 'radio': + if ($this->hasModel()) { + return Html::activeRadioList($this->model, $this->attribute, $this->items, $this->options); + } else { + return Html::radioList($this->name, $this->value, $this->items, $this->options); + } + default: + throw new InvalidConfigException("Unsupported type '{$this->type}'"); + } + } + + /** + * Default callback for checkbox/radio list item rendering. + * @param int $index item index. + * @param string $label item label. + * @param string $name input name. + * @param bool $checked whether value is checked or not. + * @param string $value input value. + * @return string generated HTML. + * @see Html::checkbox() + * @see Html::radio() + */ + public function renderItem(int $index, string $label, string $name, bool $checked, string $value): string + { + unset($index); + $labelOptions = $this->labelOptions; + $labelOptions['wrapInput'] = false; + Html::addCssClass($labelOptions, ['widget' => 'btn']); + $type = $this->type; + if ($this->encodeLabels) { + $label = Html::encode($label); + } + $options = [ + 'label' => $label, + 'labelOptions' => $labelOptions, + 'autocomplete' => 'off', + 'value' => $value, + ]; + Html::addCssClass($options, ['widget' => 'btn-check']); + + return Html::$type($name, $checked, $options); + } +} diff --git a/src/Widget.php b/src/Widget.php new file mode 100644 index 0000000..8ac7229 --- /dev/null +++ b/src/Widget.php @@ -0,0 +1,24 @@ + + */ +final class TranslationBootstrap implements BootstrapInterface +{ + /** + * @inheritDoc + */ + public function bootstrap($app) + { + $app->getI18n()->translations['yii/bootstrap5'] = [ + 'class' => GettextMessageSource::class, + 'sourceLanguage' => 'en-US', + 'basePath' => '@yii/bootstrap5/messages' + ]; + } +} diff --git a/src/messages/af/messages.mo b/src/messages/af/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/af/messages.po b/src/messages/af/messages.po new file mode 100644 index 0000000..740462b --- /dev/null +++ b/src/messages/af/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: af\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ar/messages.mo b/src/messages/ar/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ar/messages.po b/src/messages/ar/messages.po new file mode 100644 index 0000000..7bdd518 --- /dev/null +++ b/src/messages/ar/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/az/messages.mo b/src/messages/az/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/az/messages.po b/src/messages/az/messages.po new file mode 100644 index 0000000..846c5e3 --- /dev/null +++ b/src/messages/az/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/be/messages.mo b/src/messages/be/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/be/messages.po b/src/messages/be/messages.po new file mode 100644 index 0000000..ed86866 --- /dev/null +++ b/src/messages/be/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/bg/messages.mo b/src/messages/bg/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/bg/messages.po b/src/messages/bg/messages.po new file mode 100644 index 0000000..caa6282 --- /dev/null +++ b/src/messages/bg/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/bs/messages.mo b/src/messages/bs/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/bs/messages.po b/src/messages/bs/messages.po new file mode 100644 index 0000000..86a41c9 --- /dev/null +++ b/src/messages/bs/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ca/messages.mo b/src/messages/ca/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ca/messages.po b/src/messages/ca/messages.po new file mode 100644 index 0000000..3833670 --- /dev/null +++ b/src/messages/ca/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/config.php b/src/messages/config.php new file mode 100644 index 0000000..980e12c --- /dev/null +++ b/src/messages/config.php @@ -0,0 +1,62 @@ + __DIR__ . '/..', + // string, required, root directory containing message translations. + 'messagePath' => __DIR__, + // array, required, list of language codes that the extracted messages + // should be translated to. For example, ['zh-CN', 'de']. + 'languages' => ['af', 'ar', 'az', 'be', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de-CH', 'el', 'es', 'et', 'fa', 'fi', 'fr', 'he', 'hi', 'hr', 'hu', 'hy', 'id', 'it', 'ja', 'ka', 'kk', 'ko', 'kz', 'lt', 'lv', 'ms', 'nb-NO', 'nl', 'pl', 'pt', 'pt-BR', 'ro', 'ru', 'sk', 'sl', 'sr', 'sr-Latn', 'sv', 'tg', 'th', 'tr', 'uk', 'uz', 'vi', 'zh-CN', 'zh-TW'], + // string, the name of the function for translating messages. + // Defaults to 'Yii::t'. This is used as a mark to find the messages to be + // translated. You may use a string for single function name or an array for + // multiple function names. + 'translator' => 'Yii::t', + // boolean, whether to sort messages by keys when merging new messages + // with the existing ones. Defaults to false, which means the new (untranslated) + // messages will be separated from the old (translated) ones. + 'sort' => false, + // boolean, whether the message file should be overwritten with the merged messages + 'overwrite' => true, + // boolean, whether to remove messages that no longer appear in the source code. + // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks. + 'removeUnused' => true, + // boolean, whether to mark messages that no longer appear in the source code. + // Defaults to true, which means each of these messages will be enclosed with a pair of '@@' marks. + 'markUnused' => false, + // array, list of patterns that specify which files/directories should NOT be processed. + // If empty or not set, all files/directories will be processed. + // A path matches a pattern if it contains the pattern string at its end. For example, + // '/a/b' will match all files and directories ending with '/a/b'; + // the '*.svn' will match all files and directories whose name ends with '.svn'. + // and the '.svn' will match all files and directories named exactly '.svn'. + // Note, the '/' characters in a pattern matches both '/' and '\'. + // See helpers/FileHelper::findFiles() description for more details on pattern matching rules. + 'except' => [ + '.svn', + '.git', + '.gitignore', + '.gitkeep', + '.hgignore', + '.hgkeep', + '/messages', + ], + // array, list of patterns that specify which files (not directories) should be processed. + // If empty or not set, all files will be processed. + // Please refer to "except" for details about the patterns. + // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed. + 'only' => ['*.php'], + 'phpFileHeader' => '', + // Generated file format. Can be "php", "db" or "po". + 'format' => 'po', + // Connection component ID for "db" format. + //'db' => 'db', + //DocBlock used for messages array in generated PHP file. If `null`, default DocBlock will be used. + 'phpDocBlock' => null, +]; diff --git a/src/messages/cs/messages.mo b/src/messages/cs/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/cs/messages.po b/src/messages/cs/messages.po new file mode 100644 index 0000000..b7763b2 --- /dev/null +++ b/src/messages/cs/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/da/messages.mo b/src/messages/da/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/da/messages.po b/src/messages/da/messages.po new file mode 100644 index 0000000..0ba08b2 --- /dev/null +++ b/src/messages/da/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/de-CH/messages.mo b/src/messages/de-CH/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..2af4e474c73e8ac1dbd709a293f856d1ca119dba GIT binary patch literal 326 zcmca7#4?ou2v~tw28dli93aR6Vo@k90;HROSQv<>1F;+suLfcTAU*}eY(V@Ph`E6H z6A*I%@n0YYsbgb=s8a>fm6@6PN%{FD#U(|F1*R-cr6nc#dFZ0fIr+t@=z_r+`Q_+h zA^GX)IjIURMfn9O`Q;eqK&A5%%QDjwOEQ5LLG24p&dAA3EiO*YV{pu?O3h3MGGK-% aBtk_L^cBidi;7E9le2-+esDu@C;$KlSy1F;+suLfcTAU*}eY(V@Ph`E6H z6A*I%@n0YYsbgb=s8a>fm6@6PN%{FD#U(|F1*R-cr6nc#dFZ0fIr+t@=z_r+`Q_+h zA^GX)IjIURMfn9O`Q;eqK&A5%%QDjwOEQ5LLG24p&dAA3Jv=`(kHImoDm615$bcE5 akO&n~&{rr+Eh;WaP0j{N`@s#tp#T6Z^;uv5 literal 0 HcmV?d00001 diff --git a/src/messages/de/messages.po b/src/messages/de/messages.po new file mode 100644 index 0000000..5d5807f --- /dev/null +++ b/src/messages/de/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "Button" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "Schließen" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "Anzeigen" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "Dropdown anzeigen / verstecken" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "Navigation anzeigen / verstecken" + diff --git a/src/messages/el/messages.mo b/src/messages/el/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/el/messages.po b/src/messages/el/messages.po new file mode 100644 index 0000000..e62d201 --- /dev/null +++ b/src/messages/el/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/es/messages.mo b/src/messages/es/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/es/messages.po b/src/messages/es/messages.po new file mode 100644 index 0000000..4d8cc64 --- /dev/null +++ b/src/messages/es/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/et/messages.mo b/src/messages/et/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/et/messages.po b/src/messages/et/messages.po new file mode 100644 index 0000000..feb4e89 --- /dev/null +++ b/src/messages/et/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/fa/messages.mo b/src/messages/fa/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/fa/messages.po b/src/messages/fa/messages.po new file mode 100644 index 0000000..34aedda --- /dev/null +++ b/src/messages/fa/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/fi/messages.mo b/src/messages/fi/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/fi/messages.po b/src/messages/fi/messages.po new file mode 100644 index 0000000..cd95b4f --- /dev/null +++ b/src/messages/fi/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/fr/messages.mo b/src/messages/fr/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..e1fe9cbb412621f357648d6ab4fdaca9160030e6 GIT binary patch literal 309 zcmZY2O$x#=5Cz~WRb2bGQ6x8T7s!*8 z%L9=qjOO{u^{g?JwYwWdg=jZ-36<^Mp5j;u=4;ABD*yJ>aPp$zOy9Due9GhD#Xs936M)Vi%;8woES#GIz&Ce04^|rD_FQe7aq`oC-mS27MhK3?iBUFJllw9 zrYY^`EiYG%o``@R@eXl literal 0 HcmV?d00001 diff --git a/src/messages/it/messages.po b/src/messages/it/messages.po new file mode 100644 index 0000000..ff2cea5 --- /dev/null +++ b/src/messages/it/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "Bottone" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "Chiudi" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "Indicare" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "Attiva/Disattiva lista a tendina" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "Attiva/Disattiva navigazione" + diff --git a/src/messages/ja/messages.mo b/src/messages/ja/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..4986299247815789570feef4d21262021c0552d6 GIT binary patch literal 247 zcmca7#4?ou2v~qv28eAy93Y4RVo@k90;G$8SQv=gfmjZRX9F=O5buK0M}RaN5T65L zMIgQk#Fd$u`bqiuCB-F0i3O%CPNgL!`FZG~&N=zTspx{i8TsYtVj=nI={cziE=BnT zDf#7j43C@VJZ{?mxOp?f%W0jD8)rOj>SlPcaK-Z_yMR)ikDJ#%Zf<_uJpFNV!{er9 UKxL1cCOvNMc-*w%ar0^h0Bg}uvH$=8 literal 0 HcmV?d00001 diff --git a/src/messages/ja/messages.po b/src/messages/ja/messages.po new file mode 100644 index 0000000..299c01b --- /dev/null +++ b/src/messages/ja/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "ボタン" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "閉じる" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "表示" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "ドロップダウンをトグル" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ka/messages.mo b/src/messages/ka/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ka/messages.po b/src/messages/ka/messages.po new file mode 100644 index 0000000..2ebb899 --- /dev/null +++ b/src/messages/ka/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/kk/messages.mo b/src/messages/kk/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/kk/messages.po b/src/messages/kk/messages.po new file mode 100644 index 0000000..6890ad6 --- /dev/null +++ b/src/messages/kk/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: kk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ko/messages.mo b/src/messages/ko/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ko/messages.po b/src/messages/ko/messages.po new file mode 100644 index 0000000..90cbc96 --- /dev/null +++ b/src/messages/ko/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/kz/messages.mo b/src/messages/kz/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/kz/messages.po b/src/messages/kz/messages.po new file mode 100644 index 0000000..6a1fb6e --- /dev/null +++ b/src/messages/kz/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: kz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/lt/messages.mo b/src/messages/lt/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/lt/messages.po b/src/messages/lt/messages.po new file mode 100644 index 0000000..be16c5e --- /dev/null +++ b/src/messages/lt/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/lv/messages.mo b/src/messages/lv/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/lv/messages.po b/src/messages/lv/messages.po new file mode 100644 index 0000000..de2a0c3 --- /dev/null +++ b/src/messages/lv/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ms/messages.mo b/src/messages/ms/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ms/messages.po b/src/messages/ms/messages.po new file mode 100644 index 0000000..bbf71fa --- /dev/null +++ b/src/messages/ms/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ms\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/nb-NO/messages.mo b/src/messages/nb-NO/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/nb-NO/messages.po b/src/messages/nb-NO/messages.po new file mode 100644 index 0000000..76b1250 --- /dev/null +++ b/src/messages/nb-NO/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: nb_NO\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/nl/messages.mo b/src/messages/nl/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/nl/messages.po b/src/messages/nl/messages.po new file mode 100644 index 0000000..7b3a9e6 --- /dev/null +++ b/src/messages/nl/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/pl/messages.mo b/src/messages/pl/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/pl/messages.po b/src/messages/pl/messages.po new file mode 100644 index 0000000..2cc1256 --- /dev/null +++ b/src/messages/pl/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/pt-BR/messages.mo b/src/messages/pt-BR/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/pt-BR/messages.po b/src/messages/pt-BR/messages.po new file mode 100644 index 0000000..fc53756 --- /dev/null +++ b/src/messages/pt-BR/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/pt/messages.mo b/src/messages/pt/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/pt/messages.po b/src/messages/pt/messages.po new file mode 100644 index 0000000..7b0436e --- /dev/null +++ b/src/messages/pt/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ro/messages.mo b/src/messages/ro/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/ro/messages.po b/src/messages/ro/messages.po new file mode 100644 index 0000000..6433185 --- /dev/null +++ b/src/messages/ro/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/ru/messages.mo b/src/messages/ru/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..e7f1070e66ebcb90dffe7af52e94661e101ed8c8 GIT binary patch literal 364 zcmca7#4?ou2v~tw28dli93aR6Vo@k90;HROSQv<>1F;+suLfcTAU*}eJV5*!i1~o{ z2M`MYF*_pzgC>xcfYMGtx-v6UKPf-Iq`0IgvA~qYskEddKM!5hIVZn36!u|`pE^J`9F#W=Y3%f2h zT`^;ktzXP8jvvn literal 0 HcmV?d00001 diff --git a/src/messages/ru/messages.po b/src/messages/ru/messages.po new file mode 100644 index 0000000..cc9a30e --- /dev/null +++ b/src/messages/ru/messages.po @@ -0,0 +1,31 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "Кнопка" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "Закрыть" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "Показать" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "Переключить навигацию" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "Переключить навигацию" diff --git a/src/messages/sk/messages.mo b/src/messages/sk/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/sk/messages.po b/src/messages/sk/messages.po new file mode 100644 index 0000000..0ff2ce8 --- /dev/null +++ b/src/messages/sk/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/sl/messages.mo b/src/messages/sl/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/sl/messages.po b/src/messages/sl/messages.po new file mode 100644 index 0000000..cb361de --- /dev/null +++ b/src/messages/sl/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/sr-Latn/messages.mo b/src/messages/sr-Latn/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/sr-Latn/messages.po b/src/messages/sr-Latn/messages.po new file mode 100644 index 0000000..ebd5e5a --- /dev/null +++ b/src/messages/sr-Latn/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: sr_Latn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/sr/messages.mo b/src/messages/sr/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/sr/messages.po b/src/messages/sr/messages.po new file mode 100644 index 0000000..2bb7420 --- /dev/null +++ b/src/messages/sr/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/sv/messages.mo b/src/messages/sv/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/sv/messages.po b/src/messages/sv/messages.po new file mode 100644 index 0000000..7e853a8 --- /dev/null +++ b/src/messages/sv/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/tg/messages.mo b/src/messages/tg/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/tg/messages.po b/src/messages/tg/messages.po new file mode 100644 index 0000000..c770d5f --- /dev/null +++ b/src/messages/tg/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: tg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/th/messages.mo b/src/messages/th/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/th/messages.po b/src/messages/th/messages.po new file mode 100644 index 0000000..c726a9e --- /dev/null +++ b/src/messages/th/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: th\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/tr/messages.mo b/src/messages/tr/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/tr/messages.po b/src/messages/tr/messages.po new file mode 100644 index 0000000..52e57c6 --- /dev/null +++ b/src/messages/tr/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/uk/messages.mo b/src/messages/uk/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/uk/messages.po b/src/messages/uk/messages.po new file mode 100644 index 0000000..f70cb4e --- /dev/null +++ b/src/messages/uk/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/uz/messages.mo b/src/messages/uz/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/uz/messages.po b/src/messages/uz/messages.po new file mode 100644 index 0000000..66fef00 --- /dev/null +++ b/src/messages/uz/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: uz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/vi/messages.mo b/src/messages/vi/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/vi/messages.po b/src/messages/vi/messages.po new file mode 100644 index 0000000..2954c91 --- /dev/null +++ b/src/messages/vi/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: vi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/zh-CN/messages.mo b/src/messages/zh-CN/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/zh-CN/messages.po b/src/messages/zh-CN/messages.po new file mode 100644 index 0000000..5659bf3 --- /dev/null +++ b/src/messages/zh-CN/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/src/messages/zh-TW/messages.mo b/src/messages/zh-TW/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..7d18eb0e965dfcc5b69f70bcbeb99c4607e599d7 GIT binary patch literal 28 Vcmca7#4?ou3S@vZ2!jBK1^_I-0p0)r literal 0 HcmV?d00001 diff --git a/src/messages/zh-TW/messages.po b/src/messages/zh-TW/messages.po new file mode 100644 index 0000000..391f1ac --- /dev/null +++ b/src/messages/zh-TW/messages.po @@ -0,0 +1,32 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgctxt "yii/bootstrap5" +msgid "Button" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Close" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Show" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle Dropdown" +msgstr "" + +msgctxt "yii/bootstrap5" +msgid "Toggle navigation" +msgstr "" + diff --git a/tests/AccordionTest.php b/tests/AccordionTest.php new file mode 100644 index 0000000..b3d3881 --- /dev/null +++ b/tests/AccordionTest.php @@ -0,0 +1,328 @@ + [ + [ + 'label' => 'Collapsible Group Item #1', + 'content' => [ + 'test content1', + 'test content2' + ], + ], + [ + 'label' => 'Collapsible Group Item #2', + 'content' => 'Das ist das Haus vom Nikolaus', + 'contentOptions' => [ + 'class' => 'testContentOptions' + ], + 'options' => [ + 'class' => 'testClass', + 'id' => 'testId' + ], + 'footer' => 'Footer' + ], + [ + 'label' => '

      Collapsible Group Item #3

      ', + 'content' => [ + '

      test content1

      ', + '

      test content2

      ' + ], + 'contentOptions' => [ + 'class' => 'testContentOptions2' + ], + 'options' => [ + 'class' => 'testClass2', + 'id' => 'testId2' + ], + 'encode' => false, + 'footer' => 'Footer2' + ], + [ + 'label' => '

      Collapsible Group Item #4

      ', + 'content' => [ + '

      test content1

      ', + '

      test content2

      ' + ], + 'contentOptions' => [ + 'class' => 'testContentOptions3' + ], + 'options' => [ + 'class' => 'testClass3', + 'id' => 'testId3' + ], + 'encode' => true, + 'footer' => 'Footer3' + ], + ] + ], + ); + $this->assertEqualsWithoutLE(<< +
      +
      +
      +
        +
      • test content1
      • +
      • test content2
      • +
      + +
      +
      +
      +
      +
      Das ist das Haus vom Nikolaus
      + + +
      +
      +
      +
      +
        +
      • test content1

      • +
      • test content2

      • +
      + + +
      +
      +
      +
      +
        +
      • test content1

      • +
      • test content2

      • +
      + + +
      + + + HTML, + $output, + ); + } + + public function testLabelKeys(): void + { + ob_start(); + $form = ActiveForm::begin(['action' => '/something']); + ActiveForm::end(); + ob_end_clean(); + + Accordion::$counter = 0; + $output = Accordion::widget( + [ + 'items' => [ + 'Item1' => 'Content1', + 'Item2' => [ + 'content' => 'Content2', + ], + [ + 'label' => 'Item3', + 'content' => 'Content3', + ], + 'FormField' => $form->field(new DynamicModel(['test']), 'test', ['template' => '{input}']), + ], + ], + ); + $this->assertEqualsWithoutLE(<< +
      +
      +
      +
      Content1
      + +
      +
      +
      +
      +
      Content2
      + +
      +
      +
      +
      +
      Content3
      + +
      +
      +
      +
      +
      + +
      + +
      + + + HTML, + $output, + ); + } + + public function testExpandOptions(): void + { + Accordion::$counter = 0; + + $output = Accordion::widget( + [ + 'items' => [ + 'Item1' => 'Content1', + 'Item2' => [ + 'content' => 'Content2', + 'expand' => true, + ], + ], + ], + ); + $this->assertEqualsWithoutLE(<< +
      +
      +
      +
      Content1
      + +
      +
      +
      +
      +
      Content2
      + +
      + + + HTML, + $output, + ); + } + + /** + * @dataProvider \yiiunit\extensions\bootstrap5\providers\Data::invalidItems + */ + public function testMissingLabel(array $items): void + { + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage("The 'label' option is required."); + + Accordion::widget(['items' => $items]); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/8357 + */ + public function testRenderObject(): void + { + $template = ['template' => '{input}']; + ob_start(); + $form = ActiveForm::begin(['action' => '/something']); + ActiveForm::end(); + ob_end_clean(); + $model = new data\Singer; + + Accordion::$counter = 0; + $output = Accordion::widget( + [ + 'items' => [ + [ + 'label' => 'Collapsible Group Item #1', + 'content' => $form->field($model, 'firstName', $template) + ], + ] + ], + ); + + $this->assertEqualsWithoutLE(<< +
      +
      +
      +
      + +
      + +
      + + + HTML, + $output, + ); + } + + public function testAutoCloseItems(): void + { + $items = [ + [ + 'label' => 'Item 1', + 'content' => 'Content 1', + ], + [ + 'label' => 'Item 2', + 'content' => 'Content 2', + ], + ]; + + $output = Accordion::widget(['items' => $items]); + $this->assertStringContainsString('data-bs-parent="', $output); + + $output = Accordion::widget(['autoCloseItems' => false, 'items' => $items]); + $this->assertStringNotContainsString('data-bs-parent="', $output); + } + + /** + */ + public function testItemToggleTag(): void + { + $items = [ + [ + 'label' => 'Item 1', + 'content' => 'Content 1', + ], + [ + 'label' => 'Item 2', + 'content' => 'Content 2', + ], + ]; + + Accordion::$counter = 0; + $output = Accordion::widget( + [ + 'items' => $items, + 'itemToggleOptions' => [ + 'tag' => 'a', + 'class' => 'custom-toggle', + ], + ], + ); + $this->assertStringContainsString('
      assertStringNotContainsString(' $items, + 'itemToggleOptions' => [ + 'tag' => 'a', + 'class' => ['widget' => 'custom-toggle'], + ], + ], + ); + $this->assertStringContainsString('
      assertStringNotContainsString('collapse-toggle', $output); + } +} diff --git a/tests/ActiveFieldDefaultFormCheckTest.php b/tests/ActiveFieldDefaultFormCheckTest.php new file mode 100644 index 0000000..9ac196d --- /dev/null +++ b/tests/ActiveFieldDefaultFormCheckTest.php @@ -0,0 +1,240 @@ +_activeField->inline = true; + $html = $this->_activeField->checkbox()->render(); + + $expectedHtml = << +
      + + +
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testDefaultRadioByConfig(): void + { + Html::$counter = 0; + $this->_activeField->inline = true; + $html = $this->_activeField->radio()->render(); + + $expectedHtml = << +
      + + +
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testDefaultCheckboxListByConfig(): void + { + Html::$counter = 0; + $html = $this->_activeField->checkboxList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testDefaultRadioListByConfig(): void + { + Html::$counter = 0; + $html = $this->_activeField->radioList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testHorizontalLayout(): void + { + Html::$counter = 0; + ActiveForm::$counter = 0; + ob_start(); + $model = new DynamicModel(['attributeName', 'checkbox', 'gridRadios']); + $form = ActiveForm::begin([ + 'action' => '/some-action', + 'layout' => ActiveForm::LAYOUT_HORIZONTAL + ]); + echo $form->field($model, 'attributeName'); + echo $form->field($model, 'checkbox')->checkbox(['wrapperOptions' => ['class' => ['widget' => new UnsetArrayValue()]]]); + echo $form->field($model, 'gridRadios')->radioList([ + 'option1' => 'First radio', + 'option2' => 'Second radio', + 'option3' => 'Third radio' + ]); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + +
      + +
      + +
      + + HTML; + $expected2 = << +
      +
      + + +
      + +
      +
      + + HTML; + $expected3 = << + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      + +
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + } + + protected function setUp(): void + { + // dirty way to have Request object not throwing exception when running testHomeLinkNull() + $_SERVER['SCRIPT_FILENAME'] = 'index.php'; + $_SERVER['SCRIPT_NAME'] = 'index.php'; + + $this->mockWebApplication( + [ + 'container' => [ + 'definitions' => [ + 'yii\bootstrap5\ActiveField' => [ + 'checkTemplate' => "
      \n{input}\n{label}\n{error}\n{hint}\n
      ", + 'radioTemplate' => "
      \n{input}\n{label}\n{error}\n{hint}\n
      ", + 'checkHorizontalTemplate' => "{beginWrapper}\n
      \n{input}\n{label}\n{error}\n{hint}\n
      \n{endWrapper}", + 'radioHorizontalTemplate' => "{beginWrapper}\n
      \n{input}\n{label}\n{error}\n{hint}\n
      \n{endWrapper}", + 'checkOptions' => [ + 'class' => ['widget' => 'form-check-input'], + 'labelOptions' => [ + 'class' => ['widget' => 'form-check-label'] + ], + 'wrapperOptions' => [ + 'class' => ['widget' => 'form-check'] + ] + ], + 'radioOptions' => [ + 'class' => ['widget' => 'form-check-input'], + 'labelOptions' => [ + 'class' => ['widget' => 'form-check-label'] + ], + 'wrapperOptions' => [ + 'class' => ['widget' => 'form-check'] + ] + ] + ] + ] + ] + ] + ); + + $this->_helperModel = new DynamicModel(['attributeName']); + ob_start(); + $this->_helperForm = ActiveForm::begin(['action' => '/something']); + ActiveForm::end(); + ob_end_clean(); + + $this->_activeField = Yii::createObject( + [ + 'class' => 'yii\bootstrap5\ActiveField', + 'form' => $this->_helperForm + ] + ); + $this->_activeField->model = $this->_helperModel; + $this->_activeField->attribute = $this->_attributeName; + } +} diff --git a/tests/ActiveFieldTest.php b/tests/ActiveFieldTest.php new file mode 100644 index 0000000..9ef138c --- /dev/null +++ b/tests/ActiveFieldTest.php @@ -0,0 +1,326 @@ +activeField->fileInput()->render(); + + $expectedHtml = << + + + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testRangeInput(): void + { + $html = $this->activeField->rangeInput()->render(); + + $expectedHtml = << + + + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testColorInput(): void + { + $html = $this->activeField->colorInput()->render(); + + $expectedHtml = << + + + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testRadioList(): void + { + $html = $this->activeField->radioList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + // Tests : + + public function testRadioError(): void + { + $this->helperModel->addError($this->attributeName, 'Test print error message'); + $html = $this->activeField->radio()->render(); + + $expectedHtml = << +
      + + +
      Test print error message
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testRadioListError(): void + { + $this->helperModel->addError($this->attributeName, 'Test print error message'); + $html = $this->activeField->radioList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      Test print error message
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testCheckboxList(): void + { + $html = $this->activeField->checkboxList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + * @test checkbox + */ + public function testCheckboxSwitch(): void + { + $html = $this->activeField->checkbox(['switch' => true])->render(); + + $expectedHtml = << +
      + + +
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testCheckboxError(): void + { + $this->helperModel->addError($this->attributeName, 'Test print error message'); + $html = $this->activeField->checkbox()->render(); + + $expectedHtml = << +
      + + +
      Test print error message
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testCheckboxListError(): void + { + $this->helperModel->addError($this->attributeName, 'Test print error message'); + $html = $this->activeField->checkboxList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      Test print error message
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testRadioListInline(): void + { + $this->activeField->inline = true; + $html = $this->activeField->radioList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testCheckboxListInline(): void + { + $this->activeField->inline = true; + $html = $this->activeField->checkboxList([1 => 'name1', 2 => 'name2'])->render(); + + $expectedHtml = << + +
      + + +
      + +
      + + +
      +
      +
      + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/81 + */ + public function testRadioListItemOptions(): void + { + $content = $this->activeField->radioList( + [ + 1 => 'name1', + 2 => 'name2' + ], + [ + 'itemOptions' => ['data-attribute' => 'test'] + ], + )->render(); + $this->assertStringContainsString('data-attribute="test"', $content); + } + + /** + * + * @see https://github.com/yiisoft/yii2-bootstrap/issues/81 + */ + public function testCheckboxListItemOptions(): void + { + $content = $this->activeField->checkboxList( + [ + 1 => 'name1', + 2 => 'name2' + ], + [ + 'itemOptions' => ['data-attribute' => 'test']] + )->render(); + $this->assertStringContainsString('data-attribute="test"', $content); + } + + protected function setUp(): void + { + // dirty way to have Request object not throwing exception when running testHomeLinkNull() + $_SERVER['SCRIPT_FILENAME'] = "index.php"; + $_SERVER['SCRIPT_NAME'] = "index.php"; + parent::setUp(); + + Html::$counter = 0; + + $this->helperModel = new DynamicModel(['attributeName']); + ob_start(); + $this->helperForm = ActiveForm::begin(['action' => '/something']); + ActiveForm::end(); + ob_end_clean(); + + $this->activeField = new ActiveField(['form' => $this->helperForm]); + $this->activeField->model = $this->helperModel; + $this->activeField->attribute = $this->attributeName; + } +} diff --git a/tests/ActiveFormTest.php b/tests/ActiveFormTest.php new file mode 100644 index 0000000..82eb00c --- /dev/null +++ b/tests/ActiveFormTest.php @@ -0,0 +1,397 @@ + '/some-action', 'layout' => ActiveForm::LAYOUT_DEFAULT]); + echo $form->field($model, 'attributeName'); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + + +
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + } + + public function testHorizontalLayout(): void + { + Html::$counter = 0; + ActiveForm::$counter = 0; + ob_start(); + $model = new DynamicModel(['attributeName', 'checkbox', 'gridRadios']); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_HORIZONTAL]); + echo $form->field($model, 'attributeName'); + echo $form->field($model, 'checkbox')->checkbox(); + echo $form->field($model, 'gridRadios')->radioList([ + 'option1' => 'First radio', + 'option2' => 'Second radio', + 'option3' => 'Third radio' + ]); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + +
      + +
      + +
      + + HTML; + $expected2 = << +
      +
      + + +
      + +
      +
      + + HTML; + $expected3 = << + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      + +
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + } + + /** + */ + public function testHorizontalLayoutTemplateOverride(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new DynamicModel(['checkboxName']); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_HORIZONTAL]); + echo $form->field($model, 'checkboxName')->checkbox(['template' => "
      \n{input}\n{label}\n
      \n
      {error}
      "]); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + + +
      + HTML; + $this->assertContainsWithoutLE($expected, $out); + } + + public function testInlineLayout(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new DynamicModel(['attributeName', 'selectName', 'checkboxName']); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_INLINE]); + echo $form->field($model, 'attributeName'); + echo $form->field($model, 'selectName')->listBox([ + '1' => 'One', + '2' => 'Two', + '3' => 'Three' + ]); + echo $form->field($model, 'checkboxName')->checkbox(); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + + + + + HTML; + $expected2 = << + + + + + + HTML; + $expected3 = << +
      + + + + +
      + + HTML; + $this->assertContainsWithoutLE('
      assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + } + + public function testFloatingLayout(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new DynamicModel(['attributeName', 'selectName', 'checkboxName']); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_FLOATING]); + echo $form->field($model, 'attributeName'); + echo $form->field($model, 'selectName')->listBox([ + '1' => 'One', + '2' => 'Two', + '3' => 'Three' + ]); + echo $form->field($model, 'checkboxName')->checkbox(); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + +
      + + + HTML; + $expected2 = << + + +
      + + + HTML; + $expected3 = << +
      + + +
      + +
      + + HTML; + $this->assertContainsWithoutLE('assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + } + + public function testHintRendering(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new User(); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_DEFAULT]); + echo $form->field($model, 'firstName'); + echo $form->field($model, 'lastName'); + echo $form->field($model, 'username'); + echo $form->field($model, 'password')->passwordInput(); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + + +
      + + HTML; + $expected2 = << + + + +
      + + HTML; + $expected3 = << + + +
      Your username must be at least 4 characters long
      +
      + + HTML; + $expected4 = << + + +
      Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special characters, or emoji.
      +
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + $this->assertContainsWithoutLE($expected4, $out); + } + + public function testStaticControlRendering(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new User(); + $model->setAttributes( + [ + 'id' => 1, + 'firstName' => 'John', + 'lastName' => 'Doe', + ], + ); + $form = ActiveForm::begin(['action' => '/some-action', 'layout' => ActiveForm::LAYOUT_DEFAULT]); + echo $form->field($model, 'id')->staticControl(); + echo $form->field($model, 'firstName')->staticControl(); + echo $form->field($model, 'lastName')->staticControl(); + echo $form->field($model, 'username')->staticControl(); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + + +
      + + HTML; + + $expected2 = << + + + +
      + + HTML; + $expected3 = << + + + +
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + $this->assertContainsWithoutLE($expected2, $out); + $this->assertContainsWithoutLE($expected3, $out); + } + + /** + * Fixes #128 + * @see https://github.com/yiisoft/yii2-bootstrap5/issues/128 + */ + public function testInputTemplate(): void + { + $model = new User(); + $model->validate(); + + ActiveForm::$counter = 0; + ob_start(); + $form = ActiveForm::begin(); + echo $form->field($model, 'username', ['inputTemplate' => '{input}']); + ActiveForm::end(); + $out = ob_get_clean(); + + $expected = << + + +
      Your username must be at least 4 characters long
      +
      Username cannot be blank.
      + + HTML; + $this->assertContainsWithoutLE($expected, $out); + } + + /** + * Fixes #196 + */ + public function testFormNoRoleAttribute(): void + { + $form = ActiveForm::widget(); + + $this->assertStringNotContainsString('role="form"', $form); + } + + public function testErrorSummaryRendering(): void + { + ActiveForm::$counter = 0; + ob_start(); + $model = new User(); + $model->validate(); + $form = ActiveForm::begin([ + 'action' => '/some-action', + 'layout' => ActiveForm::LAYOUT_DEFAULT + ]); + echo $form->errorSummary($model); + ActiveForm::end(); + $out = ob_get_clean(); + + $this->assertContainsWithoutLE('
      'Holy guacamole! You should check in on some of those fields below.', + 'options' => [ + 'class' => ['alert-warning'] + ], + ], + ); + + $expectedHtml = << + + Holy guacamole! You should check in on some of those fields below. + + +
      + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + */ + public function testDismissibleAlert(): void + { + Alert::$counter = 0; + $html = Alert::widget(['body' => "Message1"]); + + $expectedHtml = << + + Message1 + + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap5/issues/11 + */ + public function testDismissibleAlertCustomButton(): void + { + Alert::$counter = 0; + $html = Alert::widget( + [ + 'body' => "Low Blow: Bob Loblaw's Law Blog Lobs Law Bomb", + 'options' => ['class' => 'alert-warning'], + 'closeButton' => [ + 'label' => 'Dismiss', + 'tag' => 'a', + 'class' => ['widget' => 'btn btn-outline-warning'], + 'style' => [ + 'position' => 'absolute', + 'top' => '.5rem', + 'right' => '.5rem' + ], + ], + ], + ); + + $expectedHtml = << + + Low Blow: Bob Loblaw's Law Blog Lobs Law Bomb +
      Dismiss + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } +} diff --git a/tests/BreadcrumbsTest.php b/tests/BreadcrumbsTest.php new file mode 100644 index 0000000..30833fe --- /dev/null +++ b/tests/BreadcrumbsTest.php @@ -0,0 +1,56 @@ + ['label' => 'Home', 'url' => '#'], + 'links' => [ + ['label' => 'Library', 'url' => '#'], + ['label' => 'Data'] + ], + ], + ); + + $expected = << + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testRenderWithoutHomeLink(): void + { + Breadcrumbs::$counter = 0; + $out = Breadcrumbs::widget( + [ + 'homeLink' => false, + 'links' => [ + ['label' => 'Library', 'url' => '#'], + ['label' => 'Data'] + ], + ], + ); + + $expected = << + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/ButtonDropdownTest.php b/tests/ButtonDropdownTest.php new file mode 100644 index 0000000..842bd84 --- /dev/null +++ b/tests/ButtonDropdownTest.php @@ -0,0 +1,87 @@ + ButtonDropdown::DIRECTION_UP, + 'options' => [ + 'class' => $containerClass, + ], + 'label' => 'Action', + 'dropdown' => [ + 'items' => [ + ['label' => 'DropdownA', 'url' => '/'], + ['label' => 'DropdownB', 'url' => '#'], + ], + ], + ], + ); + $this->assertStringContainsString("$containerClass dropup btn-group", $out); + } + + public function testDirection(): void + { + ButtonDropdown::$counter = 0; + $out = ButtonDropdown::widget( + [ + 'direction' => ButtonDropdown::DIRECTION_LEFT, + 'label' => 'Action', + 'dropdown' => [ + 'items' => [ + ['label' => 'ItemA', 'url' => '#'], + ['label' => 'ItemB', 'url' => '#'], + ], + ], + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testSplit(): void + { + ButtonDropdown::$counter = 0; + $out = ButtonDropdown::widget( + [ + 'direction' => ButtonDropdown::DIRECTION_DOWN, + 'label' => 'Split dropdown', + 'split' => true, + 'dropdown' => [ + 'items' => [ + ['label' => 'ItemA', 'url' => '#'], + ['label' => 'ItemB', 'url' => '#'] + ] + ] + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/ButtonGroupTest.php b/tests/ButtonGroupTest.php new file mode 100644 index 0000000..504934d --- /dev/null +++ b/tests/ButtonGroupTest.php @@ -0,0 +1,36 @@ + [ + ['label' => 'button-A'], + ['label' => 'button-B', 'visible' => true], + ['label' => 'button-C', 'visible' => false], + Button::widget(['label' => 'button-D']), + ], + ] + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/ButtonToolbarTest.php b/tests/ButtonToolbarTest.php new file mode 100644 index 0000000..babcb1e --- /dev/null +++ b/tests/ButtonToolbarTest.php @@ -0,0 +1,111 @@ + [ + 'aria-label' => 'Toolbar with button groups' + ], + 'buttonGroups' => [ + ButtonGroup::widget([ + 'options' => [ + 'aria-label' => 'First group', + 'class' => ['mr-2'] + ], + 'buttons' => [ + ['label' => '1'], + ['label' => '2'], + ['label' => '3'], + ['label' => '4'] + ] + ]), + [ + 'options' => [ + 'aria-label' => 'Second group' + ], + 'buttons' => [ + ['label' => '5'], + ['label' => '6'], + ['label' => '7'] + ] + ] + ] + ], + ); + + $expected = <<
      + + +
      +
      + +
      + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testAdditionalContent(): void + { + ButtonToolbar::$counter = 0; + $addHtml = << +
      +
      @
      +
      + + + HTML; + $out = ButtonToolbar::widget( + [ + 'options' => [ + 'aria-label' => 'Toolbar with button groups' + ], + 'buttonGroups' => [ + [ + 'options' => [ + 'aria-label' => 'First group', + 'class' => ['mr-2'] + ], + 'buttons' => [ + ['label' => '1'], + ['label' => '2'], + ['label' => '3'], + ['label' => '4'] + ] + ], + $addHtml + ] + ], + ); + + $expected = <<
      + + +
      +
      +
      +
      @
      +
      + +
      + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/CarouselTest.php b/tests/CarouselTest.php new file mode 100644 index 0000000..b2afc10 --- /dev/null +++ b/tests/CarouselTest.php @@ -0,0 +1,100 @@ + [ + [ + 'content' => '', + 'caption' => '
      First slide label

      Nulla vitae elit libero, a pharetra augue mollis interdum.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ], + ], + [ + 'content' => '', + 'caption' => '
      Second slide label

      Lorem ipsum dolor sit amet, consectetur adipiscing elit.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ], + ], + [ + 'content' => '', + 'caption' => '
      Third slide label

      Praesent commodo cursus magna, vel scelerisque nisl consectetur.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ], + ], + ], + ], + ); + + $expected = << + + + + + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + */ + public function testCrossfade(): void + { + Carousel::$counter = 0; + $out = Carousel::widget( + [ + 'crossfade' => true, + 'items' => [ + [ + 'content' => '', + 'caption' => '
      First slide label

      Nulla vitae elit libero, a pharetra augue mollis interdum.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ] + ], + [ + 'content' => '', + 'caption' => '
      Second slide label

      Lorem ipsum dolor sit amet, consectetur adipiscing elit.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ] + ], + [ + 'content' => '', + 'caption' => '
      Third slide label

      Praesent commodo cursus magna, vel scelerisque nisl consectetur.

      ', + 'captionOptions' => [ + 'class' => ['d-none', 'd-md-block'] + ] + ] + ], + ], + ); + $this->assertStringContainsString('class="carousel slide carousel-fade"', $out); + } +} diff --git a/tests/DropdownTest.php b/tests/DropdownTest.php new file mode 100644 index 0000000..bdb1aa5 --- /dev/null +++ b/tests/DropdownTest.php @@ -0,0 +1,249 @@ + [ + [ + 'label' => 'Page1' + ], + [ + 'label' => 'Dropdown1', + 'url' => '#test', + 'items' => [ + ['label' => 'Page2'], + ['label' => 'Page3'], + ] + ], + [ + 'label' => 'Dropdown2', + 'visible' => false, + 'items' => [ + ['label' => 'Page4', 'content' => 'Page4'], + ['label' => 'Page5', 'content' => 'Page5'], + ] + ] + ] + ] + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testSubMenuOptions(): void + { + Dropdown::$counter = 0; + $out = Dropdown::widget( + [ + 'submenuOptions' => [ + 'class' => 'submenu-list', + ], + 'items' => [ + [ + 'label' => 'Dropdown1', + 'items' => [ + ['label' => 'Page1', 'content' => 'Page2'], + ['label' => 'Page2', 'content' => 'Page3'], + ] + ], + '-', + [ + 'label' => 'Dropdown2', + 'items' => [ + ['label' => 'Page3', 'content' => 'Page4'], + ['label' => 'Page4', 'content' => 'Page5'], + ], + 'submenuOptions' => [ + 'class' => 'submenu-override', + ], + ] + ] + ] + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testActive(): void + { + Dropdown::$counter = 0; + $out = Dropdown::widget( + [ + 'submenuOptions' => [ + 'class' => 'submenu-list', + ], + 'items' => [ + [ + 'label' => 'Dropdown1', + 'items' => [ + ['label' => 'Page1', 'content' => 'Page2'], + ['label' => 'Page2', 'content' => 'Page3'], + ] + ], + '-', + [ + 'label' => 'Dropdown2', + 'items' => [ + ['label' => 'Page3', 'content' => 'Page3', 'url' => '/', 'active' => true], + ['label' => 'Page4', 'content' => 'Page4'], + ], + ] + ] + ] + ); + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testDisabled(): void + { + Dropdown::$counter = 0; + $out = Dropdown::widget( + [ + 'submenuOptions' => [ + 'class' => 'submenu-list', + ], + 'items' => [ + [ + 'label' => 'Dropdown1', + 'items' => [ + ['label' => 'Page1', 'content' => 'Page2'], + ['label' => 'Page2', 'content' => 'Page3'], + ], + 'disabled' => true + ], + '-', + [ + 'label' => 'Dropdown2', + 'items' => [ + ['label' => 'Page3', 'content' => 'Page3'], + ['label' => 'Page4', 'content' => 'Page4'], + ], + ] + ] + ] + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testForms(): void + { + Dropdown::$counter = 0; + $form = << +
      + + +
      +
      + + +
      +
      + + +
      + +
      + HTML; + + $out = Dropdown::widget( + [ + 'items' => [ + $form, + '-', + ['label' => 'New around here? Sign up', 'url' => '#'], + ['label' => 'Forgot password?', 'url' => '#'] + ], + ], + ); + + $expected = <<
      +
      + + +
      +
      + + +
      +
      + + +
      + +
      + + New around here? Sign up + Forgot password? + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 2825a4e..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue($example->getExample()); - } -} diff --git a/tests/HtmlTest.php b/tests/HtmlTest.php new file mode 100644 index 0000000..1248841 --- /dev/null +++ b/tests/HtmlTest.php @@ -0,0 +1,148 @@ +assertEquals($expectedHtml, Html::staticControl($value, $options)); + } + + public function testRadioList(): void + { + $this->assertEquals('
      ', Html::radioList('test')); + + $dataItems = [ + 'value1' => 'text1', + 'value2' => 'text2', + ]; + + Html::$counter = 0; + + $expected = <<
      +
      +
      +
      + HTML; + $this->assertEqualsWithoutLE($expected, Html::radioList('test', ['value2'], $dataItems)); + + Html::$counter = 0; + $expected = <<0 + 1 + HTML; + $this->assertEqualsWithoutLE( + $expected, + Html::radioList( + 'test', + ['value2'], + $dataItems, + [ + 'item' => static fn ($index, $label, $name, $checked, $value) => $index . + Html::label($label . ' ' . Html::radio($name, $checked, ['value' => $value])), + ], + ) + ); + + Html::$counter = 0; + $expected = <<
      +
      + HTML; + $this->assertEqualsWithoutLE($expected, Html::radioList('test', [], ['value' => 'label&'])); + + Html::$counter = 0; + $expected = <<
      +
      + HTML; + $this->assertEqualsWithoutLE($expected, Html::radioList('test', [], ['value' => 'label&'], ['encode' => false])); + } + + public function testCheckboxList(): void + { + $this->assertEquals('
      ', Html::checkboxList('test')); + + $dataItems = [ + 'value1' => 'text1', + 'value2' => 'text2', + ]; + + Html::$counter = 0; + + $expected = <<
      +
      +
      +
      + HTML; + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $dataItems)); + + + Html::$counter = 0; + $expected = <<0 + 1 + HTML; + $this->assertEqualsWithoutLE( + $expected, + Html::checkboxList( + 'test', + ['value2'], + $dataItems, + [ + 'item' => static fn ($index, $label, $name, $checked, $value) => $index . + Html::label($label . ' ' . Html::checkbox($name, $checked, ['value' => $value])), + ], + ) + ); + + Html::$counter = 0; + $expected = <<
      +
      + HTML; + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', 'value', ['value' => 'label&'])); + + Html::$counter = 0; + $expected = <<
      +
      + HTML; + $this->assertEqualsWithoutLE( + $expected, + Html::checkboxList('test', 'value', ['value' => 'label&'], ['encode' => false]), + ); + } + + public function testError(): void + { + $model = new DynamicModel(); + + $model->addError('foo', 'Some error message.'); + $this->assertEquals('
      Some error message.
      ', Html::error($model, 'foo')); + $this->assertEquals( + '
      Some error message.
      ', + Html::error($model, 'foo', ['class' => 'custom-class']), + ); + $this->assertEquals('
      Some error message.
      ', Html::error($model, 'foo', ['class' => null])); + $this->assertEquals( + '

      Some error message.

      ', + Html::error($model, 'foo', ['tag' => 'p']), + ); + } +} diff --git a/tests/LinkPagerTest.php b/tests/LinkPagerTest.php new file mode 100644 index 0000000..d16399d --- /dev/null +++ b/tests/LinkPagerTest.php @@ -0,0 +1,199 @@ +getPagination(5); + $output = LinkPager::widget( + [ + 'pagination' => $pagination, + 'firstPageLabel' => true, + 'lastPageLabel' => true, + ], + ); + $this->assertStringContainsString( + '
    1. 1
    2. ', + $output, + ); + $this->assertStringContainsString( + '
    3. 25
    4. ', + $output, + ); + + $output = LinkPager::widget( + [ + 'pagination' => $pagination, + 'firstPageLabel' => 'First', + 'lastPageLabel' => 'Last', + ], + ); + $this->assertStringContainsString( + '
    5. First
    6. ', + $output, + ); + $this->assertStringContainsString( + '
    7. Last
    8. ', + $output, + ); + + $output = LinkPager::widget( + [ + 'pagination' => $pagination, + 'firstPageLabel' => false, + 'lastPageLabel' => false, + ], + ); + $this->assertStringNotContainsString('
    9. ', $output); + $this->assertStringNotContainsString('
    10. ', $output); + } + + public function testDisabledPageElementOptions(): void + { + $output = LinkPager::widget( + [ + 'pagination' => $this->getPagination(0), + 'disabledListItemSubTagOptions' => ['class' => ['foo-bar']], + ], + ); + $this->assertStringContainsString('
    11. 6
    12. ', + $output, + ); + + $output = LinkPager::widget( + [ + 'pagination' => $pagination, + 'disableCurrentPageButton' => true, + ], + ); + $this->assertStringContainsString( + '
    13. 6
    14. ', + $output, + ); + } + + public function testOptionsWithTagOption(): void + { + LinkPager::$counter = 0; + $output = LinkPager::widget( + [ + 'pagination' => $this->getPagination(5), + 'options' => [ + 'tag' => 'div', + ], + ], + ); + $this->assertTrue(StringHelper::startsWith($output, '
      ')); + $this->assertTrue(StringHelper::endsWith($output, '
      ')); + } + + public function testLinkWrapOptions(): void + { + $output = LinkPager::widget( + [ + 'pagination' => $this->getPagination(1), + 'linkContainerOptions' => [ + 'tag' => 'div', + 'class' => 'my-class', + ], + ], + ); + $this->assertStringContainsString( + '', + $output + ); + $this->assertStringContainsString( + '', + $output + ); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/15536 + */ + public function testShouldTriggerInitEvent(): void + { + $initTriggered = false; + LinkPager::widget( + [ + 'pagination' => $this->getPagination(1), + 'on init' => function () use (&$initTriggered): void { + $initTriggered = true; + } + ], + ); + $this->assertTrue($initTriggered); + } + + protected function setUp(): void + { + parent::setUp(); + $this->mockWebApplication( + [ + 'components' => [ + 'urlManager' => [ + 'scriptUrl' => '/', + ], + ], + ], + ); + } + + /** + * Get pagination. + * @return Pagination + */ + private function getPagination(int $page) + { + $pagination = new Pagination(); + $pagination->setPage($page); + $pagination->totalCount = 500; + $pagination->route = 'test'; + + return $pagination; + } +} diff --git a/tests/ModalTest.php b/tests/ModalTest.php new file mode 100644 index 0000000..513ec91 --- /dev/null +++ b/tests/ModalTest.php @@ -0,0 +1,203 @@ + false, + 'bodyOptions' => ['class' => 'modal-body test', 'style' => 'text-align:center;'] + ], + ); + + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + */ + public function testContainerOptions(): void + { + Modal::$counter = 0; + + ob_start(); + Modal::begin( + [ + 'title' => 'Modal title', + 'footer' => Html::button('Close', [ + 'type' => 'button', + 'class' => ['btn', 'btn-secondary'], + 'data' => [ + 'bs-dismiss' => 'modal' + ] + ]) . "\n" . Html::button('Save changes', [ + 'type' => 'button', + 'class' => ['btn', 'btn-primary'] + ]) + ], + ); + echo '

      Woohoo, you\'re reading this text in a modal!

      '; + Modal::end(); + $out = ob_get_clean(); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testTriggerButton(): void + { + Modal::$counter = 0; + + ob_start(); + Modal::begin( + [ + 'toggleButton' => [ + 'class' => ['btn', 'btn-primary'], + 'label' => 'Launch demo modal' + ], + 'title' => 'Modal title', + 'footer' => Html::button('Close', [ + 'type' => 'button', + 'class' => ['btn', 'btn-secondary'] + ]) . "\n" . Html::button('Save changes', [ + 'type' => 'button', + 'class' => ['btn', 'btn-primary'] + ]) + ], + ); + echo '

      Woohoo, you\'re reading this text in a modal!

      '; + Modal::end(); + $out = ob_get_clean(); + + $this->assertStringContainsString( + '', + $out + ); + } + + public function testDialogOptions(): void + { + Modal::$counter = 0; + $out = Modal::widget( + [ + 'closeButton' => false, + 'dialogOptions' => ['class' => 'test', 'style' => 'text-align:center;'] + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testCenterVertical(): void + { + Modal::$counter = 0; + $out = Modal::widget( + [ + 'closeButton' => false, + 'centerVertical' => true, + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testScrollable(): void + { + Modal::$counter = 0; + $out = Modal::widget( + [ + 'closeButton' => false, + 'scrollable' => true, + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/NavBarTest.php b/tests/NavBarTest.php new file mode 100644 index 0000000..fe7c32b --- /dev/null +++ b/tests/NavBarTest.php @@ -0,0 +1,251 @@ + 'My Company', + 'brandUrl' => '/', + 'options' => [ + 'class' => 'navbar-inverse navbar-static-top navbar-frontend', + ], + ], + ); + + $expected = << +
      + My Company + + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testBrandImage(): void + { + $out = NavBar::widget( + [ + 'brandImage' => '/images/test.jpg', + 'brandUrl' => '/', + ], + ); + $this->assertStringContainsString( + '', + $out, + ); + } + + public function testBrandImageOptions(): void + { + $out = NavBar::widget( + [ + 'brandImage' => '/images/test.jpg', + 'brandImageOptions' => ['alt' => 'test image'], + 'brandUrl' => '/', + ], + ); + $this->assertStringContainsString( + 'test image', + $out, + ); + } + + public function testBrandLink(): void + { + $out = NavBar::widget( + [ + 'brandLabel' => 'Yii Framework', + 'brandUrl' => false, + ], + ); + $this->assertStringContainsString('Yii Framework', $out); + } + + public function testBrandSpan(): void + { + $out = NavBar::widget( + [ + 'brandLabel' => 'Yii Framework', + 'brandUrl' => null, + ], + ); + $this->assertStringContainsString('Yii Framework', $out); + } + + /** + */ + public function testNavAndForm(): void + { + + NavBar::$counter = 0; + + ob_start(); + NavBar::begin( + [ + 'brandLabel' => 'My Company', + 'brandUrl' => '/', + 'options' => [], + ] + ); + echo Nav::widget( + [ + 'options' => [ + 'class' => ['mr-auto'] + ], + 'items' => [ + ['label' => 'Home', 'url' => '#'], + ['label' => 'Link', 'url' => '#'], + ['label' => 'Dropdown', 'items' => [ + ['label' => 'Action', 'url' => '#'], + ['label' => 'Another action', 'url' => '#'], + '-', + ['label' => 'Something else here', 'url' => '#'], + ]] + ] + ], + ); + echo << + + + + HTML; + + NavBar::end(); + $out = ob_get_clean(); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testOffcanvasNavigation(): void + { + NavBar::$counter = 0; + + ob_start(); + NavBar::begin( + [ + 'brandLabel' => 'Offcanvas navbar', + 'brandUrl' => ['/'], + 'options' => [ + 'class' => ['navbar', 'navbar-light', 'bg-light', 'fixed-top'] + ], + 'innerContainerOptions' => [ + 'class' => ['container-fluid'] + ], + 'collapseOptions' => false, + 'offcanvasOptions' => [ + 'title' => 'Offcanvas', + 'placement' => Offcanvas::PLACEMENT_END + ], + ], + ); + echo Nav::widget( + [ + 'options' => [ + 'class' => ['navbar-nav'] + ], + 'items' => [ + ['label' => 'Home', 'url' => '#'], + ['label' => 'Link', 'url' => '#'], + ['label' => + 'Dropdown', + 'items' => [ + ['label' => 'Action', 'url' => '#'], + ['label' => 'Another action', 'url' => '#'], + '-', + ['label' => 'Something else here', 'url' => '#'], + ], + ], + ], + ], + ); + NavBar::end(); + $out = ob_get_clean(); + + $expected = << +
      + Offcanvas navbar + + +
      +
      +
      Offcanvas
      + +
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testNoCollapse(): void + { + NavBar::$counter = 0; + + $out = NavBar::widget( + [ + 'brandLabel' => 'My Company', + 'brandUrl' => '/', + 'collapseOptions' => false, + ], + ); + + $expected = << +
      + My Company + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/NavTest.php b/tests/NavTest.php new file mode 100644 index 0000000..9100600 --- /dev/null +++ b/tests/NavTest.php @@ -0,0 +1,385 @@ + [ + [ + 'label' => 'Page1', + 'content' => 'Page1', + ], + [ + 'label' => 'Dropdown1', + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2'], + ['label' => 'Page3', 'content' => 'Page3'], + ] + ], + [ + 'label' => 'Dropdown2', + 'visible' => false, + 'items' => [ + ['label' => 'Page4', 'content' => 'Page4'], + ['label' => 'Page5', 'content' => 'Page5'], + ] + ] + ] + ] + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testRenderDropdownWithDropdownOptions(): void + { + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Page1', + 'content' => 'Page1', + ], + [ + 'label' => 'Dropdown1', + 'dropdownOptions' => ['class' => 'test', 'data-id' => 't1', 'id' => 'test1'], + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2'], + ['label' => 'Page3', 'content' => 'Page3'], + ] + ], + [ + 'label' => 'Dropdown2', + 'visible' => false, + 'items' => [ + ['label' => 'Page4', 'content' => 'Page4'], + ['label' => 'Page5', 'content' => 'Page5'], + ] + ] + ] + ] + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testEmptyItems(): void + { + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Page1', + 'items' => null, + ], + [ + 'label' => 'Dropdown1', + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2'], + ['label' => 'Page3', 'content' => 'Page3'], + ], + ], + [ + 'label' => 'Page4', + 'items' => [], + ], + ], + ], + ); + + $expected = << + + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testActive(): void + { + $this->mockAction('site', 'users'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Main', + 'url' => ['site/index'], + ], + [ + 'label' => 'Admin', + 'items' => [ + ['label' => 'Users', 'url' => ['site/users']], + ['label' => 'Roles', 'url' => ['site/roles']], + ['label' => 'Statuses', 'url' => ['site/statuses']] + ], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/162 + */ + public function testExplicitActive(): void + { + $this->mockAction('site', 'index'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'activateItems' => false, + 'items' => [ + [ + 'label' => 'Item1', + 'active' => true, + ], + [ + 'label' => 'Item2', + 'url' => ['site/index'], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/162 + */ + public function testImplicitActive(): void + { + $this->mockAction('site', 'index'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Item1', + 'active' => true, + ], + [ + 'label' => 'Item2', + 'url' => ['site/index'], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/162 + */ + public function testExplicitActiveSubitems(): void + { + $this->mockAction('site', 'index'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'activateItems' => false, + 'items' => [ + [ + 'label' => 'Item1', + ], + [ + 'label' => 'Item2', + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2', 'url' => ['site/index']], + ['label' => 'Page3', 'content' => 'Page3', 'active' => true], + ], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/162 + */ + public function testImplicitActiveSubitems(): void + { + $this->mockAction('site', 'index'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Item1', + ], + [ + 'label' => 'Item2', + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2', 'url' => ['site/index']], + ['label' => 'Page3', 'content' => 'Page3', 'active' => true], + ], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + public function testDisabled(): void + { + $this->mockAction('site', 'index'); + + Nav::$counter = 0; + $out = Nav::widget( + [ + 'items' => [ + [ + 'label' => 'Item1', + 'disabled' => true + ], + [ + 'label' => 'Item2', + 'items' => [ + ['label' => 'Page2', 'content' => 'Page2', 'url' => ['site/index'], 'disabled' => true], + ['label' => 'Page3', 'content' => 'Page3', 'active' => true], + ], + ], + ], + ], + ); + + $expected = << + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + + $this->removeMockedAction(); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap/issues/96 + * @see https://github.com/yiisoft/yii2-bootstrap/issues/157 + */ + public function testDeepActivateParents(): void + { + Nav::$counter = 0; + $out = Nav::widget( + [ + 'activateParents' => true, + 'items' => [ + [ + 'label' => 'Dropdown', + 'items' => [ + [ + 'label' => 'Sub-dropdown', + 'items' => [ + ['label' => 'Page', 'content' => 'Page', 'active' => true], + ], + ], + ], + ], + ], + ], + ); + + $expected = << + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + protected function setUp(): void + { + $this->mockWebApplication( + [ + 'components' => [ + 'request' => [ + 'class' => 'yii\web\Request', + 'scriptUrl' => '/base/index.php', + 'hostInfo' => 'http://example.com/', + 'url' => '/base/index.php&r=site%2Fcurrent&id=42' + ], + 'urlManager' => [ + 'class' => 'yii\web\UrlManager', + 'baseUrl' => '/base', + 'scriptUrl' => '/base/index.php', + 'hostInfo' => 'http://example.com/', + ] + ], + ], + ); + } +} diff --git a/tests/OffcanvasTest.php b/tests/OffcanvasTest.php new file mode 100644 index 0000000..9dca244 --- /dev/null +++ b/tests/OffcanvasTest.php @@ -0,0 +1,103 @@ + false, + 'bodyOptions' => [ + 'class' => 'offcanvas-body test', + 'style' => ['text-align' => 'center'], + ], + ], + ); + + + $expected = << + +
      + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + */ + public function testOptions(): void + { + Offcanvas::$counter = 0; + + ob_start(); + Offcanvas::begin( + [ + 'title' => 'Offcanvas title', + 'headerOptions' => [ + 'data-test' => 'Test' + ], + 'titleOptions' => [ + 'tag' => 'h2' + ], + 'placement' => Offcanvas::PLACEMENT_END, + 'backdrop' => false, + 'scrolling' => true, + 'closeButton' => false, + ], + ); + echo '

      Woohoo, you\'re reading this text in an offcanvas!

      '; + Offcanvas::end(); + $out = ob_get_clean(); + + $expected = << +
      +

      Offcanvas title

      +
      +
      +

      Woohoo, you're reading this text in an offcanvas!

      +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testTriggerButton(): void + { + Offcanvas::$counter = 0; + + ob_start(); + Offcanvas::begin( + [ + 'toggleButton' => [ + 'class' => ['btn', 'btn-primary'], + 'label' => 'Launch demo offcanvas' + ], + 'title' => 'Offcanvas title', + ], + ); + echo '

      Woohoo, you\'re reading this text in an offcanvas!

      '; + Offcanvas::end(); + $out = ob_get_clean(); + + $this->assertStringContainsString( + '', + $out + ); + } +} diff --git a/tests/PopoverTest.php b/tests/PopoverTest.php new file mode 100644 index 0000000..49bd26d --- /dev/null +++ b/tests/PopoverTest.php @@ -0,0 +1,67 @@ + ['class' => ['btn', 'btn-primary']]]); + + $expected = <<Show + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testClientOptions(): void + { + Popover::$counter = 0; + Popover::widget( + [ + 'headerOptions' => ['class' => ['test-header']], + 'placement' => Popover::PLACEMENT_BOTTOM, + 'title' => 'Test Popover', + ], + ); + + $js = Yii::$app->view->js[View::POS_READY]; + + $this->assertIsArray($js); + $options = array_shift($js); + $this->assertContainsWithoutLE("(new bootstrap.Popover('#w0', {", $options); + $this->assertContainsWithoutLE("id=\u0022w0-popover\u0022", $options); + $this->assertContainsWithoutLE("class=\u0022test-header popover-header\u0022", $options); + $this->assertContainsWithoutLE('"placement":"bottom"', $options); + $this->assertContainsWithoutLE('"title":"Test Popover"', $options); + } + + public function testContent(): void + { + Popover::$counter = 0; + Popover::begin([]); + echo Html::tag('span', 'Test content', ['class' => ['test-content']]); + Popover::end(); + + $js = Yii::$app->view->js[View::POS_READY]; + + $this->assertIsArray($js); + $options = array_shift($js); + $this->assertContainsWithoutLE( + '"content":"\u003Cspan class=\u0022test-content\u0022\u003ETest content\u003C\/span\u003E"', + $options, + ); + } +} diff --git a/tests/ProgressTest.php b/tests/ProgressTest.php new file mode 100644 index 0000000..a3c107a --- /dev/null +++ b/tests/ProgressTest.php @@ -0,0 +1,104 @@ + 'Progress', + 'percent' => 25, + 'barOptions' => ['class' => 'bg-warning'] + ], + ); + + $expected = << +
      Progress
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testRender(): void + { + Progress::$counter = 0; + $out = Progress::widget( + [ + 'bars' => [ + ['label' => 'Progress', 'percent' => 25] + ], + ], + ); + + $expected = << +
      Progress
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + */ + public function testMultiple(): void + { + Progress::$counter = 0; + $out = Progress::widget( + [ + 'bars' => [ + ['label' => '', 'percent' => 15], + ['label' => '', 'percent' => 30, 'options' => ['class' => ['bg-success']]], + ['label' => '', 'percent' => 20, 'options' => ['class' => ['bg-info']]] + ], + ], + ); + + $expected = << +
      +
      +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap5/issues/121 + */ + public function testRussianLocaleRendering(): void + { + $this->mockWebApplication( + [ + 'language' => 'ru-RU', + 'sourceLanguage' => 'en-US', + ], + ); + + Progress::$counter = 0; + $out = Progress::widget([ + 'bars' => [ + ['label' => 'Progress', 'percent' => 25] + ] + ]); + + $expected = << +
      Progress
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/TabsTest.php b/tests/TabsTest.php new file mode 100644 index 0000000..3cf344b --- /dev/null +++ b/tests/TabsTest.php @@ -0,0 +1,423 @@ + [ + [ + 'label' => 'Page1', + 'content' => 'Page1', + ], + [ + 'label' => 'Page2', + 'content' => 'Page2', + ], + ] + ], + ); + $this->assertContainsWithoutLE(' +
      Content 1
      +
      Content 2
      + HTML; + $this->assertEqualsWithoutLE($expected, $html); + } + + public function testHeaderOptions(): void + { + Tabs::$counter = 0; + $html = Tabs::widget( + [ + 'items' => [ + [ + 'label' => 'Tab 1', + 'content' => '
      Content 1
      ', + ], + [ + 'label' => 'Tab 2', + 'content' => '
      Content 2
      ', + 'headerOptions' => ['class' => 'col-6'], + ], + [ + 'label' => 'Link', + 'url' => 'http://www.example.com/', + 'headerOptions' => ['class' => 'col-3'], + ], + ], + 'options' => ['class' => 'row'], + 'headerOptions' => ['class' => 'col'], + ], + ); + + $expected = << + + +
      Content 1
      +
      Content 2
      + HTML; + $this->assertEquals($expected, $html); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..d5fdf3f --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,123 @@ +assertEquals($expected, $actual); + } + + /** + * Asserting two strings equality ignoring line endings + * + */ + public function assertContainsWithoutLE(string $needle, string $haystack): void + { + $needle = str_replace("\r\n", "\n", $needle); + $haystack = str_replace("\r\n", "\n", $haystack); + + $this->assertStringContainsString($needle, $haystack); + } + + /** + * {@inheritDoc} + */ + protected function setUp(): void + { + parent::setUp(); + $this->mockWebApplication(); + } + + /** + * {@inheritDoc} + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->destroyApplication(); + } + + protected function mockWebApplication(array $config = [], string $appClass = '\yii\web\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + 'language' => 'en-US', + 'aliases' => [ + '@bower' => '@vendor/bower-asset', + '@npm' => '@vendor/npm-asset', + ], + 'components' => [ + 'i18n' => [ + 'translations' => [ + 'yii/bootstrap5*' => [ + 'class' => 'yii\i18n\GettextMessageSource', + 'sourceLanguage' => 'en-US', + 'basePath' => '@yii/bootstrap5/messages', + ], + ], + ], + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ] + ] + ], $config)); + } + + /** + * Mocks controller action with parameters + * + * @param string|null $moduleID + */ + protected function mockAction(string $controllerId, string $actionID, string $moduleID = null, array $params = []) + { + Yii::$app->controller = $controller = new Controller($controllerId, Yii::$app); + $controller->actionParams = $params; + $controller->action = new Action($actionID, $controller); + + if ($moduleID !== null) { + $controller->module = new Module($moduleID); + } + } + + /** + * Removes controller + */ + protected function removeMockedAction() + { + Yii::$app->controller = null; + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + Yii::$app = null; + Yii::$container = new Container(); + } +} diff --git a/tests/ToastTest.php b/tests/ToastTest.php new file mode 100644 index 0000000..bce24e4 --- /dev/null +++ b/tests/ToastTest.php @@ -0,0 +1,203 @@ + [ + 'class' => 'toast-body test', + 'style' => ['text-align' => 'center'], + ], + ], + ); + + $expected = << +
      + + +
      +
      + + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + */ + public function testContainerOptions(): void + { + Toast::$counter = 0; + + ob_start(); + Toast::begin( + [ + 'title' => 'Toast title', + 'dateTime' => time() - 60, + ], + ); + echo 'Woohoo, you\'re reading this text in a toast!'; + Toast::end(); + $out = ob_get_clean(); + + $expected = << +
      + Toast title + a minute ago + +
      +
      + Woohoo, you're reading this text in a toast! + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testDateTimeOptions(): void + { + Toast::$counter = 0; + $out = Toast::widget( + [ + 'title' => 'Toast title', + 'dateTime' => time() - 60, + 'dateTimeOptions' => ['class' => ['toast-date-time'], 'style' => ['text-align' => 'right']], + ], + ); + + $expected = << +
      + Toast title + a minute ago + +
      +
      + + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + public function testTitleOptions(): void + { + Toast::$counter = 0; + $out = Toast::widget( + [ + 'title' => 'Toast title', + 'titleOptions' => ['tag' => 'h5', 'style' => ['text-align' => 'left']], + ], + ); + + $expected = << +
      +
      Toast title
      + +
      +
      + + +
      + + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap5/issues/5 + */ + public function testWidgetInitialization(): void + { + Toast::$counter = 0; + ob_start(); + $toast = Toast::begin( + [ + 'title' => 'Toast title', + 'titleOptions' => ['tag' => 'h5', 'style' => ['text-align' => 'left']], + ], + ); + echo 'test'; + Toast::end(); + $out = ob_get_clean(); + + $this->assertIsArray($toast->clientOptions); + $this->assertCount(0, $toast->clientOptions); + + $js = Yii::$app->view->js[View::POS_READY]; + + $this->assertIsArray($js); + $options = array_shift($js); + + $this->assertContainsWithoutLE("(new bootstrap.Toast('#w0', {}));", $options); + } + + /** + * @see https://github.com/yiisoft/yii2-bootstrap5/issues/36 + */ + public function testWidgetNoInitialization(): void + { + Toast::$counter = 0; + ob_start(); + $toast = Toast::begin( + [ + 'title' => 'Toast title', + 'clientOptions' => false, + 'titleOptions' => ['tag' => 'h5', 'style' => ['text-align' => 'left']], + ], + ); + echo 'test'; + Toast::end(); + $out = ob_get_clean(); + + $this->assertFalse($toast->clientOptions); + $this->assertArrayHasKey(View::POS_READY, Yii::$app->view->js); + } + + public function testWidgetInitializationWithClientOptions(): void + { + Toast::$counter = 0; + ob_start(); + $toast = Toast::begin( + [ + 'title' => 'Toast title', + 'clientOptions' => ['delay' => 1000], + 'titleOptions' => ['tag' => 'h5', 'style' => ['text-align' => 'left']], + ], + ); + echo 'test'; + Toast::end(); + $out = ob_get_clean(); + + $this->assertArrayHasKey('delay', $toast->clientOptions); + $this->assertArrayHasKey(View::POS_READY, Yii::$app->view->js); + $js = Yii::$app->view->js[View::POS_READY]; + + $this->assertIsArray($js); + $options = array_shift($js); + + $this->assertContainsWithoutLE("(new bootstrap.Toast('#w0', {\"delay\":1000}));", $options); + } +} diff --git a/tests/ToggleButtonGroupTest.php b/tests/ToggleButtonGroupTest.php new file mode 100644 index 0000000..ea2faa7 --- /dev/null +++ b/tests/ToggleButtonGroupTest.php @@ -0,0 +1,109 @@ + ToggleButtonGroup::TYPE_CHECKBOX, + 'model' => new ToggleButtonGroupTestModel(), + 'attribute' => 'value', + 'items' => [ + '1' => 'item 1', + '2' => 'item 2', + ], + ], + ); + + $expectedHtml = <<
      + + +
      + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + */ + public function testCheckboxChecked(): void + { + Html::$counter = 0; + $html = ToggleButtonGroup::widget( + [ + 'type' => ToggleButtonGroup::TYPE_CHECKBOX, + 'model' => new ToggleButtonGroupTestModel(['value' => '2']), + 'attribute' => 'value', + 'items' => [ + '1' => 'item 1', + '2' => 'item 2', + ], + ], + ); + $this->assertStringContainsString('', $html); + } + + public function testRadio(): void + { + Html::$counter = 0; + $html = ToggleButtonGroup::widget( + [ + 'type' => ToggleButtonGroup::TYPE_RADIO, + 'model' => new ToggleButtonGroupTestModel(), + 'attribute' => 'value', + 'items' => [ + '1' => 'item 1', + '2' => 'item 2', + ], + ], + ); + + $expectedHtml = <<
      + + +
      + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + /** + */ + public function testRadioChecked(): void + { + Html::$counter = 0; + $html = ToggleButtonGroup::widget( + [ + 'type' => ToggleButtonGroup::TYPE_RADIO, + 'model' => new ToggleButtonGroupTestModel(['value' => '2']), + 'attribute' => 'value', + 'items' => [ + '1' => 'item 1', + '2' => 'item 2', + ], + ], + ); + $this->assertStringContainsString( + '', + $html, + ); + } +} + +class ToggleButtonGroupTestModel extends Model +{ + public $value; +} diff --git a/tests/TranslationTest.php b/tests/TranslationTest.php new file mode 100644 index 0000000..02e0c38 --- /dev/null +++ b/tests/TranslationTest.php @@ -0,0 +1,80 @@ + + */ + +namespace yiiunit\extensions\bootstrap5; + +use yii\bootstrap5\Alert; +use yii\bootstrap5\Breadcrumbs; + +class TranslationTest extends TestCase +{ + protected function setUp(): void + { + $this->mockWebApplication( + [ + 'language' => 'de-CH', + 'sourceLanguage' => 'en-US', + 'components' => [ + 'i18n' => [ + 'translations' => [ + 'yii/bootstrap5*' => [ + 'class' => 'yii\i18n\GettextMessageSource', + 'sourceLanguage' => 'en-US', + 'basePath' => '@yii/bootstrap5/messages', + ], + ], + ], + ], + ], + ); + } + + public function testTranslatedAlert(): void + { + Alert::$counter = 0; + $html = Alert::widget( + [ + 'body' => 'Heilige Guacamole! Das ist ein deutscher Test.', + 'options' => [ + 'class' => ['alert-warning'] + ], + ], + ); + + $expectedHtml = << + + Heilige Guacamole! Das ist ein deutscher Test. + + + + HTML; + $this->assertEqualsWithoutLE($expectedHtml, $html); + } + + public function testTranslatedBreadcrumb(): void + { + Breadcrumbs::$counter = 0; + $out = Breadcrumbs::widget( + [ + 'links' => [ + ['label' => 'Library', 'url' => '#'], + ['label' => 'Data'] + ] + ], + ); + + $expected = << + HTML; + $this->assertEqualsWithoutLE($expected, $out); + } +} diff --git a/tests/assets/.gitignore b/tests/assets/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/tests/assets/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..dece628 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ + + */ +class ExtendedActiveField extends ActiveField +{ + public ?array $horizontalCssClasses = [ + 'offset' => 'col-md-offset-4', + 'label' => 'col-md-4', + 'wrapper' => 'col-md-6', + 'error' => 'col-md-3', + 'hint' => 'col-md-3', + ]; +} diff --git a/tests/data/Singer.php b/tests/data/Singer.php new file mode 100644 index 0000000..28aefde --- /dev/null +++ b/tests/data/Singer.php @@ -0,0 +1,27 @@ + + */ +class Singer extends Model +{ + public $firstName; + public $lastName; + public $test; + + public function rules() + { + return [ + [['lastName'], 'default', 'value' => 'Lennon'], + [['lastName'], 'required'], + [['underscore_style'], 'yii\captcha\CaptchaValidator'], + [['test'], 'required', 'when' => fn($model) => $model->firstName === 'cebe'], + ]; + } +} diff --git a/tests/data/User.php b/tests/data/User.php new file mode 100644 index 0000000..ccda0e6 --- /dev/null +++ b/tests/data/User.php @@ -0,0 +1,44 @@ + + */ + +namespace yiiunit\extensions\bootstrap5\data; + + +use yii\base\Model; + +class User extends Model +{ + public $id; + public $firstName; + public $lastName; + public $username; + public $password; + + /** + * {@inheritdoc} + */ + public function rules() + { + return [ + ['id', 'integer'], + [['firstName', 'lastName'], 'string'], + ['username', 'string', 'min' => 4], + ['password', 'string', 'min' => 8, 'max' => '20'], + [['username', 'password'], 'required'] + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeHints() + { + return [ + 'username' => 'Your username must be at least 4 characters long', + 'password' => 'Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special characters, or emoji.' + ]; + } +} diff --git a/tests/providers/Data.php b/tests/providers/Data.php new file mode 100644 index 0000000..6401722 --- /dev/null +++ b/tests/providers/Data.php @@ -0,0 +1,33 @@ + 'test']]], // only content array without label + ]; + } + + public static function staticControl(): array + { + return [ + [ + 'foo', + [], + '' + ], + [ + '', + [], + '' + ] + ]; + } +}