Init. v1.0.0

This commit is contained in:
Mylistryx 2021-02-10 00:04:59 +03:00
commit c92be88adf
29 changed files with 4607 additions and 0 deletions

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# phpstorm project files
.idea
# netbeans project files
nbproject
# zend studio for eclipse project files
.buildpath
.project
.settings
# windows thumbnail cache
Thumbs.db
# composer
composer.phar
/vendor
/composer.lock
# Mac DS_Store Files
.DS_Store
# phpunit itself is not needed
phpunit.phar
# local phpunit config
/phpunit.xml
/tests/bootstrap.local.php
/tests/runtime
/tests/data/config.local.php
/tests/docker
/tests/dockerids
# nodejs
/node_modules
/package-lock.json
.phpunit.result.cache

49
composer.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "mylistryx/yii2-bootstrap5",
"description": "The Twitter Bootstrap v5 extension for the Yii framework",
"version": "1.0.0",
"keywords": [
"yii2",
"bootstrap",
"bootstrap5"
],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
"source": "https://github.com/mylistryx/yii2-bootstrap45"
},
"authors": [
{
"name": "Sergey Zhukovskiy",
"email": "mylistryx@gmail.com",
"homepage": "https://net23.ru/"
}
],
"minimum-stability": "dev",
"require": {
"php": "~7.4.0",
"yiisoft/yii2": "~2.0",
"npm-asset/bootstrap": "^5.0.0",
"ext-json": "*"
},
"require-dev": {
"yiisoft/yii2-coding-standards": "~2.0",
"cweagans/composer-patches": "^1.7"
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
],
"autoload": {
"psr-4": {
"yii\\bootstrap5\\": "src"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}

248
src/Accordion.php Normal file
View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Accordion renders an accordion bootstrap javascript component.
*
* For example:
*
* ```php
* echo Accordion::widget([
* 'items' => [
* // equivalent to the above
* [
* 'label' => 'Collapsible Group Item #1',
* 'content' => 'Anim pariatur cliche...',
* // open its content by default
* 'contentOptions' => ['class' => 'in']
* ],
* // another group item
* [
* 'label' => 'Collapsible Group Item #1',
* 'content' => 'Anim pariatur cliche...',
* 'contentOptions' => [...],
* 'options' => [...],
* 'expand' => true,
* ],
* // if you want to swap out .card-block with .list-group, you may use the following
* [
* 'label' => 'Collapsible Group Item #1',
* 'content' => [
* 'Anim pariatur cliche...',
* 'Anim pariatur cliche...'
* ],
* 'contentOptions' => [...],
* 'options' => [...],
* 'footer' => 'Footer' // the footer label in list-group
* ],
* ]
* ]);
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/collapse/#accordion-example
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Accordion extends Widget
{
/**
* @var array list of groups in the collapse widget. Each array element represents a single
* group with the following structure:
*
* - label: string, required, the group header label.
* - encode: bool, optional, whether this label should be HTML-encoded. This param will override
* global `$this->encodeLabels` param.
* - content: array|string|object, required, the content (HTML) of the group
* - options: array, optional, the HTML attributes of the group
* - contentOptions: optional, the HTML attributes of the group's content
*
* Since version 2.0.7 you may also specify this property as key-value pairs, where the key refers to the
* `label` and the value refers to `content`. If value is a string it is interpreted as label. If it is
* an array, it is interpreted as explained above.
*
* For example:
*
* ```php
* echo Accordion::widget([
* 'items' => [
* 'Introduction' => 'This is the first collapsable menu',
* 'Second panel' => [
* 'content' => 'This is the second collapsable menu',
* ],
* [
* 'label' => 'Third panel',
* 'content' => 'This is the third collapsable menu',
* ],
* ]
* ])
* ```
*/
public array $items = [];
/**
* @var bool whether the labels for header items should be HTML-encoded.
*/
public bool $encodeLabels = true;
/**
* @var bool whether to close other items if an item is opened. Defaults to `true` which causes an
* accordion effect. Set this to `false` to allow keeping multiple items open at once.
*/
public bool $autoCloseItems = true;
/**
* @var array the HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
* For example:
*
* ```php
* [
* 'tag' => 'div',
* 'class' => 'custom-toggle',
* ]
* ```
*
*/
public array $itemToggleOptions = [];
/**
* @return string
* @throws InvalidConfigException
*/
public function run(): string
{
$this->registerPlugin('collapse');
Html::addCssClass($this->options, ['widget' => 'accordion']);
return implode("\n", [
Html::beginTag('div', $this->options),
$this->renderItems(),
Html::endTag('div'),
]) . "\n";
}
/**
* Renders collapsible items as specified on [[items]].
* @throws InvalidConfigException if label isn't specified
* @return string the rendering result
*/
public function renderItems(): string
{
$items = [];
$index = 0;
$expanded = array_search(true, ArrayHelper::getColumn(ArrayHelper::toArray($this->items), 'expand', true));
foreach ($this->items as $key => $item) {
if (!is_array($item)) {
$item = ['content' => $item];
}
// BC compatibility: expand first item if none is expanded
if ($expanded === false && $index === 0) {
$item['expand'] = true;
}
if (!array_key_exists('label', $item)) {
if (is_int($key)) {
throw new InvalidConfigException("The 'label' option is required.");
} else {
$item['label'] = $key;
}
}
$header = ArrayHelper::remove($item, 'label');
$options = ArrayHelper::getValue($item, 'options', []);
Html::addCssClass($options, ['panel' => 'card']);
$items[] = Html::tag('div', $this->renderItem($header, $item, $index++), $options);
}
return implode("\n", $items);
}
/**
* Renders a single collapsible item group
* @param string $header a label of the item group [[items]]
* @param array $item a single item from [[items]]
* @param int $index the item index as each item group content must have an id
* @return string the rendering result
* @throws InvalidConfigException
* @throws \Exception
*/
public function renderItem(string $header, array $item, int $index): string
{
if (array_key_exists('content', $item)) {
$id = $this->options['id'] . '-collapse' . $index;
$expand = ArrayHelper::remove($item, 'expand', false);
$options = ArrayHelper::getValue($item, 'contentOptions', []);
$options['id'] = $id;
Html::addCssClass($options, ['widget' => 'collapse']);
// check if accordion expanded, if true add show class
if ($expand) {
Html::addCssClass($options, ['visibility' => 'show']);
}
if (!isset($options['aria-label'], $options['aria-labelledby'])) {
$options['aria-labelledby'] = $options['id'] . '-heading';
}
$encodeLabel = $item['encode'] ?? $this->encodeLabels;
if ($encodeLabel) {
$header = Html::encode($header);
}
$itemToggleOptions = array_merge([
'tag' => 'button',
'type' => 'button',
'data-toggle' => 'collapse',
'data-target' => '#' . $options['id'],
'aria-expanded' => $expand ? 'true' : 'false',
'aria-controls' => $options['id'],
], $this->itemToggleOptions);
$itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
if ($itemToggleTag === 'a') {
ArrayHelper::remove($itemToggleOptions, 'data-target');
$headerToggle = Html::a($header, '#' . $id, $itemToggleOptions) . "\n";
} else {
Html::addCssClass($itemToggleOptions, ['feature' => 'btn-link']);
$headerToggle = Button::widget([
'label' => $header,
'encodeLabel' => false,
'options' => $itemToggleOptions,
]) . "\n";
}
$header = Html::tag('h5', $headerToggle, ['class' => 'mb-0']);
if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
$content = Html::tag('div', $item['content'], ['class' => 'card-body']) . "\n";
} elseif (is_array($item['content'])) {
$content = Html::ul($item['content'], [
'class' => 'list-group',
'itemOptions' => [
'class' => 'list-group-item',
],
'encode' => false,
]) . "\n";
} else {
throw new InvalidConfigException('The "content" option should be a string, array or object.');
}
} else {
throw new InvalidConfigException('The "content" option is required.');
}
$group = [];
if ($this->autoCloseItems) {
$options['data-parent'] = '#' . $this->options['id'];
}
$group[] = Html::tag('div', $header, ['class' => 'card-header', 'id' => $options['id'] . '-heading']);
$group[] = Html::beginTag('div', $options);
$group[] = $content;
if (isset($item['footer'])) {
$group[] = Html::tag('div', $item['footer'], ['class' => 'card-footer']);
}
$group[] = Html::endTag('div');
return implode("\n", $group);
}
}

558
src/ActiveField.php Normal file
View File

@ -0,0 +1,558 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\helpers\ArrayHelper;
/**
* A Bootstrap 5 enhanced version of [[\yii\widgets\ActiveField]].
*
* This class adds some useful features to [[\yii\widgets\ActiveField|ActiveField]] to render all
* sorts of Bootstrap 5 form fields in different form layouts:
*
* - [[inputTemplate]] is an optional template to render complex inputs, for example input groups
* - [[horizontalCssClasses]] defines the CSS grid classes to add to label, wrapper, error and hint
* in horizontal forms
* - [[inline]]/[[inline()]] is used to render inline [[checkboxList()]] and [[radioList()]]
* - [[enableError]] can be set to `false` to disable to the error
* - [[enableLabel]] can be set to `false` to disable to the label
* - [[label()]] can be used with a `bool` argument to enable/disable the label
*
* There are also some new placeholders that you can use in the [[template]] configuration:
*
* - `{beginLabel}`: the opening label tag
* - `{labelTitle}`: the label title for use with `{beginLabel}`/`{endLabel}`
* - `{endLabel}`: the closing label tag
* - `{beginWrapper}`: the opening wrapper tag
* - `{endWrapper}`: the closing wrapper tag
*
* The wrapper tag is only used for some layouts and form elements.
*
* Note that some elements use slightly different defaults for [[template]] and other options.
* You may want to override those predefined templates for checkboxes, radio buttons, checkboxLists
* and radioLists in the [[\yii\widgets\ActiveForm::fieldConfig|fieldConfig]] of the
* [[\yii\widgets\ActiveForm]]:
*
* - [[checkTemplate]] the default template for checkboxes and radios
* - [[radioTemplate]] the template for radio buttons in default layout
* - [[checkHorizontalTemplate]] the template for checkboxes in horizontal layout
* - [[radioHorizontalTemplate]] the template for radio buttons in horizontal layout
* - [[checkEnclosedTemplate]] the template for checkboxes and radios enclosed by label
*
* Example:
*
* ```php
* use yii\bootstrap5\ActiveForm;
*
* $form = ActiveForm::begin(['layout' => 'horizontal']);
*
* // Form field without label
* echo $form->field($model, 'demo', [
* 'inputOptions' => [
* 'placeholder' => $model->getAttributeLabel('demo'),
* ],
* ])->label(false);
*
* // Inline radio list
* echo $form->field($model, 'demo')->inline()->radioList($items);
*
* // Control sizing in horizontal mode
* echo $form->field($model, 'demo', [
* 'horizontalCssClasses' => [
* 'wrapper' => 'col-sm-2',
* ]
* ]);
*
* // With 'default' layout you would use 'template' to size a specific field:
* echo $form->field($model, 'demo', [
* 'template' => '{label} <div class="row"><div class="col-sm-4">{input}{error}{hint}</div></div>'
* ]);
*
* // Input group
* echo $form->field($model, 'demo', [
* 'inputTemplate' => '<div class="input-group"><div class="input-group-prepend">
* <span class="input-group-text">@</span>
* </div>{input}</div>',
* ]);
*
* ActiveForm::end();
* ```
*
* @property-read ActiveForm $form
*
* @see ActiveForm
* @see https://getbootstrap.com/docs/5.0/components/forms/
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class ActiveField extends \yii\widgets\ActiveField
{
/**
* @var bool whether to render [[checkboxList()]] and [[radioList()]] inline.
*/
public bool $inline = false;
/**
* @var string|null optional template to render the `{input}` placeholder content
*/
public ?string $inputTemplate = null;
/**
* @var array options for the wrapper tag, used in the `{beginWrapper}` placeholder
*/
public array $wrapperOptions = [];
/**
* {@inheritdoc}
*/
public $options = ['class' => ['widget' => 'form-group']];
/**
* {@inheritdoc}
*/
public $inputOptions = ['class' => ['widget' => 'form-control']];
/**
* @var array the default options for the input checkboxes. The parameter passed to individual
* input methods (e.g. [[checkbox()]]) will be merged with this property when rendering the input tag.
*
* If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
* @since 2.0.7
*/
public array $checkOptions = [
'class' => ['widget' => 'custom-control-input'],
'labelOptions' => [
'class' => ['widget' => 'custom-control-label'],
],
];
/**
* @var array the default options for the input radios. The parameter passed to individual
* input methods (e.g. [[radio()]]) will be merged with this property when rendering the input tag.
*
* If you set a custom `id` for the input element, you may need to adjust the [[$selectors]] accordingly.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
* @since 2.0.7
*/
public array $radioOptions = [
'class' => ['widget' => 'custom-control-input'],
'labelOptions' => [
'class' => ['widget' => 'custom-control-label'],
],
];
/**
* {@inheritdoc}
*/
public $errorOptions = ['class' => 'invalid-feedback'];
/**
* {@inheritdoc}
*/
public $labelOptions = [];
/**
* {@inheritdoc}
*/
public $hintOptions = ['class' => ['widget' => 'form-text', 'text-muted'], 'tag' => 'small'];
/**
* @var null|array CSS grid classes for horizontal layout. This must be an array with these keys:
* - 'offset' the offset grid class to append to the wrapper if no label is rendered
* - 'label' the label grid class
* - 'wrapper' the wrapper grid class
* - 'error' the error grid class
* - 'hint' the hint grid class
*/
public ?array $horizontalCssClasses = [];
/**
* @var string the template for checkboxes in default layout
*/
public string $checkTemplate = "<div class=\"custom-control custom-checkbox\">\n{input}\n{label}\n{error}\n{hint}\n</div>";
/**
* @var string the template for radios in default layout
* @since 2.0.5
*/
public string $radioTemplate = "<div class=\"custom-control custom-radio\">\n{input}\n{label}\n{error}\n{hint}\n</div>";
/**
* @var string the template for checkboxes and radios in horizontal layout
*/
public string $checkHorizontalTemplate = "{beginWrapper}\n<div class=\"custom-control custom-checkbox\">\n{input}\n{label}\n{error}\n{hint}\n</div>\n{endWrapper}";
/**
* @var string the template for checkboxes and radios in horizontal layout
* @since 2.0.5
*/
public string $radioHorizontalTemplate = "{beginWrapper}\n<div class=\"custom-control custom-radio\">\n{input}\n{label}\n{error}\n{hint}\n</div>\n{endWrapper}";
/**
* @var string the `enclosed by label` template for checkboxes and radios in default layout
*/
public string $checkEnclosedTemplate = "<div class=\"form-check\">\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n</div>";
/**
* @var bool whether to render the error. Default is `true` except for layout `inline`.
*/
public bool $enableError = true;
/**
* @var bool whether to render the label. Default is `true`.
*/
public bool $enableLabel = true;
/**
* {@inheritdoc}
*/
public function __construct($config = [])
{
$layoutConfig = $this->createLayoutConfig($config);
$config = ArrayHelper::merge($layoutConfig, $config);
parent::__construct($config);
}
/**
* {@inheritdoc}
*/
public function render($content = null): string
{
if ($content === null) {
if (!isset($this->parts['{beginWrapper}'])) {
$options = $this->wrapperOptions;
$tag = ArrayHelper::remove($options, 'tag', 'div');
$this->parts['{beginWrapper}'] = Html::beginTag($tag, $options);
$this->parts['{endWrapper}'] = Html::endTag($tag);
}
if ($this->enableLabel === false) {
$this->parts['{label}'] = '';
$this->parts['{beginLabel}'] = '';
$this->parts['{labelTitle}'] = '';
$this->parts['{endLabel}'] = '';
} elseif (!isset($this->parts['{beginLabel}'])) {
$this->renderLabelParts();
}
if ($this->enableError === false) {
$this->parts['{error}'] = '';
}
if ($this->inputTemplate) {
$options = $this->inputOptions;
if ($this->form->validationStateOn === ActiveForm::VALIDATION_STATE_ON_INPUT) {
$this->addErrorClassIfNeeded($options);
}
$this->addAriaAttributes($options);
$input = $this->parts['{input}'] ?? Html::activeTextInput($this->model, $this->attribute, $options);
$this->parts['{input}'] = strtr($this->inputTemplate, ['{input}' => $input]);
}
}
return parent::render($content);
}
/**
* {@inheritdoc}
*/
public function checkbox($options = [], $enclosedByLabel = false)
{
$checkOptions = $this->checkOptions;
$options = ArrayHelper::merge($checkOptions, $options);
Html::removeCssClass($options, 'form-control');
$labelOptions = ArrayHelper::remove($options, 'labelOptions', []);
$wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', []);
$this->labelOptions = ArrayHelper::merge($this->labelOptions, $labelOptions);
$this->wrapperOptions = ArrayHelper::merge($this->wrapperOptions, $wrapperOptions);
if (!isset($options['template'])) {
$this->template = ($enclosedByLabel) ? $this->checkEnclosedTemplate : $this->checkTemplate;
} else {
$this->template = $options['template'];
}
if ($this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) {
if (!isset($options['template'])) {
$this->template = $this->checkHorizontalTemplate;
}
Html::removeCssClass($this->labelOptions, $this->horizontalCssClasses['label']);
Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']);
}
unset($options['template']);
if ($enclosedByLabel) {
if (isset($options['label'])) {
$this->parts['{labelTitle}'] = $options['label'];
}
}
return parent::checkbox($options, false);
}
/**
* {@inheritdoc}
*/
public function radio($options = [], $enclosedByLabel = false)
{
$checkOptions = $this->radioOptions;
$options = ArrayHelper::merge($checkOptions, $options);
Html::removeCssClass($options, 'form-control');
$labelOptions = ArrayHelper::remove($options, 'labelOptions', []);
$wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', []);
$this->labelOptions = ArrayHelper::merge($this->labelOptions, $labelOptions);
$this->wrapperOptions = ArrayHelper::merge($this->wrapperOptions, $wrapperOptions);
if (!isset($options['template'])) {
$this->template = $enclosedByLabel ? $this->checkEnclosedTemplate : $this->radioTemplate;
} else {
$this->template = $options['template'];
}
if ($this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) {
if (!isset($options['template'])) {
$this->template = $this->radioHorizontalTemplate;
}
Html::removeCssClass($this->labelOptions, $this->horizontalCssClasses['label']);
Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']);
}
unset($options['template']);
if ($enclosedByLabel && isset($options['label'])) {
$this->parts['{labelTitle}'] = $options['label'];
}
return parent::radio($options, false);
}
/**
* {@inheritdoc}
*/
public function checkboxList($items, $options = [])
{
if (!isset($options['item'])) {
$this->template = str_replace("\n{error}", '', $this->template);
$itemOptions = $options['itemOptions'] ?? [];
$encode = ArrayHelper::getValue($options, 'encode', true);
$itemCount = count($items) - 1;
$error = $this->error()->parts['{error}'];
$options['item'] = function ($i, $label, $name, $checked, $value) use (
$itemOptions,
$encode,
$itemCount,
$error
) {
$options = array_merge($this->checkOptions, [
'label' => $encode ? Html::encode($label) : $label,
'value' => $value,
], $itemOptions);
$wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', ['class' => ['custom-control', 'custom-checkbox']]);
if ($this->inline) {
Html::addCssClass($wrapperOptions, 'custom-control-inline');
}
$html = Html::beginTag('div', $wrapperOptions) . "\n" .
Html::checkbox($name, $checked, $options) . "\n";
if ($itemCount === $i) {
$html .= $error . "\n";
}
$html .= Html::endTag('div') . "\n";
return $html;
};
}
parent::checkboxList($items, $options);
return $this;
}
/**
* {@inheritdoc}
*/
public function radioList($items, $options = [])
{
if (!isset($options['item'])) {
$this->template = str_replace("\n{error}", '', $this->template);
$itemOptions = $options['itemOptions'] ?? [];
$encode = ArrayHelper::getValue($options, 'encode', true);
$itemCount = count($items) - 1;
$error = $this->error()->parts['{error}'];
$options['item'] = function ($i, $label, $name, $checked, $value) use (
$itemOptions,
$encode,
$itemCount,
$error
) {
$options = array_merge($this->radioOptions, [
'label' => $encode ? Html::encode($label) : $label,
'value' => $value,
], $itemOptions);
$wrapperOptions = ArrayHelper::remove($options, 'wrapperOptions', ['class' => ['custom-control', 'custom-radio']]);
if ($this->inline) {
Html::addCssClass($wrapperOptions, 'custom-control-inline');
}
$html = Html::beginTag('div', $wrapperOptions) . "\n" .
Html::radio($name, $checked, $options) . "\n";
if ($itemCount === $i) {
$html .= $error . "\n";
}
$html .= Html::endTag('div') . "\n";
return $html;
};
}
parent::radioList($items, $options);
return $this;
}
/**
* {@inheritdoc}
*/
public function listBox($items, $options = [])
{
if ($this->form->layout === ActiveForm::LAYOUT_INLINE) {
Html::removeCssClass($this->labelOptions, 'sr-only');
}
return parent::listBox($items, $options);
}
/**
* {@inheritdoc}
*/
public function dropdownList($items, $options = [])
{
if ($this->form->layout === ActiveForm::LAYOUT_INLINE) {
Html::removeCssClass($this->labelOptions, 'sr-only');
}
return parent::dropdownList($items, $options);
}
/**
* Renders Bootstrap static form control.
* @param array $options the tag options in terms of name-value pairs. These will be rendered as
* the attributes of the resulting tag. There are also a special options:
*
* - encode: bool, whether value should be HTML-encoded or not.
*
* @return $this the field object itself
* @see https://getbootstrap.com/docs/5.0/components/forms/#readonly-plain-text
*/
public function staticControl($options = []): self
{
$this->adjustLabelFor($options);
$this->parts['{input}'] = Html::activeStaticControl($this->model, $this->attribute, $options);
return $this;
}
/**
* {@inheritdoc}
*/
public function label($label = null, $options = [])
{
if (is_bool($label)) {
$this->enableLabel = $label;
if ($label === false && $this->form->layout === ActiveForm::LAYOUT_HORIZONTAL) {
Html::addCssClass($this->wrapperOptions, $this->horizontalCssClasses['offset']);
}
} else {
$this->enableLabel = true;
$this->renderLabelParts($label, $options);
parent::label($label, $options);
}
return $this;
}
/**
* @param bool $value whether to render a inline list
* @return $this the field object itself
* Make sure you call this method before [[checkboxList()]] or [[radioList()]] to have any effect.
*/
public function inline($value = true): self
{
$this->inline = (bool)$value;
return $this;
}
/**
* @param array $instanceConfig the configuration passed to this instance's constructor
* @return array the layout specific default configuration for this instance
*/
protected function createLayoutConfig(array $instanceConfig): array
{
$config = [
'hintOptions' => [
'tag' => 'small',
'class' => ['form-text', 'text-muted'],
],
'errorOptions' => [
'tag' => 'div',
'class' => 'invalid-feedback',
],
'inputOptions' => [
'class' => 'form-control',
],
'labelOptions' => [
'class' => [],
],
];
$layout = $instanceConfig['form']->layout;
if ($layout === ActiveForm::LAYOUT_HORIZONTAL) {
$config['template'] = "{label}\n{beginWrapper}\n{input}\n{error}\n{hint}\n{endWrapper}";
$config['wrapperOptions'] = [];
$config['labelOptions'] = [];
$config['options'] = [];
$cssClasses = [
'offset' => ['col-sm-10', 'offset-sm-2'],
'label' => ['col-sm-2', 'col-form-label'],
'wrapper' => 'col-sm-10',
'error' => '',
'hint' => '',
'field' => 'form-group row',
];
if (isset($instanceConfig['horizontalCssClasses'])) {
$cssClasses = ArrayHelper::merge($cssClasses, $instanceConfig['horizontalCssClasses']);
}
$config['horizontalCssClasses'] = $cssClasses;
Html::addCssClass($config['wrapperOptions'], $cssClasses['wrapper']);
Html::addCssClass($config['labelOptions'], $cssClasses['label']);
Html::addCssClass($config['errorOptions'], $cssClasses['error']);
Html::addCssClass($config['hintOptions'], $cssClasses['hint']);
Html::addCssClass($config['options'], $cssClasses['field']);
} elseif ($layout === ActiveForm::LAYOUT_INLINE) {
$config['inputOptions']['placeholder'] = true;
$config['enableError'] = false;
Html::addCssClass($config['labelOptions'], ['screenreader' => 'sr-only']);
}
return $config;
}
/**
* {@inheritdoc}
*/
public function fileInput($options = [])
{
Html::addCssClass($options, ['widget' => 'form-control-file']);
return parent::fileInput($options);
}
/**
* @param string|null $label the label or null to use model label
* @param array $options the tag options
*/
protected function renderLabelParts($label = null, $options = [])
{
$options = array_merge($this->labelOptions, $options);
if ($label === null) {
if (isset($options['label'])) {
$label = $options['label'];
unset($options['label']);
} else {
$attribute = Html::getAttributeName($this->attribute);
$label = Html::encode($this->model->getAttributeLabel($attribute));
}
}
if (!isset($options['for'])) {
$options['for'] = Html::getInputId($this->model, $this->attribute);
}
$this->parts['{beginLabel}'] = Html::beginTag('label', $options);
$this->parts['{endLabel}'] = Html::endTag('label');
if (!isset($this->parts['{labelTitle}'])) {
$this->parts['{labelTitle}'] = $label;
}
}
}

134
src/ActiveForm.php Normal file
View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
/**
* A Bootstrap 4 enhanced version of [[\yii\widgets\ActiveForm]].
*
* This class mainly adds the [[layout]] property to choose a Bootstrap 4 form layout.
* So for example to render a horizontal form you would:
*
* ```php
* use yii\bootstrap5\ActiveForm;
*
* $form = ActiveForm::begin(['layout' => 'horizontal'])
* ```
*
* This will set default values for the [[ActiveField]]
* to render horizontal form fields. In particular the [[ActiveField::template|template]]
* is set to `{label} {beginWrapper} {input} {error} {endWrapper} {hint}` and the
* [[ActiveField::horizontalCssClasses|horizontalCssClasses]] are set to:
*
* ```php
* [
* 'offset' => 'offset-sm-3',
* 'label' => 'col-sm-3',
* 'wrapper' => 'col-sm-6',
* 'error' => '',
* 'hint' => 'col-sm-3',
* ]
* ```
*
* To get a different column layout in horizontal mode you can modify those options
* through [[fieldConfig]]:
*
* ```php
* $form = ActiveForm::begin([
* 'layout' => 'horizontal',
* 'fieldConfig' => [
* 'template' => "{label}\n{beginWrapper}\n{input}\n{hint}\n{error}\n{endWrapper}",
* 'horizontalCssClasses' => [
* 'label' => 'col-sm-4',
* 'offset' => 'offset-sm-4',
* 'wrapper' => 'col-sm-8',
* 'error' => '',
* 'hint' => '',
* ],
* ],
* ]);
* ```
*
* @see ActiveField for details on the [[fieldConfig]] options
* @see https://getbootstrap.com/docs/5.0/components/forms/
*
* @author Michael Härtl <haertl.mike@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class ActiveForm extends \yii\widgets\ActiveForm
{
/**
* Default form layout
*/
const LAYOUT_DEFAULT = 'default';
/**
* Horizontal form layout
*/
const LAYOUT_HORIZONTAL = 'horizontal';
/**
* Inline form layout
*/
const LAYOUT_INLINE = 'inline';
/**
* @var string the default field class name when calling [[field()]] to create a new field.
* @see fieldConfig
*/
public $fieldClass = ActiveField::class;
/**
* @var array HTML attributes for the form tag. Default is `[]`.
*/
public $options = [];
/**
* @var string the form layout. Either [[LAYOUT_DEFAULT]], [[LAYOUT_HORIZONTAL]] or [[LAYOUT_INLINE]].
* By choosing a layout, an appropriate default field configuration is applied. This will
* render the form fields with slightly different markup for each layout. You can
* override these defaults through [[fieldConfig]].
* @see ActiveField for details on Bootstrap 4 field configuration
*/
public string $layout = self::LAYOUT_DEFAULT;
/**
* @var string the CSS class that is added to a field container when the associated attribute has validation error.
*/
public $errorCssClass = 'is-invalid';
/**
* {@inheritdoc}
*/
public $successCssClass = 'is-valid';
/**
* {@inheritdoc}
*/
public $errorSummaryCssClass = 'alert alert-danger';
/**
* {@inheritdoc}
*/
public $validationStateOn = self::VALIDATION_STATE_ON_INPUT;
/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function init()
{
if (!in_array($this->layout, [self::LAYOUT_DEFAULT, self::LAYOUT_HORIZONTAL, self::LAYOUT_INLINE])) {
throw new InvalidConfigException('Invalid layout type: ' . $this->layout);
}
if ($this->layout === self::LAYOUT_INLINE) {
Html::addCssClass($this->options, ['widget' => 'form-inline']);
}
parent::init();
}
/**
* @inheritdoc
*/
public function field($model, $attribute, $options = []): \yii\widgets\ActiveField
{
return parent::field($model, $attribute, $options);
}
}

140
src/Alert.php Normal file
View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\helpers\ArrayHelper;
/**
* Alert renders an alert bootstrap component.
*
* For example,
*
* ```php
* echo Alert::widget([
* 'options' => [
* 'class' => 'alert-info',
* ],
* 'body' => 'Say hello...',
* ]);
* ```
*
* The following example will show the content enclosed between the [[begin()]]
* and [[end()]] calls within the alert box:
*
* ```php
* Alert::begin([
* 'options' => [
* 'class' => 'alert-warning',
* ],
* ]);
*
* echo 'Say hello...';
*
* Alert::end();
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/alerts/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Alert extends Widget
{
/**
* @var string the body content in the alert component. Note that anything between
* the [[begin()]] and [[end()]] calls of the Alert widget will also be treated
* as the body content, and will be rendered before this.
*/
public $body;
/**
* @var array|false the options for rendering the close button tag.
* The close button is displayed in the header of the modal window. Clicking
* on the button will hide the modal window. If this is false, no close button will be rendered.
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'button'.
* - label: string, the label of the button. Defaults to '&times;'.
*
* The rest of the options will be rendered as the HTML attributes of the button tag.
* Please refer to the [Alert documentation](https://getbootstrap.com/docs/5.0/components/alerts/)
* for the supported HTML attributes.
*/
public $closeButton = [];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
$this->initOptions();
echo Html::beginTag('div', $this->options) . "\n";
}
/**
* {@inheritdoc}
*/
public function run()
{
echo "\n" . $this->renderBodyEnd();
echo "\n" . Html::endTag('div');
$this->registerPlugin('alert');
}
/**
* Renders the alert body and the close button (if any).
* @return string the rendering result
*/
protected function renderBodyEnd()
{
return $this->body . "\n" . $this->renderCloseButton() . "\n";
}
/**
* Renders the close button.
* @return string|null the rendering result
*/
protected function renderCloseButton(): ?string
{
if (($closeButton = $this->closeButton) !== false) {
$tag = ArrayHelper::remove($closeButton, 'tag', 'button');
$label = ArrayHelper::remove($closeButton, 'label', Html::tag('span', '&times;', [
'aria-hidden' => 'true',
]));
if ($tag === 'button' && !isset($closeButton['type'])) {
$closeButton['type'] = 'button';
}
return Html::tag($tag, $label, $closeButton);
} else {
return null;
}
}
/**
* Initializes the widget options.
* This method sets the default values for various options.
*/
protected function initOptions()
{
Html::addCssClass($this->options, ['widget' => 'alert']);
if ($this->closeButton !== false) {
$this->closeButton = array_merge([
'data-dismiss' => 'alert',
'class' => ['widget' => 'close'],
], $this->closeButton);
Html::addCssClass($this->options, ['toggle' => 'alert-dismissible']);
}
if (!isset($this->options['role'])) {
$this->options['role'] = 'alert';
}
}
}

