From c92be88adf59141a8fb3d6dcdbc47ca90d39e20e Mon Sep 17 00:00:00 2001 From: Mylistryx Date: Wed, 10 Feb 2021 00:04:59 +0300 Subject: [PATCH] Init. v1.0.0 --- .editorconfig | 14 + .gitignore | 37 +++ composer.json | 49 +++ src/Accordion.php | 248 ++++++++++++++++ src/ActiveField.php | 558 +++++++++++++++++++++++++++++++++++ src/ActiveForm.php | 134 +++++++++ src/Alert.php | 140 +++++++++ src/BaseHtml.php | 180 +++++++++++ src/BootstrapAsset.php | 20 ++ src/BootstrapPluginAsset.php | 23 ++ src/BootstrapWidgetTrait.php | 99 +++++++ src/Breadcrumbs.php | 297 +++++++++++++++++++ src/Button.php | 58 ++++ src/ButtonDropdown.php | 204 +++++++++++++ src/ButtonGroup.php | 111 +++++++ src/ButtonToolbar.php | 110 +++++++ src/Carousel.php | 200 +++++++++++++ src/Dropdown.php | 161 ++++++++++ src/Html.php | 12 + src/InputWidget.php | 13 + src/LinkPager.php | 316 ++++++++++++++++++++ src/Modal.php | 313 ++++++++++++++++++++ src/Nav.php | 292 ++++++++++++++++++ src/NavBar.php | 211 +++++++++++++ src/Progress.php | 181 ++++++++++++ src/Tabs.php | 263 +++++++++++++++++ src/Toast.php | 210 +++++++++++++ src/ToggleButtonGroup.php | 134 +++++++++ src/Widget.php | 19 ++ 29 files changed, 4607 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 src/Accordion.php create mode 100644 src/ActiveField.php create mode 100644 src/ActiveForm.php create mode 100644 src/Alert.php create mode 100644 src/BaseHtml.php create mode 100644 src/BootstrapAsset.php create mode 100644 src/BootstrapPluginAsset.php create mode 100644 src/BootstrapWidgetTrait.php create mode 100644 src/Breadcrumbs.php create mode 100644 src/Button.php create mode 100644 src/ButtonDropdown.php create mode 100644 src/ButtonGroup.php create mode 100644 src/ButtonToolbar.php create mode 100644 src/Carousel.php create mode 100644 src/Dropdown.php create mode 100644 src/Html.php create mode 100644 src/InputWidget.php create mode 100644 src/LinkPager.php create mode 100644 src/Modal.php create mode 100644 src/Nav.php create mode 100644 src/NavBar.php create mode 100644 src/Progress.php create mode 100644 src/Tabs.php create mode 100644 src/Toast.php create mode 100644 src/ToggleButtonGroup.php create mode 100644 src/Widget.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..257221d --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..580c9ab --- /dev/null +++ b/.gitignore @@ -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 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7306979 --- /dev/null +++ b/composer.json @@ -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" + } + } +} diff --git a/src/Accordion.php b/src/Accordion.php new file mode 100644 index 0000000..53dd283 --- /dev/null +++ b/src/Accordion.php @@ -0,0 +1,248 @@ + [ + * // 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 + * @author Simon Karlen + */ +class Accordion extends Widget +{ + /** + * @var array list of groups in the collapse widget. Each array element represents a single + * group with the following structure: + * + * - label: string, required, the group header label. + * - encode: bool, optional, whether this label should be HTML-encoded. This param will override + * global `$this->encodeLabels` param. + * - content: array|string|object, required, the content (HTML) of the group + * - options: array, optional, the HTML attributes of the group + * - contentOptions: optional, the HTML attributes of the group's content + * + * Since version 2.0.7 you may also specify this property as key-value pairs, where the key refers to the + * `label` and the value refers to `content`. If value is a string it is interpreted as label. If it is + * an array, it is interpreted as explained above. + * + * For example: + * + * ```php + * echo Accordion::widget([ + * 'items' => [ + * 'Introduction' => 'This is the first collapsable menu', + * 'Second panel' => [ + * 'content' => 'This is the second collapsable menu', + * ], + * [ + * 'label' => 'Third panel', + * 'content' => 'This is the third collapsable menu', + * ], + * ] + * ]) + * ``` + */ + public 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); + } +} diff --git a/src/ActiveField.php b/src/ActiveField.php new file mode 100644 index 0000000..a71bd1b --- /dev/null +++ b/src/ActiveField.php @@ -0,0 +1,558 @@ + 'horizontal']); + * + * // Form field without label + * echo $form->field($model, 'demo', [ + * 'inputOptions' => [ + * 'placeholder' => $model->getAttributeLabel('demo'), + * ], + * ])->label(false); + * + * // Inline radio list + * echo $form->field($model, 'demo')->inline()->radioList($items); + * + * // Control sizing in horizontal mode + * echo $form->field($model, 'demo', [ + * 'horizontalCssClasses' => [ + * 'wrapper' => 'col-sm-2', + * ] + * ]); + * + * // With 'default' layout you would use 'template' to size a specific field: + * echo $form->field($model, 'demo', [ + * 'template' => '{label}
{input}{error}{hint}
' + * ]); + * + * // Input group + * echo $form->field($model, 'demo', [ + * 'inputTemplate' => '
+ * @ + *
{input}
', + * ]); + * + * ActiveForm::end(); + * ``` + * + * @property-read ActiveForm $form + * + * @see ActiveForm + * @see https://getbootstrap.com/docs/5.0/components/forms/ + * + * @author Michael Härtl + * @author Simon Karlen + */ +class ActiveField extends \yii\widgets\ActiveField +{ + /** + * @var bool whether to render [[checkboxList()]] and [[radioList()]] inline. + */ + public 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 = "
\n{input}\n{label}\n{error}\n{hint}\n
"; + /** + * @var string the template for radios in default layout + * @since 2.0.5 + */ + public string $radioTemplate = "
\n{input}\n{label}\n{error}\n{hint}\n
"; + /** + * @var string the template for checkboxes and radios in horizontal layout + */ + public string $checkHorizontalTemplate = "{beginWrapper}\n
\n{input}\n{label}\n{error}\n{hint}\n
\n{endWrapper}"; + /** + * @var string the template for checkboxes and radios in horizontal layout + * @since 2.0.5 + */ + public string $radioHorizontalTemplate = "{beginWrapper}\n
\n{input}\n{label}\n{error}\n{hint}\n
\n{endWrapper}"; + /** + * @var string the `enclosed by label` template for checkboxes and radios in default layout + */ + public string $checkEnclosedTemplate = "
\n{beginLabel}\n{input}\n{labelTitle}\n{endLabel}\n{error}\n{hint}\n
"; + /** + * @var bool whether to render the error. Default is `true` except for layout `inline`. + */ + public 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; + } + } +} diff --git a/src/ActiveForm.php b/src/ActiveForm.php new file mode 100644 index 0000000..e4dab11 --- /dev/null +++ b/src/ActiveForm.php @@ -0,0 +1,134 @@ + '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 + * @author Simon Karlen + */ +class ActiveForm extends \yii\widgets\ActiveForm +{ + /** + * Default form layout + */ + const LAYOUT_DEFAULT = 'default'; + /** + * Horizontal form layout + */ + const LAYOUT_HORIZONTAL = 'horizontal'; + /** + * Inline form layout + */ + const LAYOUT_INLINE = 'inline'; + + /** + * @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); + } +} diff --git a/src/Alert.php b/src/Alert.php new file mode 100644 index 0000000..9549470 --- /dev/null +++ b/src/Alert.php @@ -0,0 +1,140 @@ + [ + * '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 + * @author Simon Karlen + */ +class Alert extends Widget +{ + /** + * @var string the body content in the alert component. Note that anything between + * the [[begin()]] and [[end()]] calls of the Alert widget will also be treated + * as the body content, and will be rendered before this. + */ + public $body; + /** + * @var array|false the options for rendering the close button tag. + * The close button is displayed in the header of the modal window. Clicking + * on the button will hide the modal window. If this is false, no close button will be rendered. + * + * The following special options are supported: + * + * - tag: string, the tag name of the button. Defaults to 'button'. + * - label: string, the label of the button. Defaults to '×'. + * + * The rest of the options will be rendered as the HTML attributes of the button tag. + * Please refer to the [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', '×', [ + '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'; + } + } +} diff --git a/src/BaseHtml.php b/src/BaseHtml.php new file mode 100644 index 0000000..2411d17 --- /dev/null +++ b/src/BaseHtml.php @@ -0,0 +1,180 @@ + '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 '
' . static::radio($name, $checked, $options) . '
'; + }; + } + + 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 '
' . Html::checkbox($name, $checked, $options) . '
'; + }; + } + + 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++; + } +} diff --git a/src/BootstrapAsset.php b/src/BootstrapAsset.php new file mode 100644 index 0000000..e2f442c --- /dev/null +++ b/src/BootstrapAsset.php @@ -0,0 +1,20 @@ + + */ +class BootstrapPluginAsset extends AssetBundle +{ + public $sourcePath = '@npm/bootstrap/dist'; + public $js = [ + 'js/bootstrap.bundle.js', + ]; + public $depends = [ + BootstrapAsset::class, + ]; +} diff --git a/src/BootstrapWidgetTrait.php b/src/BootstrapWidgetTrait.php new file mode 100644 index 0000000..810cede --- /dev/null +++ b/src/BootstrapWidgetTrait.php @@ -0,0 +1,99 @@ + + * @author Qiang Xue + * @author Paul Klimov + */ +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(); +} diff --git a/src/Breadcrumbs.php b/src/Breadcrumbs.php new file mode 100644 index 0000000..50e63c7 --- /dev/null +++ b/src/Breadcrumbs.php @@ -0,0 +1,297 @@ + isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [], + * 'options' => [], + * ]); + * ``` + * @see https://getbootstrap.com/docs/5.0/components/breadcrumb/ + * @author Alexandr Kozhevnikov + * @author Simon Karlen + */ +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 = "
  • {link}
  • \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 = "
  • {link}
  • \n"; + /** + * @var array the HTML attributes for the widgets nav container tag. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public 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; + } + +} diff --git a/src/Button.php b/src/Button.php new file mode 100644 index 0000000..dda7621 --- /dev/null +++ b/src/Button.php @@ -0,0 +1,58 @@ + 'Action', + * 'options' => ['class' => 'btn-lg'], + * ]); + * ``` + * @see https://getbootstrap.com/docs/5.0/components/buttons/ + * @author Antonio Ramirez + */ +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); + } +} diff --git a/src/ButtonDropdown.php b/src/ButtonDropdown.php new file mode 100644 index 0000000..8e4f903 --- /dev/null +++ b/src/ButtonDropdown.php @@ -0,0 +1,204 @@ + '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 + */ +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' => 'Toggle Dropdown', + '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); + } +} diff --git a/src/ButtonGroup.php b/src/ButtonGroup.php new file mode 100644 index 0000000..06fe5a7 --- /dev/null +++ b/src/ButtonGroup.php @@ -0,0 +1,111 @@ + [ + * ['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 + * @author Simon Karlen + */ +class ButtonGroup extends Widget +{ + /** + * @var array list of buttons. Each array element represents a single button + * which can be specified as a string or an array of the following structure: + * + * - label: string, required, the button label. + * - options: array, optional, the HTML attributes of the button. + * - visible: bool, optional, whether this button is visible. Defaults to true. + */ + public 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); + } +} diff --git a/src/ButtonToolbar.php b/src/ButtonToolbar.php new file mode 100644 index 0000000..21603c1 --- /dev/null +++ b/src/ButtonToolbar.php @@ -0,0 +1,110 @@ + [ + * [ + * '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 + */ +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); + } +} diff --git a/src/Carousel.php b/src/Carousel.php new file mode 100644 index 0000000..0085ac4 --- /dev/null +++ b/src/Carousel.php @@ -0,0 +1,200 @@ + [ + * // the item contains only the image + * '', + * // equivalent to the above + * ['content' => ''], + * // the item contains both the image and the caption + * [ + * 'content' => '', + * 'caption' => '

    This is title

    This is the caption text

    ', + * 'captionOptions' => ['class' => ['d-none', 'd-md-block']] + * 'options' => [...], + * ], + * ] + * ]); + * ``` + * + * @see https://getbootstrap.com/docs/5.0/components/carousel/ + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Carousel extends Widget +{ + /** + * @var array|null the labels for the previous and the next control buttons. + * If null, it means the previous and the next control buttons should not be displayed. + */ + public ?array $controls = [ + 'Previous', + 'Next', + ]; + + /** + * @var bool whether carousel indicators (
      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' => '', + * // optional, the caption (HTML) of the slide + * 'caption' => '

      This is title

      This is the caption text

      ', + * // optional the HTML attributes of the slide container + * 'options' => [], + * ] + * ``` + */ + public 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.'); + } + } +} diff --git a/src/Dropdown.php b/src/Dropdown.php new file mode 100644 index 0000000..6d3a9c6 --- /dev/null +++ b/src/Dropdown.php @@ -0,0 +1,161 @@ + + * Label + * [ + * ['label' => 'DropdownA', 'url' => '/'], + * ['label' => 'DropdownB', 'url' => '#'], + * ], + * ]); + * ?> + * + * ``` + * @see https://getbootstrap.com/docs/5.0/components/dropdowns/ + * @author Antonio Ramirez + * @author Simon Karlen + */ +class Dropdown extends Widget +{ + /** + * @var array list of menu items in the dropdown. Each array element can be either an HTML string, + * or an array representing a single menu with the following structure: + * + * - label: string, required, the label of the item link. + * - encode: bool, optional, whether to HTML-encode item label. + * - url: string|array, optional, the URL of the item link. This will be processed by [[\yii\helpers\Url::to()]]. + * If not set, the item will be treated as a menu header when the item has no sub-menu. + * - visible: bool, optional, whether this menu item is visible. Defaults to true. + * - disabled: bool, optional, whether this menu item is disabled. Defaults to false. + * - linkOptions: array, optional, the HTML attributes of the item link. + * - options: array, optional, the HTML attributes of the item. + * - active: bool, optional, whether the item should be on active state or not. + * - items: array, optional, the submenu items. The structure is the same as this property. + * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. + * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be + * merged with [[submenuOptions]]. + * + * To insert divider use `-`. + */ + public 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); + } +} diff --git a/src/Html.php b/src/Html.php new file mode 100644 index 0000000..5d46d44 --- /dev/null +++ b/src/Html.php @@ -0,0 +1,12 @@ + [ + * 'definitions' => [ + * \yii\widgets\LinkPager::class => \yii\bootstrap5\LinkPager::class, + * ], + * ], + * ``` + * + * @see https://getbootstrap.com/docs/5.0/components/pagination/ + * @author Simon Karlen + * @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 = "»\nNext"; + /** + * @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 = "«\nPrevious"; + /** + * @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]; + } +} diff --git a/src/Modal.php b/src/Modal.php new file mode 100644 index 0000000..0e7c3c9 --- /dev/null +++ b/src/Modal.php @@ -0,0 +1,313 @@ + 'Hello world', + * 'toggleButton' => ['label' => 'click me'], + * ]); + * + * echo 'Say hello...'; + * + * Modal::end(); + * ~~~ + * + * @see https://getbootstrap.com/docs/5.0/components/modal/ + * @author Antonio Ramirez + * @author Qiang Xue + * @author Simon Karlen + */ +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 '×'. + * + * 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', '×', [ + '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']); + } + } +} diff --git a/src/Nav.php b/src/Nav.php new file mode 100644 index 0000000..e910ca3 --- /dev/null +++ b/src/Nav.php @@ -0,0 +1,292 @@ + [ + * [ + * 'label' => 'Home', + * 'url' => ['site/index'], + * 'linkOptions' => [...], + * ], + * [ + * 'label' => 'Dropdown', + * 'items' => [ + * ['label' => 'Level 1 - Dropdown A', 'url' => '#'], + * '', + * '', + * ['label' => 'Level 1 - Dropdown B', 'url' => '#'], + * ], + * ], + * [ + * 'label' => 'Login', + * 'url' => ['site/login'], + * 'visible' => Yii::$app->user->isGuest + * ], + * ], + * 'options' => ['class' =>'nav-pills'], // set this to nav-tabs to get tab-styled navigation + * ]); + * ``` + * + * Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 4. + * + * @see https://getbootstrap.com/docs/5.0/components/navs/ + * @see https://getbootstrap.com/docs/5.0/components/dropdowns/ + * + * @author Antonio Ramirez + */ +class Nav extends Widget +{ + /** + * @var array list of items in the nav widget. Each array element represents a single + * menu item which can be either a string or an array with the following structure: + * + * - label: string, required, the nav item label. + * - url: optional, the item's URL. Defaults to "#". + * - visible: bool, optional, whether this menu item is visible. Defaults to true. + * - disabled: bool, optional, whether this menu item is disabled. Defaults to false. + * - linkOptions: array, optional, the HTML attributes of the item's link. + * - options: array, optional, the HTML attributes of the item container (LI). + * - active: bool, optional, whether the item should be on active state or not. + * - dropdownOptions: array, optional, the HTML options that will passed to the [[Dropdown]] widget. + * - items: array|string, optional, the configuration array for creating a [[Dropdown]] widget, + * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus. + * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for only this item. + * + * If a menu item is a string, it will be rendered directly without HTML encoding. + */ + public 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; + } +} diff --git a/src/NavBar.php b/src/NavBar.php new file mode 100644 index 0000000..88bf6f1 --- /dev/null +++ b/src/NavBar.php @@ -0,0 +1,211 @@ + '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 + * @author Alexander Kochetov + */ +class NavBar extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "nav", the name of the container tag. + * + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public 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 `` + */ + public string $togglerContent = ''; + /** + * @var array the HTML attributes of the navbar toggler button. + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + */ + public 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; + } +} diff --git a/src/Progress.php b/src/Progress.php new file mode 100644 index 0000000..a565588 --- /dev/null +++ b/src/Progress.php @@ -0,0 +1,181 @@ + 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 + * @author Alexander Makarov + * @author Simon Karlen + */ +class Progress extends Widget +{ + /** + * @var string the button label. This property will only be considered if [[bars]] is empty + */ + public $label; + /** + * @var int the amount of progress as a percentage. This property will only be considered if [[bars]] is empty + */ + public $percent = 0; + /** + * @var array the HTML attributes of the bar. This property will only be considered if [[bars]] is empty + * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered. + * @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); + } +} diff --git a/src/Tabs.php b/src/Tabs.php new file mode 100644 index 0000000..1bf0a41 --- /dev/null +++ b/src/Tabs.php @@ -0,0 +1,263 @@ + [ + * [ + * '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 + * @author Simon Karlen + */ +class Tabs extends Widget +{ + /** + * @var array list of tabs in the tabs widget. Each array element represents a single + * tab with the following structure: + * + * - label: string, required, the tab header label. + * - encode: bool, optional, whether this label should be HTML-encoded. This param will override + * global `$this->encodeLabels` param. + * - headerOptions: array, optional, the HTML attributes of the tab header. + * - linkOptions: array, optional, the HTML attributes of the tab header link tags. + * - content: string, optional, the content (HTML) of the tab pane. + * - url: string, optional, an external URL. When this is specified, clicking on this tab will bring + * the browser to this URL. This option is available since version 2.0.4. + * - options: array, optional, the HTML attributes of the tab pane container. + * - active: bool, optional, whether this item tab header and pane should be active. If no item is marked as + * 'active' explicitly - the first one will be activated. + * - visible: bool, optional, whether the item tab header and pane should be visible or not. Defaults to true. + * - disabled: bool, optional, whether the item tab header and pane should be disabled or not. Defaults to false. + * - items: array, optional, can be used instead of `content` to specify a dropdown items + * configuration array. Each item can hold three extra keys, besides the above ones: + * * active: bool, optional, whether the item tab header and pane should be visible or not. + * * content: string, required if `items` is not set. The content (HTML) of the tab pane. + * * options: optional, array, the HTML attributes of the tab content container. + */ + public 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) : ''; + } +} diff --git a/src/Toast.php b/src/Toast.php new file mode 100644 index 0000000..f3213d1 --- /dev/null +++ b/src/Toast.php @@ -0,0 +1,210 @@ + '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 + */ +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 '×'. + * + * 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', '×', [ + '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', + ]; + } + } +} diff --git a/src/ToggleButtonGroup.php b/src/ToggleButtonGroup.php new file mode 100644 index 0000000..0b17bed --- /dev/null +++ b/src/ToggleButtonGroup.php @@ -0,0 +1,134 @@ +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 + * @author Simon Karlen + */ +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, + ]); + } +} diff --git a/src/Widget.php b/src/Widget.php new file mode 100644 index 0000000..b161099 --- /dev/null +++ b/src/Widget.php @@ -0,0 +1,19 @@ +