diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 8f2209d17ff..594c69c1c12 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -228,6 +228,7 @@ Yii Framework 2 Change Log - Bug #18993: Load defaults by `attributes()` in `yii\db\ActiveRecord::loadDefaultValues()` (WinterSilence) - Bug #19021: Fix return type in PhpDoc `yii\db\Migration` functions `up()`, `down()`, `safeUp()` and `safeDown()` (WinterSilence, rhertogh) - Bug #19030: Add DI container usage to `yii\base\Widget::end()` (papppeter) +- Enh #19100: Add methods `isCustomTagAttribute`, `normalizeTagAttributes` and `mergeTagAttributes` in `yii\helper\BaseHtml` (WinterSilence) - Bug #19031: Fix displaying console help for parameters with declared types (WinterSilence) - Bug #19096: Fix `Request::getIsConsoleRequest()` may return erroneously when testing a Web application in Codeception (WinterSilence) - Enh #13105: Add yiiActiveForm `validate_only` property for skipping form auto-submission (ptolomaues) diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index ff81df22752..d0f4d0bd6c4 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -2242,6 +2242,100 @@ public static function cssStyleToArray($style) return $result; } + /** + * Checks if is custom attribute of tag by prefix [[$dataAttributes]]. + * + * @param string $name the attribute name + * @param string|null $prefix the attribute prefix returns by reference + * @return bool + * @since 2.0.44 + */ + public static function isCustomTagAttribute($name, &$prefix = null) + { + foreach (static::$dataAttributes as $prefix) { + if (StringHelper::startsWith($name, $prefix . '-')) { + return true; + } + } + $prefix = null; + + return false; + } + + /** + * Normalizes an array of tag attributes. + * + * ```php + * $attributes = Html::normalizeAttributes(['data-id' => 1, 'style' => 'height: 1px', 'class' => 'foo bar']); + * // $attributes: ['data' => ['id' => 1], 'style' => ['height' => '1px'], 'class' => ['foo', 'bar']] + * ``` + * + * @param array $attributes the attributes to normalize + * @return array + * @since 2.0.44 + */ + public static function normalizeTagAttributes(array $attributes) + { + $result = []; + + if (isset($attributes['style'])) { + if (is_string($attributes['style'])) { + $attributes['style'] = static::cssStyleToArray($attributes['style']); + } + $result['style'] = $attributes['style']; + unset($attributes['style']); + } + if (isset($attributes['class'])) { + if (is_string($attributes['class'])) { + $attributes['class'] = explode(' ', $attributes['class']); + } + $result['class'] = array_unique($attributes['class']); + unset($attributes['class']); + } + foreach ($attributes as $name => $value) { + if (is_array($value) && in_array($name, static::$dataAttributes, true)) { + $result[$name] = isset($result[$name]) ? array_merge($result[$name], $value) : $value; + } elseif (static::isCustomTagAttribute($name, $prefix)) { + if (!isset($result[$prefix])) { + $result[$prefix] = []; + } + list(, $name) = explode($prefix . '-', $name, 2); + $result[$prefix][$name] = $value; + } else { + $result[$name] = $value; + } + } + + return $result; + } + + /** + * Normalizes and merges two arrays of tag attributes. + * + * @param array $attributes The input attributes + * @param array $attributes2 The attributes to merge + * @return array + * @since 2.0.44 + */ + public static function mergeTagAttributes(array $attributes, array $attributes2) + { + $attributes = static::normalizeTagAttributes($attributes); + $attributes2 = static::normalizeTagAttributes($attributes2); + if (isset($attributes2['class'])) { + static::addCssClass($attributes, $attributes2['class']); + unset($attributes2['class']); + } + foreach ($attributes2 as $key => $value) { + if (in_array($key, static::$dataAttributes, true) || $key === 'style') { + $attributes[$key] = isset($attributes[$key]) ? array_merge($attributes[$key], $value) : $value; + } else { + $attributes[$key] = $value; + } + } + + return $attributes; + } + /** * Returns the real attribute name from the given attribute expression. * diff --git a/tests/framework/helpers/HtmlTest.php b/tests/framework/helpers/HtmlTest.php index 24c8186181b..43bd6f33edd 100644 --- a/tests/framework/helpers/HtmlTest.php +++ b/tests/framework/helpers/HtmlTest.php @@ -1411,6 +1411,126 @@ public function testRemoveCssStyle() $this->assertEquals('width: 100px;', $options['style']); } + /** + * @return array[] + */ + public function dataProviderIsCustomTagAttribute() + { + return [ + 'valid "aria" prefix' => ['aria-expanded', true], + 'valid "data" prefix' => ['data-bs-target', true], + 'valid "data-ng" prefix' => ['data-ng-id', true], + 'underscore suffix' => ['data-foo_bar', true], + 'name in camelCase' => ['dataId', false], + 'only "aria" prefix' => ['aria', false], + 'invalid "js" prefix' => ['js-action', false], + ]; + } + + /** + * @param string $name the attribute name + * @param bool $expected the expected result + * @dataProvider dataProviderIsCustomTagAttribute + */ + public function testIsCustomTagAttribute($name, $expected) + { + $this->assertEquals($expected, Html::isCustomTagAttribute($name)); + } + + /** + * @return array[] + */ + public function dataProviderNormalizeTagAttributes() + { + return [ + 'mixed "data"' => [ + [ + 'data-id' => 2, + 'data' => ['foo-bar' => 'baz'], + ], + [ + 'data' => ['id' => 2, 'foo-bar' => 'baz'], + ], + ], + 'inline "style"' => [ + [ + 'style' => 'height:1px;width:100px', + ], + [ + 'style' => ['height' => '1px', 'width' => '100px'], + ], + ], + 'inline "class"' => [ + [ + 'class' => 'form-control form-control-sm', + ], + [ + 'class' => ['form-control', 'form-control-sm'], + ], + ], + 'duplicate "class"' => [ + [ + 'class' => 'form-control form-control', + ], + [ + 'class' => ['form-control'], + ], + ], + 'all cases' => [ + [ + 'data-id' => 123, + 'data-bs-target' => '#input-name', + 'aria-hidden' => 'true', + 'style' => 'display: none;', + 'class' => 'navbar navbar-dark', + 'method' => 'POST', + 'id' => 'main-form', + ], + [ + 'data' => ['id' => 123, 'bs-target' => '#input-name'], + 'aria' => ['hidden' => 'true'], + 'style' => ['display' => 'none'], + 'class' => ['navbar', 'navbar-dark'], + 'method' => 'POST', + 'id' => 'main-form' + ], + ], + ]; + } + + /** + * @param array $attributes the attributes to normalize + * @param array $expected the normalized attributes + * @dataProvider dataProviderNormalizeTagAttributes + */ + public function testNormalizeTagAttributes(array $attributes, array $expected) + { + $this->assertEquals($expected, Html::normalizeTagAttributes($attributes)); + } + + public function testMergeTagAttributes() + { + $attributes = [ + 'data-id' => 1, + 'data-bs-target' => '#input-name', + 'style' => 'display: none;', + 'class' => ['widget' => 'navbar navbar-dark'], + 'id' => 'form-1', + ]; + $attributes2 = [ + 'data' => ['id' => 2, 'foo' => 'bar'], + 'class' => 'bg-primary', + 'id' => 'form-2', + ]; + $expected = [ + 'data' => ['id' => 2, 'bs-target' => '#input-name', 'foo' => 'bar'], + 'style' => ['display' => 'none'], + 'class' => ['widget' => 'navbar navbar-dark', 'bg-primary'], + 'id' => 'form-2', + ]; + $this->assertEquals($expected, Html::mergeTagAttributes($attributes, $attributes2)); + } + public function testBooleanAttributes() { $this->assertEquals('', Html::input('email', 'mail', null, ['required' => false]));