180
src/BaseHtml.php Normal file
View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\Model;
use yii\helpers\ArrayHelper;
/**
* BaseHtml provides concrete implementation for [[Html]].
*/
abstract class BaseHtml extends \yii\helpers\Html
{
/**
* @var int a counter used to generate [[id]] for widgets.
* @internal
*/
public static int $counter = 0;
/**
* @var string the prefix to the automatically generated widget IDs.
* @see getId()
*/
public static string $autoIdPrefix = 'i';
/**
* @var array list of tag attributes that should be specially handled when their values are of array type.
* In particular, if the value of the `data` attribute is `['name' => 'xyz', 'age' => 13]`, two attributes
* will be generated instead of one: `data-name="xyz" data-age="13"`.
* @since 2.0.3
*/
public static $dataAttributes = ['data', 'data-ng', 'ng', 'aria'];
/**
* Renders Bootstrap static form control.
*
* @param string $value static control value.
* @param array $options the tag options in terms of name-value pairs. These will be rendered as
* the attributes of the resulting tag. There are also a special options:
*
* @return string generated HTML
* @see https://getbootstrap.com/docs/5.0/components/forms/#readonly-plain-text
*/
public static function staticControl(string $value, array $options = []): string
{
static::addCssClass($options, 'form-control-plaintext');
$value = (string)$value;
$options['readonly'] = true;
return static::input('text', null, $value, $options);
}
/**
* Generates a Bootstrap static form control for the given model attribute.
* @param \yii\base\Model $model the model object.
* @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format
* about attribute expression.
* @param array $options the tag options in terms of name-value pairs. See [[staticControl()]] for details.
* @return string generated HTML
* @see staticControl()
*/
public static function activeStaticControl(Model $model, string $attribute, $options = []): string
{
if (isset($options['value'])) {
$value = $options['value'];
unset($options['value']);
} else {
$value = static::getAttributeValue($model, $attribute);
}
return static::staticControl($value, $options);
}
/**
* {@inheritdoc}
*/
public static function radioList($name, $selection = null, $items = [], $options = []): string
{
if (!isset($options['item'])) {
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
$encode = ArrayHelper::getValue($options, 'encode', true);
$options['item'] = function ($index, $label, $name, $checked, $value) use ($itemOptions, $encode) {
unset($index);
$options = array_merge(
[
'class' => 'form-check-input',
'label' => $encode ? static::encode($label) : $label,
'labelOptions' => ['class' => 'form-check-label'],
'value' => $value,
], $itemOptions);
return '<div class="form-check">' . static::radio($name, $checked, $options) . '</div>';
};
}
return parent::radioList($name, $selection, $items, $options);
}
/**
* {@inheritdoc}
*/
public static function checkboxList($name, $selection = null, $items = [], $options = []): string
{
if (!isset($options['item'])) {
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
$encode = ArrayHelper::getValue($options, 'encode', true);
$options['item'] = function ($index, $label, $name, $checked, $value) use ($itemOptions, $encode) {
unset($index);
$options = array_merge(
[
'class' => 'form-check-input',
'label' => $encode ? static::encode($label) : $label,
'labelOptions' => ['class' => 'form-check-label'],
'value' => $value,
], $itemOptions);
return '<div class="form-check">' . Html::checkbox($name, $checked, $options) . '</div>';
};
}
return parent::checkboxList($name, $selection, $items, $options);
}
/**
* @inheritdoc
*/
protected static function booleanInput($type, $name, $checked = false, $options = []): string
{
$options['checked'] = (bool)$checked;
$value = array_key_exists('value', $options) ? $options['value'] : '1';
if (isset($options['uncheck'])) {
// add a hidden field so that if the checkbox is not selected, it still submits a value
$hiddenOptions = [];
if (isset($options['form'])) {
$hiddenOptions['form'] = $options['form'];
}
$hidden = static::hiddenInput($name, $options['uncheck'], $hiddenOptions);
unset($options['uncheck']);
} else {
$hidden = '';
}
if (isset($options['label'])) {
$label = $options['label'];
$labelOptions = $options['labelOptions'] ?? [];
unset($options['label'], $options['labelOptions']);
if (!isset($options['id'])) {
$options['id'] = static::getId();
}
$input = static::input($type, $name, $value, $options);
if (isset($labelOptions['wrapInput']) && $labelOptions['wrapInput']) {
unset($labelOptions['wrapInput']);
$content = static::label($input . $label, $options['id'], $labelOptions);
} else {
$content = $input . "\n" . static::label($label, $options['id'], $labelOptions);
}
return $hidden . $content;
}
return $hidden . static::input($type, $name, $value, $options);
}
/**
* {@inheritdoc}
*/
public static function error($model, $attribute, $options = []): string
{
if (!array_key_exists('class', $options)) {
$options['class'] = ['invalid-feedback'];
}
return parent::error($model, $attribute, $options);
}
/**
* Returns an autogenerated ID
* @return string Autogenerated ID
*/
protected static function getId(): string
{
return static::$autoIdPrefix . static::$counter++;
}
}

