├── .meta-storm.xml ├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer-require-checker.json ├── composer.json ├── infection.json.dist ├── psalm.xml ├── rector.php └── src ├── Html.php ├── NoEncode.php ├── NoEncodeStringableInterface.php ├── Tag ├── A.php ├── Address.php ├── Article.php ├── Aside.php ├── Audio.php ├── B.php ├── Base │ ├── BooleanInputTag.php │ ├── InputTag.php │ ├── ListTag.php │ ├── MediaTag.php │ ├── NormalTag.php │ ├── TableCellTag.php │ ├── TableRowsContainerTag.php │ ├── Tag.php │ ├── TagContentTrait.php │ ├── TagSourcesTrait.php │ └── VoidTag.php ├── Body.php ├── Br.php ├── Button.php ├── Caption.php ├── Code.php ├── Col.php ├── Colgroup.php ├── CustomTag.php ├── Datalist.php ├── Div.php ├── Em.php ├── Fieldset.php ├── Footer.php ├── Form.php ├── H1.php ├── H2.php ├── H3.php ├── H4.php ├── H5.php ├── H6.php ├── Header.php ├── Hgroup.php ├── Hr.php ├── Html.php ├── I.php ├── Img.php ├── Input.php ├── Input │ ├── Checkbox.php │ ├── File.php │ ├── Radio.php │ └── Range.php ├── Label.php ├── Legend.php ├── Li.php ├── Link.php ├── Meta.php ├── Nav.php ├── Noscript.php ├── Ol.php ├── Optgroup.php ├── Option.php ├── P.php ├── Picture.php ├── Pre.php ├── Script.php ├── Section.php ├── Select.php ├── Small.php ├── Source.php ├── Span.php ├── Strong.php ├── Style.php ├── Table.php ├── Tbody.php ├── Td.php ├── Textarea.php ├── Tfoot.php ├── Th.php ├── Thead.php ├── Title.php ├── Tr.php ├── Track.php ├── Ul.php └── Video.php └── Widget ├── ButtonGroup.php ├── CheckboxList ├── CheckboxItem.php └── CheckboxList.php └── RadioList ├── RadioItem.php └── RadioList.php /.meta-storm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii HTML Change Log 2 | 3 | ## 3.10.1 under development 4 | 5 | - New #237: Add classes for `Code` and `Pre` tags, `Html::code()` and `Html::pre()` methods (@FrankiFixx) 6 | - Bug #245: Explicitly marking parameters as nullable (@Tigrov) 7 | 8 | ## 3.10.0 April 03, 2025 9 | 10 | - Chg #240, #242: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik) 11 | - Enh #223: Make `$content` parameter optional in `Button` factories (@FrankiFixx) 12 | - Enh #244: Allow to pass `null` to `Select::value()` method (@vjik) 13 | - Bug #232: Render `loading` attribute before `src` (@samdark) 14 | - Bug #242: Fix the nullable parameter declarations for compatibility with PHP 8.4 (@vjik) 15 | 16 | ## 3.9.0 November 29, 2024 17 | 18 | - Enh #230: Add backed enumeration value support to `Html::addCssClass()`, `Tag::addClass()` and `Tag::class()` 19 | methods (@terabytesoftw) 20 | 21 | ## 3.8.0 October 29, 2024 22 | 23 | - New #224: Add optional `wrap` parameter to `BooleanInputTag::label()` method that controls whether to wrap input tag 24 | with label tag or place them aside (@vjik) 25 | - New #225: Add `CheckboxList::checkboxLabelWrap()` and `RadioList::radioLabelWrap()` methods (@vjik) 26 | - New #227, #228: Add ability to wrap items in checkbox and radio lists by using methods 27 | `CheckboxList::checkboxWrapTag()`, `CheckboxList::checkboxWrapAttributes()`, `CheckboxList::checkboxWrapClass()`, 28 | `CheckboxList::addCheckboxWrapClass()`, `RadioList::radioWrapTag()`, `RadioList::radioWrapAttributes()`, 29 | `RadioList::radioWrapClass()` and `RadioList::addRadioWrapClass()` (@vjik) 30 | - Enh #220: Add `non-empty-string` psalm type of `Html::generateId()` method result (@vjik) 31 | - Enh #220: Add `non-empty-string|null` psalm type of `Tag::id()` method parameter (@vjik) 32 | - Enh #222: Bump minimal PHP version to 8.1 and refactor (@vjik) 33 | 34 | ## 3.7.0 September 18, 2024 35 | 36 | - New #218: Add methods `Script::nonce()` and `Script::getNonce()` for CSP (@Gerych1984, @vjik) 37 | - Enh #219: Add backed enumeration value support to `Select` tag (@vjik) 38 | 39 | ## 3.6.0 August 23, 2024 40 | 41 | - Enh #212: Throw `InvalidArgumentException` in `Html::renderAttribute()` when attribute name is empty or contains 42 | forbidden symbols (@es-sayers, @vjik) 43 | - Enh #214: Add `Stringable` and array values support to textarea tag (@vjik) 44 | - Enh #217: Add backed enumeration value support to `CheckboxList` and `RadioList` widgets (@vjik) 45 | - Bug #208: Fix output of `null` value attributes in `Html::renderTagAttributes()` (@es-sayers) 46 | 47 | ## 3.5.0 July 11, 2024 48 | 49 | - New #192: Add class for tag `hr` and method `Html::hr()` (@abdulmannans) 50 | - Enh #200: Add support for multiple elements in `aria-describedby` attribute (@arogachev) 51 | 52 | ## 3.4.0 December 26, 2023 53 | 54 | - New #182: Add ability set attributes for label of items in widgets `CheckboxList` and `RadioList` (@vjik) 55 | 56 | ## 3.3.0 December 01, 2023 57 | 58 | - New #173: Add class for tag `html` and method `Html::html()` (@dood-) 59 | - Chg #179: Replace constant `PHP_EOL` to `"\n"` (@vjik) 60 | - Enh #180: Don't add "class" attribute in `Html::addCssClass()` if passed array contains null classes only (@vjik) 61 | 62 | ## 3.2.0 November 21, 2023 63 | 64 | - New #174: Add `$attributes` parameter to `Html::ul()` and `Html::ol()` (@AmolKumarGupta) 65 | - Enh #176: Allow pass `null` as class to `Html::addCssClass()`, nulled classes will be ignored (@vjik) 66 | - Bug #171: Fix loss of keys for named class in `Html::addCssClass()` when class in passed options is a string (@vjik) 67 | 68 | ## 3.1.0 January 17, 2023 69 | 70 | - New #137: Add `$attributes` parameter to `Html::img()` (@alien-art) 71 | - New #150: Add class for tag `small` and method `Html::small()` (@dood-) 72 | - Enh #153: Add support of `yiisoft/arrays` version `^3.0` (@vjik) 73 | 74 | ## 3.0.0 November 06, 2022 75 | 76 | - New #139: Add `loading()` method to `Img` tag (@jacobbudin) 77 | - Chg #135: Raise `yiisoft/arrays` version to `^2.0` (@vjik) 78 | - Chg #136: Remove `Tag::class()` and rename `Tag::replaceClass()` to `Tag::class()` (@vjik) 79 | - Chg #140: Remove `Html::fileInput()` and `Input::file()`, rename `Input::fileControl()` to `Input::file()` (@vjik) 80 | - Chg #141: `Tag` class: remove `attributes()` method and rename `replaceAttributes()` to `attributes()` (@vjik) 81 | - Chg #141: `Range` class: remove `outputAttributes()` method and rename `replaceOutputAttributes()` 82 | to `outputAttributes()` (@vjik) 83 | - Chg #141: `File` class: remove `uncheckInputAttributes()` method and rename `replaceUncheckInputAttributes()` 84 | to `uncheckInputAttributes()` (@vjik) 85 | - Chg #141: `ButtonGroup` class: remove `buttonAttributes()` method and rename `replaceButtonAttributes()` 86 | to `buttonAttributes()` (@vjik) 87 | - Chg #141: `CheckboxList`: remove `individualInputAttributes()` and `checkboxAttributes()` methods, 88 | rename `replaceIndividualInputAttributes()` to `individualInputAttributes()` and `replaceCheckboxAttributes()` 89 | to `checkboxAttributes()` (@vjik) 90 | - Chg #141: `RadioList` class: remove `individualInputAttributes()` and `radioAttributes()` methods, 91 | rename `replaceIndividualInputAttributes()` to `individualInputAttributes()` and `replaceRadioAttributes()` 92 | to `radioAttributes()` (@vjik) 93 | - Enh #133: Raise minimum PHP version to 8.0 and refactor code (@xepozz, @vjik) 94 | - Enh #142: Make `NoEncodeStringableInterface` extend from `Stringable` interface (@vjik) 95 | - Enh #143: Minor type hinting improvements (@vjik) 96 | - Bug #143: Fix a typo in the message of exception that thrown on invalid buttons' data in `ButtonGroup` widget (@vjik) 97 | 98 | ## 2.5.0 July 09, 2022 99 | 100 | - New #122: Add `Tag::addAttributes()`, `ButtonGroup::addButtonAttributes()`, `RadioList::addRadioAttributes()`, 101 | `RadioList::addIndividualInputAttributes()`, `CheckboxList::addCheckboxAttributes()`, 102 | `CheckboxList::addIndividualInputAttributes()`, `File::addUncheckInputAttributes()`, `Range::addOutputAttributes()` and 103 | deprecate `Tag::attributes()`, `ButtonGroup::buttonAttributes()`, `RadioList::radioAttributes()`, 104 | `RadioList::individualInputAttributes()`, `CheckboxList::checkboxAttributes()`, 105 | `CheckboxList::individualInputAttributes()`, `File::uncheckInputAttributes()`, `Range::outputAttributes()` (@vjik) 106 | - New #123: Add `Tag::addClass()` and deprecate `Tag::class()` (@vjik) 107 | - New #129: Add methods `enctypeApplicationXWwwFormUrlencoded()`, `enctypeMultipartFormData()` and `enctypeTextPlain()` 108 | to `Form` tag class (@vjik) 109 | 110 | ## 2.4.0 May 19, 2022 111 | 112 | - New #97: Add classes for tags `Body`, `Article`, `Section`, `Nav`, `Aside`, `Hgroup`, `Header`, `Footer`, `Address` 113 | and methods `Html::body()`, `Html::article()`, `Html::section()`, `Html::nav()`, `Html::aside()`, `Html::hgroup()`, 114 | `Html::header()`, `Html::footer()`, `Html::address()` (@soodssr) 115 | - New #103: Add class for tag `Form` and method `Html::form()` (@vjik) 116 | - New #105: Add specialized class `File` for an input tag with type `file` and methods `Html::file()` and 117 | `Input::fileControl()` (@vjik) 118 | - New #109: Add class for tag `Datalist` and method `Html::datalist()` (@vjik) 119 | - New #109, #117: Add specialized class for input tag with type `Range` and methods `Html::range()`, 120 | `Input::range()` (@vjik) 121 | - New #111: Add widget `ButtonGroup` (@vjik) 122 | - New #111: Add method `Tag::unionAttributes()` that available for all tags (@vjik) 123 | - New #113: Add class for tag `Legend`, class for tag `Fieldset`, methods `Html::legend()` and `Html::fieldset()` (@vjik) 124 | - Enh #102: Remove psalm type `HtmlAttributes`, too obsessive for package users (@vjik) 125 | - Enh #104: Add parameter `$attributes` to methods `Html::input()`, `Html::buttonInput()`, `Html::submitInput()` 126 | and `Html::resetInput()` (@vjik) 127 | - Enh #106: Add option groups support to method `Select::optionsData()` (@vjik) 128 | - Enh #108: Add individual attributes of options and option groups support to method `Select::optionsData()` (@vjik) 129 | - Enh #115: Add methods `CheckboxList::name()` and `RadioList::name()` (@vjik) 130 | 131 | ## 2.3.0 March 25, 2022 132 | 133 | - New #95: Add class for tag `Title` and method `Html::title()` (@vjik) 134 | - New #96: Add classes for heading tags `H1-6` and methods `Html::h1()`, `Html::h2()`, `Html::h3()`, `Html::h4()`, 135 | `Html::h5()`, `Html::h6()` (@vjik) 136 | - New #100: Add classes for tags `Picture`, `Audio`, `Video`, `Source` and `Track` (@Gerych1984, @vjik) 137 | 138 | ## 2.2.1 October 24, 2021 139 | 140 | - Enh #93: Add support for `yiisoft/arrays` version `^2.0` (@vjik) 141 | 142 | ## 2.2.0 October 20, 2021 143 | 144 | - New #89: Add method `nofollow()` to the `A` tag (@soodssr) 145 | - New #90: Add method `itemsFromValues()` to widgets `RadioList` and `CheckboxList` that set items with labels equal 146 | to values (@vjik) 147 | - New #92: A third optional argument `$attributes` containing tag attributes in terms of name-value pairs has been 148 | added to methods `Html::textInput()`, `Html::hiddenInput()`, `Html::passwordInput()`, `Html::fileInput()`, 149 | `Html::radio()`, `Html::checkbox()`, `Html::textarea()` (@vjik) 150 | 151 | ## 2.1.0 September 23, 2021 152 | 153 | - New #88: Add `Noscript` tag support and shortcuts for `Script` tag via methods `Script::noscript()` 154 | and `Script::noscriptTag()` (@vjik) 155 | 156 | ## 2.0.0 August 24, 2021 157 | 158 | - New #74: Add classes for tags `Em`, `Strong`, `B` and `I` (@vjik) 159 | - New #75: Add methods `as()` and `preload()` to the `Link` tag (@vjik) 160 | - New #76: Add `NoEncode` class designed to wrap content that should not be encoded in HTML tags (@vjik) 161 | - New #78: Allow pass `null` argument to methods `Tag::class()`, `Tag::replaceClass()`, `BooleanInputTag::label()` and 162 | `BooleanInputTag::sideLabel()` (@vjik) 163 | - New #82: Add support individual attributes for inputs in `CheckboxList` and `RadioList` widgets via methods 164 | `CheckboxList::individualInputAttributes()`, `CheckboxList::replaceIndividualInputAttributes()`, 165 | `RadioList::individualInputAttributes()` and `RadioList::replaceIndividualInputAttributes()` (@vjik) 166 | - Chg #79: Do not add empty attribute value for empty strings (@vjik) 167 | - Bug #83: Fix `Html::ATTRIBUTE_ORDER` values (@terabytesoftw) 168 | 169 | ## 1.2.0 May 04, 2021 170 | 171 | - New #70: Add support `\Stringable` as content in methods `Html::tag()`, `Html::normalTag()`, `Html::a()`, 172 | `Html::label()`, `Html::option()`, `Html::div()`, `Html::span()`, `Html::p()`, `Html::li()`, `Html::caption()`, 173 | `Html::td()`, `Html::th()` (@vjik) 174 | - New #71: Add methods `Script::getContent()` and `Style::getContent()` (@vjik) 175 | 176 | ## 1.1.0 April 09, 2021 177 | 178 | - New #65: Add classes for table tags `Table`, `Caption`, `Colgroup`, `Col`, `Thead`, `Tbody`, `Tfoot`, `Tr`, `Th`, `Td` (@vjik) 179 | - New #69: Add class for tag `Br` (@vjik) 180 | 181 | ## 1.0.1 April 04, 2021 182 | 183 | - Bug #68: Fix `TagContentTrait::content()` and `TagContentTrait::addContent()` when used with named parameters (@vjik) 184 | 185 | ## 1.0.0 March 17, 2021 186 | 187 | - Initial release. 188 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii HTML

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/html/v)](https://packagist.org/packages/yiisoft/html) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/html/downloads)](https://packagist.org/packages/yiisoft/html) 11 | [![Build status](https://github.com/yiisoft/html/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/html/actions/workflows/build.yml) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/html/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/html) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fhtml%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/html/master) 14 | [![static analysis](https://github.com/yiisoft/html/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/html/actions?query=workflow%3A%22static+analysis%22) 15 | [![psalm-level](https://shepherd.dev/github/yiisoft/html/level.svg)](https://shepherd.dev/github/yiisoft/html) 16 | [![type-coverage](https://shepherd.dev/github/yiisoft/html/coverage.svg)](https://shepherd.dev/github/yiisoft/html) 17 | 18 | The package provides various tools to help with dynamic server-side generation of HTML: 19 | 20 | - Tag classes `A`, `Address`, `Article`, `Aside`, `Audio`, `B`, `Body`, `Br`, `Button`, `Caption`, `Col`, `Colgroup`, 21 | `Datalist`, `Div`, `Em`, `Fieldset`, `Footer`, `Form`, `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `Header`, `Hr`, `Hgroup`, 22 | `Html`, `I`, `Img`, `Input` (and specialized `Checkbox`, `Radio`, `Range`, `File`), `Label`, `Legend`, `Li`, `Link`, 23 | `Meta`, `Nav`, `Noscript`, `Ol`, `Optgroup`, `Option`, `P`, `Picture`, `Script`, `Section`, `Select`, `Small`, 24 | `Source`, `Span`, `Strong`, `Style`, `Table`, `Tbody`, `Td`, `Textarea`, `Tfoot`, `Th`, `Thead`, `Title`, `Tr`, 25 | `Track`, `Ul`, `Video`. 26 | - `CustomTag` class that helps to generate custom tag with any attributes. 27 | - HTML widgets `ButtonGroup`, `CheckboxList` and `RadioList`. 28 | - All tags content is automatically HTML-encoded. There is `NoEncode` class designed to wrap content that should not be encoded. 29 | - `Html` helper that has static methods to generate HTML, create tags and HTML widget objects. 30 | 31 | Note that for simple static-HTML cases, it is preferred to use HTML directly. 32 | 33 | ## Requirements 34 | 35 | - PHP 8.1 or higher. 36 | 37 | ## Installation 38 | 39 | The package could be installed with [Composer](https://getcomposer.org): 40 | 41 | ```shell 42 | composer require yiisoft/html 43 | ``` 44 | 45 | ## General usage 46 | 47 | ```php 48 | 52 | 53 | 54 | 55 | 56 | 'sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T', 60 | 'crossorigin' => 'anonymous' 61 | ] 62 | ) ?> 63 | 'stylesheet']) ?> 64 | 65 | 'footer']) ?> 66 | 'container flex-fill']) ?> 67 | 'float-left']) ?> 68 | class('float-right') 70 | ->content( 71 | 'Powered by ', 72 | Html::a( 73 | 'Yii Framework', 74 | 'https://www.yiiframework.com/', 75 | ['rel' => 'external'] 76 | ) 77 | ) ?> 78 | 79 | 80 | ``` 81 | 82 | ## Tag objects usage 83 | 84 | Tag classes allow working with a tag as an object and then get an HTML code by using `render()` method or type casting 85 | to string. For example, the following code: 86 | 87 | ```php 88 | echo \Yiisoft\Html\Tag\Div::tag() 89 | ->content( 90 | \Yiisoft\Html\Tag\A::tag() 91 | ->mailto('info@example.com') 92 | ->content('contact us') 93 | ->render() 94 | ) 95 | ->encode(false) 96 | ->id('ContactEmail') 97 | ->class('red'); 98 | ``` 99 | 100 | ... will generate the following HTML: 101 | 102 | ```html 103 |
contact us
104 | ``` 105 | 106 | ### Generating custom tags 107 | 108 | To generate custom tags, use the `CustomTag` class. For example, the following code: 109 | 110 | ```php 111 | echo \Yiisoft\Html\Tag\CustomTag::name('b') 112 | ->content('text') 113 | ->attribute('title', 'Important'); 114 | ``` 115 | 116 | ... will generate the following HTML: 117 | 118 | ```html 119 | text 120 | ``` 121 | 122 | ### Encoding tags content 123 | 124 | By default, stringable objects that implement `\Yiisoft\Html\NoEncodeStringableInterface` are not encoded, 125 | everything else is encoded. 126 | 127 | To change this behavior use `encode()` method passing one of the following values: 128 | 129 | - `null`: default behavior; 130 | - `true`: any content is encoded; 131 | - `false`: nothing is encoded. 132 | 133 | > Note: all bundled tags and widgets implement `\Yiisoft\Html\NoEncodeStringableInterface` interface and are not encoded 134 | > by default when passed as content. Their own content is encoded. 135 | 136 | Examples: 137 | 138 | ```php 139 | // <i>hello</i> 140 | echo Html::b('hello'); 141 | 142 | // hello 143 | echo Html::b('hello')->encode(false); 144 | 145 | // hello 146 | echo Html::b(Html::i('hello')); 147 | 148 | // <i>hello</i> 149 | echo Html::b(Html::i('hello'))->encode(true); 150 | ``` 151 | 152 | In order to mark a string as "do not encode" you can use `\Yiisoft\Html\NoEncode` class: 153 | 154 | ```php 155 | // hello 156 | echo Html::b(NoEncode::string('hello')); 157 | ``` 158 | 159 | ## HTML widgets usage 160 | 161 | There are multiple widgets that do not directly represent any HTML tag, but a set of tags. These help to express 162 | complex HTML in simple PHP. 163 | 164 | ### `ButtonGroup` 165 | 166 | Represents a group of buttons. 167 | 168 | ```php 169 | echo \Yiisoft\Html\Widget\ButtonGroup::create() 170 | ->buttons( 171 | \Yiisoft\Html\Html::resetButton('Reset Data'), 172 | \Yiisoft\Html\Html::resetButton('Send'), 173 | ) 174 | ->containerAttributes(['class' => 'actions']) 175 | ->buttonAttributes(['form' => 'CreatePost']); 176 | ``` 177 | 178 | Result will be: 179 | 180 | ```html 181 |
182 | 183 | 184 |
185 | ``` 186 | 187 | ### `CheckboxList` 188 | 189 | Represents a list of checkboxes. 190 | 191 | ```php 192 | echo \Yiisoft\Html\Widget\CheckboxList\CheckboxList::create('count') 193 | ->items([1 => 'One', 2 => 'Two', 5 => 'Five']) 194 | ->uncheckValue(0) 195 | ->value(2, 5) 196 | ->containerAttributes(['id' => 'main']); 197 | ``` 198 | 199 | Result will be: 200 | 201 | ```html 202 | 203 |
204 | 205 | 206 | 207 |
208 | ``` 209 | 210 | ### `RadioList` 211 | 212 | Represents a list of radio buttons. 213 | 214 | ```php 215 | echo \Yiisoft\Html\Widget\RadioList\RadioList::create('count') 216 | ->items([1 => 'One', 2 => 'Two', 5 => 'Five']) 217 | ->uncheckValue(0) 218 | ->value(2) 219 | ->containerAttributes(['id' => 'main']); 220 | ``` 221 | 222 | Result will be: 223 | 224 | ```html 225 | 226 |
227 | 228 | 229 | 230 |
231 | ``` 232 | 233 | ## `Html` helper usage 234 | 235 | `Html` helper methods are static so usage is: 236 | 237 | ```php 238 | echo \Yiisoft\Html\Html::a('Yii Framework', 'https://www.yiiframework.com/'); 239 | ``` 240 | 241 | Overall the helper has the following method groups. 242 | 243 | ### Creating tag objects 244 | 245 | #### Custom tags 246 | 247 | - tag 248 | - normalTag 249 | - voidTag 250 | 251 | #### Base tags 252 | 253 | - b 254 | - div 255 | - em 256 | - i 257 | - hr 258 | - meta 259 | - p 260 | - br 261 | - script 262 | - noscript 263 | - span 264 | - strong 265 | - small 266 | - style 267 | - title 268 | 269 | #### Media tags 270 | 271 | - img 272 | - picture 273 | - audio 274 | - video 275 | - track 276 | - source 277 | 278 | #### Heading tags 279 | 280 | - h1 281 | - h2 282 | - h3 283 | - h4 284 | - h5 285 | - h6 286 | 287 | #### Section tags 288 | 289 | - html 290 | - body 291 | - article 292 | - section 293 | - nav 294 | - aside 295 | - hgroup 296 | - header 297 | - footer 298 | - address 299 | 300 | #### List tags 301 | 302 | - ul 303 | - ol 304 | - li 305 | 306 | #### Hyperlink tags 307 | 308 | - a 309 | - mailto 310 | 311 | #### Link tags 312 | 313 | - link 314 | - cssFile 315 | - javaScriptFile 316 | 317 | #### Form tags 318 | 319 | - button 320 | - buttonInput 321 | - checkbox 322 | - file 323 | - datalist 324 | - fieldset 325 | - fileInput 326 | - form 327 | - hiddenInput 328 | - input 329 | - label 330 | - legend 331 | - optgroup 332 | - option 333 | - passwordInput 334 | - radio 335 | - resetButton 336 | - resetInput 337 | - select 338 | - submitButton 339 | - submitInput 340 | - textInput 341 | - textarea 342 | 343 | #### Table tags 344 | 345 | - table 346 | - caption 347 | - colgroup 348 | - col 349 | - thead 350 | - tbody 351 | - tfoot 352 | - tr 353 | - th 354 | - td 355 | 356 | ### Generating tag parts 357 | 358 | - openTag 359 | - closeTag 360 | - renderTagAttributes 361 | 362 | ### Creating HTML widget objects 363 | 364 | - radioList 365 | - checkboxList 366 | 367 | ### Working with tag attributes 368 | 369 | - generateId 370 | - getArrayableName 371 | - getNonArrayableName 372 | - normalizeRegexpPattern 373 | 374 | ### Encode and escape special characters 375 | 376 | - encode 377 | - encodeAttribute 378 | - encodeUnquotedAttribute 379 | - escapeJavaScriptStringValue 380 | 381 | ### Working with CSS styles and classes 382 | 383 | - addCssStyle 384 | - removeCssStyle 385 | - addCssClass 386 | - removeCssClass 387 | - cssStyleFromArray 388 | - cssStyleToArray 389 | 390 | ## Documentation 391 | 392 | - [Internals](docs/internals.md) 393 | 394 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for 395 | that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 396 | 397 | ## License 398 | 399 | The Yii HTML is free software. It is released under the terms of the BSD License. 400 | Please see [`LICENSE`](./LICENSE.md) for more information. 401 | 402 | Maintained by [Yii Software](https://www.yiiframework.com/). 403 | 404 | ## Support the project 405 | 406 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 407 | 408 | ## Follow updates 409 | 410 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 411 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 412 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 413 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 414 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 415 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "BackedEnum" 4 | ], 5 | "php-core-extensions": [ 6 | "Core", 7 | "date", 8 | "json", 9 | "pcre", 10 | "Phar", 11 | "Reflection", 12 | "SPL", 13 | "standard" 14 | ], 15 | "scan-files": [] 16 | } 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/html", 3 | "type": "library", 4 | "description": "Handy library to generate HTML", 5 | "keywords": [ 6 | "html" 7 | ], 8 | "homepage": "https://www.yiiframework.com/", 9 | "license": "BSD-3-Clause", 10 | "support": { 11 | "issues": "https://github.com/yiisoft/html/issues?state=open", 12 | "source": "https://github.com/yiisoft/html", 13 | "forum": "https://forum.yiiframework.com/", 14 | "wiki": "https://www.yiiframework.com/wiki/", 15 | "irc": "ircs://irc.libera.chat:6697/yii", 16 | "chat": "https://t.me/yii3en" 17 | }, 18 | "funding": [ 19 | { 20 | "type": "opencollective", 21 | "url": "https://opencollective.com/yiisoft" 22 | }, 23 | { 24 | "type": "github", 25 | "url": "https://github.com/sponsors/yiisoft" 26 | } 27 | ], 28 | "require": { 29 | "php": "8.1 - 8.4", 30 | "yiisoft/arrays": "^2.0 || ^3.0", 31 | "yiisoft/json": "^1.0" 32 | }, 33 | "require-dev": { 34 | "infection/infection": "^0.27.11 || ^0.29.10", 35 | "maglnet/composer-require-checker": "^4.7.1", 36 | "phpunit/phpunit": "^10.5.45", 37 | "rector/rector": "^2.0.8", 38 | "spatie/phpunit-watcher": "^1.24", 39 | "vimeo/psalm": "^5.26.1 || ^6.10.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Yiisoft\\Html\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Yiisoft\\Html\\Tests\\": "tests" 49 | } 50 | }, 51 | "config": { 52 | "sort-packages": true, 53 | "allow-plugins": { 54 | "infection/extension-installer": true, 55 | "composer/package-versions-deprecated": true 56 | } 57 | }, 58 | "scripts": { 59 | "test": "phpunit --testdox --no-interaction", 60 | "test-watch": "phpunit-watcher watch" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]); 15 | 16 | // register a single rule 17 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 18 | 19 | // define sets of rules 20 | $rectorConfig->sets([ 21 | LevelSetList::UP_TO_PHP_80, 22 | ]); 23 | 24 | $rectorConfig->skip([ 25 | ClosureToArrowFunctionRector::class, 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /src/NoEncode.php: -------------------------------------------------------------------------------- 1 | hello" 13 | * echo Html:b(NoEncode::string('hello')); 14 | * ``` 15 | */ 16 | final class NoEncode implements NoEncodeStringableInterface 17 | { 18 | private function __construct( 19 | private string $string 20 | ) { 21 | } 22 | 23 | public static function string(string $value): self 24 | { 25 | return new self($value); 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return $this->string; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NoEncodeStringableInterface.php: -------------------------------------------------------------------------------- 1 | attributes['href'] = $href; 24 | return $new; 25 | } 26 | 27 | /** 28 | * Alias for {@see href} 29 | */ 30 | public function url(?string $url): self 31 | { 32 | return $this->href($url); 33 | } 34 | 35 | public function mailto(?string $mail): self 36 | { 37 | return $this->href($mail === null ? null : 'mailto:' . $mail); 38 | } 39 | 40 | public function nofollow(): self 41 | { 42 | return $this->rel('nofollow'); 43 | } 44 | 45 | /** 46 | * @link https://www.w3.org/TR/html52/links.html#element-attrdef-a-rel 47 | */ 48 | public function rel(?string $rel): self 49 | { 50 | $new = clone $this; 51 | $new->attributes['rel'] = $rel; 52 | return $new; 53 | } 54 | 55 | /** 56 | * Default browsing context for hyperlink navigation 57 | * 58 | * @link https://www.w3.org/TR/html52/browsers.html#valid-browsing-context-names-or-keywords 59 | */ 60 | public function target(?string $contextName): self 61 | { 62 | $new = clone $this; 63 | $new->attributes['target'] = $contextName; 64 | return $new; 65 | } 66 | 67 | protected function getName(): string 68 | { 69 | return 'a'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Tag/Address.php: -------------------------------------------------------------------------------- 1 | attributes['checked'] = $checked; 31 | return $new; 32 | } 33 | 34 | /** 35 | * Label that is wraps around attribute when rendered. 36 | * 37 | * @param string|null $label Input label. 38 | * @param array $attributes Name-value set of label attributes. 39 | * @param bool $wrap Whether to wrap input with label tag. If set to `false`, label will be rendered aside with 40 | * input. 41 | */ 42 | final public function label( 43 | ?string $label, 44 | array $attributes = [], 45 | bool $wrap = true, 46 | ): static { 47 | $new = clone $this; 48 | $new->label = $label; 49 | $new->labelAttributes = $attributes; 50 | $new->labelWrap = $wrap; 51 | return $new; 52 | } 53 | 54 | /** 55 | * Label that is rendered separately and is referring input by ID. 56 | * 57 | * @param string|null $label Input label. 58 | * @param array $attributes Name-value set of label attributes. 59 | */ 60 | final public function sideLabel(?string $label, array $attributes = []): static 61 | { 62 | $new = clone $this; 63 | $new->label = $label; 64 | $new->labelAttributes = $attributes; 65 | $new->labelWrap = false; 66 | return $new; 67 | } 68 | 69 | /** 70 | * @param bool $encode Whether to encode label content. Defaults to `true`. 71 | */ 72 | final public function labelEncode(bool $encode): static 73 | { 74 | $new = clone $this; 75 | $new->labelEncode = $encode; 76 | return $new; 77 | } 78 | 79 | /** 80 | * @param bool|float|int|string|Stringable|null $value Value that corresponds to "unchecked" state of the input. 81 | */ 82 | final public function uncheckValue(bool|float|int|string|Stringable|null $value): static 83 | { 84 | $new = clone $this; 85 | $new->uncheckValue = $value === null ? null : (string) $value; 86 | return $new; 87 | } 88 | 89 | final protected function prepareAttributes(): void 90 | { 91 | $this->attributes['type'] = $this->getType(); 92 | } 93 | 94 | final protected function before(): string 95 | { 96 | $this->attributes['id'] ??= ($this->labelWrap || $this->label === null) 97 | ? null 98 | : Html::generateId(); 99 | 100 | return $this->renderUncheckInput() . 101 | ($this->labelWrap ? $this->renderLabelOpenTag($this->labelAttributes) : ''); 102 | } 103 | 104 | private function renderUncheckInput(): string 105 | { 106 | $name = (string)($this->attributes['name'] ?? ''); 107 | if (empty($name) || $this->uncheckValue === null) { 108 | return ''; 109 | } 110 | 111 | $input = Input::hidden( 112 | Html::getNonArrayableName($name), 113 | $this->uncheckValue 114 | ); 115 | 116 | // Make sure disabled input is not sending any value. 117 | if (!empty($this->attributes['disabled'])) { 118 | $input = $input->attribute('disabled', $this->attributes['disabled']); 119 | } 120 | 121 | if (!empty($this->attributes['form'])) { 122 | $input = $input->attribute('form', $this->attributes['form']); 123 | } 124 | 125 | return $input->render(); 126 | } 127 | 128 | private function renderLabelOpenTag(array $attributes): string 129 | { 130 | if ($this->label === null) { 131 | return ''; 132 | } 133 | 134 | return Html::openTag('label', $attributes); 135 | } 136 | 137 | final protected function after(): string 138 | { 139 | if ($this->label === null) { 140 | return ''; 141 | } 142 | 143 | if ($this->labelWrap) { 144 | $html = $this->label === '' ? '' : ' '; 145 | } else { 146 | $labelAttributes = array_merge($this->labelAttributes, [ 147 | 'for' => $this->attributes['id'], 148 | ]); 149 | $html = ' ' . $this->renderLabelOpenTag($labelAttributes); 150 | } 151 | 152 | $html .= $this->labelEncode ? Html::encode($this->label) : $this->label; 153 | 154 | $html .= ''; 155 | 156 | return $html; 157 | } 158 | 159 | abstract protected function getType(): string; 160 | } 161 | -------------------------------------------------------------------------------- /src/Tag/Base/InputTag.php: -------------------------------------------------------------------------------- 1 | attributes['name'] = $name; 23 | return $new; 24 | } 25 | 26 | /** 27 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-input-value 28 | * 29 | * @param bool|float|int|string|Stringable|null $value Value of the input. 30 | */ 31 | public function value(bool|float|int|string|Stringable|null $value): static 32 | { 33 | $new = clone $this; 34 | $new->attributes['value'] = $value; 35 | return $new; 36 | } 37 | 38 | /** 39 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-formelements-form 40 | * 41 | * @param string|null $formId ID of the form input belongs to. 42 | */ 43 | public function form(?string $formId): static 44 | { 45 | $new = clone $this; 46 | $new->attributes['form'] = $formId; 47 | return $new; 48 | } 49 | 50 | /** 51 | * @link https://www.w3.org/TR/html52/sec-forms.html#the-readonly-attribute 52 | * 53 | * @param bool $readOnly Whether input is read only. 54 | */ 55 | public function readonly(bool $readOnly = true): static 56 | { 57 | $new = clone $this; 58 | $new->attributes['readonly'] = $readOnly; 59 | return $new; 60 | } 61 | 62 | /** 63 | * @link https://www.w3.org/TR/html52/sec-forms.html#the-required-attribute 64 | * 65 | * @param bool $required Whether input is required. 66 | */ 67 | public function required(bool $required = true): static 68 | { 69 | $new = clone $this; 70 | $new->attributes['required'] = $required; 71 | return $new; 72 | } 73 | 74 | /** 75 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-disabledformelements-disabled 76 | * 77 | * @param bool $disabled Whether input is disabled. 78 | */ 79 | public function disabled(bool $disabled = true): static 80 | { 81 | $new = clone $this; 82 | $new->attributes['disabled'] = $disabled; 83 | return $new; 84 | } 85 | 86 | protected function getName(): string 87 | { 88 | return 'input'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Tag/Base/ListTag.php: -------------------------------------------------------------------------------- 1 | items = $items; 26 | return $new; 27 | } 28 | 29 | /** 30 | * @param string[] $strings Array of list items as strings. 31 | * @param array $attributes The tag attributes in terms of name-value pairs. 32 | * @param bool $encode Whether to encode strings passed. 33 | */ 34 | public function strings(array $strings, array $attributes = [], bool $encode = true): static 35 | { 36 | $items = array_map( 37 | static fn (string $string) => Li::tag() 38 | ->content($string) 39 | ->attributes($attributes) 40 | ->encode($encode), 41 | $strings 42 | ); 43 | return $this->items(...$items); 44 | } 45 | 46 | protected function generateContent(): string 47 | { 48 | return $this->items 49 | ? "\n" . implode("\n", $this->items) . "\n" 50 | : ''; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tag/Base/MediaTag.php: -------------------------------------------------------------------------------- 1 | fallback = $fallback === null ? null : (string) $fallback; 53 | return $new; 54 | } 55 | 56 | final public function tracks(Track ...$tracks): static 57 | { 58 | $new = clone $this; 59 | $new->tracks = $tracks; 60 | return $new; 61 | } 62 | 63 | final public function addTrack(Track $track): static 64 | { 65 | $new = clone $this; 66 | $new->tracks[] = $track; 67 | return $new; 68 | } 69 | 70 | /** 71 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-src 72 | */ 73 | final public function src(?string $src): static 74 | { 75 | return $this->attribute('src', $src); 76 | } 77 | 78 | /** 79 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-crossorigin 80 | */ 81 | final public function crossOrigin(?string $value): static 82 | { 83 | return $this->attribute('crossorigin', $value); 84 | } 85 | 86 | /** 87 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-preload 88 | */ 89 | final public function preload(?string $preload): static 90 | { 91 | return $this->attribute('preload', $preload); 92 | } 93 | 94 | /** 95 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-muted 96 | */ 97 | final public function muted(bool $muted = true): static 98 | { 99 | return $this->attribute('muted', $muted); 100 | } 101 | 102 | /** 103 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-loop 104 | */ 105 | final public function loop(bool $loop = true): static 106 | { 107 | return $this->attribute('loop', $loop); 108 | } 109 | 110 | /** 111 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-autoplay 112 | */ 113 | final public function autoplay(bool $autoplay = true): static 114 | { 115 | return $this->attribute('autoplay', $autoplay); 116 | } 117 | 118 | /** 119 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-media-autoplay 120 | */ 121 | final public function controls(bool $controls = true): static 122 | { 123 | return $this->attribute('controls', $controls); 124 | } 125 | 126 | final protected function generateContent(): string 127 | { 128 | $items = $this->sources; 129 | 130 | $hasDefaultTrack = false; 131 | foreach ($this->tracks as $track) { 132 | $isDefault = $track->isDefault(); 133 | if ($hasDefaultTrack && $isDefault) { 134 | $items[] = $track->default(false); 135 | } else { 136 | $items[] = $track; 137 | if (!$hasDefaultTrack) { 138 | $hasDefaultTrack = $isDefault; 139 | } 140 | } 141 | } 142 | 143 | if ($this->fallback) { 144 | $items[] = $this->fallback; 145 | } 146 | 147 | return $items ? "\n" . implode("\n", $items) . "\n" : ''; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Tag/Base/NormalTag.php: -------------------------------------------------------------------------------- 1 | open() . $this->generateContent() . $this->close(); 24 | } 25 | 26 | /** 27 | * @return string Opening tag. 28 | */ 29 | final public function open(): string 30 | { 31 | return '<' . $this->getName() . $this->renderAttributes() . '>' . $this->prepend(); 32 | } 33 | 34 | protected function prepend(): string 35 | { 36 | return ''; 37 | } 38 | 39 | /** 40 | * @return string Closing tag. 41 | */ 42 | final public function close(): string 43 | { 44 | return 'getName() . '>'; 45 | } 46 | 47 | abstract protected function generateContent(): string; 48 | } 49 | -------------------------------------------------------------------------------- /src/Tag/Base/TableCellTag.php: -------------------------------------------------------------------------------- 1 | attributes['colspan'] = $number; 18 | return $new; 19 | } 20 | 21 | /** 22 | * Number of rows that the cell is to span. 23 | */ 24 | final public function rowSpan(?int $number): static 25 | { 26 | $new = clone $this; 27 | $new->attributes['rowspan'] = $number; 28 | return $new; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tag/Base/TableRowsContainerTag.php: -------------------------------------------------------------------------------- 1 | rows = $rows; 23 | return $new; 24 | } 25 | 26 | /** 27 | * @param Tr ...$rows One or more rows ({@see Tr}). 28 | */ 29 | final public function addRows(Tr ...$rows): static 30 | { 31 | $new = clone $this; 32 | $new->rows = array_merge($new->rows, $rows); 33 | return $new; 34 | } 35 | 36 | final protected function generateContent(): string 37 | { 38 | return $this->rows 39 | ? "\n" . implode("\n", $this->rows) . "\n" 40 | : ''; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tag/Base/Tag.php: -------------------------------------------------------------------------------- 1 | attributes = array_merge($new->attributes, $attributes); 28 | return $new; 29 | } 30 | 31 | /** 32 | * Replace attributes with a new set. 33 | * 34 | * @param array $attributes Name-value set of attributes. 35 | */ 36 | final public function attributes(array $attributes): static 37 | { 38 | $new = clone $this; 39 | $new->attributes = $attributes; 40 | return $new; 41 | } 42 | 43 | /** 44 | * Union attributes with a new set. 45 | * 46 | * @param array $attributes Name-value set of attributes. 47 | */ 48 | final public function unionAttributes(array $attributes): static 49 | { 50 | $new = clone $this; 51 | $new->attributes += $attributes; 52 | return $new; 53 | } 54 | 55 | /** 56 | * Set attribute value. 57 | * 58 | * @param string $name Name of the attribute. 59 | * @param mixed $value Value of the attribute. 60 | */ 61 | final public function attribute(string $name, mixed $value): static 62 | { 63 | $new = clone $this; 64 | $new->attributes[$name] = $value; 65 | return $new; 66 | } 67 | 68 | /** 69 | * Set tag ID. 70 | * 71 | * @param string|null $id Tag ID. 72 | * 73 | * @psalm-param non-empty-string|null $id 74 | */ 75 | final public function id(?string $id): static 76 | { 77 | $new = clone $this; 78 | $new->attributes['id'] = $id; 79 | return $new; 80 | } 81 | 82 | /** 83 | * Add one or more CSS classes to the tag. 84 | * 85 | * @param BackedEnum|string|null ...$class One or many CSS classes. 86 | */ 87 | final public function addClass(BackedEnum|string|null ...$class): static 88 | { 89 | $new = clone $this; 90 | Html::addCssClass($new->attributes, $class); 91 | return $new; 92 | } 93 | 94 | /** 95 | * Replace current tag CSS classes with a new set of classes. 96 | * 97 | * @param BackedEnum|string|null ...$class One or many CSS classes. 98 | */ 99 | final public function class(BackedEnum|string|null ...$class): static 100 | { 101 | $new = clone $this; 102 | unset($new->attributes['class']); 103 | Html::addCssClass($new->attributes, $class); 104 | return $new; 105 | } 106 | 107 | /** 108 | * Render the current tag attributes. 109 | * 110 | * @see Html::renderTagAttributes() 111 | */ 112 | final protected function renderAttributes(): string 113 | { 114 | $this->prepareAttributes(); 115 | return Html::renderTagAttributes($this->attributes); 116 | } 117 | 118 | protected function prepareAttributes(): void 119 | { 120 | } 121 | 122 | final public function render(): string 123 | { 124 | return $this->before() . $this->renderTag() . $this->after(); 125 | } 126 | 127 | protected function before(): string 128 | { 129 | return ''; 130 | } 131 | 132 | protected function after(): string 133 | { 134 | return ''; 135 | } 136 | 137 | /** 138 | * Render tag object into its string representation. 139 | * 140 | * @return string String representation of a tag object. 141 | */ 142 | abstract protected function renderTag(): string; 143 | 144 | /** 145 | * Get tag name. 146 | * 147 | * @return string Tag name. 148 | */ 149 | abstract protected function getName(): string; 150 | 151 | final public function __toString(): string 152 | { 153 | return $this->render(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Tag/Base/TagContentTrait.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private array $content = []; 23 | 24 | /** 25 | * @param bool|null $encode Whether to encode tag content. Supported values: 26 | * - `null`: stringable objects that implement interface {@see NoEncodeStringableInterface} are not encoded, 27 | * everything else is encoded; 28 | * - `true`: any content is encoded; 29 | * - `false`: nothing is encoded. 30 | * Defaults to `null`. 31 | */ 32 | final public function encode(?bool $encode): static 33 | { 34 | $new = clone $this; 35 | $new->encode = $encode; 36 | return $new; 37 | } 38 | 39 | /** 40 | * @param bool $doubleEncode Whether already encoded HTML entities in tag content should be encoded. 41 | * Defaults to `true`. 42 | */ 43 | final public function doubleEncode(bool $doubleEncode): static 44 | { 45 | $new = clone $this; 46 | $new->doubleEncode = $doubleEncode; 47 | return $new; 48 | } 49 | 50 | /** 51 | * @param string|Stringable ...$content Tag content. 52 | */ 53 | final public function content(string|Stringable ...$content): static 54 | { 55 | $new = clone $this; 56 | $new->content = array_values($content); 57 | return $new; 58 | } 59 | 60 | /** 61 | * @param string|Stringable ...$content Tag content. 62 | */ 63 | final public function addContent(string|Stringable ...$content): static 64 | { 65 | $new = clone $this; 66 | $new->content = array_merge($new->content, array_values($content)); 67 | return $new; 68 | } 69 | 70 | /** 71 | * @return string Obtain tag content considering encoding options {@see encode()}. 72 | */ 73 | final protected function generateContent(): string 74 | { 75 | $content = ''; 76 | 77 | foreach ($this->content as $item) { 78 | if ($this->encode || ($this->encode === null && !($item instanceof NoEncodeStringableInterface))) { 79 | $item = Html::encode($item, $this->doubleEncode); 80 | } 81 | 82 | $content .= $item; 83 | } 84 | 85 | return $content; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Tag/Base/TagSourcesTrait.php: -------------------------------------------------------------------------------- 1 | sources = $sources; 23 | return $new; 24 | } 25 | 26 | public function addSource(Source $source): static 27 | { 28 | $new = clone $this; 29 | $new->sources[] = $source; 30 | return $new; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Tag/Base/VoidTag.php: -------------------------------------------------------------------------------- 1 | getName() . $this->renderAttributes() . '>'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tag/Body.php: -------------------------------------------------------------------------------- 1 | content($content); 20 | $button->attributes['type'] = 'button'; 21 | return $button; 22 | } 23 | 24 | public static function submit(string $content = ''): self 25 | { 26 | $button = self::tag()->content($content); 27 | $button->attributes['type'] = 'submit'; 28 | return $button; 29 | } 30 | 31 | public static function reset(string $content = ''): self 32 | { 33 | $button = self::tag()->content($content); 34 | $button->attributes['type'] = 'reset'; 35 | return $button; 36 | } 37 | 38 | /** 39 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-button-type 40 | */ 41 | public function type(?string $type): self 42 | { 43 | $new = clone $this; 44 | $new->attributes['type'] = $type; 45 | return $new; 46 | } 47 | 48 | /** 49 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-disabledformelements-disabled 50 | */ 51 | public function disabled(bool $disabled = true): self 52 | { 53 | $new = clone $this; 54 | $new->attributes['disabled'] = $disabled; 55 | return $new; 56 | } 57 | 58 | protected function getName(): string 59 | { 60 | return 'button'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tag/Caption.php: -------------------------------------------------------------------------------- 1 | ` element spans. 16 | */ 17 | public function span(?int $number): self 18 | { 19 | $new = clone $this; 20 | $new->attributes['span'] = $number; 21 | return $new; 22 | } 23 | 24 | protected function getName(): string 25 | { 26 | return 'col'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tag/Colgroup.php: -------------------------------------------------------------------------------- 1 | columns = $columns; 26 | return $new; 27 | } 28 | 29 | /** 30 | * @param Col ...$columns One or more columns ({@see Col}). 31 | */ 32 | public function addColumns(Col ...$columns): self 33 | { 34 | $new = clone $this; 35 | $new->columns = array_merge($new->columns, $columns); 36 | return $new; 37 | } 38 | 39 | /** 40 | * @param int|null $number The number of consecutive columns the `` element spans. 41 | */ 42 | public function span(?int $number): self 43 | { 44 | $new = clone $this; 45 | $new->attributes['span'] = $number; 46 | return $new; 47 | } 48 | 49 | protected function generateContent(): string 50 | { 51 | return $this->columns 52 | ? "\n" . implode("\n", $this->columns) . "\n" 53 | : ''; 54 | } 55 | 56 | protected function getName(): string 57 | { 58 | return 'colgroup'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tag/CustomTag.php: -------------------------------------------------------------------------------- 1 | 1, 24 | 'base' => 1, 25 | 'br' => 1, 26 | 'col' => 1, 27 | 'command' => 1, 28 | 'embed' => 1, 29 | 'hr' => 1, 30 | 'img' => 1, 31 | 'input' => 1, 32 | 'keygen' => 1, 33 | 'link' => 1, 34 | 'meta' => 1, 35 | 'param' => 1, 36 | 'source' => 1, 37 | 'track' => 1, 38 | 'wbr' => 1, 39 | ]; 40 | 41 | private const TYPE_AUTO = 0; 42 | private const TYPE_NORMAL = 1; 43 | private const TYPE_VOID = 2; 44 | 45 | private int $type = self::TYPE_AUTO; 46 | 47 | private function __construct( 48 | private string $name 49 | ) { 50 | } 51 | 52 | /** 53 | * Create a tag instance with the name provided. 54 | * 55 | * @param string $name Name of the tag. 56 | * 57 | * @psalm-param non-empty-string $name 58 | */ 59 | public static function name(string $name): self 60 | { 61 | return new self($name); 62 | } 63 | 64 | /** 65 | * Set type of the tag as normal. 66 | * Normal tags have both open and close parts. 67 | */ 68 | public function normal(): self 69 | { 70 | $new = clone $this; 71 | $new->type = self::TYPE_NORMAL; 72 | return $new; 73 | } 74 | 75 | /** 76 | * Set type of the tag as void. 77 | * Void tags should be self-closed right away. 78 | */ 79 | public function void(): self 80 | { 81 | $new = clone $this; 82 | $new->type = self::TYPE_VOID; 83 | return $new; 84 | } 85 | 86 | protected function getName(): string 87 | { 88 | return $this->name; 89 | } 90 | 91 | protected function renderTag(): string 92 | { 93 | $isVoid = $this->type === self::TYPE_VOID || 94 | ($this->type === self::TYPE_AUTO && isset(self::VOID_ELEMENTS[strtolower($this->name)])); 95 | return $isVoid ? $this->open() : ($this->open() . $this->generateContent() . $this->close()); 96 | } 97 | 98 | /** 99 | * @return string Opening tag. 100 | */ 101 | public function open(): string 102 | { 103 | return '<' . $this->getName() . $this->renderAttributes() . '>'; 104 | } 105 | 106 | /** 107 | * @return string Closing tag. 108 | */ 109 | public function close(): string 110 | { 111 | return 'getName() . '>'; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Tag/Datalist.php: -------------------------------------------------------------------------------- 1 | legend = $content === null ? null : Html::legend($content, $attributes); 22 | return $new; 23 | } 24 | 25 | public function legendTag(?Legend $legend): self 26 | { 27 | $new = clone $this; 28 | $new->legend = $legend; 29 | return $new; 30 | } 31 | 32 | /** 33 | * @link https://html.spec.whatwg.org/multipage/form-elements.html#attr-fieldset-disabled 34 | * 35 | * @param bool|null $disabled Whether fieldset is disabled. 36 | */ 37 | public function disabled(?bool $disabled = true): self 38 | { 39 | $new = clone $this; 40 | $new->attributes['disabled'] = $disabled; 41 | return $new; 42 | } 43 | 44 | /** 45 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form 46 | */ 47 | public function form(?string $formId): self 48 | { 49 | $new = clone $this; 50 | $new->attributes['form'] = $formId; 51 | return $new; 52 | } 53 | 54 | /** 55 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-name 56 | */ 57 | public function name(?string $name): self 58 | { 59 | $new = clone $this; 60 | $new->attributes['name'] = $name; 61 | return $new; 62 | } 63 | 64 | protected function prepend(): string 65 | { 66 | if ($this->legend === null) { 67 | return ''; 68 | } 69 | 70 | return "\n" . $this->legend->render() . "\n"; 71 | } 72 | 73 | protected function getName(): string 74 | { 75 | return 'fieldset'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Tag/Footer.php: -------------------------------------------------------------------------------- 1 | attributes['method'] = 'GET'; 29 | if ($url !== null) { 30 | $new->attributes['action'] = $url; 31 | } 32 | return $new; 33 | } 34 | 35 | public function post(?string $url = null): self 36 | { 37 | $new = clone $this; 38 | $new->attributes['method'] = 'POST'; 39 | if ($url !== null) { 40 | $new->attributes['action'] = $url; 41 | } 42 | return $new; 43 | } 44 | 45 | public function csrf(string|Stringable|null $token, string $name = '_csrf'): self 46 | { 47 | $new = clone $this; 48 | $new->csrfToken = $token === null ? null : (string) $token; 49 | $new->csrfName = $name; 50 | return $new; 51 | } 52 | 53 | /** 54 | * Character encodings to use for form submission. 55 | * 56 | * @link https://html.spec.whatwg.org/multipage/forms.html#attr-form-accept-charset 57 | */ 58 | public function acceptCharset(?string $charset): self 59 | { 60 | $new = clone $this; 61 | $new->attributes['accept-charset'] = $charset; 62 | return $new; 63 | } 64 | 65 | /** 66 | * The URL to use for form submission. 67 | * 68 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-action 69 | */ 70 | public function action(?string $url): self 71 | { 72 | $new = clone $this; 73 | $new->attributes['action'] = $url; 74 | return $new; 75 | } 76 | 77 | /** 78 | * Default setting for autofill feature for controls in the form. 79 | * 80 | * @link https://html.spec.whatwg.org/multipage/forms.html#attr-form-autocomplete 81 | */ 82 | public function autocomplete(bool $value = true): self 83 | { 84 | $new = clone $this; 85 | $new->attributes['autocomplete'] = $value ? 'on' : 'off'; 86 | return $new; 87 | } 88 | 89 | /** 90 | * Entry list encoding type to use for form submission. 91 | * 92 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-enctype 93 | */ 94 | public function enctype(?string $enctype): self 95 | { 96 | $new = clone $this; 97 | $new->attributes['enctype'] = $enctype; 98 | return $new; 99 | } 100 | 101 | /** 102 | * All characters are encoded before sending. 103 | * 104 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:attr-fs-enctype-urlencoded 105 | */ 106 | public function enctypeApplicationXWwwFormUrlencoded(): self 107 | { 108 | return $this->enctype(self::ENCTYPE_APPLICATION_X_WWW_FORM_URLENCODED); 109 | } 110 | 111 | /** 112 | * The type that allows file `` element(s) to upload file data. 113 | * 114 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:attr-fs-enctype-formdata 115 | */ 116 | public function enctypeMultipartFormData(): self 117 | { 118 | return $this->enctype(self::ENCTYPE_MULTIPART_FORM_DATA); 119 | } 120 | 121 | /** 122 | * Sends data without any encoding at all. Not recommended. 123 | * 124 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm:attr-fs-enctype-text 125 | */ 126 | public function enctypeTextPlain(): self 127 | { 128 | return $this->enctype(self::ENCTYPE_TEXT_PLAIN); 129 | } 130 | 131 | /** 132 | * The method content attribute specifies how the form-data should be submitted. 133 | * 134 | * @param string|null $method The method attribute value. 135 | * 136 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-method 137 | */ 138 | public function method(?string $method): self 139 | { 140 | $new = clone $this; 141 | $new->attributes['method'] = $method; 142 | return $new; 143 | } 144 | 145 | /** 146 | * A boolean attribute, which, if present, indicate that the form is not to be validated during submission. 147 | * 148 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-novalidate 149 | */ 150 | public function noValidate(bool $noValidate = true): self 151 | { 152 | $new = clone $this; 153 | $new->attributes['novalidate'] = $noValidate; 154 | return $new; 155 | } 156 | 157 | /** 158 | * Browsing context for form submission. 159 | * 160 | * @param string|null $target The target attribute value. 161 | * 162 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-target 163 | * @link https://html.spec.whatwg.org/multipage/browsers.html#valid-browsing-context-name-or-keyword 164 | */ 165 | public function target(?string $target): self 166 | { 167 | $new = clone $this; 168 | $new->attributes['target'] = $target; 169 | return $new; 170 | } 171 | 172 | protected function prepend(): string 173 | { 174 | return $this->csrfToken !== null 175 | ? "\n" . Input::hidden($this->csrfName, $this->csrfToken) 176 | : ''; 177 | } 178 | 179 | protected function getName(): string 180 | { 181 | return 'form'; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Tag/H1.php: -------------------------------------------------------------------------------- 1 | attributes['lang'] = $lang; 26 | return $new; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tag/I.php: -------------------------------------------------------------------------------- 1 | src($url); 17 | } 18 | 19 | public function src(?string $url): self 20 | { 21 | $new = clone $this; 22 | $new->attributes['src'] = $url; 23 | return $new; 24 | } 25 | 26 | public function srcset(?string ...$items): self 27 | { 28 | $items = array_filter($items, static fn ($item) => $item !== null); 29 | 30 | $new = clone $this; 31 | $new->attributes['srcset'] = $items ? implode(',', $items) : null; 32 | return $new; 33 | } 34 | 35 | /** 36 | * @param array $data 37 | */ 38 | public function srcsetData(array $data): self 39 | { 40 | $new = clone $this; 41 | 42 | $items = []; 43 | foreach ($data as $descriptor => $url) { 44 | $items[] = $url . ' ' . $descriptor; 45 | } 46 | $new->attributes['srcset'] = $items ? implode(',', $items) : null; 47 | 48 | return $new; 49 | } 50 | 51 | public function alt(?string $text): self 52 | { 53 | $new = clone $this; 54 | $new->attributes['alt'] = $text; 55 | return $new; 56 | } 57 | 58 | public function width(int|string|null $width): self 59 | { 60 | $new = clone $this; 61 | $new->attributes['width'] = $width; 62 | return $new; 63 | } 64 | 65 | public function height(int|string|null $height): self 66 | { 67 | $new = clone $this; 68 | $new->attributes['height'] = $height; 69 | return $new; 70 | } 71 | 72 | public function size(int|string|null $width, int|string|null $height): self 73 | { 74 | $new = clone $this; 75 | $new->attributes['width'] = $width; 76 | $new->attributes['height'] = $height; 77 | return $new; 78 | } 79 | 80 | public function loading(?string $loading): self 81 | { 82 | $new = clone $this; 83 | $new->attributes['loading'] = $loading; 84 | return $new; 85 | } 86 | 87 | protected function getName(): string 88 | { 89 | return 'img'; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Tag/Input.php: -------------------------------------------------------------------------------- 1 | attributes['type'] = 'hidden'; 33 | $input->attributes['name'] = $name; 34 | $input->attributes['value'] = $value; 35 | return $input; 36 | } 37 | 38 | /** 39 | * Text input. 40 | * 41 | * @link https://www.w3.org/TR/html52/sec-forms.html#text-typetext-state-and-search-state-typesearch 42 | * 43 | * @param string|null $name Name of the input. 44 | * @param bool|float|int|string|Stringable|null $value Value of the input. 45 | */ 46 | public static function text(?string $name = null, bool|float|int|string|Stringable|null $value = null): self 47 | { 48 | $input = self::tag(); 49 | $input->attributes['type'] = 'text'; 50 | $input->attributes['name'] = $name; 51 | $input->attributes['value'] = $value; 52 | return $input; 53 | } 54 | 55 | /** 56 | * Password input. 57 | * 58 | * @link https://www.w3.org/TR/html52/sec-forms.html#password-state-typepassword 59 | * 60 | * @param string|null $name Name of the input. 61 | * @param bool|float|int|string|Stringable|null $value Value of the input. 62 | */ 63 | public static function password(?string $name = null, bool|float|int|string|Stringable|null $value = null): self 64 | { 65 | $input = self::tag(); 66 | $input->attributes['type'] = 'password'; 67 | $input->attributes['name'] = $name; 68 | $input->attributes['value'] = $value; 69 | return $input; 70 | } 71 | 72 | /** 73 | * File input. 74 | * 75 | * @link https://www.w3.org/TR/html52/sec-forms.html#file-upload-state-typefile 76 | * 77 | * @param string|null $name Name of the input. 78 | * @param bool|float|int|string|Stringable|null $value Value of the input. 79 | */ 80 | public static function file(?string $name = null, bool|float|int|string|Stringable|null $value = null): File 81 | { 82 | $input = File::tag(); 83 | if ($name !== null) { 84 | $input = $input->name($name); 85 | } 86 | if ($value !== null) { 87 | $input = $input->value($value); 88 | } 89 | return $input; 90 | } 91 | 92 | /** 93 | * Checkbox input. 94 | * 95 | * @link https://www.w3.org/TR/html52/sec-forms.html#checkbox-state-typecheckbox 96 | * 97 | * @param string|null $name Name of the input. 98 | * @param bool|float|int|string|Stringable|null $value Value of the input. 99 | */ 100 | public static function checkbox(?string $name = null, bool|float|int|string|Stringable|null $value = null): Checkbox 101 | { 102 | $input = Checkbox::tag(); 103 | if ($name !== null) { 104 | $input = $input->name($name); 105 | } 106 | if ($value !== null) { 107 | $input = $input->value($value); 108 | } 109 | return $input; 110 | } 111 | 112 | /** 113 | * Radio input. 114 | * 115 | * @link https://www.w3.org/TR/html52/sec-forms.html#radio-button-state-typeradio 116 | * 117 | * @param string|null $name Name of the input. 118 | * @param bool|float|int|string|Stringable|null $value Value of the input. 119 | */ 120 | public static function radio(?string $name = null, bool|float|int|string|Stringable|null $value = null): Radio 121 | { 122 | $input = Radio::tag(); 123 | if ($name !== null) { 124 | $input = $input->name($name); 125 | } 126 | if ($value !== null) { 127 | $input = $input->value($value); 128 | } 129 | return $input; 130 | } 131 | 132 | /** 133 | * Range. 134 | * 135 | * @link https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range) 136 | * 137 | * @param string|null $name Name of the input. 138 | * @param float|int|string|Stringable|null $value Value of the input. 139 | */ 140 | public static function range(?string $name = null, float|int|string|Stringable|null $value = null): Range 141 | { 142 | $input = Range::tag(); 143 | if ($name !== null) { 144 | $input = $input->name($name); 145 | } 146 | if ($value !== null) { 147 | $input = $input->value($value); 148 | } 149 | return $input; 150 | } 151 | 152 | /** 153 | * Button. 154 | * 155 | * @link https://www.w3.org/TR/html52/sec-forms.html#button-state-typebutton 156 | * 157 | * @param string|null $label Button label. 158 | */ 159 | public static function button(?string $label = null): self 160 | { 161 | $input = self::tag(); 162 | $input->attributes['type'] = 'button'; 163 | $input->attributes['value'] = $label; 164 | return $input; 165 | } 166 | 167 | /** 168 | * Submit button. 169 | * 170 | * @link https://www.w3.org/TR/html52/sec-forms.html#submit-button-state-typesubmit 171 | * 172 | * @param string|null $label Button label. 173 | */ 174 | public static function submitButton(?string $label = null): self 175 | { 176 | $input = self::tag(); 177 | $input->attributes['type'] = 'submit'; 178 | $input->attributes['value'] = $label; 179 | return $input; 180 | } 181 | 182 | /** 183 | * Reset button. 184 | * 185 | * @link https://www.w3.org/TR/html52/sec-forms.html#reset-button-state-typereset 186 | * 187 | * @param string|null $label Button label. 188 | */ 189 | public static function resetButton(?string $label = null): self 190 | { 191 | $input = self::tag(); 192 | $input->attributes['type'] = 'reset'; 193 | $input->attributes['value'] = $label; 194 | return $input; 195 | } 196 | 197 | /** 198 | * Input with the type specified. 199 | * 200 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-input-type 201 | * 202 | * @param string|null $type Type of the input. 203 | */ 204 | public function type(?string $type): self 205 | { 206 | $new = clone $this; 207 | $new->attributes['type'] = $type; 208 | return $new; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Tag/Input/Checkbox.php: -------------------------------------------------------------------------------- 1 | uncheckValue = $value === null ? null : (string) $value; 23 | return $new; 24 | } 25 | 26 | public function addUncheckInputAttributes(array $attributes): self 27 | { 28 | $new = clone $this; 29 | $new->uncheckInputAttributes = array_merge($new->uncheckInputAttributes, $attributes); 30 | return $new; 31 | } 32 | 33 | public function uncheckInputAttributes(array $attributes): self 34 | { 35 | $new = clone $this; 36 | $new->uncheckInputAttributes = $attributes; 37 | return $new; 38 | } 39 | 40 | /** 41 | * The accept attribute value is a string that defines the file types the file input should accept. This string is 42 | * a comma-separated list of unique file type specifiers. Because a given file type may be identified in more than 43 | * one manner, it's useful to provide a thorough set of type specifiers when you need files of a given format. 44 | * 45 | * @link https://html.spec.whatwg.org/multipage/input.html#attr-input-accept 46 | */ 47 | public function accept(?string $value): self 48 | { 49 | $new = clone $this; 50 | $new->attributes['accept'] = $value; 51 | return $new; 52 | } 53 | 54 | /** 55 | * @link https://html.spec.whatwg.org/multipage/input.html#attr-input-multiple 56 | * 57 | * @param bool $multiple Whether to allow selecting multiple files. 58 | */ 59 | public function multiple(bool $multiple = true): self 60 | { 61 | $new = clone $this; 62 | $new->attributes['multiple'] = $multiple; 63 | return $new; 64 | } 65 | 66 | protected function prepareAttributes(): void 67 | { 68 | $this->attributes['type'] = 'file'; 69 | } 70 | 71 | protected function before(): string 72 | { 73 | return $this->renderUncheckInput(); 74 | } 75 | 76 | private function renderUncheckInput(): string 77 | { 78 | if ($this->uncheckValue === null) { 79 | return ''; 80 | } 81 | 82 | $name = (string)($this->attributes['name'] ?? ''); 83 | if (empty($name)) { 84 | return ''; 85 | } 86 | 87 | $input = Html::hiddenInput( 88 | Html::getNonArrayableName($name), 89 | $this->uncheckValue, 90 | $this->uncheckInputAttributes 91 | ); 92 | 93 | // Make sure disabled input is not sending any value. 94 | if (!empty($this->attributes['disabled'])) { 95 | $input = $input->attribute('disabled', $this->attributes['disabled']); 96 | } 97 | 98 | if (!empty($this->attributes['form'])) { 99 | $input = $input->attribute('form', $this->attributes['form']); 100 | } 101 | 102 | return $input->render(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Tag/Input/Radio.php: -------------------------------------------------------------------------------- 1 | attributes['max'] = $value; 42 | return $new; 43 | } 44 | 45 | /** 46 | * Minimum value. 47 | * 48 | * @link https://html.spec.whatwg.org/multipage/input.html#attr-input-min 49 | */ 50 | public function min(float|int|string|Stringable|null $value): self 51 | { 52 | $new = clone $this; 53 | $new->attributes['min'] = $value; 54 | return $new; 55 | } 56 | 57 | /** 58 | * Granularity to be matched by the form control's value. 59 | * 60 | * @link https://html.spec.whatwg.org/multipage/input.html#attr-input-step 61 | */ 62 | public function step(float|int|string|Stringable|null $value): self 63 | { 64 | $new = clone $this; 65 | $new->attributes['step'] = $value; 66 | return $new; 67 | } 68 | 69 | /** 70 | * ID of element that lists predefined options suggested to the user. 71 | * 72 | * @link https://html.spec.whatwg.org/multipage/input.html#the-list-attribute 73 | */ 74 | public function list(?string $id): self 75 | { 76 | $new = clone $this; 77 | $new->attributes['list'] = $id; 78 | return $new; 79 | } 80 | 81 | public function showOutput(bool $show = true): self 82 | { 83 | $new = clone $this; 84 | $new->showOutput = $show; 85 | return $new; 86 | } 87 | 88 | public function outputTag(string $tagName): self 89 | { 90 | if ($tagName === '') { 91 | throw new InvalidArgumentException('The output tag name it cannot be empty value.'); 92 | } 93 | 94 | $new = clone $this; 95 | $new->outputTag = $tagName; 96 | return $new; 97 | } 98 | 99 | public function addOutputAttributes(array $attributes): self 100 | { 101 | $new = clone $this; 102 | $new->outputAttributes = array_merge($this->outputAttributes, $attributes); 103 | return $new; 104 | } 105 | 106 | public function outputAttributes(array $attributes): self 107 | { 108 | $new = clone $this; 109 | $new->outputAttributes = $attributes; 110 | return $new; 111 | } 112 | 113 | protected function prepareAttributes(): void 114 | { 115 | $this->attributes['type'] = 'range'; 116 | 117 | if ($this->showOutput) { 118 | $this->fillOutputId(); 119 | $this->attributes['oninput'] = 'document.getElementById("' . $this->outputId . '").innerHTML=this.value'; 120 | } 121 | } 122 | 123 | protected function after(): string 124 | { 125 | if (!$this->showOutput) { 126 | return ''; 127 | } 128 | 129 | return "\n" . CustomTag::name($this->outputTag) 130 | ->attributes($this->outputAttributes) 131 | ->content((string) ($this->attributes['value'] ?? '-')) 132 | ->id($this->outputId) 133 | ->render(); 134 | } 135 | 136 | /** 137 | * @psalm-assert non-empty-string $this->outputId 138 | */ 139 | private function fillOutputId(): void 140 | { 141 | $id = (string) ($this->outputAttributes['id'] ?? ''); 142 | $this->outputId = $id === '' ? Html::generateId('rangeOutput') : $id; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Tag/Label.php: -------------------------------------------------------------------------------- 1 | attributes['for'] = $id; 21 | return $new; 22 | } 23 | 24 | protected function getName(): string 25 | { 26 | return 'label'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tag/Legend.php: -------------------------------------------------------------------------------- 1 | attributes['rel'] = 'stylesheet'; 18 | $link->attributes['href'] = $url; 19 | return $link; 20 | } 21 | 22 | /** 23 | * Alias for {@see href} 24 | */ 25 | public function url(?string $url): self 26 | { 27 | return $this->href($url); 28 | } 29 | 30 | /** 31 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-link-href 32 | */ 33 | public function href(?string $url): self 34 | { 35 | $new = clone $this; 36 | $new->attributes['href'] = $url; 37 | return $new; 38 | } 39 | 40 | /** 41 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-link-rel 42 | */ 43 | public function rel(?string $rel): self 44 | { 45 | $new = clone $this; 46 | $new->attributes['rel'] = $rel; 47 | return $new; 48 | } 49 | 50 | /** 51 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-link-type 52 | */ 53 | public function type(?string $type): self 54 | { 55 | $new = clone $this; 56 | $new->attributes['type'] = $type; 57 | return $new; 58 | } 59 | 60 | /** 61 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-link-title 62 | */ 63 | public function title(?string $title): self 64 | { 65 | $new = clone $this; 66 | $new->attributes['title'] = $title; 67 | return $new; 68 | } 69 | 70 | /** 71 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-link-crossorigin 72 | */ 73 | public function crossOrigin(?string $value): self 74 | { 75 | $new = clone $this; 76 | $new->attributes['crossorigin'] = $value; 77 | return $new; 78 | } 79 | 80 | /** 81 | * @link https://www.w3.org/TR/preload/#as-attribute 82 | */ 83 | public function as(?string $value): self 84 | { 85 | $new = clone $this; 86 | $new->attributes['as'] = $value; 87 | return $new; 88 | } 89 | 90 | /** 91 | * @link https://www.w3.org/TR/preload/#link-type-preload 92 | * @link https://www.w3.org/TR/preload/#as-attribute 93 | */ 94 | public function preload(string $url, ?string $as = null): self 95 | { 96 | $new = clone $this; 97 | $new->attributes['rel'] = 'preload'; 98 | $new->attributes['href'] = $url; 99 | if ($as !== null) { 100 | $new->attributes['as'] = $as; 101 | } 102 | return $new; 103 | } 104 | 105 | protected function getName(): string 106 | { 107 | return 'link'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Tag/Meta.php: -------------------------------------------------------------------------------- 1 | attributes['name'] = $name; 21 | $tag->attributes['content'] = $content; 22 | return $tag; 23 | } 24 | 25 | /** 26 | * @link https://www.w3.org/TR/html52/document-metadata.html#pragma-directives 27 | */ 28 | public static function pragmaDirective(string $name, string $content): self 29 | { 30 | $tag = self::tag(); 31 | $tag->attributes['http-equiv'] = $name; 32 | $tag->attributes['content'] = $content; 33 | return $tag; 34 | } 35 | 36 | /** 37 | * @link https://www.w3.org/TR/html52/document-metadata.html#specifying-the-documents-character-encoding 38 | */ 39 | public static function documentEncoding(string $encoding): self 40 | { 41 | $tag = self::tag(); 42 | $tag->attributes['charset'] = $encoding; 43 | return $tag; 44 | } 45 | 46 | /** 47 | * @link https://www.w3.org/TR/html52/document-metadata.html#description 48 | */ 49 | public static function description(string $content): self 50 | { 51 | $tag = self::tag(); 52 | $tag->attributes['name'] = 'description'; 53 | $tag->attributes['content'] = $content; 54 | return $tag; 55 | } 56 | 57 | /** 58 | * Metadata name 59 | */ 60 | public function name(?string $name): self 61 | { 62 | $new = clone $this; 63 | $new->attributes['name'] = $name; 64 | return $new; 65 | } 66 | 67 | /** 68 | * @link https://www.w3.org/TR/html52/document-metadata.html#dom-htmlmetaelement-httpequiv 69 | */ 70 | public function httpEquiv(?string $name): self 71 | { 72 | $new = clone $this; 73 | $new->attributes['http-equiv'] = $name; 74 | return $new; 75 | } 76 | 77 | /** 78 | * Value of the element 79 | */ 80 | public function content(?string $content): self 81 | { 82 | $new = clone $this; 83 | $new->attributes['content'] = $content; 84 | return $new; 85 | } 86 | 87 | /** 88 | * @link https://www.w3.org/TR/html52/document-metadata.html#element-attrdef-meta-charset 89 | */ 90 | public function charset(?string $charset): self 91 | { 92 | $new = clone $this; 93 | $new->attributes['charset'] = $charset; 94 | return $new; 95 | } 96 | 97 | protected function getName(): string 98 | { 99 | return 'meta'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Tag/Nav.php: -------------------------------------------------------------------------------- 1 | options = $options; 24 | return $new; 25 | } 26 | 27 | /** 28 | * Options as a set of value-content pairs. 29 | * 30 | * @param string[] $data Value-content set of options. 31 | * @param bool $encode Whether to encode option content. 32 | * @param array[] $optionsAttributes Array of option attribute sets indexed by option values from {@see $data}. 33 | */ 34 | public function optionsData(array $data, bool $encode = true, array $optionsAttributes = []): self 35 | { 36 | $options = []; 37 | foreach ($data as $value => $content) { 38 | $options[] = Option::tag() 39 | ->attributes($optionsAttributes[$value] ?? []) 40 | ->value($value) 41 | ->content($content) 42 | ->encode($encode); 43 | } 44 | return $this->options(...$options); 45 | } 46 | 47 | /** 48 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-optgroup-label 49 | */ 50 | public function label(?string $label): self 51 | { 52 | $new = clone $this; 53 | $new->attributes['label'] = $label; 54 | return $new; 55 | } 56 | 57 | /** 58 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-optgroup-disabled 59 | */ 60 | public function disabled(bool $disabled = true): self 61 | { 62 | $new = clone $this; 63 | $new->attributes['disabled'] = $disabled; 64 | return $new; 65 | } 66 | 67 | /** 68 | * @param bool|float|int|string|Stringable|null ...$value Values of options that are selected. 69 | */ 70 | public function selection(bool|float|int|string|Stringable|null ...$value): self 71 | { 72 | $new = clone $this; 73 | $new->selection = array_map('\strval', $value); 74 | return $new; 75 | } 76 | 77 | protected function generateContent(): string 78 | { 79 | $options = array_map( 80 | fn (Option $option) => $option->selected(in_array($option->getValue(), $this->selection, true)), 81 | $this->options 82 | ); 83 | 84 | return $options 85 | ? "\n" . implode("\n", $options) . "\n" 86 | : ''; 87 | } 88 | 89 | protected function getName(): string 90 | { 91 | return 'optgroup'; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Tag/Option.php: -------------------------------------------------------------------------------- 1 | attributes['value'] = $value; 28 | return $new; 29 | } 30 | 31 | /** 32 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-option-selected 33 | * 34 | * @param bool $selected Whether option is selected. 35 | */ 36 | public function selected(bool $selected = true): self 37 | { 38 | $new = clone $this; 39 | $new->attributes['selected'] = $selected; 40 | return $new; 41 | } 42 | 43 | /** 44 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-option-disabled 45 | * 46 | * @param bool $disabled Whether option is disabled. 47 | */ 48 | public function disabled(bool $disabled = true): self 49 | { 50 | $new = clone $this; 51 | $new->attributes['disabled'] = $disabled; 52 | return $new; 53 | } 54 | 55 | /** 56 | * @return string|null Get option value. 57 | */ 58 | public function getValue(): ?string 59 | { 60 | $value = ArrayHelper::getValue($this->attributes, 'value'); 61 | return $value === null ? null : (string)$value; 62 | } 63 | 64 | protected function getName(): string 65 | { 66 | return 'option'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Tag/P.php: -------------------------------------------------------------------------------- 1 | image = $image; 23 | return $new; 24 | } 25 | 26 | protected function generateContent(): string 27 | { 28 | $items = $this->sources; 29 | 30 | if ($this->image !== null) { 31 | $items[] = $this->image; 32 | } 33 | 34 | return $items ? "\n" . implode("\n", $items) . "\n" : ''; 35 | } 36 | 37 | protected function getName(): string 38 | { 39 | return 'picture'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tag/Pre.php: -------------------------------------------------------------------------------- 1 | content = $content; 32 | return $new; 33 | } 34 | 35 | public function getContent(): string 36 | { 37 | return $this->content; 38 | } 39 | 40 | /** 41 | * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce 42 | */ 43 | public function nonce(?string $nonce): self 44 | { 45 | $new = clone $this; 46 | 47 | if ($nonce === null) { 48 | unset($new->attributes['nonce']); 49 | } else { 50 | $new->attributes['nonce'] = $nonce; 51 | } 52 | 53 | return $new; 54 | } 55 | 56 | public function getNonce(): ?string 57 | { 58 | $nonce = $this->attributes['nonce'] ?? null; 59 | 60 | if (is_string($nonce) || $nonce === null) { 61 | return $nonce; 62 | } 63 | 64 | throw new LogicException( 65 | sprintf( 66 | 'Nonce should be string or null. Got %s.', 67 | get_debug_type($nonce) 68 | ) 69 | ); 70 | } 71 | 72 | /** 73 | * Alias for {@see src} 74 | */ 75 | public function url(?string $url): self 76 | { 77 | return $this->src($url); 78 | } 79 | 80 | /** 81 | * @link https://www.w3.org/TR/html52/semantics-scripting.html#element-attrdef-script-src 82 | */ 83 | public function src(?string $url): self 84 | { 85 | $new = clone $this; 86 | $new->attributes['src'] = $url; 87 | return $new; 88 | } 89 | 90 | /** 91 | * @link https://www.w3.org/TR/html52/semantics-scripting.html#element-attrdef-script-type 92 | */ 93 | public function type(?string $type): self 94 | { 95 | $new = clone $this; 96 | $new->attributes['type'] = $type; 97 | return $new; 98 | } 99 | 100 | /** 101 | * @link https://www.w3.org/TR/html52/semantics-scripting.html#element-attrdef-script-charset 102 | */ 103 | public function charset(?string $charset): self 104 | { 105 | $new = clone $this; 106 | $new->attributes['charset'] = $charset; 107 | return $new; 108 | } 109 | 110 | /** 111 | * @link https://www.w3.org/TR/html52/semantics-scripting.html#element-attrdef-script-async 112 | */ 113 | public function async(bool $async = true): self 114 | { 115 | $new = clone $this; 116 | $new->attributes['async'] = $async; 117 | return $new; 118 | } 119 | 120 | /** 121 | * @link https://www.w3.org/TR/html52/semantics-scripting.html#element-attrdef-script-defer 122 | */ 123 | public function defer(bool $defer = true): self 124 | { 125 | $new = clone $this; 126 | $new->attributes['defer'] = $defer; 127 | return $new; 128 | } 129 | 130 | public function noscript(string|Stringable|null $content): self 131 | { 132 | $new = clone $this; 133 | $new->noscript = $content === null ? null : Noscript::tag()->content($content); 134 | return $new; 135 | } 136 | 137 | public function noscriptTag(?Noscript $noscript): self 138 | { 139 | $new = clone $this; 140 | $new->noscript = $noscript; 141 | return $new; 142 | } 143 | 144 | protected function getName(): string 145 | { 146 | return 'script'; 147 | } 148 | 149 | /** 150 | * @return string Obtain tag content. 151 | */ 152 | protected function generateContent(): string 153 | { 154 | return $this->content; 155 | } 156 | 157 | protected function after(): string 158 | { 159 | return $this->noscript !== null ? (string) $this->noscript : ''; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Tag/Section.php: -------------------------------------------------------------------------------- 1 | attributes['name'] = $name; 43 | return $new; 44 | } 45 | 46 | /** 47 | * @psalm-param Stringable|scalar|BackedEnum|null ...$value One or more string values. 48 | */ 49 | public function value(Stringable|bool|float|int|string|BackedEnum|null ...$value): self 50 | { 51 | $values = array_filter( 52 | $value, 53 | static fn (mixed $v): bool => $v !== null, 54 | ); 55 | $values = array_map( 56 | static function (Stringable|bool|float|int|string|BackedEnum $v): string { 57 | return (string) ($v instanceof BackedEnum ? $v->value : $v); 58 | }, 59 | $values, 60 | ); 61 | 62 | $new = clone $this; 63 | $new->values = $values; 64 | return $new; 65 | } 66 | 67 | /** 68 | * @psalm-param iterable $values A set of values. 69 | */ 70 | public function values(iterable $values): self 71 | { 72 | $values = is_array($values) ? $values : iterator_to_array($values); 73 | 74 | return $this->value(...$values); 75 | } 76 | 77 | /** 78 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-formelements-form 79 | * 80 | * @param string|null $formId ID of the form the select belongs to. 81 | */ 82 | public function form(?string $formId): self 83 | { 84 | $new = clone $this; 85 | $new->attributes['form'] = $formId; 86 | return $new; 87 | } 88 | 89 | /** 90 | * @param Optgroup|Option ...$items Select options or option groups. 91 | */ 92 | public function items(Optgroup|Option ...$items): self 93 | { 94 | $new = clone $this; 95 | $new->items = $items; 96 | return $new; 97 | } 98 | 99 | public function options(Option ...$options): self 100 | { 101 | return $this->items(...$options); 102 | } 103 | 104 | /** 105 | * @param array $data Options data. The array keys are option values, and the array values are the corresponding 106 | * option labels. The array can also be nested (i.e. some array values are arrays too). For each sub-array, 107 | * an option group will be generated whose label is the key associated with the sub-array. 108 | * 109 | * Example: 110 | * ```php 111 | * [ 112 | * '1' => 'Santiago', 113 | * '2' => 'Concepcion', 114 | * '3' => 'Chillan', 115 | * '4' => 'Moscow' 116 | * '5' => 'San Petersburg', 117 | * '6' => 'Novosibirsk', 118 | * '7' => 'Ekaterinburg' 119 | * ]; 120 | * ``` 121 | * 122 | * Example with options groups: 123 | * ```php 124 | * [ 125 | * '1' => [ 126 | * '1' => 'Santiago', 127 | * '2' => 'Concepcion', 128 | * '3' => 'Chillan', 129 | * ], 130 | * '2' => [ 131 | * '4' => 'Moscow', 132 | * '5' => 'San Petersburg', 133 | * '6' => 'Novosibirsk', 134 | * '7' => 'Ekaterinburg' 135 | * ], 136 | * ]; 137 | * ``` 138 | * @param bool $encode Whether option content should be HTML-encoded. 139 | * @param array[] $optionsAttributes Array of option attribute sets indexed by option values from {@see $data}. 140 | * @param array[] $groupsAttributes Array of group attribute sets indexed by group labels from {@see $data}. 141 | * 142 | * @psalm-param array> $data 143 | */ 144 | public function optionsData( 145 | array $data, 146 | bool $encode = true, 147 | array $optionsAttributes = [], 148 | array $groupsAttributes = [] 149 | ): self { 150 | $items = []; 151 | foreach ($data as $value => $content) { 152 | if (is_array($content)) { 153 | $items[] = Optgroup::tag() 154 | ->label((string) $value) 155 | ->addAttributes($groupsAttributes[$value] ?? []) 156 | ->optionsData($content, $encode, $optionsAttributes); 157 | } else { 158 | $items[] = Option::tag() 159 | ->attributes($optionsAttributes[$value] ?? []) 160 | ->value($value) 161 | ->content($content) 162 | ->encode($encode); 163 | } 164 | } 165 | return $this->items(...$items); 166 | } 167 | 168 | /** 169 | * @param string|null $text Text of the option that has dummy value and is rendered 170 | * as an invitation to select a value. 171 | */ 172 | public function prompt(?string $text): self 173 | { 174 | $new = clone $this; 175 | $new->prompt = $text === null ? null : Option::tag() 176 | ->value('') 177 | ->content($text); 178 | return $new; 179 | } 180 | 181 | /** 182 | * @param Option|null $option Option that has dummy value and is rendered as an invitation to select a value. 183 | */ 184 | public function promptOption(?Option $option): self 185 | { 186 | $new = clone $this; 187 | $new->prompt = $option; 188 | return $new; 189 | } 190 | 191 | /** 192 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-disabledformelements-disabled 193 | * 194 | * @param bool $disabled Whether select input is disabled. 195 | */ 196 | public function disabled(bool $disabled = true): self 197 | { 198 | $new = clone $this; 199 | $new->attributes['disabled'] = $disabled; 200 | return $new; 201 | } 202 | 203 | /** 204 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-select-multiple 205 | * 206 | * @param bool $multiple Whether to allow selecting multiple values. 207 | */ 208 | public function multiple(bool $multiple = true): self 209 | { 210 | $new = clone $this; 211 | $new->attributes['multiple'] = $multiple; 212 | return $new; 213 | } 214 | 215 | /** 216 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-select-required 217 | * 218 | * @param bool $required Whether select input is required. 219 | */ 220 | public function required(bool $required = true): self 221 | { 222 | $new = clone $this; 223 | $new->attributes['required'] = $required; 224 | return $new; 225 | } 226 | 227 | /** 228 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-select-size 229 | * 230 | * @param int|null $size The number of options to show to the user. 231 | */ 232 | public function size(?int $size): self 233 | { 234 | $new = clone $this; 235 | $new->attributes['size'] = $size; 236 | return $new; 237 | } 238 | 239 | public function unselectValue(bool|float|int|string|Stringable|null $value): self 240 | { 241 | $new = clone $this; 242 | $new->unselectValue = $value === null ? null : (string) $value; 243 | return $new; 244 | } 245 | 246 | protected function prepareAttributes(): void 247 | { 248 | if (!empty($this->attributes['multiple']) && !empty($this->attributes['name'])) { 249 | $this->attributes['name'] = Html::getArrayableName((string) $this->attributes['name']); 250 | } 251 | } 252 | 253 | protected function generateContent(): string 254 | { 255 | $items = $this->items; 256 | if ($this->prompt) { 257 | array_unshift($items, $this->prompt); 258 | } 259 | 260 | $items = array_map( 261 | fn ($item) => $item instanceof Optgroup 262 | ? $item->selection(...$this->values) 263 | : $item->selected(in_array($item->getValue(), $this->values, true)), 264 | $items 265 | ); 266 | 267 | return $items 268 | ? "\n" . implode("\n", $items) . "\n" 269 | : ''; 270 | } 271 | 272 | protected function before(): string 273 | { 274 | $name = (string) ($this->attributes['name'] ?? ''); 275 | if ( 276 | empty($name) || 277 | ( 278 | $this->unselectValue === null && 279 | empty($this->attributes['multiple']) 280 | ) 281 | ) { 282 | return ''; 283 | } 284 | 285 | $input = Input::hidden( 286 | Html::getNonArrayableName($name), 287 | (string) $this->unselectValue 288 | ); 289 | 290 | // Make sure disabled input is not sending any value. 291 | if (!empty($this->attributes['disabled'])) { 292 | $input = $input->attribute('disabled', $this->attributes['disabled']); 293 | } 294 | 295 | if (!empty($this->attributes['form'])) { 296 | $input = $input->attribute('form', $this->attributes['form']); 297 | } 298 | 299 | return $input->render() . "\n"; 300 | } 301 | 302 | protected function getName(): string 303 | { 304 | return 'select'; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Tag/Small.php: -------------------------------------------------------------------------------- 1 | attribute('type', $type); 20 | } 21 | 22 | /** 23 | * @link https://html.spec.whatwg.org/multipage/embedded-content.html#attr-source-src 24 | */ 25 | public function src(?string $src): self 26 | { 27 | return $this->attribute('src', $src); 28 | } 29 | 30 | /** 31 | * @link https://html.spec.whatwg.org/multipage/embedded-content.html#attr-source-srcset 32 | */ 33 | public function srcset(?string ...$srcsets): self 34 | { 35 | $items = array_diff($srcsets, [null]); 36 | 37 | return $this->attribute('srcset', $items ? implode(',', $items) : null); 38 | } 39 | 40 | /** 41 | * @link https://html.spec.whatwg.org/multipage/embedded-content.html#attr-source-sizes 42 | */ 43 | public function sizes(?string ...$sizes): self 44 | { 45 | $items = array_diff($sizes, [null]); 46 | 47 | return $this->attribute('sizes', $items ? implode(',', $items) : null); 48 | } 49 | 50 | /** 51 | * @link https://html.spec.whatwg.org/multipage/embedded-content.html#attr-source-media 52 | */ 53 | public function media(?string $media): self 54 | { 55 | return $this->attribute('media', $media); 56 | } 57 | 58 | /** 59 | * @link https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-width 60 | */ 61 | public function width(int|string|null $width): self 62 | { 63 | return $this->attribute('width', $width); 64 | } 65 | 66 | /** 67 | * @link https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 68 | */ 69 | public function height(int|string|null $height): self 70 | { 71 | return $this->attribute('height', $height); 72 | } 73 | 74 | protected function getName(): string 75 | { 76 | return 'source'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Tag/Span.php: -------------------------------------------------------------------------------- 1 | content = $content; 23 | return $new; 24 | } 25 | 26 | public function getContent(): string 27 | { 28 | return $this->content; 29 | } 30 | 31 | public function media(?string $media): self 32 | { 33 | $new = clone $this; 34 | $new->attributes['media'] = $media; 35 | return $new; 36 | } 37 | 38 | public function type(?string $type): self 39 | { 40 | $new = clone $this; 41 | $new->attributes['type'] = $type; 42 | return $new; 43 | } 44 | 45 | protected function getName(): string 46 | { 47 | return 'style'; 48 | } 49 | 50 | /** 51 | * @return string Obtain tag content. 52 | */ 53 | protected function generateContent(): string 54 | { 55 | return $this->content; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tag/Table.php: -------------------------------------------------------------------------------- 1 | caption = $caption; 44 | return $new; 45 | } 46 | 47 | public function captionString(string $content, bool $encode = true): self 48 | { 49 | $caption = Caption::tag()->content($content); 50 | if (!$encode) { 51 | $caption = $caption->encode(false); 52 | } 53 | return $this->caption($caption); 54 | } 55 | 56 | /** 57 | * @param Colgroup ...$columnGroups One or more column groups ({@see Colgroup}). 58 | */ 59 | public function columnGroups(Colgroup ...$columnGroups): self 60 | { 61 | $new = clone $this; 62 | $new->columnGroups = $columnGroups; 63 | return $new; 64 | } 65 | 66 | /** 67 | * @param Colgroup ...$columnGroups One or more column groups ({@see Colgroup}). 68 | */ 69 | public function addColumnGroups(Colgroup ...$columnGroups): self 70 | { 71 | $new = clone $this; 72 | $new->columnGroups = array_merge($new->columnGroups, $columnGroups); 73 | return $new; 74 | } 75 | 76 | /** 77 | * @param Col ...$columns One or more columns ({@see Col}). 78 | */ 79 | public function columns(Col ...$columns): self 80 | { 81 | $new = clone $this; 82 | $new->columns = $columns; 83 | return $new; 84 | } 85 | 86 | /** 87 | * @param Col ...$columns One or more columns ({@see Col}). 88 | */ 89 | public function addColumns(Col ...$columns): self 90 | { 91 | $new = clone $this; 92 | $new->columns = array_merge($new->columns, $columns); 93 | return $new; 94 | } 95 | 96 | public function header(?Thead $header): self 97 | { 98 | $new = clone $this; 99 | $new->header = $header; 100 | return $new; 101 | } 102 | 103 | /** 104 | * @param Tbody ...$body One or more body ({@see Tbody}). 105 | */ 106 | public function body(Tbody ...$body): self 107 | { 108 | $new = clone $this; 109 | $new->body = $body; 110 | return $new; 111 | } 112 | 113 | /** 114 | * @param Tbody ...$body One or more body ({@see Tbody}). 115 | */ 116 | public function addBody(Tbody ...$body): self 117 | { 118 | $new = clone $this; 119 | $new->body = array_merge($new->body, $body); 120 | return $new; 121 | } 122 | 123 | /** 124 | * @param Tr ...$rows One or more rows ({@see Tr}). 125 | */ 126 | public function rows(Tr ...$rows): self 127 | { 128 | $new = clone $this; 129 | $new->rows = $rows; 130 | return $new; 131 | } 132 | 133 | /** 134 | * @param Tr ...$rows One or more rows ({@see Tr}). 135 | */ 136 | public function addRows(Tr ...$rows): self 137 | { 138 | $new = clone $this; 139 | $new->rows = array_merge($new->rows, $rows); 140 | return $new; 141 | } 142 | 143 | public function footer(?Tfoot $footer): self 144 | { 145 | $new = clone $this; 146 | $new->footer = $footer; 147 | return $new; 148 | } 149 | 150 | protected function generateContent(): string 151 | { 152 | $items = []; 153 | 154 | if ($this->caption !== null) { 155 | $items[] = $this->caption; 156 | } 157 | 158 | $items = array_merge( 159 | $items, 160 | $this->columnGroups, 161 | $this->columns, 162 | ); 163 | 164 | if ($this->header !== null) { 165 | $items[] = $this->header; 166 | } 167 | 168 | $items = array_merge( 169 | $items, 170 | $this->body, 171 | $this->rows, 172 | ); 173 | 174 | if ($this->footer !== null) { 175 | $items[] = $this->footer; 176 | } 177 | 178 | return $items 179 | ? "\n" . implode("\n", $items) . "\n" 180 | : ''; 181 | } 182 | 183 | protected function getName(): string 184 | { 185 | return 'table'; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Tag/Tbody.php: -------------------------------------------------------------------------------- 1 | attributes['name'] = $name; 25 | return $new; 26 | } 27 | 28 | public function rows(?int $count): self 29 | { 30 | $new = clone $this; 31 | $new->attributes['rows'] = $count; 32 | return $new; 33 | } 34 | 35 | public function columns(?int $count): self 36 | { 37 | $new = clone $this; 38 | $new->attributes['cols'] = $count; 39 | return $new; 40 | } 41 | 42 | /** 43 | * @param string|string[]|Stringable|null $value 44 | */ 45 | public function value(string|Stringable|array|null $value): self 46 | { 47 | $content = is_array($value) 48 | ? implode("\n", $value) 49 | : (string) $value; 50 | return $this->content($content); 51 | } 52 | 53 | /** 54 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-formelements-form 55 | */ 56 | public function form(?string $formId): self 57 | { 58 | $new = clone $this; 59 | $new->attributes['form'] = $formId; 60 | return $new; 61 | } 62 | 63 | protected function getName(): string 64 | { 65 | return 'textarea'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Tag/Tfoot.php: -------------------------------------------------------------------------------- 1 | items = $cells; 27 | return $new; 28 | } 29 | 30 | /** 31 | * @param TableCellTag ...$cells One or more cells. 32 | */ 33 | public function addCells(TableCellTag ...$cells): self 34 | { 35 | $new = clone $this; 36 | $new->items = array_merge($new->items, $cells); 37 | return $new; 38 | } 39 | 40 | /** 41 | * @param string[] $strings Array of data cells ({@see Td}) as strings. 42 | * @param array $attributes The tag attributes in terms of name-value pairs. 43 | * @param bool $encode Whether to encode strings passed. 44 | */ 45 | public function dataStrings(array $strings, array $attributes = [], bool $encode = true): self 46 | { 47 | return $this->cells(...$this->makeDataCells($strings, $attributes, $encode)); 48 | } 49 | 50 | /** 51 | * @param string[] $strings Array of data cells ({@see Td}) as strings. 52 | * @param array $attributes The tag attributes in terms of name-value pairs. 53 | * @param bool $encode Whether to encode strings passed. 54 | */ 55 | public function addDataStrings(array $strings, array $attributes = [], bool $encode = true): self 56 | { 57 | return $this->addCells(...$this->makeDataCells($strings, $attributes, $encode)); 58 | } 59 | 60 | /** 61 | * @param string[] $strings 62 | * 63 | * @return Td[] 64 | */ 65 | private function makeDataCells(array $strings, array $attributes, bool $encode): array 66 | { 67 | return array_map( 68 | static fn (string $string) => Td::tag() 69 | ->content($string) 70 | ->attributes($attributes) 71 | ->encode($encode), 72 | $strings 73 | ); 74 | } 75 | 76 | /** 77 | * @param string[] $strings Array of header cells ({@see Th}) as strings. 78 | * @param array $attributes The tag attributes in terms of name-value pairs. 79 | * @param bool $encode Whether to encode strings passed. 80 | */ 81 | public function headerStrings(array $strings, array $attributes = [], bool $encode = true): self 82 | { 83 | return $this->cells(...$this->makeHeaderCells($strings, $attributes, $encode)); 84 | } 85 | 86 | /** 87 | * @param string[] $strings Array of header cells ({@see Th}) as strings. 88 | * @param array $attributes The tag attributes in terms of name-value pairs. 89 | * @param bool $encode Whether to encode strings passed. 90 | */ 91 | public function addHeaderStrings(array $strings, array $attributes = [], bool $encode = true): self 92 | { 93 | return $this->addCells(...$this->makeHeaderCells($strings, $attributes, $encode)); 94 | } 95 | 96 | /** 97 | * @param string[] $strings 98 | * 99 | * @return Th[] 100 | */ 101 | private function makeHeaderCells(array $strings, array $attributes, bool $encode): array 102 | { 103 | return array_map( 104 | static fn (string $string) => Th::tag() 105 | ->content($string) 106 | ->attributes($attributes) 107 | ->encode($encode), 108 | $strings 109 | ); 110 | } 111 | 112 | protected function generateContent(): string 113 | { 114 | return $this->items 115 | ? "\n" . implode("\n", $this->items) . "\n" 116 | : ''; 117 | } 118 | 119 | protected function getName(): string 120 | { 121 | return 'tr'; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Tag/Track.php: -------------------------------------------------------------------------------- 1 | attributes['default'] ?? false); 23 | } 24 | 25 | /** 26 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-track-default 27 | */ 28 | public function default(bool $default = true): self 29 | { 30 | return $this->attribute('default', $default); 31 | } 32 | 33 | /** 34 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-track-kind 35 | * @see self::SUBTITLES 36 | * @see self::CAPTIONS 37 | * @see self::DESCRIPTIONS 38 | * @see self::CHAPTERS 39 | * @see self::METADATA 40 | */ 41 | public function kind(?string $kind): self 42 | { 43 | return $this->attribute('kind', $kind); 44 | } 45 | 46 | /** 47 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-track-label 48 | */ 49 | public function label(?string $label): self 50 | { 51 | return $this->attribute('label', $label); 52 | } 53 | 54 | /** 55 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-track-src 56 | */ 57 | public function src(string $src): self 58 | { 59 | return $this->attribute('src', $src); 60 | } 61 | 62 | /** 63 | * @link https://html.spec.whatwg.org/multipage/media.html#attr-track-srclang 64 | */ 65 | public function srclang(?string $srclang): self 66 | { 67 | return $this->attribute('srclang', $srclang); 68 | } 69 | 70 | protected function getName(): string 71 | { 72 | return 'track'; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Tag/Ul.php: -------------------------------------------------------------------------------- 1 | attribute('poster', $poster); 20 | } 21 | 22 | /** 23 | * @link https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-width 24 | */ 25 | public function width(int|string|null $width): self 26 | { 27 | return $this->attribute('width', $width); 28 | } 29 | 30 | /** 31 | * @link https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height 32 | */ 33 | public function height(int|string|null $height): self 34 | { 35 | return $this->attribute('height', $height); 36 | } 37 | 38 | protected function getName(): string 39 | { 40 | return 'video'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Widget/ButtonGroup.php: -------------------------------------------------------------------------------- 1 | containerTag(null); 38 | } 39 | 40 | public function containerTag(?string $name): self 41 | { 42 | $new = clone $this; 43 | $new->containerTag = $name; 44 | return $new; 45 | } 46 | 47 | public function containerAttributes(array $attributes): self 48 | { 49 | $new = clone $this; 50 | $new->containerAttributes = $attributes; 51 | return $new; 52 | } 53 | 54 | public function buttons(Button ...$buttons): self 55 | { 56 | $new = clone $this; 57 | $new->buttons = $buttons; 58 | return $new; 59 | } 60 | 61 | /** 62 | * @param array $data Array of buttons. Each button is an array with label as first element and additional 63 | * name-value pairs as attributes of button. 64 | * 65 | * Example: 66 | * ```php 67 | * [ 68 | * ['Reset', 'type' => 'reset', 'class' => 'default'], 69 | * ['Send', 'type' => 'submit', 'class' => 'primary'], 70 | * ] 71 | * ``` 72 | * @param bool $encode Whether button content should be HTML-encoded. 73 | */ 74 | public function buttonsData(array $data, bool $encode = true): self 75 | { 76 | $buttons = []; 77 | foreach ($data as $row) { 78 | if (!is_array($row) || !isset($row[0]) || !is_string($row[0])) { 79 | throw new InvalidArgumentException( 80 | 'Invalid buttons data. A data row must be array with label as first element ' . 81 | 'and additional name-value pairs as attributes of button.' 82 | ); 83 | } 84 | $label = $row[0]; 85 | unset($row[0]); 86 | $buttons[] = Html::button($label, $row)->encode($encode); 87 | } 88 | return $this->buttons(...$buttons); 89 | } 90 | 91 | public function addButtonAttributes(array $attributes): self 92 | { 93 | $new = clone $this; 94 | $new->buttonAttributes = array_merge($new->buttonAttributes, $attributes); 95 | return $new; 96 | } 97 | 98 | public function buttonAttributes(array $attributes): self 99 | { 100 | $new = clone $this; 101 | $new->buttonAttributes = $attributes; 102 | return $new; 103 | } 104 | 105 | public function disabled(?bool $disabled = true): self 106 | { 107 | $new = clone $this; 108 | $new->buttonAttributes['disabled'] = $disabled; 109 | return $new; 110 | } 111 | 112 | /** 113 | * Specifies the form element the buttons belongs to. The value of this attribute must be the ID attribute of a form 114 | * element in the same document. 115 | * 116 | * @param string|null $id ID of a form. 117 | * 118 | * @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form 119 | */ 120 | public function form(?string $id): self 121 | { 122 | $new = clone $this; 123 | $new->buttonAttributes['form'] = $id; 124 | return $new; 125 | } 126 | 127 | public function separator(string $separator): self 128 | { 129 | $new = clone $this; 130 | $new->separator = $separator; 131 | return $new; 132 | } 133 | 134 | public function render(): string 135 | { 136 | if (empty($this->buttons)) { 137 | return ''; 138 | } 139 | 140 | if (empty($this->buttonAttributes)) { 141 | $lines = $this->buttons; 142 | } else { 143 | $lines = []; 144 | foreach ($this->buttons as $button) { 145 | $lines[] = $button->unionAttributes($this->buttonAttributes); 146 | } 147 | } 148 | 149 | $html = []; 150 | if (!empty($this->containerTag)) { 151 | $html[] = Html::openTag($this->containerTag, $this->containerAttributes); 152 | } 153 | $html[] = implode($this->separator, $lines); 154 | if (!empty($this->containerTag)) { 155 | $html[] = Html::closeTag($this->containerTag); 156 | } 157 | 158 | return implode("\n", $html); 159 | } 160 | 161 | public function __toString(): string 162 | { 163 | return $this->render(); 164 | } 165 | 166 | private function __construct() 167 | { 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Widget/CheckboxList/CheckboxItem.php: -------------------------------------------------------------------------------- 1 | 48 | */ 49 | private array $values = []; 50 | 51 | /** 52 | * @psalm-var Closure(CheckboxItem):string|null 53 | */ 54 | private ?Closure $itemFormatter = null; 55 | 56 | private function __construct( 57 | private string $name 58 | ) { 59 | } 60 | 61 | public static function create(string $name): self 62 | { 63 | return new self($name); 64 | } 65 | 66 | public function name(string $name): self 67 | { 68 | $new = clone $this; 69 | $new->name = $name; 70 | return $new; 71 | } 72 | 73 | public function withoutContainer(): self 74 | { 75 | $new = clone $this; 76 | $new->containerTag = null; 77 | return $new; 78 | } 79 | 80 | public function containerTag(?string $name): self 81 | { 82 | $new = clone $this; 83 | $new->containerTag = $name; 84 | return $new; 85 | } 86 | 87 | public function containerAttributes(array $attributes): self 88 | { 89 | $new = clone $this; 90 | $new->containerAttributes = $attributes; 91 | return $new; 92 | } 93 | 94 | public function checkboxWrapTag(?string $name): self 95 | { 96 | $new = clone $this; 97 | $new->checkboxWrapTag = $name; 98 | return $new; 99 | } 100 | 101 | public function checkboxWrapAttributes(array $attributes): self 102 | { 103 | $new = clone $this; 104 | $new->checkboxWrapAttributes = $attributes; 105 | return $new; 106 | } 107 | 108 | public function checkboxWrapClass(?string ...$class): self 109 | { 110 | $new = clone $this; 111 | $new->checkboxWrapAttributes['class'] = array_filter($class, static fn ($c) => $c !== null); 112 | return $new; 113 | } 114 | 115 | public function addCheckboxWrapClass(?string ...$class): self 116 | { 117 | $new = clone $this; 118 | Html::addCssClass( 119 | $new->checkboxWrapAttributes, 120 | array_filter($class, static fn ($c) => $c !== null), 121 | ); 122 | return $new; 123 | } 124 | 125 | public function addCheckboxAttributes(array $attributes): self 126 | { 127 | $new = clone $this; 128 | $new->checkboxAttributes = array_merge($new->checkboxAttributes, $attributes); 129 | return $new; 130 | } 131 | 132 | public function checkboxAttributes(array $attributes): self 133 | { 134 | $new = clone $this; 135 | $new->checkboxAttributes = $attributes; 136 | return $new; 137 | } 138 | 139 | public function addCheckboxLabelAttributes(array $attributes): self 140 | { 141 | $new = clone $this; 142 | $new->checkboxLabelAttributes = array_merge($new->checkboxLabelAttributes, $attributes); 143 | return $new; 144 | } 145 | 146 | public function checkboxLabelAttributes(array $attributes): self 147 | { 148 | $new = clone $this; 149 | $new->checkboxLabelAttributes = $attributes; 150 | return $new; 151 | } 152 | 153 | public function checkboxLabelWrap(bool $wrap): self 154 | { 155 | $new = clone $this; 156 | $new->checkboxLabelWrap = $wrap; 157 | return $new; 158 | } 159 | 160 | /** 161 | * @param array[] $attributes 162 | */ 163 | public function addIndividualInputAttributes(array $attributes): self 164 | { 165 | $new = clone $this; 166 | $new->individualInputAttributes = array_replace($new->individualInputAttributes, $attributes); 167 | return $new; 168 | } 169 | 170 | /** 171 | * @param array[] $attributes 172 | */ 173 | public function individualInputAttributes(array $attributes): self 174 | { 175 | $new = clone $this; 176 | $new->individualInputAttributes = $attributes; 177 | return $new; 178 | } 179 | 180 | /** 181 | * @param string[] $items 182 | * @param bool $encodeLabels Whether labels should be encoded. 183 | */ 184 | public function items(array $items, bool $encodeLabels = true): self 185 | { 186 | $new = clone $this; 187 | $new->items = $items; 188 | $new->encodeLabels = $encodeLabels; 189 | return $new; 190 | } 191 | 192 | /** 193 | * Fills items from an array provided. Array values are used for both input labels and input values. 194 | * 195 | * @param bool[]|float[]|int[]|string[]|Stringable[] $values 196 | * @param bool $encodeLabels Whether labels should be encoded. 197 | */ 198 | public function itemsFromValues(array $values, bool $encodeLabels = true): self 199 | { 200 | $values = array_map('\strval', $values); 201 | 202 | return $this->items( 203 | array_combine($values, $values), 204 | $encodeLabels 205 | ); 206 | } 207 | 208 | public function value(bool|string|int|float|Stringable|BackedEnum ...$value): self 209 | { 210 | $new = clone $this; 211 | $new->values = array_map( 212 | static fn ($v): string => (string) ($v instanceof BackedEnum ? $v->value : $v), 213 | array_values($value) 214 | ); 215 | return $new; 216 | } 217 | 218 | /** 219 | * @psalm-param iterable $values 220 | */ 221 | public function values(iterable $values): self 222 | { 223 | $values = is_array($values) ? $values : iterator_to_array($values); 224 | return $this->value(...$values); 225 | } 226 | 227 | /** 228 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-formelements-form 229 | */ 230 | public function form(?string $formId): self 231 | { 232 | $new = clone $this; 233 | $new->checkboxAttributes['form'] = $formId; 234 | return $new; 235 | } 236 | 237 | /** 238 | * @link https://www.w3.org/TR/html52/sec-forms.html#the-readonly-attribute 239 | */ 240 | public function readonly(bool $readonly = true): self 241 | { 242 | $new = clone $this; 243 | $new->checkboxAttributes['readonly'] = $readonly; 244 | return $new; 245 | } 246 | 247 | /** 248 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-disabledformelements-disabled 249 | */ 250 | public function disabled(bool $disabled = true): self 251 | { 252 | $new = clone $this; 253 | $new->checkboxAttributes['disabled'] = $disabled; 254 | return $new; 255 | } 256 | 257 | public function uncheckValue(bool|float|int|string|Stringable|null $value): self 258 | { 259 | $new = clone $this; 260 | $new->uncheckValue = $value === null ? null : (string)$value; 261 | return $new; 262 | } 263 | 264 | public function separator(string $separator): self 265 | { 266 | $new = clone $this; 267 | $new->separator = $separator; 268 | return $new; 269 | } 270 | 271 | /** 272 | * @psalm-param Closure(CheckboxItem):string|null $formatter 273 | */ 274 | public function itemFormatter(?Closure $formatter): self 275 | { 276 | $new = clone $this; 277 | $new->itemFormatter = $formatter; 278 | return $new; 279 | } 280 | 281 | public function render(): string 282 | { 283 | $name = Html::getArrayableName($this->name); 284 | 285 | if ($this->checkboxWrapTag === null) { 286 | $beforeCheckbox = ''; 287 | $afterCheckbox = ''; 288 | } else { 289 | $beforeCheckbox = Html::openTag($this->checkboxWrapTag, $this->checkboxWrapAttributes) . "\n"; 290 | $afterCheckbox = "\n" . Html::closeTag($this->checkboxWrapTag); 291 | } 292 | 293 | $lines = []; 294 | $index = 0; 295 | foreach ($this->items as $value => $label) { 296 | $item = new CheckboxItem( 297 | $index, 298 | $name, 299 | $value, 300 | ArrayHelper::isIn($value, $this->values), 301 | array_merge( 302 | $this->checkboxAttributes, 303 | $this->individualInputAttributes[$value] ?? [], 304 | ['name' => $name, 'value' => $value] 305 | ), 306 | $label, 307 | $this->encodeLabels, 308 | $this->checkboxLabelAttributes, 309 | $this->checkboxLabelWrap, 310 | ); 311 | $lines[] = $beforeCheckbox . $this->formatItem($item) . $afterCheckbox; 312 | $index++; 313 | } 314 | 315 | $html = []; 316 | if ($this->uncheckValue !== null) { 317 | $html[] = $this->renderUncheckInput(); 318 | } 319 | if (!empty($this->containerTag)) { 320 | $html[] = Html::openTag($this->containerTag, $this->containerAttributes); 321 | } 322 | if ($lines) { 323 | $html[] = implode($this->separator, $lines); 324 | } 325 | if (!empty($this->containerTag)) { 326 | $html[] = Html::closeTag($this->containerTag); 327 | } 328 | 329 | return implode("\n", $html); 330 | } 331 | 332 | private function renderUncheckInput(): string 333 | { 334 | return 335 | Input::hidden( 336 | Html::getNonArrayableName($this->name), 337 | $this->uncheckValue 338 | ) 339 | ->addAttributes( 340 | array_merge( 341 | [ 342 | // Make sure disabled input is not sending any value 343 | 'disabled' => $this->checkboxAttributes['disabled'] ?? null, 344 | 'form' => $this->checkboxAttributes['form'] ?? null, 345 | ], 346 | $this->individualInputAttributes[$this->uncheckValue] ?? [] 347 | ) 348 | ) 349 | ->render(); 350 | } 351 | 352 | private function formatItem(CheckboxItem $item): string 353 | { 354 | if ($this->itemFormatter !== null) { 355 | return ($this->itemFormatter)($item); 356 | } 357 | 358 | $checkbox = Html::checkbox($item->name, $item->value, $item->checkboxAttributes) 359 | ->checked($item->checked) 360 | ->label($item->label, $item->labelAttributes, $item->labelWrap) 361 | ->labelEncode($item->encodeLabel); 362 | 363 | return $checkbox->render(); 364 | } 365 | 366 | public function __toString(): string 367 | { 368 | return $this->render(); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/Widget/RadioList/RadioItem.php: -------------------------------------------------------------------------------- 1 | name = $name; 64 | return $new; 65 | } 66 | 67 | public function withoutContainer(): self 68 | { 69 | $new = clone $this; 70 | $new->containerTag = null; 71 | return $new; 72 | } 73 | 74 | public function containerTag(?string $name): self 75 | { 76 | $new = clone $this; 77 | $new->containerTag = $name; 78 | return $new; 79 | } 80 | 81 | public function containerAttributes(array $attributes): self 82 | { 83 | $new = clone $this; 84 | $new->containerAttributes = $attributes; 85 | return $new; 86 | } 87 | 88 | public function radioWrapTag(?string $name): self 89 | { 90 | $new = clone $this; 91 | $new->radioWrapTag = $name; 92 | return $new; 93 | } 94 | 95 | public function radioWrapAttributes(array $attributes): self 96 | { 97 | $new = clone $this; 98 | $new->radioWrapAttributes = $attributes; 99 | return $new; 100 | } 101 | 102 | public function radioWrapClass(?string ...$class): self 103 | { 104 | $new = clone $this; 105 | $new->radioWrapAttributes['class'] = array_filter($class, static fn ($c) => $c !== null); 106 | return $new; 107 | } 108 | 109 | public function addRadioWrapClass(?string ...$class): self 110 | { 111 | $new = clone $this; 112 | Html::addCssClass( 113 | $new->radioWrapAttributes, 114 | array_filter($class, static fn ($c) => $c !== null), 115 | ); 116 | return $new; 117 | } 118 | 119 | public function addRadioAttributes(array $attributes): self 120 | { 121 | $new = clone $this; 122 | $new->radioAttributes = array_merge($new->radioAttributes, $attributes); 123 | return $new; 124 | } 125 | 126 | public function radioAttributes(array $attributes): self 127 | { 128 | $new = clone $this; 129 | $new->radioAttributes = $attributes; 130 | return $new; 131 | } 132 | 133 | public function addRadioLabelAttributes(array $attributes): self 134 | { 135 | $new = clone $this; 136 | $new->radioLabelAttributes = array_merge($new->radioLabelAttributes, $attributes); 137 | return $new; 138 | } 139 | 140 | public function radioLabelAttributes(array $attributes): self 141 | { 142 | $new = clone $this; 143 | $new->radioLabelAttributes = $attributes; 144 | return $new; 145 | } 146 | 147 | public function radioLabelWrap(bool $wrap): self 148 | { 149 | $new = clone $this; 150 | $new->radioLabelWrap = $wrap; 151 | return $new; 152 | } 153 | 154 | /** 155 | * @param array[] $attributes 156 | */ 157 | public function addIndividualInputAttributes(array $attributes): self 158 | { 159 | $new = clone $this; 160 | $new->individualInputAttributes = array_replace($new->individualInputAttributes, $attributes); 161 | return $new; 162 | } 163 | 164 | /** 165 | * @param array[] $attributes 166 | */ 167 | public function individualInputAttributes(array $attributes): self 168 | { 169 | $new = clone $this; 170 | $new->individualInputAttributes = $attributes; 171 | return $new; 172 | } 173 | 174 | /** 175 | * @param string[] $items 176 | * @param bool $encodeLabels Whether labels should be encoded. 177 | */ 178 | public function items(array $items, bool $encodeLabels = true): self 179 | { 180 | $new = clone $this; 181 | $new->items = $items; 182 | $new->encodeLabels = $encodeLabels; 183 | return $new; 184 | } 185 | 186 | /** 187 | * Fills items from an array provided. Array values are used for both input labels and input values. 188 | * 189 | * @param bool[]|float[]|int[]|string[]|Stringable[] $values 190 | * @param bool $encodeLabels Whether labels should be encoded. 191 | */ 192 | public function itemsFromValues(array $values, bool $encodeLabels = true): self 193 | { 194 | $values = array_map('\strval', $values); 195 | 196 | return $this->items( 197 | array_combine($values, $values), 198 | $encodeLabels 199 | ); 200 | } 201 | 202 | public function value(bool|float|int|string|Stringable|BackedEnum|null $value): self 203 | { 204 | $new = clone $this; 205 | $new->value = $value === null 206 | ? null 207 | : (string) ($value instanceof BackedEnum ? $value->value : $value); 208 | return $new; 209 | } 210 | 211 | /** 212 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-formelements-form 213 | */ 214 | public function form(?string $formId): self 215 | { 216 | $new = clone $this; 217 | $new->radioAttributes['form'] = $formId; 218 | return $new; 219 | } 220 | 221 | /** 222 | * @link https://www.w3.org/TR/html52/sec-forms.html#the-readonly-attribute 223 | */ 224 | public function readonly(bool $readonly = true): self 225 | { 226 | $new = clone $this; 227 | $new->radioAttributes['readonly'] = $readonly; 228 | return $new; 229 | } 230 | 231 | /** 232 | * @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-disabledformelements-disabled 233 | */ 234 | public function disabled(bool $disabled = true): self 235 | { 236 | $new = clone $this; 237 | $new->radioAttributes['disabled'] = $disabled; 238 | return $new; 239 | } 240 | 241 | public function uncheckValue(bool|float|int|string|Stringable|null $value): self 242 | { 243 | $new = clone $this; 244 | $new->uncheckValue = $value === null ? null : (string) $value; 245 | return $new; 246 | } 247 | 248 | public function separator(string $separator): self 249 | { 250 | $new = clone $this; 251 | $new->separator = $separator; 252 | return $new; 253 | } 254 | 255 | /** 256 | * @psalm-param Closure(RadioItem):string|null $formatter 257 | */ 258 | public function itemFormatter(?Closure $formatter): self 259 | { 260 | $new = clone $this; 261 | $new->itemFormatter = $formatter; 262 | return $new; 263 | } 264 | 265 | public function render(): string 266 | { 267 | if ($this->radioWrapTag === null) { 268 | $beforeRadio = ''; 269 | $afterRadio = ''; 270 | } else { 271 | $beforeRadio = Html::openTag($this->radioWrapTag, $this->radioWrapAttributes) . "\n"; 272 | $afterRadio = "\n" . Html::closeTag($this->radioWrapTag); 273 | } 274 | 275 | $lines = []; 276 | $index = 0; 277 | foreach ($this->items as $value => $label) { 278 | $item = new RadioItem( 279 | $index, 280 | $this->name, 281 | $value, 282 | $this->value !== null && $this->value == $value, 283 | array_merge( 284 | $this->radioAttributes, 285 | $this->individualInputAttributes[$value] ?? [], 286 | ['name' => $this->name, 'value' => $value] 287 | ), 288 | $label, 289 | $this->encodeLabels, 290 | $this->radioLabelAttributes, 291 | $this->radioLabelWrap, 292 | ); 293 | $lines[] = $beforeRadio . $this->formatItem($item) . $afterRadio; 294 | $index++; 295 | } 296 | 297 | $html = []; 298 | if ($this->uncheckValue !== null) { 299 | $html[] = $this->renderUncheckInput(); 300 | } 301 | if (!empty($this->containerTag)) { 302 | $html[] = Html::openTag($this->containerTag, $this->containerAttributes); 303 | } 304 | if ($lines) { 305 | $html[] = implode($this->separator, $lines); 306 | } 307 | if (!empty($this->containerTag)) { 308 | $html[] = Html::closeTag($this->containerTag); 309 | } 310 | 311 | return implode("\n", $html); 312 | } 313 | 314 | private function renderUncheckInput(): string 315 | { 316 | return 317 | Input::hidden( 318 | Html::getNonArrayableName($this->name), 319 | $this->uncheckValue 320 | ) 321 | ->addAttributes( 322 | array_merge( 323 | [ 324 | // Make sure disabled input is not sending any value 325 | 'disabled' => $this->radioAttributes['disabled'] ?? null, 326 | 'form' => $this->radioAttributes['form'] ?? null, 327 | ], 328 | $this->individualInputAttributes[$this->uncheckValue] ?? [] 329 | ) 330 | ) 331 | ->render(); 332 | } 333 | 334 | private function formatItem(RadioItem $item): string 335 | { 336 | if ($this->itemFormatter !== null) { 337 | return ($this->itemFormatter)($item); 338 | } 339 | 340 | $radio = Html::radio($item->name, $item->value, $item->radioAttributes) 341 | ->checked($item->checked) 342 | ->label($item->label, $item->labelAttributes, $item->labelWrap) 343 | ->labelEncode($item->encodeLabel); 344 | 345 | return $radio->render(); 346 | } 347 | 348 | public function __toString(): string 349 | { 350 | return $this->render(); 351 | } 352 | } 353 | --------------------------------------------------------------------------------