├── CHANGELOG.md
├── .phpunit-watcher.yml
├── infection.json.dist
├── psalm.xml
├── src
├── Assets
│ ├── JqueryAsset.php
│ └── BootstrapAsset.php
├── Widget.php
├── Button.php
├── ButtonToolbar.php
├── ButtonGroup.php
├── BootstrapWidgetTrait.php
├── Alert.php
├── Progress.php
├── Breadcrumbs.php
├── Dropdown.php
├── Carousel.php
├── ButtonDropdown.php
├── Accordion.php
├── NavBar.php
├── Modal.php
├── Tabs.php
└── Nav.php
├── LICENSE.md
├── composer.json
├── .styleci.yml
└── README.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Framework bootstrap4 extension Change Log
2 |
3 | ## 1.0.0 under development
4 |
5 | - Initial release
6 |
--------------------------------------------------------------------------------
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "badge": {
10 | "branch": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Assets/JqueryAsset.php:
--------------------------------------------------------------------------------
1 | [
30 | 'dist/jquery.js',
31 | ],
32 | ];
33 | }
34 |
--------------------------------------------------------------------------------
/src/Assets/BootstrapAsset.php:
--------------------------------------------------------------------------------
1 | [
34 | 'css/bootstrap.css',
35 | 'js/bootstrap.bundle.js',
36 | ],
37 | ];
38 |
39 | public array $depends = [
40 | JqueryAsset::class,
41 | ];
42 | }
43 |
--------------------------------------------------------------------------------
/src/Widget.php:
--------------------------------------------------------------------------------
1 | autoGenerate) {
24 | $this->id = self::$autoIdPrefix . static::$counter++;
25 | }
26 |
27 | return $this->id;
28 | }
29 |
30 | /**
31 | * Set the Id of the widget.
32 | *
33 | * @param string $value
34 | *
35 | * @return $this
36 | */
37 | public function setId(string $value): self
38 | {
39 | $this->id = $value;
40 |
41 | return $this;
42 | }
43 |
44 | /**
45 | * Counter used to generate {@see id} for widgets.
46 | *
47 | * @param int $value
48 | */
49 | public static function counter(int $value): void
50 | {
51 | self::$counter = $value;
52 | }
53 |
54 | /**
55 | * The prefix to the automatically generated widget IDs.
56 | *
57 | * @param string $value
58 | *
59 | * {@see getId()}
60 | */
61 | public static function autoIdPrefix(string $value): void
62 | {
63 | self::$autoIdPrefix = $value;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/yii-bootstrap4",
3 | "type": "library",
4 | "description": "Yii Framework Twitter Bootstrap 4 Extension",
5 | "keywords": [
6 | "yii",
7 | "bootstrap4"
8 | ],
9 | "license": "BSD-3-Clause",
10 | "support": {
11 | "source": "https://github.com/yiisoft/yii-bootstrap4",
12 | "issues": "https://github.com/yiisoft/yii-bootstrap4/issues",
13 | "forum": "http://www.yiiframework.com/forum/",
14 | "wiki": "http://www.yiiframework.com/wiki/",
15 | "irc": "irc://irc.freenode.net/yii"
16 | },
17 | "minimum-stability": "dev",
18 | "prefer-stable": true,
19 | "provide": {
20 | "yiisoft/yii-bootstrap": "1.0"
21 | },
22 | "require": {
23 | "php": "^7.4|^8.0",
24 | "npm-asset/bootstrap": "^4.5.3",
25 | "npm-asset/jquery": "^3.5.1",
26 | "oomphinc/composer-installers-extender": "^2.0.0",
27 | "yiisoft/arrays": "^1.0",
28 | "yiisoft/assets": "^1.0@dev",
29 | "yiisoft/html": "^3.0@dev",
30 | "yiisoft/json": "^1.0",
31 | "yiisoft/widget": "^3.0@dev"
32 | },
33 | "require-dev": {
34 | "phpunit/phpunit": "^9.4",
35 | "roave/infection-static-analysis-plugin": "^1.5",
36 | "spatie/phpunit-watcher": "^1.23",
37 | "vimeo/psalm": "^4.2",
38 | "yiisoft/aliases": "^1.0",
39 | "yiisoft/di": "^3.0@dev"
40 | },
41 | "autoload": {
42 | "psr-4": {
43 | "Yiisoft\\Yii\\Bootstrap4\\": "src"
44 | }
45 | },
46 | "autoload-dev": {
47 | "psr-4": {
48 | "Yiisoft\\Yii\\Bootstrap4\\Tests\\": "tests"
49 | }
50 | },
51 | "extra": {
52 | "branch-alias": {
53 | "dev-master": "3.0.x-dev"
54 | },
55 | "installer-types": [
56 | "npm-asset"
57 | ],
58 | "installer-paths": {
59 | "./node_modules/{$name}": [
60 | "type:npm-asset"
61 | ]
62 | }
63 | },
64 | "config": {
65 | "sort-packages": true
66 | },
67 | "repositories": [
68 | {
69 | "type": "composer",
70 | "url": "https://asset-packagist.org"
71 | }
72 | ],
73 | "scripts": {
74 | "test": "phpunit --testdox --no-interaction",
75 | "test-watch": "phpunit-watcher watch"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 | - resources
11 | - views
12 | - public
13 | - templates
14 | not-name:
15 | - UnionCar.php
16 | - TimerUnionTypes.php
17 | - schema1.php
18 |
19 | enabled:
20 | - alpha_ordered_traits
21 | - array_indentation
22 | - array_push
23 | - combine_consecutive_issets
24 | - combine_consecutive_unsets
25 | - combine_nested_dirname
26 | - declare_strict_types
27 | - dir_constant
28 | - final_static_access
29 | - fully_qualified_strict_types
30 | - function_to_constant
31 | - hash_to_slash_comment
32 | - is_null
33 | - logical_operators
34 | - magic_constant_casing
35 | - magic_method_casing
36 | - method_separation
37 | - modernize_types_casting
38 | - native_function_casing
39 | - native_function_type_declaration_casing
40 | - no_alias_functions
41 | - no_empty_comment
42 | - no_empty_phpdoc
43 | - no_empty_statement
44 | - no_extra_block_blank_lines
45 | - no_short_bool_cast
46 | - no_superfluous_elseif
47 | - no_unneeded_control_parentheses
48 | - no_unneeded_curly_braces
49 | - no_unneeded_final_method
50 | - no_unset_cast
51 | - no_unused_imports
52 | - no_unused_lambda_imports
53 | - no_useless_else
54 | - no_useless_return
55 | - normalize_index_brace
56 | - php_unit_dedicate_assert
57 | - php_unit_dedicate_assert_internal_type
58 | - php_unit_expectation
59 | - php_unit_mock
60 | - php_unit_mock_short_will_return
61 | - php_unit_namespaced
62 | - php_unit_no_expectation_annotation
63 | - phpdoc_no_empty_return
64 | - phpdoc_no_useless_inheritdoc
65 | - phpdoc_order
66 | - phpdoc_property
67 | - phpdoc_scalar
68 | - phpdoc_separation
69 | - phpdoc_singular_inheritdoc
70 | - phpdoc_trim
71 | - phpdoc_trim_consecutive_blank_line_separation
72 | - phpdoc_type_to_var
73 | - phpdoc_types
74 | - phpdoc_types_order
75 | - print_to_echo
76 | - regular_callable_call
77 | - return_assignment
78 | - self_accessor
79 | - self_static_accessor
80 | - set_type_to_cast
81 | - short_array_syntax
82 | - short_list_syntax
83 | - simplified_if_return
84 | - single_quote
85 | - standardize_not_equals
86 | - ternary_to_null_coalescing
87 | - trailing_comma_in_multiline_array
88 | - unalign_double_arrow
89 | - unalign_equals
90 |
--------------------------------------------------------------------------------
/src/Button.php:
--------------------------------------------------------------------------------
1 | label('Action')
17 | * ->options(['class' => 'btn-lg']);
18 | * ```
19 | */
20 | class Button extends Widget
21 | {
22 | private string $tagName = 'button';
23 | private string $label = 'Button';
24 | private bool $encodeLabels = true;
25 | private array $options = [];
26 |
27 | protected function run(): string
28 | {
29 | if (!isset($this->options['id'])) {
30 | $this->options['id'] = "{$this->getId()}-button";
31 | }
32 |
33 | Html::addCssClass($this->options, ['widget' => 'btn']);
34 |
35 | $this->registerPlugin('button', $this->options);
36 |
37 | return Html::tag(
38 | $this->tagName,
39 | $this->encodeLabels ? Html::encode($this->label) : $this->label,
40 | $this->options
41 | );
42 | }
43 |
44 | /**
45 | * Whether the label should be HTML-encoded.
46 | *
47 | * @param bool $value
48 | *
49 | * @return $this
50 | */
51 | public function encodeLabels(bool $value): self
52 | {
53 | $this->encodeLabels = $value;
54 |
55 | return $this;
56 | }
57 |
58 | /**
59 | * The button label
60 | *
61 | * @param string $value
62 | *
63 | * @return $this
64 | */
65 | public function label(string $value): self
66 | {
67 | $this->label = $value;
68 |
69 | return $this;
70 | }
71 |
72 | /**
73 | * The HTML attributes for the widget container tag. The following special options are recognized.
74 | *
75 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
76 | *
77 | * @param array $value
78 | *
79 | * @return $this
80 | */
81 | public function options(array $value): self
82 | {
83 | $this->options = $value;
84 |
85 | return $this;
86 | }
87 |
88 | /**
89 | * The tag to use to render the button.
90 | *
91 | * @param string $value
92 | *
93 | * @return $this
94 | */
95 | public function tagName(string $value): self
96 | {
97 | $this->tagName = $value;
98 |
99 | return $this;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/ButtonToolbar.php:
--------------------------------------------------------------------------------
1 | buttonGroups([
23 | * [
24 | * 'buttons' => [
25 | * ['label' => '1', 'class' => ['btn-secondary']],
26 | * ['label' => '2', 'class' => ['btn-secondary']],
27 | * ['label' => '3', 'class' => ['btn-secondary']],
28 | * ['label' => '4', 'class' => ['btn-secondary']]
29 | * ],
30 | * 'class' => ['mr-2']
31 | * ],
32 | * [
33 | * 'buttons' => [
34 | * ['label' => '5', 'class' => ['btn-secondary']],
35 | * ['label' => '6', 'class' => ['btn-secondary']],
36 | * ['label' => '7', 'class' => ['btn-secondary']]
37 | * ],
38 | * 'class' => ['mr-2']
39 | * ],
40 | * [
41 | * 'buttons' => [
42 | * ['label' => '8', 'class' => ['btn-secondary']]
43 | * ]
44 | * ]
45 | * ]);
46 | * ```
47 | *
48 | * Pressing on the button should be handled via JavaScript. See the following for details:
49 | */
50 | class ButtonToolbar extends Widget
51 | {
52 | private array $buttonGroups = [];
53 | private array $options = [];
54 |
55 | protected function run(): string
56 | {
57 | if (!isset($this->options['id'])) {
58 | $this->options['id'] = "{$this->getId()}-button-toolbar";
59 | }
60 |
61 | Html::addCssClass($this->options, ['widget' => 'btn-toolbar']);
62 |
63 | if (!isset($this->options['role'])) {
64 | $this->options['role'] = 'toolbar';
65 | }
66 |
67 | return Html::tag('div', $this->renderButtonGroups(), $this->options);
68 | }
69 |
70 | /**
71 | * Generates the button groups that compound the toolbar as specified on {@see buttonGroups}.
72 | *
73 | * @throws InvalidConfigException
74 | *
75 | * @return string the rendering result.
76 | */
77 | protected function renderButtonGroups(): string
78 | {
79 | $buttonGroups = [];
80 |
81 | foreach ($this->buttonGroups as $group) {
82 | if (is_array($group)) {
83 | if (!isset($group['buttons'])) {
84 | continue;
85 | }
86 |
87 | $buttonGroups[] = ButtonGroup::widget()
88 | ->buttons($group['buttons'])
89 | ->options($group['options'])
90 | ->render();
91 | } else {
92 | $buttonGroups[] = $group;
93 | }
94 | }
95 |
96 | return implode("\n", $buttonGroups);
97 | }
98 |
99 | /**
100 | * List of buttons groups. Each array element represents a single group which can be specified as a string or an
101 | * array of the following structure:
102 | *
103 | * - buttons: array list of buttons. Either as array or string representation
104 | * - options: array optional, the HTML attributes of the button group.
105 | * - encodeLabels: bool whether to HTML-encode the button labels.
106 | *
107 | * @param array $value
108 | *
109 | * @return $this
110 | */
111 | public function buttonGroups(array $value): self
112 | {
113 | $this->buttonGroups = $value;
114 |
115 | return $this;
116 | }
117 |
118 | /**
119 | * The HTML attributes for the container tag. The following special options are recognized.
120 | *
121 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
122 | *
123 | * @param array $value
124 | *
125 | * @return $this
126 | */
127 | public function options(array $value): self
128 | {
129 | $this->options = $value;
130 |
131 | return $this;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/ButtonGroup.php:
--------------------------------------------------------------------------------
1 | buttons([
23 | * ['label' => 'A'],
24 | * ['label' => 'B'],
25 | * ['label' => 'C', 'visible' => false],
26 | * ]);
27 | *
28 | * // button group with an item as a string
29 | * echo ButtonGroup::widget()
30 | * ->buttons([
31 | * Button::widget()
32 | * ->label('A'),
33 | * ['label' => 'B'],
34 | * ]);
35 | * ```
36 | *
37 | * Pressing on the button should be handled via JavaScript. See the following for details:
38 | */
39 | class ButtonGroup extends Widget
40 | {
41 | private array $buttons = [];
42 | private bool $encodeLabels = true;
43 | private array $options = [];
44 |
45 | protected function run(): string
46 | {
47 | if (!isset($this->options['id'])) {
48 | $this->options['id'] = "{$this->getId()}-button-group";
49 | }
50 |
51 | Html::addCssClass($this->options, ['widget' => 'btn-group']);
52 |
53 | if (!isset($this->options['role'])) {
54 | $this->options['role'] = 'group';
55 | }
56 |
57 | return Html::tag('div', $this->renderButtons(), $this->options);
58 | }
59 |
60 | /**
61 | * Generates the buttons that compound the group as specified on {@see buttons}.
62 | *
63 | * @throws InvalidConfigException
64 | *
65 | * @return string the rendering result.
66 | */
67 | protected function renderButtons(): string
68 | {
69 | $buttons = [];
70 |
71 | foreach ($this->buttons as $button) {
72 | if (is_array($button)) {
73 | $visible = ArrayHelper::remove($button, 'visible', true);
74 |
75 | if ($visible === false) {
76 | continue;
77 | }
78 |
79 | if (!isset($button['encodeLabel'])) {
80 | $button['encodeLabel'] = $this->encodeLabels;
81 | }
82 |
83 | if (!isset($button['options']['type'])) {
84 | ArrayHelper::setValueByPath($button, 'options.type', 'button');
85 | }
86 |
87 | $buttons[] = Button::widget()
88 | ->encodeLabels($button['encodeLabel'])
89 | ->label($button['label'])
90 | ->options($button['options'])
91 | ->render();
92 | } else {
93 | $buttons[] = $button;
94 | }
95 | }
96 |
97 | return implode("\n", $buttons);
98 | }
99 |
100 | /**
101 | * List of buttons. Each array element represents a single button which can be specified as a string or an array of
102 | * the following structure:
103 | *
104 | * - label: string, required, the button label.
105 | * - options: array, optional, the HTML attributes of the button.
106 | * - visible: bool, optional, whether this button is visible. Defaults to true.
107 | *
108 | * @param array $value
109 | *
110 | * @return $this
111 | */
112 | public function buttons(array $value): self
113 | {
114 | $this->buttons = $value;
115 |
116 | return $this;
117 | }
118 |
119 | /**
120 | * Whether to HTML-encode the button labels.
121 | *
122 | * @param bool $value
123 | *
124 | * @return $this
125 | */
126 | public function encodeLabels(bool $value): self
127 | {
128 | $this->encodeLabels = $value;
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * The HTML attributes for the widget container tag. The following special options are recognized.
135 | *
136 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
137 | *
138 | * @param array $value
139 | *
140 | * @return $this
141 | */
142 | public function options(array $value): self
143 | {
144 | $this->options = $value;
145 |
146 | return $this;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Yii Framework Twitter Bootstrap 4 Extension
9 |
10 |
11 |
12 | > ⚠️ Please do not start new projects with Bootstrap 4. There is [Bootstrap 5 package available](https://github.com/yiisoft/yii-bootstrap5).
13 |
14 | This [Yii Framework] extension encapsulates [Twitter Bootstrap 4] components
15 | and plugins in terms of Yii widgets, and thus makes using Bootstrap components/plugins
16 | in Yii applications extremely easy.
17 |
18 | [Yii Framework]: http://www.yiiframework.com/
19 | [Twitter Bootstrap 4]: https://getbootstrap.com/docs/4.1/getting-started/introduction/
20 |
21 | Documentation is at [docs/guide/README.md](docs/guide/README.md).
22 |
23 | [](https://packagist.org/packages/yiisoft/yii-bootstrap4)
24 | [](https://packagist.org/packages/yiisoft/yii-bootstrap4)
25 | [](https://github.com/yiisoft/yii-bootstrap4/actions?query=workflow%3Abuild)
26 | [](https://scrutinizer-ci.com/g/yiisoft/yii-bootstrap4/?branch=master)
27 | [](https://scrutinizer-ci.com/g/yiisoft/yii-bootstrap4/?branch=master)
28 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/yii-bootstrap4/master)
29 | [](https://github.com/yiisoft/yii-bootstrap4/actions?query=workflow%3A%22static+analysis%22)
30 | [](https://shepherd.dev/github/yiisoft/yii-bootstrap4)
31 |
32 | ### Installation
33 |
34 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
35 |
36 | ```
37 | composer require yiisoft/yii-bootstrap4
38 | ```
39 |
40 | ### Usage
41 |
42 | For example, the following
43 | single line of code in a view file would render a Bootstrap Progress plugin:
44 |
45 | ```php
46 | = Progress::widget()->percent('60')->label(test); ?>
47 | ```
48 | Read [Documentation](docs/guide/README.md) for more information
49 |
50 | ### Unit testing
51 |
52 | The package is tested with [PHPUnit](https://phpunit.de/). To run tests:
53 |
54 | ```shell
55 | ./vendor/bin/phpunit
56 | ```
57 |
58 | ### Mutation testing
59 |
60 | The package tests are checked with [Infection](https://infection.github.io/) mutation framework. To run it:
61 |
62 | ```shell
63 | ./vendor/bin/infection
64 | ```
65 |
66 | ### Static analysis
67 |
68 | The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis:
69 |
70 | ```shell
71 | ./vendor/bin/psalm
72 | ```
73 |
74 | ### Support the project
75 |
76 | [](https://opencollective.com/yiisoft)
77 |
78 | ### Follow updates
79 |
80 | [](https://www.yiiframework.com/)
81 | [](https://twitter.com/yiiframework)
82 | [](https://t.me/yii3en)
83 | [](https://www.facebook.com/groups/yiitalk)
84 | [](https://yiiframework.com/go/slack)
85 |
86 | ## License
87 |
88 | The Yii Framework Twitter Bootstrap 4 Extension is free software. It is released under the terms of the BSD License.
89 | Please see [`LICENSE`](./LICENSE.md) for more information.
90 |
91 | Maintained by [Yii Software](https://www.yiiframework.com/).
92 |
--------------------------------------------------------------------------------
/src/BootstrapWidgetTrait.php:
--------------------------------------------------------------------------------
1 | assetManager !== null) {
49 | $this->assetManager->register([
50 | BootstrapAsset::class,
51 | ]);
52 | }
53 |
54 | if ($this->enableClientOptions !== false) {
55 | $optionsString = Json::htmlEncode($this->clientOptions);
56 | $js = "jQuery('#$id').$name($optionsString);";
57 |
58 | if ($this->webView !== null) {
59 | $this->webView->registerJs($js);
60 | }
61 | }
62 |
63 | $this->registerClientEvents($id);
64 | }
65 |
66 | /**
67 | * Registers JS event handlers that are listed in {@see clientEvents}.
68 | *
69 | * @param string $id
70 | */
71 | protected function registerClientEvents(string $id): void
72 | {
73 | if ($this->clientEvents) {
74 | $js = [];
75 |
76 | foreach ($this->clientEvents as $event => $handler) {
77 | $js[] = "jQuery('#$id').on('$event', $handler);";
78 | }
79 |
80 | if ($this->webView !== null) {
81 | $this->webView->registerJs(implode("\n", $js));
82 | }
83 | }
84 | }
85 |
86 | public function assetManager(AssetManager $value): self
87 | {
88 | $this->assetManager = $value;
89 |
90 | return $this;
91 | }
92 |
93 | /**
94 | * The event handlers for the underlying Bootstrap JS plugin.
95 | *
96 | * Please refer to the corresponding Bootstrap plugin Web page for possible events.
97 | *
98 | * For example, [this page](http://getbootstrap.com/javascript/#modals) shows how to use the "Modal" plugin and the
99 | * supported events (e.g. "shown").
100 | *
101 | * @param array $value
102 | *
103 | * @return $this
104 | */
105 | public function clientEvents(array $value): self
106 | {
107 | $this->clientEvents = $value;
108 |
109 | return $this;
110 | }
111 |
112 | /**
113 | * The options for the underlying Bootstrap JS plugin.
114 | *
115 | * Please refer to the corresponding Bootstrap plugin Web page for possible options.
116 | *
117 | * For example, [this page](http://getbootstrap.com/javascript/#modals) shows how to use the "Modal" plugin and the
118 | * supported options (e.g. "remote").
119 | *
120 | * @param array $value
121 | *
122 | * @return $this
123 | */
124 | public function clientOptions(array $value): self
125 | {
126 | $this->clientOptions = $value;
127 |
128 | return $this;
129 | }
130 |
131 | public function getClientOptions(): array
132 | {
133 | return $this->clientOptions;
134 | }
135 |
136 | /**
137 | * Enable/Disable script Bootstrap JS plugin.
138 | *
139 | * @param bool $value
140 | *
141 | * @return $this
142 | */
143 | public function enableClientOptions(bool $value): self
144 | {
145 | $this->enableClientOptions = $value;
146 |
147 | return $this;
148 | }
149 |
150 | public function getEnableClientOptions(): bool
151 | {
152 | return $this->enableClientOptions;
153 | }
154 |
155 | public function webView(object $value): self
156 | {
157 | $this->webView = $value;
158 |
159 | return $this;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Alert.php:
--------------------------------------------------------------------------------
1 | options([
19 | * 'class' => 'alert-info',
20 | * ])
21 | * ->body('Say hello...');
22 | * ```
23 | */
24 | class Alert extends Widget
25 | {
26 | private ?string $body = null;
27 | private array $closeButton = [];
28 | private bool $closeButtonEnabled = true;
29 | private array $options = [];
30 |
31 | protected function run(): string
32 | {
33 | if (!isset($this->options['id'])) {
34 | $this->options['id'] = "{$this->getId()}-alert";
35 | }
36 |
37 | $this->initOptions();
38 |
39 | $this->registerPlugin('alert', $this->options);
40 |
41 | return Html::beginTag('div', $this->options) . "\n"
42 | . "\n" . $this->renderBodyEnd()
43 | . "\n" . Html::endTag('div');
44 | }
45 |
46 | /**
47 | * Renders the alert body and the close button (if any).
48 | *
49 | * @throws JsonException
50 | *
51 | * @return string the rendering result
52 | */
53 | protected function renderBodyEnd(): string
54 | {
55 | return $this->body . "\n" . $this->renderCloseButton() . "\n";
56 | }
57 |
58 | /**
59 | * Renders the close button.
60 | *
61 | * @throws JsonException
62 | *
63 | * @return string the rendering result.
64 | */
65 | protected function renderCloseButton(): ?string
66 | {
67 | if ($this->closeButtonEnabled === false) {
68 | return null;
69 | }
70 |
71 | $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button');
72 | $label = ArrayHelper::remove($this->closeButton, 'label', Html::tag('span', '×', [
73 | 'aria-hidden' => 'true',
74 | ]));
75 |
76 | if ($tag === 'button' && !isset($this->closeButton['type'])) {
77 | $this->closeButton['type'] = 'button';
78 | }
79 |
80 | return Html::tag($tag, $label, $this->closeButton);
81 | }
82 |
83 | /**
84 | * Initializes the widget options.
85 | *
86 | * This method sets the default values for various options.
87 | */
88 | protected function initOptions(): void
89 | {
90 | Html::addCssClass($this->options, ['widget' => 'alert']);
91 |
92 | if ($this->closeButtonEnabled !== false) {
93 | $this->closeButton = [
94 | 'data-dismiss' => 'alert',
95 | 'class' => ['widget' => 'close'],
96 | ];
97 |
98 | Html::addCssClass($this->options, ['alert-dismissible']);
99 | }
100 |
101 | if (!isset($this->options['role'])) {
102 | $this->options['role'] = 'alert';
103 | }
104 | }
105 |
106 | /**
107 | * The body content in the alert component. Alert widget will also be treated as the body content, and will be
108 | * rendered before this.
109 | *
110 | * @param string|null $value
111 | *
112 | * @return $this
113 | */
114 | public function body(?string $value): self
115 | {
116 | $this->body = $value;
117 |
118 | return $this;
119 | }
120 |
121 | /**
122 | * The options for rendering the close button tag.
123 | *
124 | * The close button is displayed in the header of the modal window. Clicking on the button will hide the modal
125 | * window. If {@see closeButtonEnabled} is false, no close button will be rendered.
126 | *
127 | * The following special options are supported:
128 | *
129 | * - tag: string, the tag name of the button. Defaults to 'button'.
130 | * - label: string, the label of the button. Defaults to '×'.
131 | *
132 | * The rest of the options will be rendered as the HTML attributes of the button tag.
133 | *
134 | * Please refer to the [Alert documentation](http://getbootstrap.com/components/#alerts) for the supported HTML
135 | * attributes.
136 | *
137 | * @param array $value
138 | *
139 | * @return $this
140 | */
141 | public function closeButton(array $value): self
142 | {
143 | $this->closeButton = $value;
144 |
145 | return $this;
146 | }
147 |
148 | /**
149 | * Enable/Disable close button.
150 | *
151 | * @param bool $value
152 | *
153 | * @return $this
154 | */
155 | public function closeButtonEnabled(bool $value): self
156 | {
157 | $this->closeButtonEnabled = $value;
158 |
159 | return $this;
160 | }
161 |
162 | /**
163 | * The HTML attributes for the widget container tag. The following special options are recognized.
164 | *
165 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
166 | *
167 | * @param array $value
168 | *
169 | * @return $this
170 | */
171 | public function options(array $value): self
172 | {
173 | $this->options = $value;
174 |
175 | return $this;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/Progress.php:
--------------------------------------------------------------------------------
1 | percent('60')
26 | * ->label(test);
27 | *
28 | * // styled
29 | * echo Progress::widget()
30 | * ->bars([
31 | * ['percent' => '65', 'options' => ['class' => 'bg-danger']]
32 | * ]);
33 | *
34 | * // striped
35 | * echo Progress::widget()
36 | * ->bars([
37 | * ['percent' => '70', 'options' => ['class' => 'bg-warning progress-bar-striped']]
38 | * ]);
39 | *
40 | * // striped animated
41 | * echo Progress::widget()
42 | * ->percent('70')
43 | * ->options'(['class' => 'bg-success progress-bar-animated progress-bar-striped']);
44 | *
45 | * // stacked bars
46 | * echo Progress::widget()
47 | * ->bars([
48 | * ['percent' => '30', 'options' => ['class' => 'bg-danger']],
49 | * ['percent' => '30', 'label' => 'test', 'options' => ['class' => 'bg-success']],
50 | * ['percent' => '35', 'options' => ['class' => 'bg-warning']],
51 | * ]);
52 | * ```
53 | */
54 | class Progress extends Widget
55 | {
56 | private ?string $label = null;
57 | private ?string $percent = null;
58 | private array $bars = [];
59 | private array $options = [];
60 | private array $barOptions = [];
61 |
62 | protected function run(): string
63 | {
64 | if (!isset($this->options['id'])) {
65 | $this->options['id'] = "{$this->getId()}-progress";
66 | }
67 |
68 | Html::addCssClass($this->options, ['widget' => 'progress']);
69 |
70 | return $this->renderProgress();
71 | }
72 |
73 | /**
74 | * Renders the progress.
75 | *
76 | * @throws JsonException|RuntimeException if the "percent" option is not set in a stacked progress bar.
77 | *
78 | * @return string the rendering result.
79 | */
80 | protected function renderProgress(): string
81 | {
82 | $out = Html::beginTag('div', $this->options) . "\n";
83 |
84 | if (empty($this->bars)) {
85 | $this->bars = [
86 | ['label' => $this->label, 'percent' => $this->percent, 'options' => $this->barOptions],
87 | ];
88 | }
89 |
90 | $bars = [];
91 |
92 | foreach ($this->bars as $bar) {
93 | $label = ArrayHelper::getValue($bar, 'label', '');
94 | if (!isset($bar['percent'])) {
95 | throw new RuntimeException('The "percent" option is required.');
96 | }
97 | $options = ArrayHelper::getValue($bar, 'options', []);
98 | $bars[] = $this->renderBar($bar['percent'], $label, $options);
99 | }
100 |
101 | $out .= implode("\n", $bars) . "\n";
102 | $out .= Html::endTag('div');
103 |
104 | return $out;
105 | }
106 |
107 | /**
108 | * Generates a bar.
109 | *
110 | * @param string $percent the percentage of the bar
111 | * @param string $label , optional, the label to display at the bar
112 | * @param array $options the HTML attributes of the bar
113 | *
114 | * @throws JsonException
115 | *
116 | * @return string the rendering result.
117 | */
118 | protected function renderBar(string $percent, string $label = '', array $options = []): string
119 | {
120 | $valuePercent = (float)trim(rtrim($percent, '%'));
121 |
122 | $options = array_merge($options, [
123 | 'role' => 'progressbar',
124 | 'aria-valuenow' => $percent,
125 | 'aria-valuemin' => 0,
126 | 'aria-valuemax' => 100,
127 | ]);
128 |
129 | Html::addCssClass($options, ['widget' => 'progress-bar']);
130 | Html::addCssStyle($options, ['width' => $valuePercent . '%'], true);
131 |
132 | return Html::tag('div', $label, $options);
133 | }
134 |
135 | /**
136 | * Set of bars that are stacked together to form a single progress bar.
137 | *
138 | * Each bar is an array of the following structure:
139 | *
140 | * ```php
141 | * [
142 | * // required, the amount of progress as a percentage.
143 | * 'percent' => '30',
144 | * // optional, the label to be displayed on the bar
145 | * 'label' => '30%',
146 | * // optional, array, additional HTML attributes for the bar tag
147 | * 'options' => [],
148 | * ]
149 | * ```
150 | *
151 | * @param array $value
152 | *
153 | * @return $this
154 | */
155 | public function bars(array $value): self
156 | {
157 | $this->bars = $value;
158 |
159 | return $this;
160 | }
161 |
162 | /**
163 | * The HTML attributes of the bar. This property will only be considered if {@see bars} is empty.
164 | *
165 | * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
166 | *
167 | * @param array $value
168 | *
169 | * @return $this
170 | */
171 | public function barOptions(array $value): self
172 | {
173 | $this->barOptions = $value;
174 |
175 | return $this;
176 | }
177 |
178 | /**
179 | * The button label.
180 | *
181 | * @param string|null $value
182 | *
183 | * @return $this
184 | */
185 | public function label(?string $value): self
186 | {
187 | $this->label = $value;
188 |
189 | return $this;
190 | }
191 |
192 | /**
193 | * The HTML attributes for the widget container tag. The following special options are recognized.
194 | *
195 | * @param array $value
196 | *
197 | * @return $this
198 | *
199 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
200 | */
201 | public function options(array $value): self
202 | {
203 | $this->options = $value;
204 |
205 | return $this;
206 | }
207 |
208 | /**
209 | * The amount of progress as a percentage.
210 | *
211 | * @param string|null $value
212 | *
213 | * @return $this
214 | */
215 | public function percent(?string $value): self
216 | {
217 | $this->percent = $value;
218 |
219 | return $this;
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/Breadcrumbs.php:
--------------------------------------------------------------------------------
1 | links(['label' => !empty($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : []]);
25 | * ```
26 | */
27 | class Breadcrumbs extends Widget
28 | {
29 | private string $tag = 'ol';
30 | private bool $encodeLabels = true;
31 | private array $homeLink = [];
32 | private array $links = [];
33 | private string $itemTemplate = "{link}\n";
34 | private string $activeItemTemplate = "{link}\n";
35 | private array $navOptions = ['aria-label' => 'breadcrumb'];
36 | private array $options = [];
37 |
38 | protected function run(): string
39 | {
40 | if (!isset($this->options['id'])) {
41 | $this->options['id'] = "{$this->getId()}-breadcrumb";
42 | }
43 |
44 | Html::addCssClass($this->options, ['widget' => 'breadcrumb']);
45 |
46 | $this->registerPlugin('breadcrumb', $this->options);
47 |
48 | if (empty($this->links)) {
49 | return '';
50 | }
51 |
52 | $links = [];
53 |
54 | if ($this->homeLink === []) {
55 | $links[] = $this->renderItem([
56 | 'label' => 'Home',
57 | 'url' => '/',
58 | ], $this->itemTemplate);
59 | } else {
60 | $links[] = $this->renderItem($this->homeLink, $this->itemTemplate);
61 | }
62 |
63 | foreach ($this->links as $link) {
64 | if (!is_array($link)) {
65 | $link = ['label' => $link];
66 | }
67 |
68 | $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate);
69 | }
70 |
71 | return Html::tag('nav', Html::tag($this->tag, implode('', $links), $this->options), $this->navOptions);
72 | }
73 |
74 | /**
75 | * Renders a single breadcrumb item.
76 | *
77 | * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional.
78 | * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the
79 | * link.
80 | *
81 | * @throws JsonException|RuntimeException if `$link` does not have "label" element.
82 | *
83 | * @return string the rendering result
84 | */
85 | protected function renderItem(array $link, string $template): string
86 | {
87 | $encodeLabel = ArrayHelper::remove($link, 'encode', $this->encodeLabels);
88 |
89 | if (array_key_exists('label', $link)) {
90 | $label = $encodeLabel ? Html::encode($link['label']) : $link['label'];
91 | } else {
92 | throw new RuntimeException('The "label" element is required for each link.');
93 | }
94 |
95 | if (isset($link['template'])) {
96 | $template = $link['template'];
97 | }
98 |
99 | if (isset($link['url'])) {
100 | $options = $link;
101 | unset($options['template'], $options['label'], $options['url']);
102 | $linkHtml = Html::a($label, $link['url'], $options);
103 | } else {
104 | $linkHtml = $label;
105 | }
106 |
107 | return strtr($template, ['{link}' => $linkHtml]);
108 | }
109 |
110 | /**
111 | * The template used to render each active item in the breadcrumbs. The token `{link}` will be replaced with the
112 | * actual HTML link for each active item.
113 | *
114 | * @param string $value
115 | *
116 | * @return $this
117 | */
118 | public function activeItemTemplate(string $value): self
119 | {
120 | $this->activeItemTemplate = $value;
121 |
122 | return $this;
123 | }
124 |
125 | /**
126 | * Whether to HTML-encode the link labels.
127 | *
128 | * @param bool $value
129 | *
130 | * @return $this
131 | */
132 | public function encodeLabels(bool $value): self
133 | {
134 | $this->encodeLabels = $value;
135 |
136 | return $this;
137 | }
138 |
139 | /**
140 | * The first hyperlink in the breadcrumbs (called home link).
141 | *
142 | * Please refer to {@see links} on the format of the link.
143 | *
144 | * If this property is not set, it will default to a link pointing with the label 'Home'. If this property is false,
145 | * the home link will not be rendered.
146 | *
147 | * @param array $value
148 | *
149 | * @return $this
150 | */
151 | public function homeLink(array $value): self
152 | {
153 | $this->homeLink = $value;
154 |
155 | return $this;
156 | }
157 |
158 | /**
159 | * The template used to render each inactive item in the breadcrumbs. The token `{link}` will be replaced with the
160 | * actual HTML link for each inactive item.
161 | *
162 | * @param string $value
163 | *
164 | * @return $this
165 | */
166 | public function itemTemplate(string $value): self
167 | {
168 | $this->itemTemplate = $value;
169 |
170 | return $this;
171 | }
172 |
173 | /**
174 | * List of links to appear in the breadcrumbs. If this property is empty, the widget will not render anything. Each
175 | * array element represents a single link in the breadcrumbs with the following structure:
176 | *
177 | * ```php
178 | * [
179 | * 'label' => 'label of the link', // required
180 | * 'url' => 'url of the link', // optional, will be processed by Url::to()
181 | * 'template' => 'own template of the item', // optional, if not set $this->itemTemplate will be used
182 | * ]
183 | * ```
184 | *
185 | * @param array $value
186 | *
187 | * @return $this
188 | */
189 | public function links(array $value): self
190 | {
191 | $this->links = $value;
192 |
193 | return $this;
194 | }
195 |
196 | /**
197 | * The HTML attributes for the widgets nav container tag.
198 | *
199 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
200 | *
201 | * @param array $value
202 | *
203 | * @return $this
204 | */
205 | public function navOptions(array $value): self
206 | {
207 | $this->navOptions = $value;
208 |
209 | return $this;
210 | }
211 |
212 | /**
213 | * The HTML attributes for the widget container tag. The following special options are recognized.
214 | *
215 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
216 | *
217 | * @param array $value
218 | *
219 | * @return $this
220 | */
221 | public function options(array $value): self
222 | {
223 | $this->options = $value;
224 |
225 | return $this;
226 | }
227 |
228 | /**
229 | * The name of the breadcrumb container tag.
230 | *
231 | * @param string $value
232 | *
233 | * @return $this
234 | */
235 | public function tag(string $value): self
236 | {
237 | $this->tag = $value;
238 |
239 | return $this;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Dropdown.php:
--------------------------------------------------------------------------------
1 |
24 | * items([
27 | * ['label' => 'DropdownA', 'url' => '/'],
28 | * ['label' => 'DropdownB', 'url' => '#'],
29 | * ]);
30 | * ?>
31 | *
32 | * ```
33 | */
34 | class Dropdown extends Widget
35 | {
36 | private array $items = [];
37 | private bool $encodeLabels = true;
38 | private array $submenuOptions = [];
39 | private array $options = [];
40 |
41 | protected function run(): string
42 | {
43 | if (!isset($this->options['id'])) {
44 | $this->options['id'] = "{$this->getId()}-dropdown";
45 | }
46 |
47 | Html::addCssClass($this->options, ['widget' => 'dropdown-menu']);
48 |
49 | $this->registerClientEvents($this->options['id']);
50 |
51 | return $this->renderItems($this->items, $this->options);
52 | }
53 |
54 | /**
55 | * Renders menu items.
56 | *
57 | * @param array $items the menu items to be rendered
58 | * @param array $options the container HTML attributes
59 | *
60 | * @throws JsonException|RuntimeException if the label option is not specified in one of the items.
61 | *
62 | * @return string the rendering result.
63 | */
64 | protected function renderItems(array $items, array $options = []): string
65 | {
66 | $lines = [];
67 |
68 | foreach ($items as $item) {
69 | if (is_string($item)) {
70 | $item = ['label' => $item, 'encode' => false, 'enclose' => false];
71 | }
72 |
73 | if (isset($item['visible']) && !$item['visible']) {
74 | continue;
75 | }
76 |
77 | if (!array_key_exists('label', $item)) {
78 | throw new RuntimeException('The "label" option is required.');
79 | }
80 |
81 | $encodeLabel = $item['encode'] ?? $this->encodeLabels;
82 | $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
83 | $itemOptions = ArrayHelper::getValue($item, 'options', []);
84 | $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
85 | $active = ArrayHelper::getValue($item, 'active', false);
86 | $disabled = ArrayHelper::getValue($item, 'disabled', false);
87 | $enclose = ArrayHelper::getValue($item, 'enclose', true);
88 |
89 | Html::addCssClass($linkOptions, 'dropdown-item');
90 |
91 | if ($disabled) {
92 | ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
93 | ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
94 | Html::addCssClass($linkOptions, 'disabled');
95 | } elseif ($active) {
96 | Html::addCssClass($linkOptions, 'active');
97 | }
98 |
99 | $url = $item['url'] ?? null;
100 |
101 | if (empty($item['items'])) {
102 | if ($label === '-') {
103 | $content = Html::div('', ['class' => 'dropdown-divider']);
104 | } elseif ($enclose === false) {
105 | $content = $label;
106 | } elseif ($url === null) {
107 | $content = Html::tag('h6', $label, ['class' => 'dropdown-header']);
108 | } else {
109 | $content = Html::a($label, $url, $linkOptions);
110 | }
111 |
112 | $lines[] = $content;
113 | } else {
114 | $submenuOptions = $this->submenuOptions;
115 |
116 | if (isset($item['submenuOptions'])) {
117 | $submenuOptions = array_merge($submenuOptions, $item['submenuOptions']);
118 | }
119 |
120 | Html::addCssClass($submenuOptions, ['dropdown-submenu']);
121 | Html::addCssClass($linkOptions, ['dropdown-toggle']);
122 |
123 | $lines[] = Html::beginTag(
124 | 'div',
125 | array_merge_recursive(['class' => ['dropdown'], 'aria-expanded' => 'false'], $itemOptions)
126 | );
127 |
128 | $lines[] = Html::a($label, $url, array_merge([
129 | 'data-toggle' => 'dropdown',
130 | 'aria-haspopup' => 'true',
131 | 'aria-expanded' => 'false',
132 | 'role' => 'button',
133 | ], $linkOptions));
134 |
135 | $lines[] = self::widget()
136 | ->items($item['items'])
137 | ->options($submenuOptions)
138 | ->submenuOptions($submenuOptions)
139 | ->encodeLabels($this->encodeLabels)
140 | ->run();
141 | $lines[] = Html::endTag('div');
142 | }
143 | }
144 |
145 | return Html::tag('div', implode("\n", $lines), $options);
146 | }
147 |
148 | /**
149 | * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
150 | * single menu with the following structure:
151 | *
152 | * - label: string, required, the label of the item link.
153 | * - encode: bool, optional, whether to HTML-encode item label.
154 | * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
155 | * If not set, the item will be treated as a menu header when the item has no sub-menu.
156 | * - visible: bool, optional, whether this menu item is visible. Defaults to true.
157 | * - linkOptions: array, optional, the HTML attributes of the item link.
158 | * - options: array, optional, the HTML attributes of the item.
159 | * - items: array, optional, the submenu items. The structure is the same as this property.
160 | * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
161 | * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
162 | * merged with {@see submenuOptions}.
163 | *
164 | * To insert divider use `-`.
165 | *
166 | * @param array $value
167 | *
168 | * @return $this
169 | */
170 | public function items(array $value): self
171 | {
172 | $this->items = $value;
173 |
174 | return $this;
175 | }
176 |
177 | /**
178 | * Whether the labels for header items should be HTML-encoded.
179 | *
180 | * @param bool $value
181 | *
182 | * @return $this
183 | */
184 | public function encodeLabels(bool $value): self
185 | {
186 | $this->encodeLabels = $value;
187 |
188 | return $this;
189 | }
190 |
191 | /**
192 | * The HTML attributes for sub-menu container tags.
193 | *
194 | * @param array $value
195 | *
196 | * @return $this
197 | */
198 | public function submenuOptions(array $value): self
199 | {
200 | $this->submenuOptions = $value;
201 |
202 | return $this;
203 | }
204 |
205 | /**
206 | * @param array $value the HTML attributes for the widget container tag. The following special options are
207 | * recognized.
208 | *
209 | * @return $this
210 | *
211 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
212 | */
213 | public function options(array $value): self
214 | {
215 | $this->options = $value;
216 |
217 | return $this;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/Carousel.php:
--------------------------------------------------------------------------------
1 | items([
24 | * // the item contains only the image
25 | * '
',
26 | * // equivalent to the above
27 | * ['content' => '
'],
28 | * // the item contains both the image and the caption
29 | * [
30 | * 'content' => '
',
31 | * 'caption' => 'This is title
This is the caption text
',
32 | * 'captionOptions' => ['class' => ['d-none', 'd-md-block']],
33 | * 'options' => [...],
34 | * ],
35 | * ]);
36 | * ```
37 | */
38 | class Carousel extends Widget
39 | {
40 | private ?array $controls = [
41 | 'Previous',
42 | 'Next',
43 | ];
44 | private bool $showIndicators = true;
45 | private array $items = [];
46 | private bool $crossfade = false;
47 | private array $options = ['data-ride' => 'carousel'];
48 |
49 | protected function run(): string
50 | {
51 | if (!isset($this->options['id'])) {
52 | $this->options['id'] = "{$this->getId()}-carousel";
53 | }
54 |
55 | Html::addCssClass($this->options, ['widget' => 'carousel', 'slide']);
56 |
57 | if ($this->crossfade) {
58 | Html::addCssClass($this->options, 'carousel-fade');
59 | }
60 |
61 | $this->registerPlugin('carousel', $this->options);
62 |
63 | return implode("\n", [
64 | Html::beginTag('div', $this->options),
65 | $this->renderIndicators(),
66 | $this->renderItems(),
67 | $this->renderControls(),
68 | Html::endTag('div'),
69 | ]) . "\n";
70 | }
71 |
72 | /**
73 | * Renders carousel indicators.
74 | */
75 | public function renderIndicators(): string
76 | {
77 | if ($this->showIndicators === false) {
78 | return '';
79 | }
80 |
81 | $indicators = [];
82 |
83 | for ($i = 0, $count = count($this->items); $i < $count; $i++) {
84 | $options = ['data-target' => '#' . $this->options['id'], 'data-slide-to' => $i];
85 | if ($i === 0) {
86 | Html::addCssClass($options, 'active');
87 | }
88 | $indicators[] = Html::tag('li', '', $options);
89 | }
90 |
91 | return Html::tag('ol', implode("\n", $indicators), ['class' => ['carousel-indicators']]);
92 | }
93 |
94 | /**
95 | * Renders carousel items as specified on {@see items}.
96 | */
97 | public function renderItems(): string
98 | {
99 | $items = [];
100 |
101 | foreach ($this->items as $i => $iValue) {
102 | $items[] = $this->renderItem($iValue, $i);
103 | }
104 |
105 | return Html::tag('div', implode("\n", $items), ['class' => 'carousel-inner']);
106 | }
107 |
108 | /**
109 | * Renders a single carousel item
110 | *
111 | * @param array|string $item a single item from {@see items}
112 | * @param int $index the item index as the first item should be set to `active`.
113 | *
114 | * @throws JsonException|RuntimeException if the item is invalid.
115 | *
116 | * @return string the rendering result.
117 | */
118 | public function renderItem($item, int $index): string
119 | {
120 | if (is_string($item)) {
121 | $content = $item;
122 | $caption = null;
123 | $options = [];
124 | } elseif (isset($item['content'])) {
125 | $content = $item['content'];
126 | $caption = ArrayHelper::getValue($item, 'caption');
127 |
128 | if ($caption !== null) {
129 | $captionOptions = ArrayHelper::remove($item, 'captionOptions', []);
130 | Html::addCssClass($captionOptions, ['widget' => 'carousel-caption']);
131 |
132 | $caption = Html::tag('div', $caption, $captionOptions);
133 | }
134 |
135 | $options = ArrayHelper::getValue($item, 'options', []);
136 | } else {
137 | throw new RuntimeException('The "content" option is required.');
138 | }
139 |
140 | Html::addCssClass($options, ['widget' => 'carousel-item']);
141 |
142 | if ($index === 0) {
143 | Html::addCssClass($options, 'active');
144 | }
145 |
146 | return Html::tag('div', $content . "\n" . $caption, $options);
147 | }
148 |
149 | /**
150 | * Renders previous and next control buttons.
151 | *
152 | * @throws JsonException|RuntimeException if {@see controls} is invalid.
153 | */
154 | public function renderControls(): ?string
155 | {
156 | if (isset($this->controls[0], $this->controls[1])) {
157 | return Html::a($this->controls[0], '#' . $this->options['id'], [
158 | 'class' => 'carousel-control-prev',
159 | 'data-slide' => 'prev',
160 | 'role' => 'button',
161 | ]) . "\n"
162 | . Html::a($this->controls[1], '#' . $this->options['id'], [
163 | 'class' => 'carousel-control-next',
164 | 'data-slide' => 'next',
165 | 'role' => 'button',
166 | ]);
167 | }
168 |
169 | if ($this->controls === null) {
170 | return '';
171 | }
172 |
173 | throw new RuntimeException(
174 | 'The "controls" property must be either null or an array of two elements.'
175 | );
176 | }
177 |
178 | /**
179 | * The labels for the previous and the next control buttons.
180 | *
181 | * If null, it means the previous and the next control buttons should not be displayed.
182 | *
183 | * @param array|null $value
184 | *
185 | * @return $this
186 | */
187 | public function controls(?array $value): self
188 | {
189 | $this->controls = $value;
190 |
191 | return $this;
192 | }
193 |
194 | /**
195 | * Animate slides with a fade transition instead of a slide. Defaults to `false`.
196 | *
197 | * @param bool $value
198 | *
199 | * @return $this
200 | */
201 | public function crossfade(bool $value): self
202 | {
203 | $this->crossfade = $value;
204 |
205 | return $this;
206 | }
207 |
208 | /**
209 | * List of slides in the carousel. Each array element represents a single slide with the following structure:
210 | *
211 | * ```php
212 | * [
213 | * // required, slide content (HTML), such as an image tag
214 | * 'content' => '
',
215 | * // optional, the caption (HTML) of the slide
216 | * 'caption' => 'This is title
This is the caption text
',
217 | * // optional the HTML attributes of the slide container
218 | * 'options' => [],
219 | * ]
220 | * ```
221 | *
222 | * @param array $value
223 | *
224 | * @return $this
225 | */
226 | public function items(array $value): self
227 | {
228 | $this->items = $value;
229 |
230 | return $this;
231 | }
232 |
233 | /**
234 | * The HTML attributes for the container tag. The following special options are recognized.
235 | *
236 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
237 | *
238 | * @param array $value
239 | *
240 | * @return $this
241 | */
242 | public function options(array $value): self
243 | {
244 | $this->options = $value;
245 |
246 | return $this;
247 | }
248 |
249 | /**
250 | * Whether carousel indicators ( tag with anchors to items) should be displayed or not.
251 | *
252 | * @param bool $value
253 | *
254 | * @return $this
255 | */
256 | public function showIndicators(bool $value): self
257 | {
258 | $this->showIndicators = $value;
259 |
260 | return $this;
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/src/ButtonDropdown.php:
--------------------------------------------------------------------------------
1 | label('Action')
20 | * ->dropdown'([
21 | * 'items' => [
22 | * ['label' => 'DropdownA', 'url' => '/'],
23 | * ['label' => 'DropdownB', 'url' => '#'],
24 | * ],
25 | * ]);
26 | * ```
27 | */
28 | class ButtonDropdown extends Widget
29 | {
30 | /**
31 | * The css class part of dropdown
32 | */
33 | public const DIRECTION_DOWN = 'down';
34 |
35 | /**
36 | * The css class part of dropleft
37 | */
38 | public const DIRECTION_LEFT = 'left';
39 |
40 | /**
41 | * The css class part of dropright
42 | */
43 | public const DIRECTION_RIGHT = 'right';
44 |
45 | /**
46 | * The css class part of dropup
47 | */
48 | public const DIRECTION_UP = 'up';
49 |
50 | private string $label = 'Button';
51 | private array $options = [];
52 | private array $buttonOptions = [];
53 | private array $dropdown = [];
54 | private string $direction = self::DIRECTION_DOWN;
55 | private bool $split = false;
56 | private string $tagName = 'button';
57 | private bool $encodeLabels = true;
58 | private string $dropdownClass = Dropdown::class;
59 | private bool $renderContainer = true;
60 |
61 | protected function run(): string
62 | {
63 | /** Set options id to button options id to ensure correct css selector in plugin initialisation */
64 | if (empty($this->options['id'])) {
65 | $id = $this->getId();
66 |
67 | $this->options['id'] = "{$id}-button-dropdown";
68 | $this->buttonOptions['id'] = "{$id}-button";
69 | }
70 |
71 | $html = $this->renderButton() . "\n" . $this->renderDropdown();
72 |
73 | if ($this->renderContainer) {
74 | Html::addCssClass($this->options, ['widget' => 'drop' . $this->direction, 'btn-group']);
75 |
76 | $options = $this->options;
77 | $tag = ArrayHelper::remove($options, 'tag', 'div');
78 | $html = Html::tag($tag, $html, $options);
79 | }
80 |
81 | $this->registerPlugin('dropdown', $this->options);
82 |
83 | return $html;
84 | }
85 |
86 | /**
87 | * Generates the button dropdown.
88 | *
89 | * @throws InvalidConfigException
90 | *
91 | * @return string the rendering result.
92 | */
93 | protected function renderButton(): string
94 | {
95 | Html::addCssClass($this->buttonOptions, ['widget' => 'btn']);
96 |
97 | $label = $this->label;
98 |
99 | if ($this->encodeLabels) {
100 | $label = Html::encode($label);
101 | }
102 |
103 | if ($this->split) {
104 | $buttonOptions = $this->buttonOptions;
105 |
106 | $this->buttonOptions['data-toggle'] = 'dropdown';
107 | $this->buttonOptions['aria-haspopup'] = 'true';
108 | $this->buttonOptions['aria-expanded'] = 'false';
109 |
110 | Html::addCssClass($this->buttonOptions, ['toggle' => 'dropdown-toggle dropdown-toggle-split']);
111 |
112 | unset($buttonOptions['id']);
113 |
114 | $splitButton = Button::widget()
115 | ->label('Toggle Dropdown')
116 | ->encodeLabels(false)
117 | ->options($this->buttonOptions)
118 | ->render();
119 | } else {
120 | $buttonOptions = $this->buttonOptions;
121 |
122 | Html::addCssClass($buttonOptions, ['toggle' => 'dropdown-toggle']);
123 |
124 | $buttonOptions['data-toggle'] = 'dropdown';
125 | $buttonOptions['aria-haspopup'] = 'true';
126 | $buttonOptions['aria-expanded'] = 'false';
127 | $splitButton = '';
128 | }
129 |
130 | if (!isset($buttonOptions['href']) && ($this->tagName === 'a')) {
131 | $buttonOptions['href'] = '#';
132 | $buttonOptions['role'] = 'button';
133 | }
134 |
135 | return Button::widget()
136 | ->tagName($this->tagName)
137 | ->label($label)
138 | ->options($buttonOptions)
139 | ->encodeLabels(false)
140 | ->render()
141 | . "\n" . $splitButton;
142 | }
143 |
144 | /**
145 | * Generates the dropdown menu.
146 | *
147 | * @return string the rendering result.
148 | */
149 | protected function renderDropdown(): string
150 | {
151 | $dropdownClass = $this->dropdownClass;
152 |
153 | return $dropdownClass::widget()
154 | ->items($this->dropdown['items'])
155 | ->render();
156 | }
157 |
158 | /**
159 | * The HTML attributes of the button.
160 | *
161 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
162 | *
163 | * @param array $value
164 | *
165 | * @return $this
166 | */
167 | public function buttonOptions(array $value): self
168 | {
169 | $this->buttonOptions = $value;
170 |
171 | return $this;
172 | }
173 |
174 | /**
175 | * The drop-direction of the widget.
176 | *
177 | * Possible values are 'left', 'right', 'up', or 'down' (default)
178 | *
179 | * @param string $value
180 | *
181 | * @return $this
182 | */
183 | public function direction(string $value): self
184 | {
185 | $this->direction = $value;
186 |
187 | return $this;
188 | }
189 |
190 | /**
191 | * The configuration array for example:
192 | *
193 | * ```php
194 | * [
195 | * 'items' => [
196 | * ['label' => 'DropdownA', 'url' => '/'],
197 | * ['label' => 'DropdownB', 'url' => '#'],
198 | * ],
199 | * ]
200 | * ```
201 | *
202 | * {@see Dropdown}
203 | *
204 | * @param array $value
205 | *
206 | * @return $this
207 | */
208 | public function dropdown(array $value): self
209 | {
210 | $this->dropdown = $value;
211 |
212 | return $this;
213 | }
214 |
215 | /**
216 | * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
217 | *
218 | * @param string $value
219 | *
220 | * @return $this
221 | */
222 | public function dropdownClass(string $value): self
223 | {
224 | $this->dropdownClass = $value;
225 |
226 | return $this;
227 | }
228 |
229 | /**
230 | * Whether the label should be HTML-encoded.
231 | *
232 | * @param bool $value
233 | *
234 | * @return $this
235 | */
236 | public function encodeLabels(bool $value): self
237 | {
238 | $this->encodeLabels = $value;
239 |
240 | return $this;
241 | }
242 |
243 | /**
244 | * The button label.
245 | *
246 | * @param string $value
247 | *
248 | * @return $this
249 | */
250 | public function label(string $value): self
251 | {
252 | $this->label = $value;
253 |
254 | return $this;
255 | }
256 |
257 | /**
258 | * The HTML attributes for the container tag. The following special options are recognized.
259 | *
260 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
261 | *
262 | * @param array $value
263 | *
264 | * @return $this
265 | */
266 | public function options(array $value): self
267 | {
268 | $this->options = $value;
269 |
270 | return $this;
271 | }
272 |
273 | /**
274 | * Whether to render the container using the {@see options} as HTML attributes. If set to `false`, the container
275 | * element enclosing the button and dropdown will NOT be rendered.
276 | *
277 | * @param bool $value
278 | *
279 | * @return $this
280 | */
281 | public function renderContainer(bool $value): self
282 | {
283 | $this->renderContainer = $value;
284 |
285 | return $this;
286 | }
287 |
288 | /**
289 | * Whether to display a group of split-styled button group.
290 | *
291 | * @param bool $value
292 | *
293 | * @return $this
294 | */
295 | public function split(bool $value): self
296 | {
297 | $this->split = $value;
298 |
299 | return $this;
300 | }
301 |
302 | /**
303 | * The tag to use to render the button.
304 | *
305 | * @param string $value
306 | *
307 | * @return $this
308 | */
309 | public function tagName(string $value): self
310 | {
311 | $this->tagName = $value;
312 |
313 | return $this;
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/Accordion.php:
--------------------------------------------------------------------------------
1 | items([
30 | * // equivalent to the above
31 | * [
32 | * 'label' => 'Collapsible Group Item #1',
33 | * 'content' => 'Anim pariatur cliche...',
34 | * // open its content by default
35 | * 'contentOptions' => ['class' => 'in'],
36 | * ],
37 | * // another group item
38 | * [
39 | * 'label' => 'Collapsible Group Item #1',
40 | * 'content' => 'Anim pariatur cliche...',
41 | * 'contentOptions' => [...],
42 | * 'options' => [...],
43 | * 'expand' => true,
44 | * ],
45 | * // if you want to swap out .card-block with .list-group, you may use the following
46 | * [
47 | * 'label' => 'Collapsible Group Item #1',
48 | * 'content' => [
49 | * 'Anim pariatur cliche...',
50 | * 'Anim pariatur cliche...',
51 | * ],
52 | * 'contentOptions' => [...],
53 | * 'options' => [...],
54 | * 'footer' => 'Footer' // the footer label in list-group,
55 | * ],
56 | * ]);
57 | * ```
58 | */
59 | class Accordion extends Widget
60 | {
61 | private array $items = [];
62 | private bool $encodeLabels = true;
63 | private bool $autoCloseItems = true;
64 | private array $itemToggleOptions = [];
65 | private array $options = [];
66 |
67 | protected function run(): string
68 | {
69 | if (!isset($this->options['id'])) {
70 | $this->options['id'] = "{$this->getId()}-accordion";
71 | }
72 |
73 | $this->registerPlugin('collapse', $this->options);
74 |
75 | Html::addCssClass($this->options, 'accordion');
76 |
77 | return implode("\n", [
78 | Html::beginTag('div', $this->options),
79 | $this->renderItems(),
80 | Html::endTag('div'),
81 | ]) . "\n";
82 | }
83 |
84 | /**
85 | * Renders collapsible items as specified on {@see items}.
86 | *
87 | * @throws RuntimeException|JsonException|
88 | *
89 | * @return string the rendering result.
90 | */
91 | public function renderItems(): string
92 | {
93 | $items = [];
94 | $index = 0;
95 | $expanded = array_search(true, array_column($this->items, 'expand'), true);
96 |
97 | foreach ($this->items as $key => $item) {
98 | if (!is_array($item)) {
99 | $item = ['content' => $item];
100 | }
101 |
102 | if ($expanded === false && $index === 0) {
103 | $item['expand'] = true;
104 | }
105 |
106 | if (!array_key_exists('label', $item)) {
107 | if (is_int($key)) {
108 | throw new RuntimeException('The "label" option is required.');
109 | }
110 |
111 | $item['label'] = $key;
112 | }
113 |
114 | $header = ArrayHelper::remove($item, 'label');
115 | $options = ArrayHelper::getValue($item, 'options', []);
116 |
117 | Html::addCssClass($options, ['panel' => 'card']);
118 |
119 | $items[] = Html::tag('div', $this->renderItem($header, $item, $index++), $options);
120 | }
121 |
122 | return implode("\n", $items);
123 | }
124 |
125 | /**
126 | * Renders a single collapsible item group.
127 | *
128 | * @param string $header a label of the item group {@see items}
129 | * @param array $item a single item from {@see items}
130 | * @param int $index the item index as each item group content must have an id.
131 | *
132 | * @throws JsonException|RuntimeException
133 | *
134 | * @return string the rendering result
135 | */
136 | public function renderItem(string $header, array $item, int $index): string
137 | {
138 | if (array_key_exists('content', $item)) {
139 | $id = $this->options['id'] . '-collapse' . $index;
140 | $expand = ArrayHelper::remove($item, 'expand', false);
141 | $options = ArrayHelper::getValue($item, 'contentOptions', []);
142 | $options['id'] = $id;
143 |
144 | Html::addCssClass($options, ['widget' => 'collapse']);
145 |
146 | if ($expand) {
147 | Html::addCssClass($options, 'show');
148 | }
149 |
150 | if (!isset($options['aria-label'], $options['aria-labelledby'])) {
151 | $options['aria-labelledby'] = $options['id'] . '-heading';
152 | }
153 |
154 | $encodeLabel = $item['encode'] ?? $this->encodeLabels;
155 |
156 | if ($encodeLabel) {
157 | $header = Html::encode($header);
158 | }
159 |
160 | $itemToggleOptions = array_merge([
161 | 'tag' => 'button',
162 | 'type' => 'button',
163 | 'data-toggle' => 'collapse',
164 | 'data-target' => '#' . $options['id'],
165 | 'aria-expanded' => $expand ? 'true' : 'false',
166 | 'aria-controls' => $options['id'],
167 | ], $this->itemToggleOptions);
168 | $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
169 |
170 | /** @psalm-suppress ConflictingReferenceConstraint */
171 | if ($itemToggleTag === 'a') {
172 | ArrayHelper::remove($itemToggleOptions, 'data-target');
173 | $headerToggle = Html::a($header, '#' . $id, $itemToggleOptions) . "\n";
174 | } else {
175 | Html::addCssClass($itemToggleOptions, 'btn-link');
176 | $headerToggle = Button::widget()
177 | ->label($header)
178 | ->encodeLabels(false)
179 | ->options($itemToggleOptions)
180 | ->render() . "\n";
181 | }
182 |
183 | $header = Html::tag('h5', $headerToggle, ['class' => 'mb-0']);
184 |
185 | if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
186 | $content = Html::tag('div', $item['content'], ['class' => 'card-body']) . "\n";
187 | } elseif (is_array($item['content'])) {
188 | $content = Html::ul($item['content'], [
189 | 'class' => 'list-group',
190 | 'itemOptions' => [
191 | 'class' => 'list-group-item',
192 | ],
193 | 'encode' => false,
194 | ]) . "\n";
195 | } else {
196 | throw new RuntimeException('The "content" option should be a string, array or object.');
197 | }
198 | } else {
199 | throw new RuntimeException('The "content" option is required.');
200 | }
201 |
202 | $group = [];
203 |
204 | if ($this->autoCloseItems) {
205 | $options['data-parent'] = '#' . $this->options['id'];
206 | }
207 |
208 | $group[] = Html::tag('div', $header, ['class' => 'card-header', 'id' => $options['id'] . '-heading']);
209 | $group[] = Html::beginTag('div', $options);
210 | $group[] = $content;
211 |
212 | if (isset($item['footer'])) {
213 | $group[] = Html::tag('div', $item['footer'], ['class' => 'card-footer']);
214 | }
215 |
216 | $group[] = Html::endTag('div');
217 |
218 | return implode("\n", $group);
219 | }
220 |
221 | /**
222 | * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect.
223 | *
224 | * Set this to `false` to allow keeping multiple items open at once.
225 | *
226 | * @param bool $value
227 | *
228 | * @return $this
229 | */
230 | public function autoCloseItems(bool $value): self
231 | {
232 | $this->autoCloseItems = $value;
233 |
234 | return $this;
235 | }
236 |
237 | /**
238 | * Whether the labels for header items should be HTML-encoded.
239 | *
240 | * @param bool $value
241 | *
242 | * @return $this
243 | */
244 | public function encodeLabels(bool $value): self
245 | {
246 | $this->encodeLabels = $value;
247 |
248 | return $this;
249 | }
250 |
251 | /**
252 | * List of groups in the collapse widget. Each array element represents a single group with the following structure:
253 | *
254 | * - label: string, required, the group header label.
255 | * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global
256 | * `$this->encodeLabels` param.
257 | * - content: array|string|object, required, the content (HTML) of the group
258 | * - options: array, optional, the HTML attributes of the group
259 | * - contentOptions: optional, the HTML attributes of the group's content
260 | *
261 | * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers
262 | * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained
263 | * above.
264 | *
265 | * For example:
266 | *
267 | * ```php
268 | * echo Accordion::widget([
269 | * 'items' => [
270 | * 'Introduction' => 'This is the first collapsible menu',
271 | * 'Second panel' => [
272 | * 'content' => 'This is the second collapsible menu',
273 | * ],
274 | * [
275 | * 'label' => 'Third panel',
276 | * 'content' => 'This is the third collapsible menu',
277 | * ],
278 | * ]
279 | * ])
280 | * ```
281 | *
282 | * @param array $value
283 | *
284 | * @return $this
285 | */
286 | public function items(array $value): self
287 | {
288 | $this->items = $value;
289 |
290 | return $this;
291 | }
292 |
293 | /**
294 | * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
295 | *
296 | * For example:
297 | *
298 | * ```php
299 | * [
300 | * 'tag' => 'div',
301 | * 'class' => 'custom-toggle',
302 | * ]
303 | * ```
304 | *
305 | * @param array $value
306 | *
307 | * @return $this
308 | */
309 | public function itemToggleOptions(array $value): self
310 | {
311 | $this->itemToggleOptions = $value;
312 |
313 | return $this;
314 | }
315 |
316 | /**
317 | * The HTML attributes for the widget container tag. The following special options are recognized.
318 | *
319 | * @param array $value
320 | *
321 | * @return $this
322 | *
323 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
324 | */
325 | public function options(array $value): self
326 | {
327 | $this->options = $value;
328 |
329 | return $this;
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/src/NavBar.php:
--------------------------------------------------------------------------------
1 | getId() !== null) {
19 | * $menuItems = [
20 | * [
21 | * 'label' => 'About',
22 | * 'url' => '/about',
23 | * ],
24 | * [
25 | * 'label' => 'Contact',
26 | * 'url' => '/contact',
27 | * ],
28 | * [
29 | * 'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
30 | * 'url' => '/logout'
31 | * ],
32 | * ];
33 | * } else {
34 | * $menuItems = [
35 | * [
36 | * 'label' => 'About',
37 | * 'url' => '/about',
38 | * ],
39 | * [
40 | * 'label' => 'Contact',
41 | * 'url' => '/contact',
42 | * ],
43 | * [
44 | * 'label' => 'Login',
45 | * 'url' => '/login',
46 | * ],
47 | * ];
48 | * }
49 | *
50 | * brandLabel('My Application Basic')
52 | * ->brandUrl('/')
53 | * ->options([
54 | * 'class' => 'navbar navbar-dark bg-dark navbar-expand-lg text-white',
55 | * ])
56 | * ->begin();
57 | *
58 | * echo Nav::widget()
59 | * ->currentPath($currentPath)
60 | * ->items($menuItems)
61 | * ->options([
62 | * 'class' => 'navbar-nav float-right ml-auto'
63 | * ]);
64 | *
65 | * echo NavBar::end(); ?>
66 | * ```
67 | * Note: $currentPath it must be injected from each controller to the main controller.
68 | *
69 | * SiteController.php
70 | *
71 | * ```php
72 | *
73 | * public function index(ServerRequestInterface $request): ResponseInterface
74 | * {
75 | * $response = $this->responseFactory->createResponse();
76 | * $currentPath = $request->getUri()->getPath();
77 | * $output = $this->render('index', ['currentPath' => $currentPath]);
78 | * $response->getBody()->write($output);
79 | *
80 | * return $response;
81 | * }
82 | * ```
83 | *
84 | * Controller.php
85 | *
86 | * ```php
87 | * private function renderContent($content, array $parameters = []): string
88 | * {
89 | * $user = $this->user->getIdentity();
90 | * $layout = $this->findLayoutFile($this->layout);
91 | *
92 | * if ($layout !== null) {
93 | * return $this->view->renderFile(
94 | * $layout,
95 | * [
96 | * 'aliases' => $this->aliases,
97 | * 'content' => $content,
98 | * 'user' => $user,
99 | * 'params' => $this->params,
100 | * 'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
101 | * ],
102 | * $this
103 | * );
104 | * }
105 | *
106 | * return $content;
107 | * }
108 | * ```
109 | */
110 | class NavBar extends Widget
111 | {
112 | private array $collapseOptions = [];
113 | private ?string $brandLabel = null;
114 | private ?string $brandImage = null;
115 | private ?string $brandUrl = '/';
116 | private array $brandOptions = [];
117 | private string $screenReaderToggleText = 'Toggle navigation';
118 | private string $togglerContent = '';
119 | private array $togglerOptions = [];
120 | private bool $renderInnerContainer = true;
121 | private array $innerContainerOptions = [];
122 | private array $options = [];
123 |
124 | public function begin(): ?string
125 | {
126 | parent::begin();
127 |
128 | if (!isset($this->options['id'])) {
129 | $id = $this->getId();
130 | $this->options['id'] = "{$id}-navbar";
131 | $this->collapseOptions['id'] = "{$id}-collapse";
132 | }
133 |
134 | if (empty($this->options['class'])) {
135 | Html::addCssClass($this->options, ['widget' => 'navbar', 'navbar-expand-lg', 'navbar-light', 'bg-light']);
136 | } else {
137 | Html::addCssClass($this->options, ['widget' => 'navbar']);
138 | }
139 |
140 | $navOptions = $this->options;
141 | $navTag = ArrayHelper::remove($navOptions, 'tag', 'nav');
142 | $brand = '';
143 |
144 | if (!isset($this->innerContainerOptions['class'])) {
145 | Html::addCssClass($this->innerContainerOptions, 'container');
146 | }
147 |
148 | if ($this->brandImage !== null) {
149 | $this->brandLabel = Html::img($this->brandImage);
150 | }
151 |
152 | if ($this->brandLabel !== null) {
153 | Html::addCssClass($this->brandOptions, ['widget' => 'navbar-brand']);
154 | if (empty($this->brandUrl)) {
155 | $brand = Html::tag('span', $this->brandLabel, $this->brandOptions);
156 | } else {
157 | $brand = Html::a(
158 | $this->brandLabel,
159 | $this->brandUrl,
160 | $this->brandOptions
161 | );
162 | }
163 | }
164 |
165 | Html::addCssClass($this->collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']);
166 | $collapseOptions = $this->collapseOptions;
167 | $collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div');
168 |
169 | $htmlStart = Html::beginTag($navTag, $navOptions) . "\n";
170 |
171 | if ($this->renderInnerContainer) {
172 | $htmlStart .= Html::beginTag('div', $this->innerContainerOptions) . "\n";
173 | }
174 |
175 | $htmlStart .= $brand . "\n";
176 | $htmlStart .= $this->renderToggleButton() . "\n";
177 |
178 | $htmlStart .= Html::beginTag($collapseTag, $collapseOptions) . "\n";
179 |
180 | return $htmlStart;
181 | }
182 |
183 | protected function run(): string
184 | {
185 | $tag = ArrayHelper::remove($this->collapseOptions, 'tag', 'div');
186 |
187 | $htmlRun = Html::endTag($tag) . "\n";
188 |
189 | if ($this->renderInnerContainer) {
190 | $htmlRun .= Html::endTag('div') . "\n";
191 | }
192 |
193 | $tag = ArrayHelper::remove($this->options, 'tag', 'nav');
194 |
195 | $htmlRun .= Html::endTag($tag);
196 |
197 | return $htmlRun;
198 | }
199 |
200 | /**
201 | * Renders collapsible toggle button.
202 | *
203 | * @throws JsonException
204 | *
205 | * @return string the rendering toggle button.
206 | */
207 | protected function renderToggleButton(): string
208 | {
209 | $options = $this->togglerOptions;
210 |
211 | Html::addCssClass($options, ['widget' => 'navbar-toggler']);
212 |
213 | return Html::button(
214 | $this->togglerContent,
215 | ArrayHelper::merge($options, [
216 | 'type' => 'button',
217 | 'data' => [
218 | 'toggle' => 'collapse',
219 | 'target' => '#' . $this->collapseOptions['id'],
220 | ],
221 | 'aria-controls' => $this->collapseOptions['id'],
222 | 'aria-expanded' => 'false',
223 | 'aria-label' => $this->screenReaderToggleText,
224 | ])
225 | );
226 | }
227 |
228 | /**
229 | * The HTML attributes for the container tag. The following special options are recognized.
230 | *
231 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
232 | *
233 | * @param array $value
234 | *
235 | * @return $this
236 | */
237 | public function collapseOptions(array $value): self
238 | {
239 | $this->collapseOptions = $value;
240 |
241 | return $this;
242 | }
243 |
244 | /**
245 | * The text of the brand or empty if it's not used. Note that this is not HTML-encoded.
246 | *
247 | * @param string|null $value
248 | *
249 | * @return $this
250 | *
251 | * {@see https://getbootstrap.com/docs/4.2/components/navbar/}
252 | */
253 | public function brandLabel(?string $value): self
254 | {
255 | $this->brandLabel = $value;
256 |
257 | return $this;
258 | }
259 |
260 | /**
261 | * Src of the brand image or empty if it's not used. Note that this param will override `$this->brandLabel` param.
262 | *
263 | * @param string|null $value
264 | *
265 | * @return $this
266 | *
267 | * {@see https://getbootstrap.com/docs/4.2/components/navbar/}
268 | */
269 | public function brandImage(?string $value): self
270 | {
271 | $this->brandImage = $value;
272 |
273 | return $this;
274 | }
275 |
276 | /**
277 | * The URL for the brand's hyperlink tag and will be used for the "href" attribute of the brand link. Default value
278 | * is '/' will be used. You may set it to `null` if you want to have no link at all.
279 | *
280 | * @param string|null $value
281 | *
282 | * @return $this
283 | */
284 | public function brandUrl(?string $value): self
285 | {
286 | $this->brandUrl = $value;
287 |
288 | return $this;
289 | }
290 |
291 | /**
292 | * The HTML attributes of the brand link.
293 | *
294 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
295 | *
296 | * @param array $value
297 | *
298 | * @return $this
299 | */
300 | public function brandOptions(array $value): self
301 | {
302 | $this->brandOptions = $value;
303 |
304 | return $this;
305 | }
306 |
307 | /**
308 | * Text to show for screen readers for the button to toggle the navbar.
309 | *
310 | * @param string $value
311 | *
312 | * @return $this
313 | */
314 | public function screenReaderToggleText(string $value): self
315 | {
316 | $this->screenReaderToggleText = $value;
317 |
318 | return $this;
319 | }
320 |
321 | /**
322 | * The toggle button content. Defaults to bootstrap 4 default ``.
323 | *
324 | * @param string $value
325 | *
326 | * @return $this
327 | */
328 | public function togglerContent(string $value): self
329 | {
330 | $this->togglerContent = $value;
331 |
332 | return $this;
333 | }
334 |
335 | /**
336 | * The HTML attributes of the navbar toggler button.
337 | *
338 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
339 | *
340 | * @param array $value
341 | *
342 | * @return $this
343 | */
344 | public function togglerOptions(array $value): self
345 | {
346 | $this->togglerOptions = $value;
347 |
348 | return $this;
349 | }
350 |
351 | /**
352 | * Whether the navbar content should be included in an inner div container which by default adds left and right
353 | * padding. Set this to false for a 100% width navbar.
354 | *
355 | * @param bool $value
356 | *
357 | * @return $this
358 | */
359 | public function renderInnerContainer(bool $value): self
360 | {
361 | $this->renderInnerContainer = $value;
362 |
363 | return $this;
364 | }
365 |
366 | /**
367 | * The HTML attributes of the inner container.
368 | *
369 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
370 | *
371 | * @param array $value
372 | *
373 | * @return $this
374 | */
375 | public function innerContainerOptions(array $value): self
376 | {
377 | $this->innerContainerOptions = $value;
378 |
379 | return $this;
380 | }
381 |
382 | /**
383 | * The HTML attributes for the widget container tag. The following special options are recognized.
384 | *
385 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
386 | *
387 | * @param array $value
388 | *
389 | * @return $this
390 | */
391 | public function options(array $value): self
392 | {
393 | $this->options = $value;
394 |
395 | return $this;
396 | }
397 | }
398 |
--------------------------------------------------------------------------------
/src/Modal.php:
--------------------------------------------------------------------------------
1 | title('Hello world
')
22 | * ->toggleButton(['label' => 'click me'])
23 | * ->begin();
24 | *
25 | * echo 'Say hello...';
26 | *
27 | * echo Modal::end();
28 | * ```
29 | */
30 | class Modal extends Widget
31 | {
32 | /**
33 | * The additional css class of large modal
34 | */
35 | public const SIZE_LARGE = 'modal-lg';
36 |
37 | /**
38 | * The additional css class of small modal
39 | */
40 | public const SIZE_SMALL = 'modal-sm';
41 |
42 | /**
43 | * The additional css class of default modal
44 | */
45 | public const SIZE_DEFAULT = '';
46 |
47 | private ?string $title = null;
48 | private array $titleOptions = [];
49 | private array $headerOptions = [];
50 | private array $bodyOptions = [];
51 | private ?string $footer = null;
52 | private array $footerOptions = [];
53 | private ?string $size = null;
54 | private array $closeButton = [];
55 | private bool $closeButtonEnabled = true;
56 | private array $toggleButton = [];
57 | private bool $toggleButtonEnabled = true;
58 | private array $options = [];
59 |
60 | public function begin(): ?string
61 | {
62 | parent::begin();
63 |
64 | if (!isset($this->options['id'])) {
65 | $this->options['id'] = "{$this->getId()}-modal";
66 | }
67 |
68 | $this->initOptions();
69 |
70 | return
71 | $this->renderToggleButton() . "\n" .
72 | Html::beginTag('div', $this->options) . "\n" .
73 | Html::beginTag('div', ['class' => 'modal-dialog ' . $this->size]) . "\n" .
74 | Html::beginTag('div', ['class' => 'modal-content']) . "\n" .
75 | $this->renderHeader() . "\n" .
76 | $this->renderBodyBegin() . "\n";
77 | }
78 |
79 | protected function run(): string
80 | {
81 | $this->registerPlugin('modal', $this->options);
82 |
83 | return
84 | "\n" . $this->renderBodyEnd() .
85 | "\n" . $this->renderFooter() .
86 | "\n" . Html::endTag('div') . // modal-content
87 | "\n" . Html::endTag('div') . // modal-dialog
88 | "\n" . Html::endTag('div');
89 | }
90 |
91 | /**
92 | * Renders the header HTML markup of the modal.
93 | *
94 | * @throws JsonException
95 | *
96 | * @return string the rendering result
97 | */
98 | protected function renderHeader(): string
99 | {
100 | $button = $this->renderCloseButton();
101 |
102 | if ($this->title !== null) {
103 | Html::addCssClass($this->titleOptions, ['widget' => 'modal-title']);
104 | $header = Html::tag('h5', $this->title, $this->titleOptions);
105 | } else {
106 | $header = '';
107 | }
108 |
109 | if ($button !== null) {
110 | $header .= "\n" . $button;
111 | } elseif ($header === '') {
112 | return '';
113 | }
114 |
115 | Html::addCssClass($this->headerOptions, ['widget' => 'modal-header']);
116 |
117 | return Html::tag('div', "\n" . $header . "\n", $this->headerOptions);
118 | }
119 |
120 | /**
121 | * Renders the opening tag of the modal body.
122 | *
123 | * @throws JsonException
124 | *
125 | * @return string the rendering result
126 | */
127 | protected function renderBodyBegin(): string
128 | {
129 | Html::addCssClass($this->bodyOptions, ['widget' => 'modal-body']);
130 |
131 | return Html::beginTag('div', $this->bodyOptions);
132 | }
133 |
134 | /**
135 | * Renders the closing tag of the modal body.
136 | *
137 | * @return string the rendering result
138 | */
139 | protected function renderBodyEnd(): string
140 | {
141 | return Html::endTag('div');
142 | }
143 |
144 | /**
145 | * Renders the HTML markup for the footer of the modal.
146 | *
147 | * @throws JsonException
148 | *
149 | * @return string the rendering result
150 | */
151 | protected function renderFooter(): ?string
152 | {
153 | if ($this->footer === null) {
154 | return null;
155 | }
156 |
157 | Html::addCssClass($this->footerOptions, ['widget' => 'modal-footer']);
158 | return Html::tag('div', "\n" . $this->footer . "\n", $this->footerOptions);
159 | }
160 |
161 | /**
162 | * Renders the toggle button.
163 | *
164 | * @throws JsonException
165 | *
166 | * @return string the rendering result
167 | */
168 | protected function renderToggleButton(): ?string
169 | {
170 | if ($this->toggleButtonEnabled === false) {
171 | return null;
172 | }
173 |
174 | $tag = ArrayHelper::remove($this->toggleButton, 'tag', 'button');
175 | $label = ArrayHelper::remove($this->toggleButton, 'label', 'Show');
176 |
177 | return Html::tag($tag, $label, $this->toggleButton);
178 | }
179 |
180 | /**
181 | * Renders the close button.
182 | *
183 | * @throws JsonException
184 | *
185 | * @return string the rendering result.
186 | */
187 | protected function renderCloseButton(): ?string
188 | {
189 | if ($this->closeButtonEnabled === false) {
190 | return null;
191 | }
192 |
193 | $tag = ArrayHelper::remove($this->closeButton, 'tag', 'button');
194 | $label = ArrayHelper::remove($this->closeButton, 'label', Html::tag('span', '×', [
195 | 'aria-hidden' => 'true',
196 | ]));
197 |
198 | return Html::tag($tag, $label, $this->closeButton);
199 | }
200 |
201 | /**
202 | * Initializes the widget options.
203 | *
204 | * This method sets the default values for various options.
205 | */
206 | protected function initOptions(): void
207 | {
208 | $this->options = array_merge([
209 | 'class' => 'fade',
210 | 'role' => 'dialog',
211 | 'tabindex' => -1,
212 | 'aria-hidden' => 'true',
213 | ], $this->options);
214 |
215 | Html::addCssClass($this->options, ['widget' => 'modal']);
216 |
217 | if ($this->getEnableClientOptions() !== false) {
218 | $this->clientOptions(array_merge(['show' => false], $this->getClientOptions()));
219 | }
220 |
221 | $this->titleOptions = array_merge([
222 | 'id' => $this->options['id'] . '-label',
223 | ], $this->titleOptions);
224 |
225 | if (!isset($this->options['aria-label'], $this->options['aria-labelledby']) && $this->title !== null) {
226 | $this->options['aria-labelledby'] = $this->titleOptions['id'];
227 | }
228 |
229 | if ($this->closeButtonEnabled !== false) {
230 | $this->closeButton = array_merge([
231 | 'data-dismiss' => 'modal',
232 | 'class' => 'close',
233 | 'type' => 'button',
234 | ], $this->closeButton);
235 | }
236 |
237 | if ($this->toggleButton !== []) {
238 | $this->toggleButton = array_merge([
239 | 'data-toggle' => 'modal',
240 | 'type' => 'button',
241 | ], $this->toggleButton);
242 | if (!isset($this->toggleButton['data-target']) && !isset($this->toggleButton['href'])) {
243 | $this->toggleButton['data-target'] = '#' . $this->options['id'];
244 | }
245 | }
246 | }
247 |
248 | /**
249 | * Body options.
250 | *
251 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
252 | *
253 | * @param array $value
254 | *
255 | * @return $this
256 | */
257 | public function bodyOptions(array $value): self
258 | {
259 | $this->bodyOptions = $value;
260 |
261 | return $this;
262 | }
263 |
264 | /**
265 | * The options for rendering the close button tag.
266 | *
267 | * The close button is displayed in the header of the modal window. Clicking on the button will hide the modal
268 | * window. If {@see closeButtonEnabled} is false, no close button will be rendered.
269 | *
270 | * The following special options are supported:
271 | *
272 | * - tag: string, the tag name of the button. Defaults to 'button'.
273 | * - label: string, the label of the button. Defaults to '×'.
274 | *
275 | * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
276 | * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
277 | *
278 | * @param array $value
279 | *
280 | * @return $this
281 | */
282 | public function closeButton(array $value): self
283 | {
284 | $this->closeButton = $value;
285 |
286 | return $this;
287 | }
288 |
289 | /**
290 | * Enable/Disable close button.
291 | *
292 | * @param bool $value
293 | *
294 | * @return $this
295 | */
296 | public function closeButtonEnabled(bool $value): self
297 | {
298 | $this->closeButtonEnabled = $value;
299 |
300 | return $this;
301 | }
302 |
303 | /**
304 | * The footer content in the modal window.
305 | *
306 | * @param string|null $value
307 | *
308 | * @return $this
309 | */
310 | public function footer(?string $value): self
311 | {
312 | $this->footer = $value;
313 |
314 | return $this;
315 | }
316 |
317 | /**
318 | * Additional footer options.
319 | *
320 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
321 | */
322 | public function footerOptions(array $value): self
323 | {
324 | $this->footerOptions = $value;
325 |
326 | return $this;
327 | }
328 |
329 | /**
330 | * Additional header options.
331 | *
332 | * @param array $value
333 | *
334 | * @return $this
335 | *
336 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
337 | */
338 | public function headerOptions(array $value): self
339 | {
340 | $this->headerOptions = $value;
341 |
342 | return $this;
343 | }
344 |
345 | /**
346 | * @param array $value the HTML attributes for the widget container tag. The following special options are
347 | * recognized.
348 | *
349 | * @return $this
350 | *
351 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
352 | */
353 | public function options(array $value): self
354 | {
355 | $this->options = $value;
356 |
357 | return $this;
358 | }
359 |
360 | /**
361 | * The title content in the modal window.
362 | *
363 | * @param string|null $value
364 | *
365 | * @return $this
366 | */
367 | public function title(?string $value): self
368 | {
369 | $this->title = $value;
370 |
371 | return $this;
372 | }
373 |
374 | /**
375 | * Additional title options.
376 | *
377 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
378 | *
379 | * @param array $value
380 | *
381 | * @return $this
382 | */
383 | public function titleOptions(array $value): self
384 | {
385 | $this->titleOptions = $value;
386 |
387 | return $this;
388 | }
389 |
390 | /**
391 | * The options for rendering the toggle button tag.
392 | *
393 | * The toggle button is used to toggle the visibility of the modal window. If {@see toggleButtonEnabled} is false,
394 | * no toggle button will be rendered.
395 | *
396 | * The following special options are supported:
397 | *
398 | * - tag: string, the tag name of the button. Defaults to 'button'.
399 | * - label: string, the label of the button. Defaults to 'Show'.
400 | *
401 | * The rest of the options will be rendered as the HTML attributes of the button tag. Please refer to the
402 | * [Modal plugin help](http://getbootstrap.com/javascript/#modals) for the supported HTML attributes.
403 | *
404 | * @param array $value
405 | *
406 | * @return $this
407 | */
408 | public function toggleButton(array $value): self
409 | {
410 | $this->toggleButton = $value;
411 |
412 | return $this;
413 | }
414 |
415 | /**
416 | * Enable/Disable toggle button.
417 | *
418 | * @param bool $value
419 | *
420 | * @return $this
421 | */
422 | public function toggleButtonEnabled(bool $value): self
423 | {
424 | $this->toggleButtonEnabled = $value;
425 |
426 | return $this;
427 | }
428 |
429 | /**
430 | * The modal size. Can be {@see SIZE_LARGE} or {@see SIZE_SMALL}, or null for default.
431 | *
432 | * @param string|null $value
433 | *
434 | * @return $this
435 | */
436 | public function size(?string $value): self
437 | {
438 | $this->size = $value;
439 |
440 | return $this;
441 | }
442 | }
443 |
--------------------------------------------------------------------------------
/src/Tabs.php:
--------------------------------------------------------------------------------
1 | items([
24 | * [
25 | * 'label' => 'One',
26 | * 'content' => 'Anim pariatur cliche...',
27 | * 'active' => true,
28 | * ],
29 | * [
30 | * 'label' => 'Two',
31 | * 'content' => 'Anim pariatur cliche...',
32 | * 'headerOptions' => [...],
33 | * 'options' => ['id' => 'myveryownID'],
34 | * ],
35 | * [
36 | * 'label' => 'Example',
37 | * 'url' => 'http://www.example.com',
38 | * ],
39 | * [
40 | * 'label' => 'Dropdown',
41 | * 'items' => [
42 | * [
43 | * 'label' => 'DropdownA',
44 | * 'content' => 'DropdownA, Anim pariatur cliche...',
45 | * ],
46 | * [
47 | * 'label' => 'DropdownB',
48 | * 'content' => 'DropdownB, Anim pariatur cliche...',
49 | * ],
50 | * [
51 | * 'label' => 'External Link',
52 | * 'url' => 'http://www.example.com',
53 | * ],
54 | * ],
55 | * ],
56 | * ]);
57 | * ```
58 | */
59 | class Tabs extends Widget
60 | {
61 | private array $panes = [];
62 | private array $items = [];
63 | private array $itemOptions = [];
64 | private array $headerOptions = [];
65 | private array $linkOptions = [];
66 | private bool $encodeLabels = true;
67 | private string $navType = 'nav-tabs';
68 | private bool $renderTabContent = true;
69 | private array $tabContentOptions = [];
70 | private string $dropdownClass = Dropdown::class;
71 | private array $options = [];
72 |
73 | protected function run(): string
74 | {
75 | if (!isset($this->options['id'])) {
76 | $this->options['id'] = "{$this->getId()}-tabs";
77 | }
78 |
79 | Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]);
80 | Html::addCssClass($this->tabContentOptions, 'tab-content');
81 |
82 | $this->registerPlugin('tab', $this->options);
83 | $this->prepareItems($this->items);
84 |
85 | return Nav::widget()
86 | ->dropdownClass($this->dropdownClass)
87 | ->options(ArrayHelper::merge(['role' => 'tablist'], $this->options))
88 | ->items($this->items)
89 | ->encodeLabels($this->encodeLabels)
90 | ->render()
91 | . $this->renderPanes($this->panes);
92 | }
93 |
94 | /**
95 | * Renders tab items as specified on {@see items}.
96 | *
97 | * @param array $items
98 | * @param string $prefix
99 | *
100 | * @throws JsonException|RuntimeException
101 | */
102 | protected function prepareItems(array &$items, string $prefix = ''): void
103 | {
104 | if (!$this->hasActiveTab()) {
105 | $this->activateFirstVisibleTab();
106 | }
107 |
108 | foreach ($items as $n => $item) {
109 | $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
110 | $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n);
111 |
112 | /** {@see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339} */
113 | unset($items[$n]['options']['id']);
114 |
115 | if (!ArrayHelper::remove($item, 'visible', true)) {
116 | continue;
117 | }
118 |
119 | if (!array_key_exists('label', $item)) {
120 | throw new RuntimeException('The "label" option is required.');
121 | }
122 |
123 | $selected = ArrayHelper::getValue($item, 'active', false);
124 | $disabled = ArrayHelper::getValue($item, 'disabled', false);
125 | $headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions);
126 |
127 | if (isset($item['items'])) {
128 | $this->prepareItems($items[$n]['items'], '-dd' . $n);
129 | continue;
130 | }
131 |
132 | ArrayHelper::setValue($items[$n], 'options', $headerOptions);
133 |
134 | if (isset($item['url'])) {
135 | continue;
136 | }
137 |
138 | ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
139 | ArrayHelper::setValueByPath($items[$n], 'linkOptions.data.toggle', 'tab');
140 | ArrayHelper::setValueByPath($items[$n], 'linkOptions.role', 'tab');
141 | ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-controls', $options['id']);
142 |
143 | if (!$disabled) {
144 | ArrayHelper::setValueByPath($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
145 | }
146 |
147 | Html::addCssClass($options, ['widget' => 'tab-pane']);
148 |
149 | if ($selected) {
150 | Html::addCssClass($options, 'active');
151 | }
152 |
153 | if ($this->renderTabContent) {
154 | $tag = ArrayHelper::remove($options, 'tag', 'div');
155 | $this->panes[] = Html::tag($tag, $item['content'] ?? '', $options);
156 | }
157 | }
158 | }
159 |
160 | /**
161 | * @return bool if there's active tab defined.
162 | */
163 | protected function hasActiveTab(): bool
164 | {
165 | foreach ($this->items as $item) {
166 | if (isset($item['active']) && $item['active'] === true) {
167 | return true;
168 | }
169 | }
170 |
171 | return false;
172 | }
173 |
174 | /**
175 | * Sets the first visible tab as active.
176 | *
177 | * This method activates the first tab that is visible and not explicitly set to inactive (`'active' => false`).
178 | */
179 | protected function activateFirstVisibleTab(): void
180 | {
181 | foreach ($this->items as $i => $item) {
182 | $active = ArrayHelper::getValue($item, 'active', null);
183 | $visible = ArrayHelper::getValue($item, 'visible', true);
184 | $disabled = ArrayHelper::getValue($item, 'disabled', false);
185 |
186 | if ($visible && $active !== false && ($disabled !== true)) {
187 | $this->items[$i]['active'] = true;
188 | return;
189 | }
190 | }
191 | }
192 |
193 | /**
194 | * Renders tab panes.
195 | *
196 | * @param array $panes
197 | *
198 | * @throws JsonException
199 | *
200 | * @return string the rendering result.
201 | */
202 | public function renderPanes(array $panes): string
203 | {
204 | return $this->renderTabContent ? ("\n" . Html::tag('div', implode("\n", $panes), $this->tabContentOptions)) : '';
205 | }
206 |
207 | /**
208 | * Name of a class to use for rendering dropdowns withing this widget. Defaults to {@see Dropdown}.
209 | *
210 | * @param string $value
211 | *
212 | * @return $this
213 | */
214 | public function dropdownClass(string $value): self
215 | {
216 | $this->dropdownClass = $value;
217 |
218 | return $this;
219 | }
220 |
221 | /**
222 | * Whether the labels for header items should be HTML-encoded.
223 | *
224 | * @param bool $value
225 | *
226 | * @return $this
227 | */
228 | public function encodeLabels(bool $value): self
229 | {
230 | $this->encodeLabels = $value;
231 |
232 | return $this;
233 | }
234 |
235 | /**
236 | * List of HTML attributes for the header container tags. This will be overwritten by the "headerOptions" set in
237 | * individual {@see items}.
238 | *
239 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
240 | *
241 | * @param array $value
242 | *
243 | * @return $this
244 | */
245 | public function headerOptions(array $value): self
246 | {
247 | $this->headerOptions = $value;
248 |
249 | return $this;
250 | }
251 |
252 | /**
253 | * List of tabs in the tabs widget. Each array element represents a single tab with the following structure:
254 | *
255 | * - label: string, required, the tab header label.
256 | * - encode: bool, optional, whether this label should be HTML-encoded. This param will override
257 | * global `$this->encodeLabels` param.
258 | * - headerOptions: array, optional, the HTML attributes of the tab header.
259 | * - linkOptions: array, optional, the HTML attributes of the tab header link tags.
260 | * - content: string, optional, the content (HTML) of the tab pane.
261 | * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
262 | * the browser to this URL.
263 | * - options: array, optional, the HTML attributes of the tab pane container.
264 | * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
265 | * 'active' explicitly - the first one will be activated.
266 | * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
267 | * - items: array, optional, can be used instead of `content` to specify a dropdown items
268 | * configuration array. Each item can hold three extra keys, besides the above ones:
269 | * * active: bool, optional, whether the item tab header and pane should be visible or not.
270 | * * content: string, required if `items` is not set. The content (HTML) of the tab pane.
271 | * * contentOptions: optional, array, the HTML attributes of the tab content container.
272 | *
273 | * @param array $value
274 | *
275 | * @return $this
276 | */
277 | public function items(array $value): self
278 | {
279 | $this->items = $value;
280 |
281 | return $this;
282 | }
283 |
284 | /**
285 | * List of HTML attributes for the item container tags. This will be overwritten by the "options" set in individual
286 | * {@see items}. The following special options are recognized.
287 | *
288 | * @param array $value
289 | *
290 | * @return $this
291 | *
292 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
293 | */
294 | public function itemOptions(array $value): self
295 | {
296 | $this->itemOptions = $value;
297 |
298 | return $this;
299 | }
300 |
301 | /**
302 | * List of HTML attributes for the tab header link tags. This will be overwritten by the "linkOptions" set in
303 | * individual {@see items}.
304 | *
305 | * @param array $value
306 | *
307 | * @return $this
308 | *
309 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
310 | */
311 | public function linkOptions(array $value): self
312 | {
313 | $this->linkOptions = $value;
314 |
315 | return $this;
316 | }
317 |
318 | /**
319 | * Specifies the Bootstrap tab styling.
320 | *
321 | * @param string $value
322 | *
323 | * @return $this
324 | */
325 | public function navType(string $value): self
326 | {
327 | $this->navType = $value;
328 |
329 | return $this;
330 | }
331 |
332 | /**
333 | * The HTML attributes for the widget container tag. The following special options are recognized.
334 | *
335 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
336 | *
337 | * @param array $value
338 | *
339 | * @return $this
340 | */
341 | public function options(array $value): self
342 | {
343 | $this->options = $value;
344 |
345 | return $this;
346 | }
347 |
348 | /**
349 | * Tab panes (contents).
350 | *
351 | * @param array $value
352 | *
353 | * @return $this
354 | */
355 | public function panes(array $value): self
356 | {
357 | $this->panes = $value;
358 |
359 | return $this;
360 | }
361 |
362 | /**
363 | * Whether to render the `tab-content` container and its content. You may set this property to be false so that you
364 | * can manually render `tab-content` yourself in case your tab contents are complex.
365 | *
366 | * @param bool $value
367 | *
368 | * @return $this
369 | */
370 | public function renderTabContent(bool $value): self
371 | {
372 | $this->renderTabContent = $value;
373 |
374 | return $this;
375 | }
376 |
377 | /**
378 | * List of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
379 | *
380 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
381 | *
382 | * @param array $value
383 | *
384 | * @return $this
385 | */
386 | public function tabContentOptions(array $value): self
387 | {
388 | $this->tabContentOptions = $value;
389 |
390 | return $this;
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/src/Nav.php:
--------------------------------------------------------------------------------
1 | getId() !== null) {
22 | * $menuItems = [
23 | * [
24 | * 'label' => 'About',
25 | * 'url' => '/about',
26 | * ],
27 | * [
28 | * 'label' => 'Contact',
29 | * 'url' => '/contact',
30 | * ],
31 | * [
32 | * 'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
33 | * 'url' => '/logout'
34 | * ],
35 | * ];
36 | * } else {
37 | * $menuItems = [
38 | * [
39 | * 'label' => 'About',
40 | * 'url' => '/about',
41 | * ],
42 | * [
43 | * 'label' => 'Contact',
44 | * 'url' => '/contact',
45 | * ],
46 | * [
47 | * 'label' => 'Login',
48 | * 'url' => '/login',
49 | * ],
50 | * ];
51 | * }
52 | *
53 | * echo Nav::widget()
54 | * ->currentPath($currentPath)
55 | * ->items($menuItems)
56 | * ->options([
57 | * 'class' => 'navbar-nav float-right ml-auto'
58 | * ]);
59 | *
60 | * Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 3.
61 | * Note: $currentPath it must be injected from each controller to the main controller.
62 | *
63 | * SiteController.php
64 | *
65 | * ```php
66 | *
67 | * public function index(ServerRequestInterface $request): ResponseInterface
68 | * {
69 | * $response = $this->responseFactory->createResponse();
70 | * $currentPath = $request->getUri()->getPath();
71 | * $output = $this->render('index', ['currentPath' => $currentPath]);
72 | * $response->getBody()->write($output);
73 | *
74 | * return $response;
75 | * }
76 | * ```
77 | *
78 | * Controller.php
79 | *
80 | * ```php
81 | * private function renderContent($content, array $parameters = []): string
82 | * {
83 | * $user = $this->user->getIdentity();
84 | * $layout = $this->findLayoutFile($this->layout);
85 | *
86 | * if ($layout !== null) {
87 | * return $this->view->renderFile(
88 | * $layout,
89 | * [
90 | * 'aliases' => $this->aliases,
91 | * 'content' => $content,
92 | * 'user' => $user,
93 | * 'params' => $this->params,
94 | * 'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
95 | * ],
96 | * $this
97 | * );
98 | * }
99 | *
100 | * return $content;
101 | * }
102 | * ```
103 | *
104 | * {@see http://getbootstrap.com/components/#dropdowns}
105 | * {@see http://getbootstrap.com/components/#nav}
106 | */
107 | class Nav extends Widget
108 | {
109 | private string $label = '';
110 | private array $items = [];
111 | private bool $encodeLabels = true;
112 | private bool $activateItems = true;
113 | private bool $activateParents = false;
114 | private ?string $currentPath = null;
115 | private array $params = [];
116 | private string $dropdownClass = Dropdown::class;
117 | private array $options = [];
118 |
119 | protected function run(): string
120 | {
121 | if (!isset($this->options['id'])) {
122 | $this->options['id'] = "{$this->getId()}-nav";
123 | }
124 |
125 | Html::addCssClass($this->options, ['widget' => 'nav']);
126 |
127 | return $this->renderItems();
128 | }
129 |
130 | /**
131 | * Renders widget items.
132 | *
133 | * @throws JsonException|RuntimeException
134 | *
135 | * @return string
136 | */
137 | public function renderItems(): string
138 | {
139 | $items = [];
140 |
141 | foreach ($this->items as $i => $item) {
142 | if (isset($item['visible']) && !$item['visible']) {
143 | continue;
144 | }
145 |
146 | $items[] = $this->renderItem($item);
147 | }
148 |
149 | return Html::tag('ul', implode("\n", $items), $this->options);
150 | }
151 |
152 | /**
153 | * Renders a widget's item.
154 | *
155 | * @param array|string $item the item to render.
156 | *
157 | * @throws JsonException|RuntimeException
158 | *
159 | * @return string the rendering result.
160 | */
161 | public function renderItem($item): string
162 | {
163 | if (is_string($item)) {
164 | return $item;
165 | }
166 |
167 | if (!isset($item['label'])) {
168 | throw new RuntimeException('The "label" option is required.');
169 | }
170 |
171 | $encodeLabel = $item['encode'] ?? $this->encodeLabels;
172 | $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
173 | $options = ArrayHelper::getValue($item, 'options', []);
174 | $items = ArrayHelper::getValue($item, 'items');
175 | $url = ArrayHelper::getValue($item, 'url', '#');
176 | $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
177 | $disabled = ArrayHelper::getValue($item, 'disabled', false);
178 | $active = $this->isItemActive($item);
179 |
180 | if (empty($items)) {
181 | $items = '';
182 | } else {
183 | $linkOptions['data-toggle'] = 'dropdown';
184 |
185 | Html::addCssClass($options, ['widget' => 'dropdown']);
186 | Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle']);
187 |
188 | if (is_array($items)) {
189 | $items = $this->isChildActive($items, $active);
190 | $items = $this->renderDropdown($items, $item);
191 | }
192 | }
193 |
194 | Html::addCssClass($options, 'nav-item');
195 | Html::addCssClass($linkOptions, 'nav-link');
196 |
197 | if ($disabled) {
198 | ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
199 | ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
200 | Html::addCssClass($linkOptions, 'disabled');
201 | } elseif ($this->activateItems && $active) {
202 | Html::addCssClass($linkOptions, 'active');
203 | }
204 |
205 | return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options);
206 | }
207 |
208 | /**
209 | * Renders the given items as a dropdown.
210 | *
211 | * This method is called to create sub-menus.
212 | *
213 | * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
214 | * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
215 | * array.
216 | *
217 | * @return string the rendering result.
218 | */
219 | protected function renderDropdown(array $items, array $parentItem): string
220 | {
221 | $dropdownClass = $this->dropdownClass;
222 |
223 | return $dropdownClass::widget()
224 | ->enableClientOptions(false)
225 | ->encodeLabels($this->encodeLabels)
226 | ->items($items)
227 | ->options(ArrayHelper::getValue($parentItem, 'dropdownOptions', []))
228 | ->render();
229 | }
230 |
231 | /**
232 | * Check to see if a child item is active optionally activating the parent.
233 | *
234 | * @param array $items
235 | * @param bool $active should the parent be active too
236 | *
237 | * @return array
238 | *
239 | * {@see items}
240 | */
241 | protected function isChildActive(array $items, bool &$active): array
242 | {
243 | foreach ($items as $i => $child) {
244 | if (is_array($child) && !ArrayHelper::getValue($child, 'visible', true)) {
245 | continue;
246 | }
247 |
248 | if ($this->isItemActive($child)) {
249 | ArrayHelper::setValue($items[$i], 'active', true);
250 | if ($this->activateParents) {
251 | $active = true;
252 | }
253 | }
254 |
255 | if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
256 | $activeParent = false;
257 | $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
258 |
259 | if ($activeParent) {
260 | $items[$i]['options'] ??= [];
261 | Html::addCssClass($items[$i]['options'], 'active');
262 | $active = true;
263 | }
264 | }
265 | }
266 |
267 | return $items;
268 | }
269 |
270 | /**
271 | * Checks whether a menu item is active.
272 | *
273 | * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
274 | * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
275 | * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
276 | * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
277 | *
278 | * @param array|string $item the menu item to be checked
279 | *
280 | * @return bool whether the menu item is active
281 | */
282 | protected function isItemActive($item): bool
283 | {
284 | if (isset($item['active'])) {
285 | return ArrayHelper::getValue($item, 'active', false);
286 | }
287 |
288 | return isset($item['url']) && $this->currentPath !== '/' && $item['url'] === $this->currentPath && $this->activateItems;
289 | }
290 |
291 | /**
292 | * List of items in the nav widget. Each array element represents a single menu item which can be either a string
293 | * or an array with the following structure:
294 | *
295 | * - label: string, required, the nav item label.
296 | * - url: optional, the item's URL. Defaults to "#".
297 | * - visible: bool, optional, whether this menu item is visible. Defaults to true.
298 | * - linkOptions: array, optional, the HTML attributes of the item's link.
299 | * - options: array, optional, the HTML attributes of the item container (LI).
300 | * - active: bool, optional, whether the item should be on active state or not.
301 | * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
302 | * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
303 | * representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
304 | * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
305 | * only this item.
306 | *
307 | * If a menu item is a string, it will be rendered directly without HTML encoding.
308 | *
309 | * @param array $value
310 | *
311 | * @return $this
312 | */
313 | public function items(array $value): self
314 | {
315 | $this->items = $value;
316 |
317 | return $this;
318 | }
319 |
320 | /**
321 | * Whether the nav items labels should be HTML-encoded.
322 | *
323 | * @param bool $value
324 | *
325 | * @return $this
326 | */
327 | public function encodeLabels(bool $value): self
328 | {
329 | $this->encodeLabels = $value;
330 |
331 | return $this;
332 | }
333 |
334 | public function label(string $value): self
335 | {
336 | $this->label = $value;
337 |
338 | return $this;
339 | }
340 |
341 | /**
342 | * Whether to automatically activate items according to whether their currentPath matches the currently requested.
343 | *
344 | * @param bool $value
345 | *
346 | * @return $this
347 | *
348 | * {@see isItemActive}
349 | */
350 | public function activateItems(bool $value): self
351 | {
352 | $this->activateItems = $value;
353 |
354 | return $this;
355 | }
356 |
357 | /**
358 | * Whether to activate parent menu items when one of the corresponding child menu items is active.
359 | *
360 | * @param bool $value
361 | *
362 | * @return $this
363 | */
364 | public function activateParents(bool $value): self
365 | {
366 | $this->activateParents = $value;
367 |
368 | return $this;
369 | }
370 |
371 | /**
372 | * Allows you to assign the current path of the url from request controller.
373 | *
374 | * @param string|null $value
375 | *
376 | * @return $this
377 | */
378 | public function currentPath(?string $value): self
379 | {
380 | $this->currentPath = $value;
381 |
382 | return $this;
383 | }
384 |
385 | /**
386 | * The parameters used to determine if a menu item is active or not. If not set, it will use `$_GET`.
387 | *
388 | * @param array $value
389 | *
390 | * @return $this
391 | *
392 | * {@see currentPath}
393 | * {@see isItemActive}
394 | */
395 | public function params(array $value): self
396 | {
397 | $this->params = $value;
398 |
399 | return $this;
400 | }
401 |
402 | /**
403 | * Name of a class to use for rendering dropdowns within this widget. Defaults to {@see Dropdown}.
404 | *
405 | * @param string $value
406 | *
407 | * @return $this
408 | */
409 | public function dropdownClass(string $value): self
410 | {
411 | $this->dropdownClass = $value;
412 |
413 | return $this;
414 | }
415 |
416 | /**
417 | * The HTML attributes for the widget container tag. The following special options are recognized.
418 | *
419 | * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
420 | *
421 | * @param array $value
422 | *
423 | * @return $this
424 | */
425 | public function options(array $value): self
426 | {
427 | $this->options = $value;
428 |
429 | return $this;
430 | }
431 | }
432 |
--------------------------------------------------------------------------------