20
src/BootstrapAsset.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\web\AssetBundle;
class BootstrapAsset extends AssetBundle
{
public $sourcePath = '@npm/bootstrap/dist';
public $css = [
'css/bootstrap.css',
];
public $js = [
'js/bootstrap.bundle.js',
];
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\web\AssetBundle;
/**
* Asset bundle for the Twitter bootstrap javascript files.
*
* @author Qiang Xue <qiang.xue@gmail.com>
*/
class BootstrapPluginAsset extends AssetBundle
{
public $sourcePath = '@npm/bootstrap/dist';
public $js = [
'js/bootstrap.bundle.js',
];
public $depends = [
BootstrapAsset::class,
];
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\Json;
/**
* BootstrapWidgetTrait is the trait, which provides basic for all bootstrap widgets features.
*
* Note: class, which uses this trait must declare public field named `options` with the array default value:
*
* ```php
* class MyWidget extends \yii\base\Widget
* {
* use BootstrapWidgetTrait;
*
* public $options = [];
* }
* ```
*
* This field is not present in the trait in order to avoid possible PHP Fatal error on definition conflict.
*
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Paul Klimov <klimov.paul@gmail.com>
*/
trait BootstrapWidgetTrait
{
/**
* @var array the options for the underlying Bootstrap JS plugin.
* Please refer to the corresponding Bootstrap plugin Web page for possible options.
* For example, [this page](http://getbootstrap.com/javascript/#modals) shows
* how to use the "Modal" plugin and the supported options (e.g. "remote").
*/
public array $clientOptions = [];
/**
* @var array the event handlers for the underlying Bootstrap JS plugin.
* Please refer to the corresponding Bootstrap plugin Web page for possible events.
* For example, [this page](http://getbootstrap.com/javascript/#modals) shows
* how to use the "Modal" plugin and the supported events (e.g. "shown").
*/
public array $clientEvents = [];
/**
* Initializes the widget.
* This method will register the bootstrap asset bundle. If you override this method,
* make sure you call the parent implementation first.
* @throws InvalidConfigException
*/
public function init()
{
parent::init();
if (!isset($this->options['id'])) {
$this->options['id'] = $this->getId();
}
}
/**
* Registers a specific Bootstrap plugin and the related events
* @param string $name the name of the Bootstrap plugin
*/
protected function registerPlugin(string $name)
{
$view = $this->getView();
BootstrapPluginAsset::register($view);
$id = $this->options['id'];
if ($this->clientOptions !== []) {
$options = empty($this->clientOptions) ? '' : Json::htmlEncode($this->clientOptions);
$js = "jQuery('#$id').$name($options);";
$view->registerJs($js);
}
$this->registerClientEvents();
}
/**
* Registers JS event handlers that are listed in [[clientEvents]].
*/
protected function registerClientEvents()
{
if (!empty($this->clientEvents)) {
$id = $this->options['id'];
$js = [];
foreach ($this->clientEvents as $event => $handler) {
$js[] = "jQuery('#$id').on('$event', $handler);";
}
$this->getView()->registerJs(implode("\n", $js));
}
}
abstract function getView();
}

297
src/Breadcrumbs.php Normal file
View File

@ -0,0 +1,297 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\bootstrap5;
use JsonException;
use RuntimeException;
use Yii;
use yii\helpers\ArrayHelper;
/**
* Breadcrumbs represents a bootstrap 4 version of [[\yii\widgets\Breadcrumbs]]. It displays
* a list of links indicating the position of the current page in the whole site hierarchy.
*
* To use Breadcrumbs, you need to configure its [[links]] property, which specifies the links to be displayed. For example,
*
* ```php
* echo Breadcrumbs::widget([
* 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
* 'options' => [],
* ]);
* ```
* @see https://getbootstrap.com/docs/5.0/components/breadcrumb/
* @author Alexandr Kozhevnikov <onmotion1@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Breadcrumbs extends Widget
{
/**
* @var string the name of the breadcrumb container tag.
*/
public string $tag = 'ol';
/**
* @var bool whether to HTML-encode the link labels.
*/
public bool $encodeLabels = true;
/**
* @var array the first hyperlink in the breadcrumbs (called home link).
* Please refer to [[links]] on the format of the link.
* If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]]
* with the label 'Home'. If this property is false, the home link will not be rendered.
*/
public array $homeLink = [];
/**
* @var array list of links to appear in the breadcrumbs. If this property is empty,
* the widget will not render anything. Each array element represents a single link in the breadcrumbs
* with the following structure:
*
* ```php
* [
* 'label' => 'label of the link', // required
* 'url' => 'url of the link', // optional, will be processed by Url::to()
* 'template' => 'own template of the item', // optional, if not set $this->itemTemplate will be used
* ]
* ```
*
*
*/
public array $links = [];
/**
* @var string the template used to render each inactive item in the breadcrumbs. The token `{link}`
* will be replaced with the actual HTML link for each inactive item.
*/
public string $itemTemplate = "<li class=\"breadcrumb-item\">{link}</li>\n";
/**
* @var string the template used to render each active item in the breadcrumbs. The token `{link}`
* will be replaced with the actual HTML link for each active item.
*/
public string $activeItemTemplate = "<li class=\"breadcrumb-item active\" aria-current=\"page\">{link}</li>\n";
/**
* @var array the HTML attributes for the widgets nav container tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $navOptions = ['aria-label' => 'breadcrumb'];
/**
* Initializes the widget.
* If you override this method, make sure you call the parent implementation first.
*/
public function init()
{
parent::init();
$this->clientOptions = [];
Html::addCssClass($this->options, ['widget' => 'breadcrumb']);
}
public function run(): string
{
if (!isset($this->options['id'])) {
$this->options['id'] = "{$this->getId()}-breadcrumb";
}
/** @psalm-suppress InvalidArgument */
Html::addCssClass($this->options, ['widget' => 'breadcrumb']);
$this->registerPlugin('breadcrumb');
if (empty($this->links)) {
return '';
}
$links = [];
if ($this->homeLink === []) {
$links[] = $this->renderItem([
'label' => 'Home',
'url' => '/',
], $this->itemTemplate);
} else {
$links[] = $this->renderItem($this->homeLink, $this->itemTemplate);
}
foreach ($this->links as $link) {
if (!is_array($link)) {
$link = ['label' => $link];
}
$links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate);
}
return Html::tag('nav', Html::tag($this->tag, implode('', $links), $this->options), $this->navOptions);
}
/**
* Renders a single breadcrumb item.
*
* @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional.
* @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the
* link.
*
* @throws JsonException|RuntimeException if `$link` does not have "label" element.
*
* @return string the rendering result
*/
protected function renderItem(array $link, string $template): string
{
$encodeLabel = ArrayHelper::remove($link, 'encode', $this->encodeLabels);
if (array_key_exists('label', $link)) {
$label = $encodeLabel ? Html::encode($link['label']) : $link['label'];
} else {
throw new RuntimeException('The "label" element is required for each link.');
}
if (isset($link['template'])) {
$template = $link['template'];
}
if (isset($link['url'])) {
$options = $link;
unset($options['template'], $options['label'], $options['url']);
$linkHtml = Html::a($label, $link['url'], $options);
} else {
$linkHtml = $label;
}
return strtr($template, ['{link}' => $linkHtml]);
}
/**
* The template used to render each active item in the breadcrumbs. The token `{link}` will be replaced with the
* actual HTML link for each active item.
*
* @param string $value
*
* @return $this
*/
public function activeItemTemplate(string $value): self
{
$this->activeItemTemplate = $value;
return $this;
}
/**
* Whether to HTML-encode the link labels.
*
* @param bool $value
*
* @return $this
*/
public function encodeLabels(bool $value): self
{
$this->encodeLabels = $value;
return $this;
}
/**
* The first hyperlink in the breadcrumbs (called home link).
*
* Please refer to {@see links} on the format of the link.
*
* If this property is not set, it will default to a link pointing with the label 'Home'. If this property is false,
* the home link will not be rendered.
*
* @param array $value
*
* @return $this
*/
public function homeLink(array $value): self
{
$this->homeLink = $value;
return $this;
}
/**
* The template used to render each inactive item in the breadcrumbs. The token `{link}` will be replaced with the
* actual HTML link for each inactive item.
*
* @param string $value
*
* @return $this
*/
public function itemTemplate(string $value): self
{
$this->itemTemplate = $value;
return $this;
}
/**
* List of links to appear in the breadcrumbs. If this property is empty, the widget will not render anything. Each
* array element represents a single link in the breadcrumbs with the following structure:
*
* ```php
* [
* 'label' => 'label of the link', // required
* 'url' => 'url of the link', // optional, will be processed by Url::to()
* 'template' => 'own template of the item', // optional, if not set $this->itemTemplate will be used
* ]
* ```
*
* @param array $value
*
* @return $this
*/
public function links(array $value): self
{
$this->links = $value;
return $this;
}
/**
* The HTML attributes for the widgets nav container tag.
*
* {@see \yii\helpers\Html::renderTagAttributes()} for details on how attributes are being rendered.
*
* @param array $value
*
* @return $this
*/
public function navOptions(array $value): self
{
$this->navOptions = $value;
return $this;
}
/**
* The HTML attributes for the widget container tag. The following special options are recognized.
*
* {@see \yii\helpers\Html::renderTagAttributes()} for details on how attributes are being rendered.
*
* @param array $value
*
* @return $this
*/
public function options(array $value): self
{
$this->options = $value;
return $this;
}
/**
* The name of the breadcrumb container tag.
*
* @param string $value
*
* @return $this
*/
public function tag(string $value): self
{
$this->tag = $value;
return $this;
}
}

