├── .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 |
4 |
5 |
Yii HTML
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/html)
10 | [](https://packagist.org/packages/yiisoft/html)
11 | [](https://github.com/yiisoft/html/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/html)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/html/master)
14 | [](https://github.com/yiisoft/html/actions?query=workflow%3A%22static+analysis%22)
15 | [](https://shepherd.dev/github/yiisoft/html)
16 | [](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 | = Meta::pragmaDirective('X-UA-Compatible', 'IE=edge') ?>
54 | = Meta::data('viewport', 'width=device-width, initial-scale=1') ?>
55 |
56 | = Html::cssFile(
57 | 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css',
58 | [
59 | 'integrity' => 'sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T',
60 | 'crossorigin' => 'anonymous'
61 | ]
62 | ) ?>
63 | = Html::cssFile('/css/site.css', ['rel' => 'stylesheet']) ?>
64 |
65 | = Html::openTag('footer', ['class' => 'footer']) ?>
66 | = Html::openTag('div', ['class' => 'container flex-fill']) ?>
67 | = Html::p('', ['class' => 'float-left']) ?>
68 | = Html::p()
69 | ->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 | = Html::closeTag('div') ?>
79 | = Html::closeTag('footer') ?>
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 |
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 | [](https://opencollective.com/yiisoft)
407 |
408 | ## Follow updates
409 |
410 | [](https://www.yiiframework.com/)
411 | [](https://twitter.com/yiiframework)
412 | [](https://t.me/yii3en)
413 | [](https://www.facebook.com/groups/yiitalk)
414 | [](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 '' . $this->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 '' . $this->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 |
--------------------------------------------------------------------------------