58
src/Button.php Normal file
View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
/**
* Button renders a bootstrap button.
*
* For example,
*
* ```php
* echo Button::widget([
* 'label' => 'Action',
* 'options' => ['class' => 'btn-lg'],
* ]);
* ```
* @see https://getbootstrap.com/docs/5.0/components/buttons/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
*/
class Button extends Widget
{
/**
* @var string the tag to use to render the button
*/
public string $tagName = 'button';
/**
* @var string the button label
*/
public string $label = 'Button';
/**
* @var bool whether the label should be HTML-encoded.
*/
public bool $encodeLabel = true;
/**
* Initializes the widget.
* If you override this method, make sure you call the parent implementation first.
*/
public function init()
{
parent::init();
$this->clientOptions = [];
Html::addCssClass($this->options, ['widget' => 'btn']);
}
/**
* {@inheritdoc}
* @return string
*/
public function run(): string
{
$this->registerPlugin('button');
return Html::tag($this->tagName, $this->encodeLabel ? Html::encode($this->label) : $this->label,
$this->options);
}
}

204
src/ButtonDropdown.php Normal file
View File

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
/**
* ButtonDropdown renders a group or split button dropdown bootstrap component.
*
* For example,
*
* ```php
* // a button group using Dropdown widget
* echo ButtonDropdown::widget([
* 'label' => 'Action',
* 'dropdown' => [
* 'items' => [
* ['label' => 'DropdownA', 'url' => '/'],
* ['label' => 'DropdownB', 'url' => '#'],
* ],
* ],
* ]);
* ```
* @see https://getbootstrap.com/docs/5.0/components/buttons/
* @see https://getbootstrap.com/docs/5.0/components/dropdowns/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
*/
class ButtonDropdown extends Widget
{
/**
* The css class part of dropdown
*/
const DIRECTION_DOWN = 'down';
/**
* The css class part of dropleft
*/
const DIRECTION_LEFT = 'left';
/**
* The css class part of dropright
*/
const DIRECTION_RIGHT = 'right';
/**
* The css class part of dropup
*/
const DIRECTION_UP = 'up';
/**
* @var string the button label
*/
public string $label = 'Button';
/**
* @var array the HTML attributes for the container tag. The following special options are recognized:
*
* - tag: string, defaults to "div", the name of the container tag.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $options = [];
/**
* @var array the HTML attributes of the button.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $buttonOptions = [];
/**
* @var array the configuration array for [[Dropdown]].
*/
public array $dropdown = [];
/**
* @var string the drop-direction of the widget
*
* Possible values are 'left', 'right', 'up', or 'down' (default)
*/
public string $direction = self::DIRECTION_DOWN;
/**
* @var bool whether to display a group of split-styled button group.
*/
public bool $split = false;
/**
* @var string the tag to use to render the button
*/
public string $tagName = 'button';
/**
* @var bool whether the label should be HTML-encoded.
*/
public bool $encodeLabel = true;
/**
* @var string name of a class to use for rendering dropdowns withing this widget. Defaults to [[Dropdown]].
*/
public string $dropdownClass = Dropdown::class;
/**
* @var bool whether to render the container using the [[options]] as HTML attributes. If set to `false`,
* the container element enclosing the button and dropdown will NOT be rendered.
*/
public bool $renderContainer = true;
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
if (!isset($this->buttonOptions['id'])) {
$this->buttonOptions['id'] = $this->options['id'] . '-button';
}
}
/**
* {@inheritdoc}
* @return string
* @throws \Throwable
*/
public function run(): string
{
$html = $this->renderButton() . "\n" . $this->renderDropdown();
if ($this->renderContainer) {
Html::addCssClass($this->options, ['widget' => 'drop' . $this->direction, 'btn-group']);
$options = $this->options;
$tag = ArrayHelper::remove($options, 'tag', 'div');
$html = Html::tag($tag, $html, $options);
}
// Set options id to button options id to ensure correct css selector in plugin initialisation
$this->options['id'] = $this->buttonOptions['id'];
$this->registerPlugin('dropdown');
return $html;
}
/**
* Generates the button dropdown.
* @return string the rendering result.
* @throws \Throwable
*/
protected function renderButton(): string
{
Html::addCssClass($this->buttonOptions, ['widget' => 'btn']);
$label = $this->label;
if ($this->encodeLabel) {
$label = Html::encode($label);
}
if ($this->split) {
$buttonOptions = $this->buttonOptions;
$this->buttonOptions['data-toggle'] = 'dropdown';
$this->buttonOptions['aria-haspopup'] = 'true';
$this->buttonOptions['aria-expanded'] = 'false';
Html::addCssClass($this->buttonOptions, ['toggle' => 'dropdown-toggle dropdown-toggle-split']);
unset($buttonOptions['id']);
$splitButton = Button::widget([
'label' => '<span class="sr-only">Toggle Dropdown</span>',
'encodeLabel' => false,
'options' => $this->buttonOptions,
'view' => $this->getView(),
]);
} else {
$buttonOptions = $this->buttonOptions;
Html::addCssClass($buttonOptions, ['toggle' => 'dropdown-toggle']);
$buttonOptions['data-toggle'] = 'dropdown';
$buttonOptions['aria-haspopup'] = 'true';
$buttonOptions['aria-expanded'] = 'false';
$splitButton = '';
}
if (isset($buttonOptions['href'])) {
if (is_array($buttonOptions['href'])) {
$buttonOptions['href'] = Url::to($buttonOptions['href']);
}
} else {
if ($this->tagName === 'a') {
$buttonOptions['href'] = '#';
$buttonOptions['role'] = 'button';
}
}
return Button::widget([
'tagName' => $this->tagName,
'label' => $label,
'options' => $buttonOptions,
'encodeLabel' => false,
'view' => $this->getView(),
]) . "\n" . $splitButton;
}
/**
* Generates the dropdown menu.
* @return string the rendering result.
* @throws \Throwable
*/
protected function renderDropdown(): string
{
$config = $this->dropdown;
$config['clientOptions'] = false;
$config['view'] = $this->getView();
/** @var Widget $dropdownClass */
$dropdownClass = $this->dropdownClass;
return $dropdownClass::widget($config);
}
}

111
src/ButtonGroup.php Normal file
View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\helpers\ArrayHelper;
/**
* ButtonGroup renders a button group bootstrap component.
*
* For example,
*
* ```php
* // a button group with items configuration
* echo ButtonGroup::widget([
* 'buttons' => [
* ['label' => 'A'],
* ['label' => 'B'],
* ['label' => 'C', 'visible' => false],
* ]
* ]);
*
* // button group with an item as a string
* echo ButtonGroup::widget([
* 'buttons' => [
* Button::widget(['label' => 'A']),
* ['label' => 'B'],
* ]
* ]);
* ```
*
* Pressing on the button should be handled via JavaScript. See the following for details:
*
* @see https://getbootstrap.com/docs/5.0/components/buttons/
* @see https://getbootstrap.com/docs/5.0/components/button-group/
*
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class ButtonGroup extends Widget
{
/**
* @var array list of buttons. Each array element represents a single button
* which can be specified as a string or an array of the following structure:
*
* - label: string, required, the button label.
* - options: array, optional, the HTML attributes of the button.
* - visible: bool, optional, whether this button is visible. Defaults to true.
*/
public array $buttons = [];
/**
* @var bool whether to HTML-encode the button labels.
*/
public bool $encodeLabels = true;
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'btn-group']);
if (!isset($this->options['role'])) {
$this->options['role'] = 'group';
}
}
/**
* {@inheritdoc}
* @return string
* @throws \Throwable
*/
public function run(): string
{
BootstrapAsset::register($this->getView());
return Html::tag('div', $this->renderButtons(), $this->options);
}
/**
* Generates the buttons that compound the group as specified on [[buttons]].
* @return string the rendering result.
* @throws \Throwable
*/
protected function renderButtons(): string
{
$buttons = [];
foreach ($this->buttons as $button) {
if (is_array($button)) {
$visible = ArrayHelper::remove($button, 'visible', true);
if ($visible === false) {
continue;
}
$button['view'] = $this->getView();
if (!isset($button['encodeLabel'])) {
$button['encodeLabel'] = $this->encodeLabels;
}
if (!isset($button['options'], $button['options']['type'])) {
ArrayHelper::setValue($button, 'options.type', 'button');
}
$buttons[] = Button::widget($button);
} else {
$buttons[] = $button;
}
}
return implode("\n", $buttons);
}
}

110
src/ButtonToolbar.php Normal file
View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
/**
* ButtonToolbar Combines sets of button groups into button toolbars for more complex components.
* Use utility classes as needed to space out groups, buttons, and more.
*
* For example,
*
* ```php
* // a button toolbar with items configuration
* echo ButtonToolbar::widget([
* 'buttonGroups' => [
* [
* 'buttons' => [
* ['label' => '1', 'options' => ['class' => ['btn-secondary']]],
* ['label' => '2', 'options' => ['class' => ['btn-secondary']]],
* ['label' => '3', 'options' => ['class' => ['btn-secondary']]],
* ['label' => '4', 'options' => ['class' => ['btn-secondary']]]
* ],
* 'class' => ['mr-2']
* ],
* [
* 'buttons' => [
* ['label' => '5', 'options' => ['class' => ['btn-secondary']]],
* ['label' => '6', 'options' => ['class' => ['btn-secondary']]],
* ['label' => '7', 'options' => ['class' => ['btn-secondary']]]
* ],
* 'class' => ['mr-2']
* ],
* [
* 'buttons' => [
* ['label' => '8', 'options' => ['class' => ['btn-secondary']]]
* ]
* ]
* ]
* ]);
* ```
*
* Pressing on the button should be handled via JavaScript. See the following for details:
*
* @see https://getbootstrap.com/docs/5.0/components/buttons/
* @see https://getbootstrap.com/docs/5.0/components/button-group/#button-toolbar
*
* @author Simon Karlen <simi.albi@outlook.com>
*/
class ButtonToolbar extends Widget
{
/**
* @var array list of buttons groups. Each array element represents a single group
* which can be specified as a string or an array of the following structure:
*
* - buttons: array list of buttons. Either as array or string representation
* - options: array optional, the HTML attributes of the button group.
* - encodeLabels: bool whether to HTML-encode the button labels.
*/
public array $buttonGroups = [];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'btn-toolbar']);
if (!isset($this->options['role'])) {
$this->options['role'] = 'toolbar';
}
}
/**
* {@inheritdoc}
* @return string
* @throws \Throwable
*/
public function run(): string
{
BootstrapAsset::register($this->getView());
return Html::tag('div', $this->renderButtonGroups(), $this->options);
}
/**
* Generates the button groups that compound the toolbar as specified on [[buttonGroups]].
* @return string the rendering result.
* @throws \Throwable
*/
protected function renderButtonGroups(): string
{
$buttonGroups = [];
foreach ($this->buttonGroups as $group) {
if (is_array($group)) {
$group['view'] = $this->getView();
if (!isset($group['buttons'])) {
continue;
}
$buttonGroups[] = ButtonGroup::widget($group);
} else {
$buttonGroups[] = $group;
}
}
return implode("\n", $buttonGroups);
}
}

200
src/Carousel.php Normal file
View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Carousel renders a carousel bootstrap javascript component.
*
* For example:
*
* ```php
* echo Carousel::widget([
* 'items' => [
* // the item contains only the image
* '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>',
* // equivalent to the above
* ['content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-02.jpg"/>'],
* // the item contains both the image and the caption
* [
* 'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-03.jpg"/>',
* 'caption' => '<h4>This is title</h4><p>This is the caption text</p>',
* 'captionOptions' => ['class' => ['d-none', 'd-md-block']]
* 'options' => [...],
* ],
* ]
* ]);
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/carousel/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Carousel extends Widget
{
/**
* @var array|null the labels for the previous and the next control buttons.
* If null, it means the previous and the next control buttons should not be displayed.
*/
public ?array $controls = [
'<span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous</span>',
'<span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next</span>',
];
/**
* @var bool whether carousel indicators (<ol> tag with anchors to items) should be displayed or not.
*/
public bool $showIndicators = true;
/**
* @var array list of slides in the carousel. Each array element represents a single
* slide with the following structure:
*
* ```php
* [
* // required, slide content (HTML), such as an image tag
* 'content' => '<img src="http://twitter.github.io/bootstrap/assets/img/bootstrap-mdo-sfmoma-01.jpg"/>',
* // optional, the caption (HTML) of the slide
* 'caption' => '<h4>This is title</h4><p>This is the caption text</p>',
* // optional the HTML attributes of the slide container
* 'options' => [],
* ]
* ```
*/
public array $items = [];
/**
* @var bool Animate slides with a fade transition instead of a slide. Defaults to `false`
*/
public bool $crossfade = false;
/**
* {@inheritdoc}
*/
public array $options = ['data-bs-ride' => 'carousel'];
/**
* Initializes the widget.
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'carousel slide']);
if ($this->crossfade) {
Html::addCssClass($this->options, ['animation' => 'carousel-fade']);
}
}
/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function run()
{
$this->registerPlugin('carousel');
return implode("\n", [
Html::beginTag('div', $this->options),
$this->renderIndicators(),
$this->renderItems(),
$this->renderControls(),
Html::endTag('div'),
]) . "\n";
}
/**
* Renders carousel indicators.
* @return string the rendering result
*/
public function renderIndicators()
{
if ($this->showIndicators === false) {
return '';
}
$indicators = [];
for ($i = 0, $count = count($this->items); $i < $count; $i++) {
$options = ['data-target' => '#' . $this->options['id'], 'data-slide-to' => $i];
if ($i === 0) {
Html::addCssClass($options, ['activate' => 'active']);
}
$indicators[] = Html::tag('li', '', $options);
}
return Html::tag('ol', implode("\n", $indicators), ['class' => ['carousel-indicators']]);
}
/**
* Renders carousel items as specified on [[items]].
* @return string the rendering result
* @throws InvalidConfigException
*/
public function renderItems()
{
$items = [];
for ($i = 0, $count = count($this->items); $i < $count; $i++) {
$items[] = $this->renderItem($this->items[$i], $i);
}
return Html::tag('div', implode("\n", $items), ['class' => 'carousel-inner']);
}
/**
* Renders a single carousel item
* @param string|array $item a single item from [[items]]
* @param int $index the item index as the first item should be set to `active`
* @return string the rendering result
* @throws InvalidConfigException if the item is invalid
*/
public function renderItem($item, $index)
{
if (is_string($item)) {
$content = $item;
$caption = null;
$options = [];
} elseif (isset($item['content'])) {
$content = $item['content'];
$caption = ArrayHelper::getValue($item, 'caption');
if ($caption !== null) {
$captionOptions = ArrayHelper::remove($item, 'captionOptions', []);
Html::addCssClass($captionOptions, ['widget' => 'carousel-caption']);
$caption = Html::tag('div', $caption, $captionOptions);
}
$options = ArrayHelper::getValue($item, 'options', []);
} else {
throw new InvalidConfigException('The "content" option is required.');
}
Html::addCssClass($options, ['widget' => 'carousel-item']);
if ($index === 0) {
Html::addCssClass($options, ['activate' => 'active']);
}
return Html::tag('div', $content . "\n" . $caption, $options);
}
/**
* Renders previous and next control buttons.
* @throws InvalidConfigException if [[controls]] is invalid.
*/
public function renderControls()
{
if (isset($this->controls[0], $this->controls[1])) {
return Html::a($this->controls[0], '#' . $this->options['id'], [
'class' => 'carousel-control-prev',
'data-slide' => 'prev',
'role' => 'button',
]) . "\n"
. Html::a($this->controls[1], '#' . $this->options['id'], [
'class' => 'carousel-control-next',
'data-slide' => 'next',
'role' => 'button',
]);
} elseif ($this->controls === false) {
return '';
} else {
throw new InvalidConfigException('The "controls" property must be either false or an array of two elements.');
}
}
}

161
src/Dropdown.php Normal file
View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Dropdown renders a Bootstrap dropdown menu component.
*
* For example,
*
* ```php
* <div class="dropdown">
* <a href="#" data-toggle="dropdown" class="dropdown-toggle">Label <b class="caret"></b></a>
* <?php
* echo Dropdown::widget([
* 'items' => [
* ['label' => 'DropdownA', 'url' => '/'],
* ['label' => 'DropdownB', 'url' => '#'],
* ],
* ]);
* ?>
* </div>
* ```
* @see https://getbootstrap.com/docs/5.0/components/dropdowns/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Dropdown extends Widget
{
/**
* @var array list of menu items in the dropdown. Each array element can be either an HTML string,
* or an array representing a single menu with the following structure:
*
* - label: string, required, the label of the item link.
* - encode: bool, optional, whether to HTML-encode item label.
* - url: string|array, optional, the URL of the item link. This will be processed by [[\yii\helpers\Url::to()]].
* If not set, the item will be treated as a menu header when the item has no sub-menu.
* - visible: bool, optional, whether this menu item is visible. Defaults to true.
* - disabled: bool, optional, whether this menu item is disabled. Defaults to false.
* - linkOptions: array, optional, the HTML attributes of the item link.
* - options: array, optional, the HTML attributes of the item.
* - active: bool, optional, whether the item should be on active state or not.
* - items: array, optional, the submenu items. The structure is the same as this property.
* Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
* - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
* merged with [[submenuOptions]].
*
* To insert divider use `-`.
*/
public array $items = [];
/**
* @var bool whether the labels for header items should be HTML-encoded.
*/
public bool $encodeLabels = true;
/**
* @var array|null the HTML attributes for sub-menu container tags.
*/
public ?array $submenuOptions = [];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'dropdown-menu']);
}
/**
* Renders the widget.
* @return string
* @throws InvalidConfigException
*/
public function run(): string
{
BootstrapPluginAsset::register($this->getView());
$this->registerClientEvents();
return $this->renderItems($this->items, $this->options);
}
/**
* Renders menu items.
* @param array $items the menu items to be rendered
* @param array $options the container HTML attributes
* @return string the rendering result.
* @throws InvalidConfigException if the label option is not specified in one of the items.
* @throws \Exception
*/
protected function renderItems(array $items, array $options = []): string
{
$lines = [];
foreach ($items as $item) {
if (is_string($item)) {
$lines[] = ($item === '-')
? Html::tag('div', '', ['class' => 'dropdown-divider'])
: $item;
continue;
}
if (isset($item['visible']) && !$item['visible']) {
continue;
}
if (!array_key_exists('label', $item)) {
throw new InvalidConfigException("The 'label' option is required.");
}
$encodeLabel = $item['encode'] ?? $this->encodeLabels;
$label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
$itemOptions = ArrayHelper::getValue($item, 'options', []);
$linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
$active = ArrayHelper::getValue($item, 'active', false);
$disabled = ArrayHelper::getValue($item, 'disabled', false);
Html::addCssClass($linkOptions, ['widget' => 'dropdown-item']);
if ($disabled) {
ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
Html::addCssClass($linkOptions, ['disable' => 'disabled']);
} elseif ($active) {
Html::addCssClass($linkOptions, ['activate' => 'active']);
}
$url = array_key_exists('url', $item) ? $item['url'] : null;
if (empty($item['items'])) {
if ($url === null) {
$content = Html::tag('h6', $label, ['class' => 'dropdown-header']);
} else {
$content = Html::a($label, $url, $linkOptions);
}
$lines[] = $content;
} else {
$submenuOptions = $this->submenuOptions;
if (isset($item['submenuOptions'])) {
$submenuOptions = array_merge($submenuOptions, $item['submenuOptions']);
}
Html::addCssClass($submenuOptions, ['widget' => 'dropdown-submenu dropdown-menu']);
Html::addCssClass($linkOptions, ['toggle' => 'dropdown-toggle']);
$lines[] = Html::beginTag('div', array_merge_recursive(['class' => ['dropdown'], 'aria-expanded' => 'false'], $itemOptions));
$lines[] = Html::a($label, $url, array_merge([
'data-toggle' => 'dropdown',
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
'role' => 'button',
], $linkOptions));
$lines[] = static::widget([
'items' => $item['items'],
'options' => $submenuOptions,
'submenuOptions' => $submenuOptions,
'encodeLabels' => $this->encodeLabels,
]);
$lines[] = Html::endTag('div');
}
}
return Html::tag('div', implode("\n", $lines), $options);
}
}

12
src/Html.php Normal file
View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
/**
* Class Html
*/
class Html extends BaseHtml
{
}

13
src/InputWidget.php Normal file
View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
/**
* Class InputWidget
*/
abstract class InputWidget extends \yii\widgets\InputWidget
{
use BootstrapWidgetTrait;
}

316
src/LinkPager.php Normal file
View File

@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\data\Pagination;
use yii\helpers\ArrayHelper;
/**
* LinkPager represents a bootstrap 4 version of [[\yii\widgets\LinkPager]]. It displays a list of hyperlinks
* that lead to different pages of target.
*
* LinkPager works with a [[\yii\widget\Pagination]] object which specifies the total number
* of pages and the current page number.
*
* To apply LinkPager globally e.g. in all GridViews, set in configuration DI:
*
* ```php
* 'container' => [
* 'definitions' => [
* \yii\widgets\LinkPager::class => \yii\bootstrap5\LinkPager::class,
* ],
* ],
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/pagination/
* @author Simon Karlen <simi.albi@outlook.com>
* @since 2.0.2
*
* @property-read array $pageRange
*/
class LinkPager extends Widget
{
/**
* @var Pagination the pagination object that this pager is associated with.
* You must set this property in order to make LinkPager work.
*/
public Pagination $pagination;
/**
* @var array HTML attributes for the pager container tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $options = [];
/**
* @var array HTML attributes for the pager list tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $listOptions = ['class' => ['pagination']];
/**
* @var array HTML attributes which will be applied to all link containers
*/
public array $linkContainerOptions = ['class' => ['page-item']];
/**
* @var array HTML attributes for the link in a pager container tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $linkOptions = ['class' => ['page-link']];
/**
* @var string the CSS class for the each page button.
*/
public string $pageCssClass;
/**
* @var string the CSS class for the "first" page button.
*/
public string $firstPageCssClass = 'first';
/**
* @var string the CSS class for the "last" page button.
*/
public string $lastPageCssClass = 'last';
/**
* @var string the CSS class for the "previous" page button.
*/
public string $prevPageCssClass = 'prev';
/**
* @var string the CSS class for the "next" page button.
*/
public string $nextPageCssClass = 'next';
/**
* @var string the CSS class for the active (currently selected) page button.
*/
public string $activePageCssClass = 'active';
/**
* @var string the CSS class for the disabled page buttons.
*/
public string $disabledPageCssClass = 'disabled';
/**
* @var array the options for the disabled tag to be generated inside the disabled list element.
* In order to customize the html tag, please use the tag key.
*
* ```php
* $disabledListItemSubTagOptions = ['class' => 'disabled-link'];
* ```
*/
public array $disabledListItemSubTagOptions = [];
/**
* @var int maximum number of page buttons that can be displayed. Defaults to 10.
*/
public int $maxButtonCount = 10;
/**
* @var string|bool the label for the "next" page button. Note that this will NOT be HTML-encoded.
* If this property is false, the "next" page button will not be displayed.
*/
public $nextPageLabel = "<span aria-hidden=\"true\">&raquo;</span>\n<span class=\"sr-only\">Next</span>";
/**
* @var string|bool the text label for the "previous" page button. Note that this will NOT be HTML-encoded.
* If this property is false, the "previous" page button will not be displayed.
*/
public $prevPageLabel = "<span aria-hidden=\"true\">&laquo;</span>\n<span class=\"sr-only\">Previous</span>";
/**
* @var string|bool the text label for the "first" page button. Note that this will NOT be HTML-encoded.
* If it's specified as true, page number will be used as label.
* Default is false that means the "first" page button will not be displayed.
*/
public $firstPageLabel = false;
/**
* @var string|bool the text label for the "last" page button. Note that this will NOT be HTML-encoded.
* If it's specified as true, page number will be used as label.
* Default is false that means the "last" page button will not be displayed.
*/
public $lastPageLabel = false;
/**
* @var bool whether to register link tags in the HTML header for prev, next, first and last page.
* Defaults to `false` to avoid conflicts when multiple pagers are used on one page.
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2
* @see registerLinkTags()
*/
public bool $registerLinkTags = false;
/**
* @var bool Hide widget when only one page exist.
*/
public bool $hideOnSinglePage = true;
/**
* @var bool whether to render current page button as disabled.
*/
public bool $disableCurrentPageButton = false;
/**
* Initializes the pager.
* @throws InvalidConfigException
*/
public function init()
{
parent::init();
if ($this->pagination === null) {
throw new InvalidConfigException('The "pagination" property must be set.');
}
}
/**
* Executes the widget.
* This overrides the parent implementation by displaying the generated page buttons.
* @return string
*/
public function run(): string
{
if ($this->registerLinkTags) {
$this->registerLinkTags();
}
$options = $this->options;
$tag = ArrayHelper::remove($options, 'tag', 'nav');
$html = Html::beginTag($tag, $options);
$html .= $this->renderPageButtons();
$html .= Html::endTag($tag);
return $html;
}
/**
* Registers relational link tags in the html header for prev, next, first and last page.
* These links are generated using [[\yii\data\Pagination::getLinks()]].
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.1.2
*/
protected function registerLinkTags()
{
$view = $this->getView();
foreach ($this->pagination->getLinks() as $rel => $href) {
$view->registerLinkTag(['rel' => $rel, 'href' => $href], $rel);
}
}
/**
* Renders the page buttons.
* @return string the rendering result
*/
protected function renderPageButtons(): string
{
$pageCount = $this->pagination->getPageCount();
if ($pageCount < 2 && $this->hideOnSinglePage) {
return '';
}
$buttons = [];
$currentPage = $this->pagination->getPage();
// first page
$firstPageLabel = $this->firstPageLabel === true ? '1' : $this->firstPageLabel;
if ($firstPageLabel !== false) {
$buttons[] = $this->renderPageButton(
$firstPageLabel,
0,
$this->firstPageCssClass,
$currentPage <= 0,
false
);
}
// prev page
if ($this->prevPageLabel !== false) {
if (($page = $currentPage - 1) < 0) {
$page = 0;
}
$buttons[] = $this->renderPageButton(
$this->prevPageLabel,
$page,
$this->prevPageCssClass,
$currentPage <= 0,
false
);
}
// internal pages
[$beginPage, $endPage] = $this->getPageRange();
for ($i = $beginPage; $i <= $endPage; ++$i) {
$buttons[] = $this->renderPageButton(
$i + 1,
$i,
'',
$this->disableCurrentPageButton && $i == $currentPage,
$i == $currentPage
);
}
// next page
if ($this->nextPageLabel !== false) {
if (($page = $currentPage + 1) >= $pageCount - 1) {
$page = $pageCount - 1;
}
$buttons[] = $this->renderPageButton(
$this->nextPageLabel,
$page,
$this->nextPageCssClass,
$currentPage >= $pageCount - 1,
false
);
}
// last page
$lastPageLabel = $this->lastPageLabel === true ? $pageCount : $this->lastPageLabel;
if ($lastPageLabel !== false) {
$buttons[] = $this->renderPageButton(
$lastPageLabel,
$pageCount - 1,
$this->lastPageCssClass,
$currentPage >= $pageCount - 1,
false
);
}
$options = $this->listOptions;
$tag = ArrayHelper::remove($options, 'tag', 'ul');
return Html::tag($tag, implode("\n", $buttons), $options);
}
/**
* Renders a page button.
* You may override this method to customize the generation of page buttons.
* @param string $label the text label for the button
* @param int $page the page number
* @param string $class the CSS class for the page button.
* @param bool $disabled whether this page button is disabled
* @param bool $active whether this page button is active
* @return string the rendering result
*/
protected function renderPageButton(string $label, int $page, string $class, bool $disabled, bool $active): string
{
$options = $this->linkContainerOptions;
$linkWrapTag = ArrayHelper::remove($options, 'tag', 'li');
Html::addCssClass($options, empty($class) ? $this->pageCssClass : $class);
$linkOptions = $this->linkOptions;
$linkOptions['data-page'] = $page;
if ($active) {
Html::addCssClass($options, $this->activePageCssClass);
}
if ($disabled) {
Html::addCssClass($options, $this->disabledPageCssClass);
$disabledItemOptions = $this->disabledListItemSubTagOptions;
$linkOptions = ArrayHelper::merge($linkOptions, $disabledItemOptions);
$linkOptions['tabindex'] = '-1';
}
return Html::tag($linkWrapTag, Html::a($label, $this->pagination->createUrl($page), $linkOptions), $options);
}
/**
* @return array the begin and end pages that need to be displayed.
*/
protected function getPageRange(): array
{
$currentPage = $this->pagination->getPage();
$pageCount = $this->pagination->getPageCount();
$beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2));
if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) {
$endPage = $pageCount - 1;
$beginPage = max(0, $endPage - $this->maxButtonCount + 1);
}
return [$beginPage, $endPage];
}
}

313
src/Modal.php Normal file
View File

@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\helpers\ArrayHelper;
/**
* Modal renders a modal window that can be toggled by clicking on a button.
*
* The following example will show the content enclosed between the [[begin()]]
* and [[end()]] calls within the modal window:
*
* ~~~php
* Modal::begin([
* 'title' => 'Hello world',
* 'toggleButton' => ['label' => 'click me'],
* ]);
*
* echo 'Say hello...';
*
* Modal::end();
* ~~~
*
* @see https://getbootstrap.com/docs/5.0/components/modal/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Modal extends Widget
{
/**
* The additional css class of extra large modal
* @since 2.0.3
*/
const SIZE_EXTRA_LARGE = 'modal-xl';
/**
* The additional css class of large modal
*/
const SIZE_LARGE = 'modal-lg';
/**
* The additional css class of small modal
*/
const SIZE_SMALL = 'modal-sm';
/**
* The additional css class of default modal
*/
const SIZE_DEFAULT = '';
/**
* @var string the tile content in the modal window.
*/
public $title;
/**
* @var array additional title options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $titleOptions = [];
/**
* @var array additional header options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $headerOptions = [];
/**
* @var array body options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $bodyOptions = [];
/**
* @var string the footer content in the modal window.
*/
public $footer;
/**
* @var array additional footer options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $footerOptions = [];
/**
* @var string the modal size. Can be [[SIZE_LARGE]] or [[SIZE_SMALL]], or empty for default.
*/
public $size;
/**
* @var array|false the options for rendering the close button tag.
* The close button is displayed in the header of the modal window. Clicking
* on the button will hide the modal window. If this is false, no close button will be rendered.
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'button'.
* - label: string, the label of the button. Defaults to '&times;'.
*
* The rest of the options will be rendered as the HTML attributes of the button tag.
* Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals)
* for the supported HTML attributes.
*/
public $closeButton = [];
/**
* @var array|false the options for rendering the toggle button tag.
* The toggle button is used to toggle the visibility of the modal window.
* If this property is false, no toggle button will be rendered.
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'button'.
* - label: string, the label of the button. Defaults to 'Show'.
*
* The rest of the options will be rendered as the HTML attributes of the button tag.
* Please refer to the [Modal plugin help](http://getbootstrap.com/javascript/#modals)
* for the supported HTML attributes.
*/
public $toggleButton = false;
/**
* @var boolean whether to center the modal vertically
*
* When true the modal-dialog-centered class will be added to the modal-dialog
* @since 2.0.9
*/
public $centerVertical = false;
/**
* @var boolean whether to make the modal body scrollable
*
* When true the modal-dialog-scrollable class will be added to the modal-dialog
* @since 2.0.9
*/
public $scrollable = false;
/**
* @var array modal dialog options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
* @since 2.0.9
*/
public $dialogOptions = [];
/**
* Initializes the widget.
*/
public function init()
{
parent::init();
$this->initOptions();
echo $this->renderToggleButton() . "\n";
echo Html::beginTag('div', $this->options) . "\n";
echo Html::beginTag('div', $this->dialogOptions) . "\n";
echo Html::beginTag('div', ['class' => 'modal-content']) . "\n";
echo $this->renderHeader() . "\n";
echo $this->renderBodyBegin() . "\n";
}
/**
* Renders the widget.
*/
public function run()
{
echo "\n" . $this->renderBodyEnd();
echo "\n" . $this->renderFooter();
echo "\n" . Html::endTag('div'); // modal-content
echo "\n" . Html::endTag('div'); // modal-dialog
echo "\n" . Html::endTag('div');
$this->registerPlugin('modal');
}
/**
* Renders the header HTML markup of the modal
* @return string the rendering result
*/
protected function renderHeader()
{
$button = $this->renderCloseButton();
if ($this->title !== null) {
Html::addCssClass($this->titleOptions, ['widget' => 'modal-title']);
$header = Html::tag('h5', $this->title, $this->titleOptions);
} else {
$header = '';
}
if ($button !== null) {
$header .= "\n" . $button;
} elseif ($header === '') {
return '';
}
Html::addCssClass($this->headerOptions, ['widget' => 'modal-header']);
return Html::tag('div', "\n" . $header . "\n", $this->headerOptions);
}
/**
* Renders the opening tag of the modal body.
* @return string the rendering result
*/
protected function renderBodyBegin()
{
Html::addCssClass($this->bodyOptions, ['widget' => 'modal-body']);
return Html::beginTag('div', $this->bodyOptions);
}
/**
* Renders the closing tag of the modal body.
* @return string the rendering result
*/
protected function renderBodyEnd()
{
return Html::endTag('div');
}
/**
* Renders the HTML markup for the footer of the modal
* @return string the rendering result
*/
protected function renderFooter()
{
if ($this->footer !== null) {
Html::addCssClass($this->footerOptions, ['widget' => 'modal-footer']);
return Html::tag('div', "\n" . $this->footer . "\n", $this->footerOptions);
} else {
return null;
}
}
/**
* Renders the toggle button.
* @return string the rendering result
*/
protected function renderToggleButton()
{
if (($toggleButton = $this->toggleButton) !== false) {
$tag = ArrayHelper::remove($toggleButton, 'tag', 'button');
$label = ArrayHelper::remove($toggleButton, 'label', 'Show');
return Html::tag($tag, $label, $toggleButton);
} else {
return null;
}
}
/**
* Renders the close button.
* @return string the rendering result
*/
protected function renderCloseButton()
{
if (($closeButton = $this->closeButton) !== false) {
$tag = ArrayHelper::remove($closeButton, 'tag', 'button');
$label = ArrayHelper::remove($closeButton, 'label', Html::tag('span', '&times;', [
'aria-hidden' => 'true',
]));
return Html::tag($tag, $label, $closeButton);
} else {
return null;
}
}
/**
* Initializes the widget options.
* This method sets the default values for various options.
*/
protected function initOptions()
{
$this->options = array_merge([
'class' => 'fade',
'role' => 'dialog',
'tabindex' => -1,
'aria-hidden' => 'true',
], $this->options);
Html::addCssClass($this->options, ['widget' => 'modal']);
if ($this->clientOptions !== false) {
$this->clientOptions = array_merge(['show' => false], $this->clientOptions);
}
$this->titleOptions = array_merge([
'id' => $this->options['id'] . '-label',
], $this->titleOptions);
if (!isset($this->options['aria-label'], $this->options['aria-labelledby']) && $this->title !== null) {
$this->options['aria-labelledby'] = $this->titleOptions['id'];
}
if ($this->closeButton !== false) {
$this->closeButton = array_merge([
'data-dismiss' => 'modal',
'class' => 'close',
'type' => 'button',
], $this->closeButton);
}
if ($this->toggleButton !== false) {
$this->toggleButton = array_merge([
'data-toggle' => 'modal',
'type' => 'button',
], $this->toggleButton);
if (!isset($this->toggleButton['data-target']) && !isset($this->toggleButton['href'])) {
$this->toggleButton['data-target'] = '#' . $this->options['id'];
}
}
$this->dialogOptions = array_merge([
'role' => 'document',
], $this->dialogOptions);
Html::addCssClass($this->dialogOptions, ['widget' => 'modal-dialog']);
if ($this->size) {
Html::addCssClass($this->dialogOptions, ['size' => $this->size]);
}
if ($this->centerVertical) {
Html::addCssClass($this->dialogOptions, ['align' => 'modal-dialog-centered']);
}
if ($this->scrollable) {
Html::addCssClass($this->dialogOptions, ['scroll' => 'modal-dialog-scrollable']);
}
}
}

292
src/Nav.php Normal file
View File

@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
use Yii;
/**
* Nav renders a nav HTML component.
*
* For example:
*
* ```php
* echo Nav::widget([
* 'items' => [
* [
* 'label' => 'Home',
* 'url' => ['site/index'],
* 'linkOptions' => [...],
* ],
* [
* 'label' => 'Dropdown',
* 'items' => [
* ['label' => 'Level 1 - Dropdown A', 'url' => '#'],
* '<div class="dropdown-divider"></div>',
* '<div class="dropdown-header">Dropdown Header</div>',
* ['label' => 'Level 1 - Dropdown B', 'url' => '#'],
* ],
* ],
* [
* 'label' => 'Login',
* 'url' => ['site/login'],
* 'visible' => Yii::$app->user->isGuest
* ],
* ],
* 'options' => ['class' =>'nav-pills'], // set this to nav-tabs to get tab-styled navigation
* ]);
* ```
*
* Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 4.
*
* @see https://getbootstrap.com/docs/5.0/components/navs/
* @see https://getbootstrap.com/docs/5.0/components/dropdowns/
*
* @author Antonio Ramirez <amigo.cobos@gmail.com>
*/
class Nav extends Widget
{
/**
* @var array list of items in the nav widget. Each array element represents a single
* menu item which can be either a string or an array with the following structure:
*
* - label: string, required, the nav item label.
* - url: optional, the item's URL. Defaults to "#".
* - visible: bool, optional, whether this menu item is visible. Defaults to true.
* - disabled: bool, optional, whether this menu item is disabled. Defaults to false.
* - linkOptions: array, optional, the HTML attributes of the item's link.
* - options: array, optional, the HTML attributes of the item container (LI).
* - active: bool, optional, whether the item should be on active state or not.
* - dropdownOptions: array, optional, the HTML options that will passed to the [[Dropdown]] widget.
* - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget,
* or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
* - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for only this item.
*
* If a menu item is a string, it will be rendered directly without HTML encoding.
*/
public array $items = [];
/**
* @var bool whether the nav items labels should be HTML-encoded.
*/
public bool $encodeLabels = true;
/**
* @var bool whether to automatically activate items according to whether their route setting
* matches the currently requested route.
* @see isItemActive
*/
public bool $activateItems = true;
/**
* @var bool whether to activate parent menu items when one of the corresponding child menu items is active.
*/
public bool $activateParents = false;
/**
* @var string|null the route used to determine if a menu item is active or not.
* If not set, it will use the route of the current request.
* @see params
* @see isItemActive
*/
public ?string $route = null;
/**
* @var array|null the parameters used to determine if a menu item is active or not.
* If not set, it will use `$_GET`.
* @see route
* @see isItemActive
*/
public ?array $params = null;
/**
* @var string name of a class to use for rendering dropdowns within this widget. Defaults to [[Dropdown]].
*/
public string $dropdownClass = Dropdown::class;
/**
* Initializes the widget.
*/
public function init()
{
parent::init();
if ($this->route === null && Yii::$app->controller !== null) {
$this->route = Yii::$app->controller->getRoute();
}
if ($this->params === null) {
$this->params = Yii::$app->request->getQueryParams();
}
Html::addCssClass($this->options, ['widget' => 'nav']);
}
/**
* Renders the widget.
* @return string
* @throws InvalidConfigException
*/
public function run(): string
{
BootstrapAsset::register($this->getView());
return $this->renderItems();
}
/**
* Renders widget items.
* @return string
* @throws InvalidConfigException
*/
public function renderItems(): string
{
$items = [];
foreach ($this->items as $i => $item) {
if (isset($item['visible']) && !$item['visible']) {
continue;
}
$items[] = $this->renderItem($item);
}
return Html::tag('ul', implode("\n", $items), $this->options);
}
/**
* Renders a widget's item.
* @param string|array $item the item to render.
* @return string the rendering result.
* @throws InvalidConfigException
* @throws \Throwable
*/
public function renderItem($item): string
{
if (is_string($item)) {
return $item;
}
if (!isset($item['label'])) {
throw new InvalidConfigException("The 'label' option is required.");
}
$encodeLabel = $item['encode'] ?? $this->encodeLabels;
$label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
$options = ArrayHelper::getValue($item, 'options', []);
$items = ArrayHelper::getValue($item, 'items');
$url = ArrayHelper::getValue($item, 'url', '#');
$linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
$disabled = ArrayHelper::getValue($item, 'disabled', false);
$active = $this->isItemActive($item);
if (empty($items)) {
$items = '';
Html::addCssClass($options, ['widget' => 'nav-item']);
Html::addCssClass($linkOptions, ['widget' => 'nav-link']);
} else {
$linkOptions['data-toggle'] = 'dropdown';
Html::addCssClass($options, ['widget' => 'dropdown nav-item']);
Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle nav-link']);
if (is_array($items)) {
$items = $this->isChildActive($items, $active);
$items = $this->renderDropdown($items, $item);
}
}
if ($disabled) {
ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
Html::addCssClass($linkOptions, ['disable' => 'disabled']);
} elseif ($this->activateItems && $active) {
Html::addCssClass($linkOptions, ['activate' => 'active']);
}
return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options);
}
/**
* Renders the given items as a dropdown.
* This method is called to create sub-menus.
* @param array $items the given items. Please refer to [[Dropdown::items]] for the array structure.
* @param array $parentItem the parent item information. Please refer to [[items]] for the structure of this array.
* @return string the rendering result.
* @throws \Throwable
*/
protected function renderDropdown(array $items, array $parentItem): string
{
/** @var Widget $dropdownClass */
$dropdownClass = $this->dropdownClass;
return $dropdownClass::widget([
'options' => ArrayHelper::getValue($parentItem, 'dropdownOptions', []),
'items' => $items,
'encodeLabels' => $this->encodeLabels,
'clientOptions' => false,
'view' => $this->getView(),
]);
}
/**
* Check to see if a child item is active optionally activating the parent.
* @param array $items @see items
* @param bool $active should the parent be active too
* @return array @see items
*/
protected function isChildActive(array $items, bool &$active): array
{
foreach ($items as $i => $child) {
if (is_array($child) && !ArrayHelper::getValue($child, 'visible', true)) {
continue;
}
if ($this->isItemActive($child)) {
ArrayHelper::setValue($items[$i], 'active', true);
if ($this->activateParents) {
$active = true;
}
}
$childItems = ArrayHelper::getValue($child, 'items');
if (is_array($childItems)) {
$activeParent = false;
$items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
if ($activeParent) {
Html::addCssClass($items[$i]['options'], ['activate' => 'active']);
$active = true;
}
}
}
return $items;
}
/**
* Checks whether a menu item is active.
* This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item.
* When the `url` option of a menu item is specified in terms of an array, its first element is treated
* as the route for the item and the rest of the elements are the associated parameters.
* Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item
* be considered active.
* @param array $item the menu item to be checked
* @return bool whether the menu item is active
*/
protected function isItemActive(array $item): bool
{
if (!$this->activateItems) {
return false;
}
if (isset($item['active'])) {
return ArrayHelper::getValue($item, 'active', false);
}
if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
$route = $item['url'][0];
if ($route[0] !== '/' && Yii::$app->controller) {
$route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
}
if (ltrim($route, '/') !== $this->route) {
return false;
}
unset($item['url']['#']);
if (count($item['url']) > 1) {
$params = $item['url'];
unset($params[0]);
foreach ($params as $name => $value) {
if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) {
return false;
}
}
}
return true;
}
return false;
}
}

211
src/NavBar.php Normal file
View File

@ -0,0 +1,211 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\bootstrap5;
use Yii;
use yii\helpers\ArrayHelper;
/**
* NavBar renders a navbar HTML component.
*
* Any content enclosed between the [[begin()]] and [[end()]] calls of NavBar
* is treated as the content of the navbar. You may use widgets such as [[Nav]]
* or [[\yii\widgets\Menu]] to build up such content. For example,
*
* ```php
* use yii\bootstrap5\NavBar;
* use yii\bootstrap5\Nav;
*
* NavBar::begin(['brandLabel' => 'NavBar Test']);
* echo Nav::widget([
* 'items' => [
* ['label' => 'Home', 'url' => ['/site/index']],
* ['label' => 'About', 'url' => ['/site/about']],
* ],
* 'options' => ['class' => 'navbar-nav'],
* ]);
* NavBar::end();
* ```
*
* @property-write array $containerOptions
*
* @see https://getbootstrap.com/docs/5.0/components/navbar/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Alexander Kochetov <creocoder@gmail.com>
*/
class NavBar extends Widget
{
/**
* @var array the HTML attributes for the widget container tag. The following special options are recognized:
*
* - tag: string, defaults to "nav", the name of the container tag.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $options = [];
/**
* @var array the HTML attributes for the container tag. The following special options are recognized:
*
* - tag: string, defaults to "div", the name of the container tag.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $collapseOptions = [];
/**
* @var string|bool the text of the brand or false if it's not used. Note that this is not HTML-encoded.
* @see https://getbootstrap.com/docs/5.0/components/navbar/
*/
public $brandLabel = false;
/**
* @var string|bool src of the brand image or false if it's not used. Note that this param will override `$this->brandLabel` param.
* @see https://getbootstrap.com/docs/5.0/components/navbar/
* @since 2.0.8
*/
public $brandImage = false;
/**
* @var array|string|bool $url the URL for the brand's hyperlink tag. This parameter will be processed by [[\yii\helpers\Url::to()]]
* and will be used for the "href" attribute of the brand link. Default value is false that means
* [[\yii\web\Application::homeUrl]] will be used.
* You may set it to `null` if you want to have no link at all.
*/
public $brandUrl = false;
/**
* @var array the HTML attributes of the brand link.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $brandOptions = [];
/**
* @var string text to show for screen readers for the button to toggle the navbar.
*/
public string $screenReaderToggleText = 'Toggle navigation';
/**
* @var string the toggle button content. Defaults to bootstrap 4 default `<span class="navbar-toggler-icon"></span>`
*/
public string $togglerContent = '<span class="navbar-toggler-icon"></span>';
/**
* @var array the HTML attributes of the navbar toggler button.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $togglerOptions = [];
/**
* @var bool whether the navbar content should be included in an inner div container which by default
* adds left and right padding. Set this to false for a 100% width navbar.
*/
public bool $renderInnerContainer = true;
/**
* @var array the HTML attributes of the inner container.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $innerContainerOptions = [];
/**
* {@inheritdoc}
*/
public array $clientOptions = [];
/**
* Initializes the widget.
*/
public function init()
{
parent::init();
if (!isset($this->options['class']) || empty($this->options['class'])) {
Html::addCssClass($this->options, [
'widget' => 'navbar',
'toggle' => 'navbar-expand-lg',
'navbar-light',
'bg-light'
]);
} else {
Html::addCssClass($this->options, ['widget' => 'navbar']);
}
$navOptions = $this->options;
$navTag = ArrayHelper::remove($navOptions, 'tag', 'nav');
$brand = '';
if (!isset($this->innerContainerOptions['class'])) {
Html::addCssClass($this->innerContainerOptions, ['panel' => 'container']);
}
if (!isset($this->collapseOptions['id'])) {
$this->collapseOptions['id'] = "{$this->options['id']}-collapse";
}
if ($this->brandImage !== false) {
$this->brandLabel = Html::img($this->brandImage);
}
if ($this->brandLabel !== false) {
Html::addCssClass($this->brandOptions, ['widget' => 'navbar-brand']);
if ($this->brandUrl === null) {
$brand = Html::tag('span', $this->brandLabel, $this->brandOptions);
} else {
$brand = Html::a(
$this->brandLabel,
$this->brandUrl === false ? Yii::$app->homeUrl : $this->brandUrl,
$this->brandOptions
);
}
}
Html::addCssClass($this->collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']);
$collapseOptions = $this->collapseOptions;
$collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div');
echo Html::beginTag($navTag, $navOptions) . "\n";
if ($this->renderInnerContainer) {
echo Html::beginTag('div', $this->innerContainerOptions) . "\n";
}
echo $brand . "\n";
echo $this->renderToggleButton() . "\n";
echo Html::beginTag($collapseTag, $collapseOptions) . "\n";
}
/**
* Renders the widget.
*/
public function run()
{
$tag = ArrayHelper::remove($this->collapseOptions, 'tag', 'div');
echo Html::endTag($tag) . "\n";
if ($this->renderInnerContainer) {
echo Html::endTag('div') . "\n";
}
$tag = ArrayHelper::remove($this->options, 'tag', 'nav');
echo Html::endTag($tag);
BootstrapPluginAsset::register($this->getView());
}
/**
* Renders collapsible toggle button.
* @return string the rendering toggle button.
*/
protected function renderToggleButton(): string
{
$options = $this->togglerOptions;
Html::addCssClass($options, ['widget' => 'navbar-toggler']);
return Html::button(
$this->togglerContent,
ArrayHelper::merge($options, [
'type' => 'button',
'data' => [
'toggle' => 'collapse',
'target' => '#' . $this->collapseOptions['id'],
],
'aria-controls' => $this->collapseOptions['id'],
'aria-expanded' => 'false',
'aria-label' => $this->screenReaderToggleText,
])
);
}
/**
* Container options setter for backwards compatibility
* @param array $collapseOptions
* @deprecated
*/
public function setContainerOptions(array $collapseOptions)
{
$this->collapseOptions = $collapseOptions;
}
}

181
src/Progress.php Normal file
View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Progress renders a bootstrap progress bar component.
*
* For example,
*
* ```php
* // default with label
* echo Progress::widget([
* 'percent' => 60,
* 'label' => 'test'
* ]);
* // or
* echo Progress::widget([
* 'bars' => [
* ['percent' => 60, 'label' => 'test']
* ]
* ]);
*
* // styled
* echo Progress::widget([
* 'percent' => 65,
* 'barOptions' => ['class' => 'bg-danger']
* ]);
* // or
* echo Progress::widget([
* 'bars' => [
* ['percent' => 65, 'options' => ['class' => 'bg-danger']]
* ]
* ]);
*
* // striped
* echo Progress::widget([
* 'percent' => 70,
* 'barOptions' => ['class' => ['bg-warning', 'progress-bar-striped']]
* ]);
* // or
* echo Progress::widget([
* 'bars' => [
* ['percent' => 70, 'options' => ['class' => ['bg-warning', 'progress-bar-striped']]]
* ]
* ]);
*
* // striped animated
* echo Progress::widget([
* 'percent' => 70,
* 'barOptions' => ['class' => ['bg-success', 'progress-bar-animated', 'progress-bar-striped']]
* ]);
* // or
* echo Progress::widget([
* 'bars' => [
* ['percent' => 70, 'options' => ['class' => ['bg-success', 'progress-bar-animated', 'progress-bar-striped']]]
* ]
* ]);
*
* // stacked bars
* echo Progress::widget([
* 'bars' => [
* ['percent' => 30, 'options' => ['class' => 'bg-danger']],
* ['percent' => 30, 'label' => 'test', 'options' => ['class' => 'bg-success']],
* ['percent' => 35, 'options' => ['class' => 'bg-warning']],
* ]
* ]);
* ```
* @see https://getbootstrap.com/docs/5.0/components/progress/
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Alexander Makarov <sam@rmcreative.ru>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Progress extends Widget
{
/**
* @var string the button label. This property will only be considered if [[bars]] is empty
*/
public $label;
/**
* @var int the amount of progress as a percentage. This property will only be considered if [[bars]] is empty
*/
public $percent = 0;
/**
* @var array the HTML attributes of the bar. This property will only be considered if [[bars]] is empty
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
* @since 2.0.6
*/
public $barOptions = [];
/**
* @var array a set of bars that are stacked together to form a single progress bar.
* Each bar is an array of the following structure:
*
* ```php
* [
* // required, the amount of progress as a percentage.
* 'percent' => 30,
* // optional, the label to be displayed on the bar
* 'label' => '30%',
* // optional, array, additional HTML attributes for the bar tag
* 'options' => [],
* ]
* ```
*/
public $bars;
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'progress']);
}
/**
* {@inheritdoc}
* @throws InvalidConfigException
*/
public function run()
{
BootstrapAsset::register($this->getView());
return $this->renderProgress();
}
/**
* Renders the progress.
* @return string the rendering result.
* @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar.
*/
protected function renderProgress()
{
$out = Html::beginTag('div', $this->options) . "\n";
if (empty($this->bars)) {
$this->bars = [
['label' => $this->label, 'percent' => $this->percent, 'options' => $this->barOptions],
];
}
$bars = [];
foreach ($this->bars as $bar) {
$label = ArrayHelper::getValue($bar, 'label', '');
if (!isset($bar['percent'])) {
throw new InvalidConfigException("The 'percent' option is required.");
}
$options = ArrayHelper::getValue($bar, 'options', []);
$bars[] = $this->renderBar($bar['percent'], $label, $options);
}
$out .= implode("\n", $bars) . "\n";
$out .= Html::endTag('div');
return $out;
}
/**
* Generates a bar
* @param int $percent the percentage of the bar
* @param string $label , optional, the label to display at the bar
* @param array $options the HTML attributes of the bar
* @return string the rendering result.
*/
protected function renderBar($percent, $label = '', $options = [])
{
$percent = (float)trim(rtrim((string)$percent, '%'));
$options = array_merge($options, [
'role' => 'progressbar',
'aria-valuenow' => $percent,
'aria-valuemin' => 0,
'aria-valuemax' => 100,
]);
Html::addCssClass($options, ['widget' => 'progress-bar']);
Html::addCssStyle($options, ['width' => $percent . '%'], true);
return Html::tag('div', $label, $options);
}
}

263
src/Tabs.php Normal file
View File

@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Tabs renders a Tab bootstrap javascript component.
*
* For example:
*
* ```php
* echo Tabs::widget([
* 'items' => [
* [
* 'label' => 'One',
* 'content' => 'Anim pariatur cliche...',
* 'active' => true
* ],
* [
* 'label' => 'Two',
* 'content' => 'Anim pariatur cliche...',
* 'headerOptions' => [...],
* 'options' => ['id' => 'myveryownID'],
* ],
* [
* 'label' => 'Example',
* 'url' => 'http://www.example.com',
* ],
* [
* 'label' => 'Dropdown',
* 'items' => [
* [
* 'label' => 'DropdownA',
* 'content' => 'DropdownA, Anim pariatur cliche...',
* ],
* [
* 'label' => 'DropdownB',
* 'content' => 'DropdownB, Anim pariatur cliche...',
* ],
* [
* 'label' => 'External Link',
* 'url' => 'http://www.example.com',
* ],
* ],
* ],
* ],
* ]);
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/navs/#tabs
* @see https://getbootstrap.com/docs/5.0/components/card/#navigation
* @author Antonio Ramirez <amigo.cobos@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Tabs extends Widget
{
/**
* @var array list of tabs in the tabs widget. Each array element represents a single
* tab with the following structure:
*
* - label: string, required, the tab header label.
* - encode: bool, optional, whether this label should be HTML-encoded. This param will override
* global `$this->encodeLabels` param.
* - headerOptions: array, optional, the HTML attributes of the tab header.
* - linkOptions: array, optional, the HTML attributes of the tab header link tags.
* - content: string, optional, the content (HTML) of the tab pane.
* - url: string, optional, an external URL. When this is specified, clicking on this tab will bring
* the browser to this URL. This option is available since version 2.0.4.
* - options: array, optional, the HTML attributes of the tab pane container.
* - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as
* 'active' explicitly - the first one will be activated.
* - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true.
* - disabled: bool, optional, whether the item tab header and pane should be disabled or not. Defaults to false.
* - items: array, optional, can be used instead of `content` to specify a dropdown items
* configuration array. Each item can hold three extra keys, besides the above ones:
* * active: bool, optional, whether the item tab header and pane should be visible or not.
* * content: string, required if `items` is not set. The content (HTML) of the tab pane.
* * options: optional, array, the HTML attributes of the tab content container.
*/
public array $items = [];
/**
* @var array list of HTML attributes for the item container tags. This will be overwritten
* by the "options" set in individual [[items]]. The following special options are recognized:
*
* - tag: string, defaults to "div", the tag name of the item container tags.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $itemOptions = [];
/**
* @var array list of HTML attributes for the header container tags. This will be overwritten
* by the "headerOptions" set in individual [[items]].
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $headerOptions = [];
/**
* @var array list of HTML attributes for the tab header link tags. This will be overwritten
* by the "linkOptions" set in individual [[items]].
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $linkOptions = [];
/**
* @var bool whether the labels for header items should be HTML-encoded.
*/
public bool $encodeLabels = true;
/**
* @var string specifies the Bootstrap tab styling.
*/
public string $navType = 'nav-tabs';
/**
* @var bool whether to render the `tab-content` container and its content. You may set this property
* to be false so that you can manually render `tab-content` yourself in case your tab contents are complex.
*/
public bool $renderTabContent = true;
/**
* @var array list of HTML attributes for the `tab-content` container. This will always contain the CSS class `tab-content`.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $tabContentOptions = [];
/**
* @var string name of a class to use for rendering dropdowns withing this widget. Defaults to [[Dropdown]].
*/
public string $dropdownClass = Dropdown::class;
/**
* @var array Tab panes (contents)
*/
protected array $panes = [];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
Html::addCssClass($this->options, ['widget' => 'nav', $this->navType]);
Html::addCssClass($this->tabContentOptions, ['panel' => 'tab-content']);
}
/**
* {@inheritdoc}
* @throws InvalidConfigException
* @throws \Throwable
*/
public function run(): string
{
$this->registerPlugin('tab');
$this->prepareItems($this->items);
return Nav::widget([
'dropdownClass' => $this->dropdownClass,
'options' => ArrayHelper::merge(['role' => 'tablist'], $this->options),
'items' => $this->items,
'encodeLabels' => $this->encodeLabels,
]) . $this->renderPanes($this->panes);
}
/**
* Renders tab items as specified on [[items]].
*
* @param array $items
* @param string $prefix
* @throws InvalidConfigException
*/
protected function prepareItems(array &$items, string $prefix = '')
{
if (!$this->hasActiveTab()) {
$this->activateFirstVisibleTab();
}
foreach ($items as $n => $item) {
$options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
$options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . $prefix . '-tab' . $n);
unset($items[$n]['options']['id']); // @see https://github.com/yiisoft/yii2-bootstrap4/issues/108#issuecomment-465219339
if (!ArrayHelper::remove($item, 'visible', true)) {
continue;
}
if (!array_key_exists('label', $item)) {
throw new InvalidConfigException("The 'label' option is required.");
}
$selected = ArrayHelper::getValue($item, 'active', false);
$disabled = ArrayHelper::getValue($item, 'disabled', false);
$headerOptions = ArrayHelper::getValue($item, 'headerOptions', $this->headerOptions);
if (isset($item['items'])) {
$this->prepareItems($items[$n]['items'], '-dd' . $n);
continue;
} else {
ArrayHelper::setValue($items[$n], 'options', $headerOptions);
if (!isset($item['url'])) {
ArrayHelper::setValue($items[$n], 'url', '#' . $options['id']);
ArrayHelper::setValue($items[$n], 'linkOptions.data.toggle', 'tab');
ArrayHelper::setValue($items[$n], 'linkOptions.role', 'tab');
ArrayHelper::setValue($items[$n], 'linkOptions.aria-controls', $options['id']);
if (!$disabled) {
ArrayHelper::setValue($items[$n], 'linkOptions.aria-selected', $selected ? 'true' : 'false');
}
} else {
continue;
}
}
Html::addCssClass($options, ['widget' => 'tab-pane']);
if ($selected) {
Html::addCssClass($options, ['activate' => 'active']);
}
if ($this->renderTabContent) {
$tag = ArrayHelper::remove($options, 'tag', 'div');
$this->panes[] = Html::tag($tag, $item['content'] ?? '', $options);
}
}
}
/**
* @return bool if there's active tab defined
*/
protected function hasActiveTab(): bool
{
foreach ($this->items as $item) {
if (isset($item['active']) && $item['active'] === true) {
return true;
}
}
return false;
}
/**
* Sets the first visible tab as active.
*
* This method activates the first tab that is visible and
* not explicitly set to inactive (`'active' => false`).
*/
protected function activateFirstVisibleTab()
{
foreach ($this->items as $i => $item) {
$active = ArrayHelper::getValue($item, 'active', null);
$visible = ArrayHelper::getValue($item, 'visible', true);
$disabled = ArrayHelper::getValue($item, 'disabled', false);
if ($visible && $active !== false && $disabled !== true) {
$this->items[$i]['active'] = true;
return;
}
}
}
/**
* Renders tab panes.
*
* @param array $panes
* @return string the rendering result.
*/
public function renderPanes(array $panes): string
{
return $this->renderTabContent ? "\n" . Html::tag('div', implode("\n", $panes), $this->tabContentOptions) : '';
}
}

210
src/Toast.php Normal file
View File

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use Yii;
use yii\helpers\ArrayHelper;
/**
* Toasts renders an toast bootstrap component.
*
* For example,
*
* ```php
* echo Toast::widget([
* 'title' => 'Hello world!',
* 'dateTime' => 'now',
* 'body' => 'Say hello...',
* ]);
* ```
*
* The following example will show the content enclosed between the [[begin()]]
* and [[end()]] calls within the toast box:
*
* ```php
* Toast::begin([
* 'title' => 'Hello world!',
* 'dateTime' => 'now'
* ]);
*
* echo 'Say hello...';
*
* Toast::end();
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/toasts/
* @author Simon Karlen <simi.albi@outlook.com>
*/
class Toast extends Widget
{
/**
* @var string|null the body content in the alert component. Note that anything between
* the [[begin()]] and [[end()]] calls of the Toast widget will also be treated
* as the body content, and will be rendered before this.
*/
public ?string $body = null;
/**
* @var string|null The title content in the toast.
*/
public ?string $title = null;
/**
* @var int|string|\DateTime|\DateTimeInterface|\DateInterval|false The date time the toast message references to.
* This will be formatted as relative time (via formatter component). It will be omitted if
* set to `false` (default).
*/
public $dateTime = false;
/**
* @var array the options for rendering the close button tag.
* The close button is displayed in the header of the toast. Clicking on the button will hide the toast.
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'button'.
* - label: string, the label of the button. Defaults to '&times;'.
*
* The rest of the options will be rendered as the HTML attributes of the button tag.
* Please refer to the [Toast documentation](https://getbootstrap.com/docs/5.0/components/toasts/)
* for the supported HTML attributes.
*/
public array $closeButton = [];
/**
* @var array additional title options
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'strong'.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $titleOptions = [];
/**
* @var array additional date time part options
*
* The following special options are supported:
*
* - tag: string, the tag name of the button. Defaults to 'small'.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $dateTimeOptions = [];
/**
* @var array additional header options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $headerOptions = [];
/**
* @var array body options
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $bodyOptions = [];
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
$this->initOptions();
echo Html::beginTag('div', $this->options) . "\n";
echo $this->renderHeader() . "\n";
echo $this->renderBodyBegin() . "\n";
}
/**
* {@inheritdoc}
*/
public function run()
{
echo "\n" . $this->renderBodyEnd();
echo "\n" . Html::endTag('div');
$this->registerPlugin('toast');
}
/**
* Renders the header HTML markup of the modal
* @return string the rendering result
*/
protected function renderHeader(): string
{
$button = $this->renderCloseButton();
$tag = ArrayHelper::remove($this->titleOptions, 'tag', 'strong');
Html::addCssClass($this->titleOptions, ['widget' => 'mr-auto']);
$title = Html::tag($tag, $this->title === null ? '' : $this->title, $this->titleOptions);
if ($this->dateTime !== false) {
$tag = ArrayHelper::remove($this->dateTimeOptions, 'tag', 'small');
Html::addCssClass($this->dateTimeOptions, ['widget' => 'text-muted']);
$title .= "\n" . Html::tag($tag, Yii::$app->formatter->asRelativeTime($this->dateTime), $this->dateTimeOptions);
}
$title .= "\n" . $button;
Html::addCssClass($this->headerOptions, ['widget' => 'toast-header']);
return Html::tag('div', "\n" . $title . "\n", $this->headerOptions);
}
/**
* Renders the opening tag of the toast body.
* @return string the rendering result
*/
protected function renderBodyBegin(): string
{
Html::addCssClass($this->bodyOptions, ['widget' => 'toast-body']);
return Html::beginTag('div', $this->bodyOptions);
}
/**
* Renders the toast body and the close button (if any).
* @return string the rendering result
*/
protected function renderBodyEnd(): string
{
return $this->body . "\n" . Html::endTag('div');
}
/**
* Renders the close button.
* @return string the rendering result
*/
protected function renderCloseButton(): string
{
$tag = ArrayHelper::remove($this->closeButton, 'tag', 'button');
$label = ArrayHelper::remove($this->closeButton, 'label', Html::tag('span', '&times;', [
'aria-hidden' => 'true',
]));
return Html::tag($tag, "\n" . $label . "\n", $this->closeButton);
}
/**
* Initializes the widget options.
* This method sets the default values for various options.
*/
protected function initOptions()
{
Html::addCssClass($this->options, ['widget' => 'toast']);
$this->closeButton = array_merge([
'aria' => ['label' => 'Close'],
'data' => ['dismiss' => 'toast'],
'class' => ['widget' => 'ml-2 mb-1 close'],
'type' => 'button',
], $this->closeButton);
if (!isset($this->options['role'])) {
$this->options['role'] = 'alert';
}
if (!isset($this->options['aria']) && !isset($this->options['aria-live'])) {
$this->options['aria'] = [
'live' => 'assertive',
'atomic' => 'true',
];
}
}
}

134
src/ToggleButtonGroup.php Normal file
View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
use yii\base\InvalidConfigException;
/**
* ToggleButtonGroup allows rendering form inputs Checkbox/Radio toggle button groups.
*
* You can use this widget in an [[yii\bootstrap5\ActiveForm|ActiveForm]] using the [[yii\widgets\ActiveField::widget()|widget()]]
* method, for example like this:
*
* ```php
* <?= $form->field($model, 'item_id')->widget(\yii\bootstrap5\ToggleButtonGroup::class, [
* 'type' => \yii\bootstrap5\ToggleButtonGroup::TYPE_CHECKBOX
* 'items' => [
* 'fooValue' => 'BarLabel',
* 'barValue' => 'BazLabel'
* ]
* ]) ?>
* ```
*
* @see https://getbootstrap.com/docs/5.0/components/buttons/#checkbox-and-radio-buttons
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @author Simon Karlen <simi.albi@outlook.com>
*/
class ToggleButtonGroup extends InputWidget
{
/**
* Checkbox type
*/
public const TYPE_CHECKBOX = 'checkbox';
/**
* Radio type
*/
public const TYPE_RADIO = 'radio';
/**
* @var string input type, can be [[TYPE_CHECKBOX]] or [[TYPE_RADIO]]
*/
public string $type;
/**
* @var array the data item used to generate the checkboxes.
* The array values are the labels, while the array keys are the corresponding checkbox or radio values.
*/
public array $items = [];
/**
* @var array, the HTML attributes for the label (button) tag.
* @see Html::checkbox()
* @see Html::radio()
*/
public array $labelOptions = [
'class' => ['btn', 'btn-secondary'],
];
/**
* @var bool whether the items labels should be HTML-encoded.
*/
public bool $encodeLabels = true;
/**
* {@inheritdoc}
*/
public function init()
{
parent::init();
$this->registerPlugin('button');
Html::addCssClass($this->options, ['widget' => 'btn-group-toggle']);
$this->options['data-toggle'] = 'buttons';
}
/**
* {@inheritdoc}
* @return string
* @throws InvalidConfigException
*/
public function run(): string
{
if (!isset($this->options['item'])) {
$this->options['item'] = [$this, 'renderItem'];
}
switch ($this->type) {
case 'checkbox':
if ($this->hasModel()) {
return Html::activeCheckboxList($this->model, $this->attribute, $this->items, $this->options);
} else {
return Html::checkboxList($this->name, $this->value, $this->items, $this->options);
}
case 'radio':
if ($this->hasModel()) {
return Html::activeRadioList($this->model, $this->attribute, $this->items, $this->options);
} else {
return Html::radioList($this->name, $this->value, $this->items, $this->options);
}
default:
throw new InvalidConfigException("Unsupported type '{$this->type}'");
}
}
/**
* Default callback for checkbox/radio list item rendering.
* @param int $index item index.
* @param string $label item label.
* @param string $name input name.
* @param bool $checked whether value is checked or not.
* @param string $value input value.
* @return string generated HTML.
* @see Html::checkbox()
* @see Html::radio()
*/
public function renderItem(int $index, string $label, string $name, bool $checked, string $value): string
{
unset($index);
$labelOptions = $this->labelOptions;
$labelOptions['wrapInput'] = true;
Html::addCssClass($labelOptions, ['widget' => 'btn']);
if ($checked) {
Html::addCssClass($labelOptions, ['activate' => 'active']);
}
$type = $this->type;
if ($this->encodeLabels) {
$label = Html::encode($label);
}
return Html::$type($name, $checked, [
'label' => $label,
'labelOptions' => $labelOptions,
'autocomplete' => 'off',
'value' => $value,
]);
}
}

19
src/Widget.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace yii\bootstrap5;
/**
* \yii\bootstrap5\Widget is the base class for all bootstrap5 widgets.
*/
class Widget extends \yii\base\Widget
{
use BootstrapWidgetTrait;
/**
* @var array the HTML attributes for the widget container tag.
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public array $options = [];
}