├── LICENSE.md ├── README.md ├── composer.json └── src ├── Contracts ├── Elements │ └── HtmlElement.php ├── Html.php ├── Renderable.php └── Selectable.php ├── Elements ├── A.php ├── Button.php ├── Concerns │ ├── HasAttributes.php │ ├── HasAutofocusAttribute.php │ ├── HasChildElements.php │ ├── HasConditionalMethods.php │ ├── HasDisabledAttribute.php │ ├── HasMinMaxLengthAttributes.php │ ├── HasNameAttribute.php │ ├── HasPlaceholderAttribute.php │ ├── HasReadonlyAttribute.php │ ├── HasRequiredAttribute.php │ ├── HasTargetAttribute.php │ ├── HasTypeAttribute.php │ └── HasValueAttribute.php ├── Div.php ├── Dl.php ├── Fieldset.php ├── File.php ├── Form.php ├── HtmlElement.php ├── I.php ├── Img.php ├── Input.php ├── Label.php ├── Legend.php ├── Link.php ├── ListElement.php ├── Meta.php ├── Ol.php ├── Optgroup.php ├── Option.php ├── P.php ├── Select.php ├── Span.php ├── Textarea.php └── Ul.php ├── Entities ├── Attributes.php ├── Attributes │ ├── AbstractAttribute.php │ ├── ClassAttribute.php │ └── MiscAttribute.php └── ChildrenCollection.php ├── Exceptions ├── InvalidChildException.php ├── InvalidHtmlException.php ├── MissingTagException.php └── VoidElementException.php └── Html.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) ARCANEDEV(c) - php-html 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-html 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcanedev/php-html", 3 | "description": "A simple way to create html tags with php.", 4 | "homepage": "https://github.com/ARCANEDEV/php-html", 5 | "keywords": ["arcanedev", "html", "form", "tags"], 6 | "authors": [ 7 | { 8 | "name": "ARCANEDEV", 9 | "email": "arcanedev.maroc@gmail.com", 10 | "homepage": "https://github.com/arcanedev-maroc", 11 | "role": "Developer" 12 | } 13 | ], 14 | "type": "library", 15 | "license": "MIT", 16 | "require": { 17 | "php": "^8.2", 18 | "illuminate/support": "^11.0" 19 | }, 20 | "require-dev": { 21 | "ext-dom": "*", 22 | "laravel/pint": "^1.14", 23 | "phpunit/phpunit": "^10.5|^11.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Arcanedev\\Html\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Arcanedev\\Html\\Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "phpunit --colors=always", 37 | "test:dox": "phpunit --testdox --colors=always", 38 | "test:ci": "phpunit --coverage-text", 39 | "cs:fix": "pint -v", 40 | "cs:test": "pint --test -v" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-develop": "8.x-dev" 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/Contracts/Elements/HtmlElement.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface HtmlElement extends Renderable 16 | { 17 | /* ----------------------------------------------------------------- 18 | | Getters & Setters 19 | | ----------------------------------------------------------------- 20 | */ 21 | 22 | /** 23 | * Get the element attributes. 24 | */ 25 | public function getAttributes(): Attributes; 26 | 27 | /* ----------------------------------------------------------------- 28 | | Main Methods 29 | | ----------------------------------------------------------------- 30 | */ 31 | 32 | /** 33 | * Set an attribute. 34 | * 35 | * @return $this 36 | */ 37 | public function attribute(string $name, mixed $value = null): static; 38 | 39 | /** 40 | * Set the attributes. 41 | * 42 | * @return $this 43 | */ 44 | public function attributes(iterable $attributes): static; 45 | } 46 | -------------------------------------------------------------------------------- /src/Contracts/Html.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | interface Html 33 | { 34 | /* ----------------------------------------------------------------- 35 | | Main Methods 36 | | ----------------------------------------------------------------- 37 | */ 38 | 39 | /** 40 | * Make an `a` tag. 41 | */ 42 | public function a(?string $href = null, ?string $content = null): A; 43 | 44 | /** 45 | * Make a `button` tag. 46 | */ 47 | public function button(mixed $content = null, ?string $type = null): Button; 48 | 49 | /** 50 | * Make a checkbox input. 51 | */ 52 | public function checkbox(?string $name = null, ?bool $checked = null, string|int $value = '1'): Input; 53 | 54 | /** 55 | * Parse and render `class` attribute. 56 | */ 57 | public function class(iterable|string $classes): string; 58 | 59 | /** 60 | * Make a date input. 61 | */ 62 | public function date(?string $name = null, ?string $value = null, bool $format = true): Input; 63 | 64 | /** 65 | * Make a datetime input. 66 | */ 67 | public function datetime(?string $name = null, ?string $value = null, ?bool $format = true): Input; 68 | 69 | /** 70 | * Make a div element. 71 | */ 72 | public function div(mixed $content = null): Div; 73 | 74 | /** 75 | * Make a description list. 76 | */ 77 | public function dl(array $attributes = []): Dl; 78 | 79 | /** 80 | * Make a custom tag element. 81 | */ 82 | public function element(string $tag): HtmlElement; 83 | 84 | /** 85 | * Make an email input. 86 | */ 87 | public function email(?string $name = null, ?string $value = null): Input; 88 | 89 | /** 90 | * Make a fieldset tag. 91 | */ 92 | public function fieldset(mixed $legend = null): Fieldset; 93 | 94 | /** 95 | * Make a file input. 96 | */ 97 | public function file(?string $name = null): File; 98 | 99 | /** 100 | * Make a form tag. 101 | */ 102 | public function form(string $method = 'POST', ?string $action = null): Form; 103 | 104 | /** 105 | * Make a hidden input. 106 | */ 107 | public function hidden(?string $name = null, ?string $value = null): Input; 108 | 109 | /** 110 | * Make an i tag. 111 | */ 112 | public function i(?string $content = null): I; 113 | 114 | /** 115 | * Make an input tag. 116 | */ 117 | public function input(?string $type = null, ?string $name = null, mixed $value = null): Input; 118 | 119 | /** 120 | * Make an image tag. 121 | */ 122 | public function img(?string $src = null, ?string $alt = null): Img; 123 | 124 | /** 125 | * Make a label tag. 126 | */ 127 | public function label(mixed $content = null, ?string $for = null): Label; 128 | 129 | /** 130 | * Make a legend tag. 131 | */ 132 | public function legend(HtmlElement|string|null $content = null): Legend; 133 | 134 | /** 135 | * Make a mailto link. 136 | */ 137 | public function mailto(string $email, ?string $content = null): A; 138 | 139 | /** 140 | * Make a number input. 141 | */ 142 | public function number( 143 | ?string $name = null, 144 | mixed $value = null, 145 | mixed $min = null, 146 | mixed $max = null, 147 | mixed $step = null 148 | ): Input; 149 | 150 | /** 151 | * Make an ordered list. 152 | */ 153 | public function ol(array $attributes = []): Ol; 154 | 155 | /** 156 | * Make an option tag. 157 | */ 158 | public function option(?string $text = null, mixed $value = null, bool $selected = false): Option; 159 | 160 | /** 161 | * Make a password input. 162 | */ 163 | public function password(?string $name = null): Input; 164 | 165 | /** 166 | * Make a radio input. 167 | */ 168 | public function radio(?string $name = null, ?bool $checked = null, mixed $value = null): Input; 169 | 170 | /** 171 | * Make a range input. 172 | */ 173 | public function range( 174 | ?string $name = null, 175 | mixed $value = null, 176 | mixed $min = null, 177 | mixed $max = null, 178 | mixed $step = null 179 | ): Input; 180 | 181 | /** 182 | * Make a reset button. 183 | */ 184 | public function reset(mixed $content = null): Button; 185 | 186 | /** 187 | * Make a select tag. 188 | */ 189 | public function select(?string $name = null, iterable $options = [], mixed $value = null): Select; 190 | 191 | /** 192 | * Make a span tag. 193 | */ 194 | public function span(mixed $content = null): Span; 195 | 196 | /** 197 | * Make a submit button. 198 | */ 199 | public function submit(?string $text = null): Button; 200 | 201 | /** 202 | * Make a tel link. 203 | */ 204 | public function telLink(string $phoneNumber, mixed $text = null): A; 205 | 206 | /** 207 | * Make a text input. 208 | */ 209 | public function text(string $name, ?string $value = null): Input; 210 | 211 | /** 212 | * Make a textarea tag. 213 | */ 214 | public function textarea(?string $name = null, ?string $value = null): Textarea; 215 | 216 | /** 217 | * Make a time input. 218 | */ 219 | public function time(?string $name = null, ?string $value = null, bool $format = true): Input; 220 | 221 | /** 222 | * Make an unordered list. 223 | */ 224 | public function ul(array $attributes = []): Ul; 225 | } 226 | -------------------------------------------------------------------------------- /src/Contracts/Renderable.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface Renderable extends Htmlable 16 | { 17 | /* ----------------------------------------------------------------- 18 | | Main Methods 19 | | ----------------------------------------------------------------- 20 | */ 21 | 22 | /** 23 | * Render the object as a string of HTML. 24 | */ 25 | public function render(): HtmlString; 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/Selectable.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface Selectable 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Main Methods 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * Add the selected attribute. 21 | */ 22 | public function selected(): static; 23 | 24 | /** 25 | * Add the selected if it fulfill the condition. 26 | */ 27 | public function selectedIf(bool $condition): static; 28 | 29 | /** 30 | * Remove the selected attribute. 31 | */ 32 | public function unselected(): static; 33 | } 34 | -------------------------------------------------------------------------------- /src/Elements/A.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class A extends HtmlElement 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Traits 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | use HasTargetAttribute; 22 | 23 | /* ----------------------------------------------------------------- 24 | | Properties 25 | | ----------------------------------------------------------------- 26 | */ 27 | 28 | protected string $tag = 'a'; 29 | 30 | /* ----------------------------------------------------------------- 31 | | Main Methods 32 | | ----------------------------------------------------------------- 33 | */ 34 | 35 | /** 36 | * Set the href url. 37 | * 38 | * @return $this 39 | */ 40 | public function href(string $href): static 41 | { 42 | return $this->attribute('href', $href); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Elements/Button.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Button extends HtmlElement 18 | { 19 | /* ----------------------------------------------------------------- 20 | | Traits 21 | | ----------------------------------------------------------------- 22 | */ 23 | 24 | use HasDisabledAttribute; 25 | use HasNameAttribute; 26 | use HasTypeAttribute; 27 | use HasValueAttribute; 28 | 29 | /* ----------------------------------------------------------------- 30 | | Properties 31 | | ----------------------------------------------------------------- 32 | */ 33 | 34 | protected string $tag = 'button'; 35 | 36 | /* ----------------------------------------------------------------- 37 | | Main Methods 38 | | ----------------------------------------------------------------- 39 | */ 40 | 41 | /** 42 | * Set as submit button. 43 | * 44 | * @return $this 45 | */ 46 | public function submit(): static 47 | { 48 | return $this->type('submit'); 49 | } 50 | 51 | /** 52 | * Set as reset button. 53 | * 54 | * @return $this 55 | */ 56 | public function reset(): static 57 | { 58 | return $this->type('reset'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasAttributes.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | trait HasAttributes 16 | { 17 | /* ----------------------------------------------------------------- 18 | | Properties 19 | | ----------------------------------------------------------------- 20 | */ 21 | 22 | /** 23 | * The element's attributes. 24 | */ 25 | protected Attributes $attributes; 26 | 27 | /* ----------------------------------------------------------------- 28 | | Getters & Setters 29 | | ----------------------------------------------------------------- 30 | */ 31 | 32 | /** 33 | * Get the element attributes. 34 | */ 35 | public function getAttributes(): Attributes 36 | { 37 | return $this->attributes; 38 | } 39 | 40 | /* ----------------------------------------------------------------- 41 | | Main Methods 42 | | ----------------------------------------------------------------- 43 | */ 44 | 45 | /** 46 | * Reset the attributes. 47 | * 48 | * @return $this 49 | */ 50 | public function initAttributes(): static 51 | { 52 | $this->attributes = new Attributes(); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Set an attribute. 59 | * 60 | * @return $this 61 | */ 62 | public function attribute(string $name, mixed $value = null): static 63 | { 64 | return tap(clone $this, function (HtmlElement $elt) use ($name, $value): void { 65 | $elt->getAttributes()->set($name, $value); 66 | }); 67 | } 68 | 69 | /** 70 | * Set the attributes. 71 | * 72 | * @return $this 73 | */ 74 | public function attributes(iterable $attributes): static 75 | { 76 | return tap(clone $this, function (HtmlElement $elt) use ($attributes): void { 77 | $elt->getAttributes()->setMany($attributes); 78 | }); 79 | } 80 | 81 | /** 82 | * Forget attribute. 83 | * 84 | * @return $this 85 | */ 86 | public function forgetAttribute(string $name): static 87 | { 88 | if ( ! $this->hasAttribute($name)) { 89 | return $this; 90 | } 91 | 92 | return tap(clone $this, function (self $elt) use ($name): void { 93 | $elt->getAttributes()->forget($name); 94 | }); 95 | } 96 | 97 | /** 98 | * Get an attribute. 99 | * 100 | * @return \Arcanedev\Html\Entities\Attributes\MiscAttribute|mixed 101 | */ 102 | public function getAttribute(string $name, mixed $default = null): mixed 103 | { 104 | return $this->getAttributes()->get($name, $default); 105 | } 106 | 107 | /** 108 | * Check if attribute exists. 109 | */ 110 | public function hasAttribute(string $attribute): bool 111 | { 112 | return $this->getAttributes()->has($attribute); 113 | } 114 | 115 | /** 116 | * Get the attribute's value. 117 | */ 118 | protected function getAttributeValue(string $name): ?string 119 | { 120 | if ( ! $this->hasAttribute($name)) { 121 | return null; 122 | } 123 | 124 | return $this->getAttribute($name)->value(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasAutofocusAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasAutofocusAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the autofocus attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function autofocus(bool $autofocus = true): static 27 | { 28 | return $autofocus 29 | ? $this->attribute('autofocus') 30 | : $this->forgetAttribute('autofocus'); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasChildElements.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | trait HasChildElements 17 | { 18 | /* ----------------------------------------------------------------- 19 | | Properties 20 | | ----------------------------------------------------------------- 21 | */ 22 | 23 | /** 24 | * The element's children. 25 | */ 26 | protected ChildrenCollection $children; 27 | 28 | /* ----------------------------------------------------------------- 29 | | Getters & Setters 30 | | ----------------------------------------------------------------- 31 | */ 32 | 33 | /** 34 | * Get the children elements. 35 | */ 36 | public function getChildren(): ChildrenCollection 37 | { 38 | return $this->children; 39 | } 40 | 41 | /** 42 | * Set the children elements. 43 | * 44 | * @return $this 45 | */ 46 | public function setChildren(ChildrenCollection $children): static 47 | { 48 | $this->children = $children; 49 | 50 | return $this; 51 | } 52 | 53 | /* ----------------------------------------------------------------- 54 | | Main Methods 55 | | ----------------------------------------------------------------- 56 | */ 57 | 58 | /** 59 | * Init the children elements. 60 | * 61 | * @return $this 62 | */ 63 | public function initChildren(): static 64 | { 65 | return $this->setChildren(new ChildrenCollection()); 66 | } 67 | 68 | /** 69 | * Alias for `addChild`. 70 | * 71 | * @return $this 72 | */ 73 | public function children(mixed $children, Closure|array|null $mapper = null): static 74 | { 75 | return $this->addChild($children, $mapper); 76 | } 77 | 78 | /** 79 | * Add a child element to the parent. 80 | * 81 | * @return $this 82 | */ 83 | public function addChild(mixed $child, Closure|array|null $mapper = null): static 84 | { 85 | if ($child === null) { 86 | return $this; 87 | } 88 | 89 | return tap(clone $this, function (HtmlElement $elt) use ($child, $mapper): void { 90 | $elt->setChildren( 91 | $elt->getChildren()->merge(ChildrenCollection::parse($child, $mapper)) 92 | ); 93 | }); 94 | } 95 | 96 | /** 97 | * Replace all children with an array of elements. 98 | * 99 | * @return $this 100 | */ 101 | public function setNewChildren(mixed $children, Closure|array|null $mapper = null): static 102 | { 103 | return tap(clone $this) 104 | ->initChildren() 105 | ->addChild($children, $mapper); 106 | } 107 | 108 | /** 109 | * Alias for `prependChildren`. 110 | * 111 | * @return $this 112 | */ 113 | public function prependChild(mixed $children, Closure|array|null $mapper = null): static 114 | { 115 | return $this->prependChildren($children, $mapper); 116 | } 117 | 118 | /** 119 | * Prepend children elements. 120 | * 121 | * @return $this 122 | */ 123 | public function prependChildren(mixed $children, Closure|array|null $mapper = null): static 124 | { 125 | return tap(clone $this, function (HtmlElement $elt) use ($children, $mapper): void { 126 | $elt->getChildren() 127 | ->prepend(ChildrenCollection::parse($children, $mapper)); 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasConditionalMethods.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | trait HasConditionalMethods 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Properties 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * The supported conditions. 23 | */ 24 | protected array $supportedConditions = [ 25 | 'If', 26 | 'Unless', 27 | 'IfNotNull', 28 | ]; 29 | 30 | /* ----------------------------------------------------------------- 31 | | Main Methods 32 | | ----------------------------------------------------------------- 33 | */ 34 | 35 | /** 36 | * Conditionally transform the element. 37 | * Note that since elements are immutable, you'll need to return a new instance from the callback. 38 | * 39 | * @return $this|mixed 40 | */ 41 | public function if(bool $condition, Closure $callback): mixed 42 | { 43 | return $condition ? $callback($this) : $this; 44 | } 45 | 46 | /** 47 | * Conditionally transform the element. 48 | * Note that since elements are immutable, you'll need to return a new instance from the callback. 49 | * 50 | * @return $this|mixed 51 | */ 52 | public function unless(bool $condition, Closure $callback): mixed 53 | { 54 | return $this->if( ! $condition, $callback); 55 | } 56 | 57 | /** 58 | * Conditionally transform the element. 59 | * Note that since elements are immutable, you'll need to return a new instance from the callback. 60 | * 61 | * @return $this|mixed 62 | */ 63 | public function ifNotNull(mixed $value, Closure $callback): mixed 64 | { 65 | return $this->if($value !== null, $callback); 66 | } 67 | 68 | /* ----------------------------------------------------------------- 69 | | Other Methods 70 | | ----------------------------------------------------------------- 71 | */ 72 | 73 | /** 74 | * Call the if condition. 75 | * 76 | * @return $this|mixed 77 | */ 78 | protected function callConditionalMethod(string $conditions, string $method, array $arguments): mixed 79 | { 80 | $value = array_shift($arguments); 81 | $callback = fn(): self => $this->{$method}(...$arguments); 82 | 83 | return match ($conditions) { 84 | 'If' => $this->if((bool) $value, $callback), 85 | 'Unless' => $this->unless((bool) $value, $callback), 86 | 'IfNotNull' => $this->ifNotNull($value, $callback), 87 | default => $this, 88 | }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasDisabledAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasDisabledAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the disabled attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function disabled(bool $disabled = true): static 27 | { 28 | return $disabled 29 | ? $this->attribute('disabled', 'disabled') 30 | : $this->forgetAttribute('disabled'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasMinMaxLengthAttributes.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasMinMaxLengthAttributes 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the `minlength` attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function minlength(int $length): static 27 | { 28 | return $this->attribute('minlength', $length); 29 | } 30 | 31 | /** 32 | * Add the `maxlength` attribute. 33 | * 34 | * @return $this 35 | */ 36 | public function maxlength(int $length): static 37 | { 38 | return $this->attribute('maxlength', $length); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasNameAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasNameAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the name attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function name(string $name): static 27 | { 28 | return $this->attribute('name', $name); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasPlaceholderAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasPlaceholderAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the placeholder attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function placeholder(string $placeholder): static 27 | { 28 | return $this->attribute('placeholder', $placeholder); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasReadonlyAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasReadonlyAttribute 15 | { 16 | /** 17 | * Add the readonly attribute. 18 | * 19 | * @return $this 20 | */ 21 | public function isReadonly(bool $readonly = true): static 22 | { 23 | return $readonly 24 | ? $this->attribute('readonly') 25 | : $this->forgetAttribute('readonly'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasRequiredAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasRequiredAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the required attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function required(bool $required = true): static 27 | { 28 | return $required 29 | ? $this->attribute('required') 30 | : $this->forgetAttribute('required'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasTargetAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait HasTargetAttribute 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Main Methods 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * Add the target attribute. 21 | * 22 | * @return $this 23 | */ 24 | public function target(string $target): static 25 | { 26 | return $this->attribute('target', $target); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasTypeAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasTypeAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the type attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function type(string $type): static 27 | { 28 | return $this->attribute('type', $type); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Elements/Concerns/HasValueAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @mixin \Arcanedev\Html\Elements\Concerns\HasAttributes 13 | */ 14 | trait HasValueAttribute 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | /** 22 | * Add the value attribute. 23 | * 24 | * @return $this 25 | */ 26 | public function value(mixed $value): static 27 | { 28 | return $this->attribute('value', $value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Elements/Div.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Div extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'div'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Elements/Dl.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Dl extends HtmlElement 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Properties 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | protected string $tag = 'dl'; 22 | 23 | /* ----------------------------------------------------------------- 24 | | Main Methods 25 | | ----------------------------------------------------------------- 26 | */ 27 | 28 | /** 29 | * Add a term item. 30 | * 31 | * @return $this 32 | */ 33 | public function dt(mixed $value, array $attributes = []): static 34 | { 35 | return $this->addChild( 36 | $this->makeTerm($value, $attributes) 37 | ); 38 | } 39 | 40 | /** 41 | * Add a definition item. 42 | * 43 | * @return $this 44 | */ 45 | public function dd(mixed $value, array $attributes = []): static 46 | { 47 | return $this->addChild( 48 | $this->makeDefinition($value, $attributes) 49 | ); 50 | } 51 | 52 | /** 53 | * Add list items. 54 | * 55 | * @return $this 56 | */ 57 | public function items(iterable $items, array $attributes = []): static 58 | { 59 | $dtAttributes = Arr::pull($attributes, 'dt', []); 60 | $ddAttributes = Arr::pull($attributes, 'dd', []); 61 | $dlItems = []; 62 | 63 | foreach ($items as $term => $definitions) { 64 | // DT 65 | $dlItems[] = $this->makeTerm($term, $attributes) 66 | ->attributes($dtAttributes); 67 | // DD 68 | foreach (Arr::wrap($definitions) as $definition) { 69 | $dlItems[] = $this->makeDefinition($definition, $attributes) 70 | ->attributes($ddAttributes); 71 | } 72 | } 73 | 74 | return $this->children($dlItems); 75 | } 76 | 77 | /* ----------------------------------------------------------------- 78 | | Other Methods 79 | | ----------------------------------------------------------------- 80 | */ 81 | 82 | /** 83 | * Make a term item. 84 | */ 85 | protected function makeTerm(mixed $value, array $attributes = []): HtmlElement 86 | { 87 | return static::withTag('dt')->attributes($attributes)->html($value); 88 | } 89 | 90 | /** 91 | * Make a definition item. 92 | */ 93 | protected function makeDefinition(mixed $value, array $attributes = []): HtmlElement 94 | { 95 | return static::withTag('dd')->attributes($attributes)->html($value); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Elements/Fieldset.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Fieldset extends HtmlElement 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Properties 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | use HasDisabledAttribute; 22 | 23 | /* ----------------------------------------------------------------- 24 | | Properties 25 | | ----------------------------------------------------------------- 26 | */ 27 | 28 | protected string $tag = 'fieldset'; 29 | 30 | /* ----------------------------------------------------------------- 31 | | Main Methods 32 | | ----------------------------------------------------------------- 33 | */ 34 | 35 | /** 36 | * Set the legend. 37 | * 38 | * @return $this 39 | */ 40 | public function legend(mixed $content): static 41 | { 42 | return $this->prependChild( 43 | Legend::make()->text($content) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Elements/File.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class File extends HtmlElement 17 | { 18 | /* ----------------------------------------------------------------- 19 | | Traits 20 | | ----------------------------------------------------------------- 21 | */ 22 | 23 | use HasAutofocusAttribute; 24 | use HasNameAttribute; 25 | use HasRequiredAttribute; 26 | 27 | /* ----------------------------------------------------------------- 28 | | Constants 29 | | ----------------------------------------------------------------- 30 | */ 31 | 32 | public const ACCEPT_AUDIO = 'audio/*'; 33 | public const ACCEPT_VIDEO = 'video/*'; 34 | public const ACCEPT_IMAGE = 'image/*'; 35 | 36 | /* ----------------------------------------------------------------- 37 | | Properties 38 | | ----------------------------------------------------------------- 39 | */ 40 | 41 | protected string $tag = 'input'; 42 | 43 | /* ----------------------------------------------------------------- 44 | | Constructor 45 | | ----------------------------------------------------------------- 46 | */ 47 | 48 | /** 49 | * File constructor. 50 | */ 51 | public function __construct() 52 | { 53 | parent::__construct(); 54 | 55 | $this->getAttributes()->set('type', 'file'); 56 | } 57 | 58 | /* ----------------------------------------------------------------- 59 | | Main Methods 60 | | ----------------------------------------------------------------- 61 | */ 62 | 63 | /** 64 | * Add the accept attribute. 65 | * 66 | * @return $this 67 | */ 68 | public function accept(string $type): static 69 | { 70 | return $this->attribute('accept', $type); 71 | } 72 | 73 | /** 74 | * Add the accept attribute (audios). 75 | * 76 | * @return $this 77 | */ 78 | public function acceptAudio(): static 79 | { 80 | return $this->accept(self::ACCEPT_AUDIO); 81 | } 82 | 83 | /** 84 | * Add the accept attribute (videos). 85 | * 86 | * @return $this 87 | */ 88 | public function acceptVideo(): static 89 | { 90 | return $this->accept(self::ACCEPT_VIDEO); 91 | } 92 | 93 | /** 94 | * Add the accept attribute (images). 95 | * 96 | * @return $this 97 | */ 98 | public function acceptImage(): static 99 | { 100 | return $this->accept(self::ACCEPT_IMAGE); 101 | } 102 | 103 | /** 104 | * Add the multiple attribute. 105 | * 106 | * @return $this 107 | */ 108 | public function multiple(): static 109 | { 110 | return $this->attribute('multiple'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Elements/Form.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Form extends HtmlElement 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Traits 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | use HasTargetAttribute; 22 | 23 | /* ----------------------------------------------------------------- 24 | | Properties 25 | | ----------------------------------------------------------------- 26 | */ 27 | 28 | protected string $tag = 'form'; 29 | 30 | /* ----------------------------------------------------------------- 31 | | Main Methods 32 | | ----------------------------------------------------------------- 33 | */ 34 | 35 | /** 36 | * Add the action attribute. 37 | * 38 | * @return $this 39 | */ 40 | public function action(?string $action): static 41 | { 42 | return $this->attribute('action', $action); 43 | } 44 | 45 | /** 46 | * Add the method attribute. 47 | * 48 | * @return $this 49 | */ 50 | public function method(string $method): static 51 | { 52 | $method = mb_strtoupper($method); 53 | 54 | return $this->attribute('method', $method === 'GET' ? 'GET' : 'POST'); 55 | } 56 | 57 | /** 58 | * Add the novalidate attribute. 59 | * 60 | * @return $this 61 | */ 62 | public function novalidate(bool $novalidate = true): static 63 | { 64 | return $novalidate 65 | ? $this->attribute('novalidate') 66 | : $this->forgetAttribute('novalidate'); 67 | } 68 | 69 | /** 70 | * Add the enctype attribute for files. 71 | * 72 | * @return $this 73 | */ 74 | public function acceptsFiles(): static 75 | { 76 | return $this->attribute('enctype', 'multipart/form-data'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Elements/HtmlElement.php: -------------------------------------------------------------------------------- 1 | 18 | * 19 | * @method \Arcanedev\Html\Elements\HtmlElement|mixed attributeIf(bool $condition, string $attribute, mixed $value = null) 20 | * @method \Arcanedev\Html\Elements\HtmlElement|mixed attributeUnless(bool $condition, string $attribute, mixed $value = null) 21 | * @method \Arcanedev\Html\Elements\HtmlElement|mixed attributeIfNotNull(mixed $valueToCheck, string $attribute, mixed $value = null) 22 | */ 23 | class HtmlElement implements HtmlElementContract 24 | { 25 | /* ----------------------------------------------------------------- 26 | | Traits 27 | | ----------------------------------------------------------------- 28 | */ 29 | 30 | use HasAttributes; 31 | use HasChildElements; 32 | use HasConditionalMethods; 33 | use Macroable { 34 | __call as __callMacro; 35 | } 36 | 37 | /* ----------------------------------------------------------------- 38 | | Properties 39 | | ----------------------------------------------------------------- 40 | */ 41 | 42 | /** 43 | * The tag type. 44 | */ 45 | protected string $tag; 46 | 47 | /* ----------------------------------------------------------------- 48 | | Constructor 49 | | ----------------------------------------------------------------- 50 | */ 51 | 52 | /** 53 | * HtmlElement constructor. 54 | */ 55 | public function __construct() 56 | { 57 | $this->initAttributes(); 58 | $this->initChildren(); 59 | } 60 | 61 | /* ----------------------------------------------------------------- 62 | | Magic Methods 63 | | ----------------------------------------------------------------- 64 | */ 65 | 66 | /** 67 | * Render the element to string (magic method). 68 | */ 69 | public function __toString(): string 70 | { 71 | return $this->toHtml(); 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | public function __call($name, array $arguments = []) 78 | { 79 | if (Str::endsWith($name, $this->supportedConditions)) { 80 | foreach ($this->supportedConditions as $condition) { 81 | if (method_exists($this, $method = str_replace($condition, '', $name))) { 82 | return $this->callConditionalMethod($condition, $method, $arguments); 83 | } 84 | } 85 | } 86 | 87 | return $this->__callMacro($name, $arguments); 88 | } 89 | 90 | /** 91 | * Clone the object. 92 | */ 93 | public function __clone() 94 | { 95 | $this->attributes = clone $this->attributes; 96 | $this->children = clone $this->children; 97 | } 98 | 99 | /** 100 | * Make a html element. 101 | * 102 | * @return $this 103 | */ 104 | public static function make(): static 105 | { 106 | return new static(); 107 | } 108 | 109 | /** 110 | * Create a element with tag. 111 | */ 112 | public static function withTag(string $tag): static 113 | { 114 | return static::make()->setTag($tag); 115 | } 116 | 117 | /** 118 | * Set an id attribute. 119 | * 120 | * @return $this 121 | */ 122 | public function id(string $id): static 123 | { 124 | return $this->attribute('id', $id); 125 | } 126 | 127 | /** 128 | * Get the class attribute. 129 | */ 130 | public function classList(): ClassAttribute 131 | { 132 | return $this->getAttributes()->classList(); 133 | } 134 | 135 | /** 136 | * Add a class (alias). 137 | * 138 | * @return $this 139 | */ 140 | public function class(iterable|string $class): static 141 | { 142 | return tap(clone $this, function (HtmlElement $elt) use ($class): void { 143 | $elt->getAttributes()->addClass($class); 144 | }); 145 | } 146 | 147 | /** 148 | * Push a class to the list. 149 | * 150 | * @return $this 151 | */ 152 | public function pushClass(string $class): static 153 | { 154 | return tap(clone $this, function (HtmlElement $elt) use ($class): void { 155 | $elt->classList()->push($class); 156 | }); 157 | } 158 | 159 | /** 160 | * Set the style attribute. 161 | * 162 | * @return $this 163 | */ 164 | public function style(array|string $style): static 165 | { 166 | if (is_array($style)) { 167 | $style = implode('; ', array_map( 168 | fn($value, $attribute) => "{$attribute}: {$value}", 169 | $style, 170 | array_keys($style) 171 | )); 172 | } 173 | 174 | return $this->attribute('style', $style); 175 | } 176 | 177 | /** 178 | * Set the data attribute. 179 | * 180 | * @return $this 181 | */ 182 | public function data(array|string $name, mixed $value = null): static 183 | { 184 | return $this->attributes( 185 | Collection::make(is_array($name) ? $name : [$name => $value]) 186 | ->mapWithKeys(fn($mapValue, $mapKey) => ["data-{$mapKey}" => $mapValue]) 187 | ); 188 | } 189 | 190 | /** 191 | * Set the text. 192 | * 193 | * @return $this 194 | */ 195 | public function text(mixed $text, bool $doubleEncode = true): static 196 | { 197 | return $this->html(e($text, $doubleEncode)); 198 | } 199 | 200 | /** 201 | * Add a html child/children. 202 | * 203 | * @return $this 204 | */ 205 | public function html(mixed $html): static 206 | { 207 | if ($this->isVoidElement()) { 208 | throw InvalidHtmlException::onTag($this->getTag()); 209 | } 210 | 211 | return $this->setNewChildren($html); 212 | } 213 | 214 | /** 215 | * Open the html element. 216 | */ 217 | public function open(): HtmlString 218 | { 219 | $attributes = $this->getAttributes(); 220 | 221 | $html = $attributes->isNotEmpty() 222 | ? "<{$this->getTag()} {$attributes->render()}>" 223 | : "<{$this->getTag()}>"; 224 | 225 | return new HtmlString( 226 | $html . $this->getChildren()->toHtml() 227 | ); 228 | } 229 | 230 | /** 231 | * Close the html element. 232 | */ 233 | public function close(): HtmlString 234 | { 235 | return new HtmlString( 236 | $this->isVoidElement() ? '' : "getTag()}>" 237 | ); 238 | } 239 | 240 | /** 241 | * Render the element to HtmlString object. 242 | */ 243 | public function render(): HtmlString 244 | { 245 | return new HtmlString($this->toHtml()); 246 | } 247 | 248 | /** 249 | * Render the element to string. 250 | */ 251 | public function toHtml(): string 252 | { 253 | return $this->open()->toHtml() . 254 | $this->close()->toHtml(); 255 | } 256 | 257 | /* ----------------------------------------------------------------- 258 | | Check Methods 259 | | ----------------------------------------------------------------- 260 | */ 261 | 262 | /** 263 | * Check if the tag is a void element. 264 | */ 265 | public function isVoidElement(): bool 266 | { 267 | return in_array($this->getTag(), [ 268 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 269 | 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr', 270 | ]); 271 | } 272 | 273 | /* ----------------------------------------------------------------- 274 | | Setters & Getters 275 | | ----------------------------------------------------------------- 276 | */ 277 | 278 | /** 279 | * Set the tag property. 280 | */ 281 | protected function setTag(string $tag): static 282 | { 283 | $this->tag = $tag; 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * Get the tag type. 290 | * 291 | * @throws MissingTagException 292 | */ 293 | protected function getTag(): string 294 | { 295 | $this->ensureHasTag(); 296 | 297 | return $this->tag; 298 | } 299 | 300 | /** 301 | * Ensure the tag property is defined. 302 | * 303 | * @throws MissingTagException 304 | */ 305 | protected function ensureHasTag(): void 306 | { 307 | if (empty($this->tag)) { 308 | throw MissingTagException::onClass(static::class); 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/Elements/I.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class I extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * The tag type. 21 | */ 22 | protected string $tag = 'i'; 23 | } 24 | -------------------------------------------------------------------------------- /src/Elements/Img.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Img extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'img'; 20 | 21 | /* ----------------------------------------------------------------- 22 | | Main Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Set the src attribute. 28 | * 29 | * @return $this 30 | */ 31 | public function src(string $src): static 32 | { 33 | return $this->attribute('src', $src); 34 | } 35 | 36 | /** 37 | * Set the alt attribute. 38 | * 39 | * @return $this 40 | */ 41 | public function alt(string $alt): static 42 | { 43 | return $this->attribute('alt', $alt); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Elements/Input.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Input extends HtmlElement 23 | { 24 | /* ----------------------------------------------------------------- 25 | | Traits 26 | | ----------------------------------------------------------------- 27 | */ 28 | 29 | use HasAutofocusAttribute; 30 | 31 | use HasDisabledAttribute; 32 | 33 | use HasMinMaxLengthAttributes; 34 | 35 | use HasNameAttribute; 36 | 37 | use HasPlaceholderAttribute; 38 | 39 | use HasReadonlyAttribute; 40 | 41 | use HasRequiredAttribute; 42 | 43 | use HasTypeAttribute; 44 | 45 | use HasValueAttribute; 46 | 47 | /* ----------------------------------------------------------------- 48 | | Properties 49 | | ----------------------------------------------------------------- 50 | */ 51 | 52 | protected string $tag = 'input'; 53 | 54 | /* ----------------------------------------------------------------- 55 | | Main Methods 56 | | ----------------------------------------------------------------- 57 | */ 58 | 59 | /** 60 | * Add the checked attribute. 61 | * 62 | * @return $this 63 | */ 64 | public function checked(bool $checked = true): static 65 | { 66 | $type = $this->getAttributeValue('type'); 67 | 68 | return $this->if($type === 'checkbox', fn(self $input) => $checked 69 | ? $input->attribute('checked') 70 | : $input->forgetAttribute('checked')); 71 | } 72 | 73 | /** 74 | * Remove the checked attribute. 75 | * 76 | * @return $this 77 | */ 78 | public function unchecked(): static 79 | { 80 | return $this->checked(false); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Elements/Label.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Label extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'label'; 20 | 21 | /* ----------------------------------------------------------------- 22 | | Main Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Set the for attribute. 28 | * 29 | * @return $this 30 | */ 31 | public function for(string $for): static 32 | { 33 | return $this->attribute('for', $for); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Elements/Legend.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Legend extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'legend'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Elements/Link.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Link extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'link'; 20 | 21 | /* ----------------------------------------------------------------- 22 | | Main Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Set the href url. 28 | * 29 | * @return $this 30 | */ 31 | public function href(string $href): static 32 | { 33 | return $this->attribute('href', $href); 34 | } 35 | 36 | /** 37 | * Set the rel value. 38 | * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types 39 | * 40 | * @return $this 41 | */ 42 | public function rel(string $value): static 43 | { 44 | return $this->attribute('rel', $value); 45 | } 46 | 47 | /** 48 | * Set the rel as stylesheet. 49 | * 50 | * @return $this 51 | */ 52 | public function stylesheet(string $href): static 53 | { 54 | return $this->rel('stylesheet')->href($href); 55 | } 56 | 57 | /** 58 | * Set the rel as icon. 59 | * 60 | * @return $this 61 | */ 62 | public function icon(string $href): static 63 | { 64 | return $this->rel('icon')->href($href); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Elements/ListElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class ListElement extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Main Methods 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * Make an item. 21 | */ 22 | abstract protected function makeItem(mixed $value, array $attributes): HtmlElement; 23 | 24 | /** 25 | * Add an item. 26 | * 27 | * @return $this 28 | */ 29 | public function item(mixed $value, array $attributes = []): static 30 | { 31 | return $this->addChild($value, fn($value) => $this->makeItem($value, $attributes)); 32 | } 33 | 34 | /** 35 | * Add multiple items. 36 | * 37 | * @param iterable $items 38 | * @param array $attributes 39 | * 40 | * @return $this 41 | */ 42 | public function items(iterable $items, array $attributes = []): static 43 | { 44 | return $this->children($items, fn($value) => $this->makeItem( 45 | is_array($value) ? static::make()->items($value) : $value, // Create nested items if the value is array 46 | $attributes 47 | )); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Elements/Meta.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Meta extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'meta'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Elements/Ol.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Ol extends ListElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'ol'; 20 | 21 | /* ----------------------------------------------------------------- 22 | | Other Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Make an item. 28 | */ 29 | protected function makeItem(mixed $value, array $attributes): HtmlElement 30 | { 31 | return static::withTag('li')->attributes($attributes)->html($value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Elements/Optgroup.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Optgroup extends HtmlElement 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Traits 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | use HasDisabledAttribute; 22 | 23 | /* ----------------------------------------------------------------- 24 | | Properties 25 | | ----------------------------------------------------------------- 26 | */ 27 | 28 | protected string $tag = 'optgroup'; 29 | 30 | /* ----------------------------------------------------------------- 31 | | Main Methods 32 | | ----------------------------------------------------------------- 33 | */ 34 | 35 | /** 36 | * Set the label attribute. 37 | * 38 | * @return $this 39 | */ 40 | public function label(string $label): static 41 | { 42 | return $this->attribute('label', $label); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Elements/Option.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @method \Arcanedev\Html\Elements\Option selectedUnless(bool $condition) 16 | */ 17 | class Option extends HtmlElement implements Selectable 18 | { 19 | /* ----------------------------------------------------------------- 20 | | Traits 21 | | ----------------------------------------------------------------- 22 | */ 23 | 24 | use HasDisabledAttribute; 25 | use HasValueAttribute; 26 | 27 | /* ----------------------------------------------------------------- 28 | | Properties 29 | | ----------------------------------------------------------------- 30 | */ 31 | 32 | protected string $tag = 'option'; 33 | 34 | /* ----------------------------------------------------------------- 35 | | Main Methods 36 | | ----------------------------------------------------------------- 37 | */ 38 | 39 | /** 40 | * Add the selected if it fulfill the condition. 41 | * 42 | * @return $this 43 | */ 44 | public function selectedIf(bool $condition): static 45 | { 46 | return $condition 47 | ? $this->attribute('selected') 48 | : $this->forgetAttribute('selected'); 49 | } 50 | 51 | /** 52 | * Add the selected attribute. 53 | */ 54 | public function selected(): static 55 | { 56 | return $this->selectedIf(true); 57 | } 58 | 59 | /** 60 | * Remove the selected attribute. 61 | */ 62 | public function unselected(): static 63 | { 64 | return $this->selectedIf(false); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Elements/P.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Select extends HtmlElement 21 | { 22 | /* ----------------------------------------------------------------- 23 | | Traits 24 | | ----------------------------------------------------------------- 25 | */ 26 | 27 | use HasAutofocusAttribute; 28 | use HasDisabledAttribute; 29 | use HasNameAttribute; 30 | use HasReadonlyAttribute; 31 | use HasRequiredAttribute; 32 | 33 | /* ----------------------------------------------------------------- 34 | | Properties 35 | | ----------------------------------------------------------------- 36 | */ 37 | 38 | protected string $tag = 'select'; 39 | 40 | protected array $options = []; 41 | 42 | protected mixed $value = ''; 43 | 44 | /* ----------------------------------------------------------------- 45 | | Main Methods 46 | | ----------------------------------------------------------------- 47 | */ 48 | 49 | /** 50 | * Set the select input as multiple. 51 | * 52 | * @return $this 53 | */ 54 | public function multiple(): static 55 | { 56 | /** @var self $elt */ 57 | $elt = with(clone $this)->attribute('multiple'); 58 | $name = $elt->getAttribute('name'); 59 | 60 | return $elt->if( 61 | $name && ! Str::endsWith($name->value(), '[]'), 62 | fn(self $elt) => $elt->name($name->value() . '[]') 63 | )->applyValueToOptions(); 64 | } 65 | 66 | /** 67 | * Add options. 68 | * 69 | * @return $this 70 | */ 71 | public function options(iterable $options, array $attributes = [], array $groupAttributes = []): static 72 | { 73 | return $this->children( 74 | $options, 75 | fn($text, $value) => is_array($text) || $text instanceof Collection 76 | ? $this->makeOptionsGroup($value, $text, $attributes, $groupAttributes[$value] ?? []) 77 | : $this->makeOption($value, $text, $attributes[$value] ?? []) 78 | ); 79 | } 80 | 81 | /** 82 | * Add a placeholder option. 83 | * 84 | * @return $this 85 | */ 86 | public function placeholder(string $text, mixed $value = null, bool $disabled = false): static 87 | { 88 | return $this->prependChild( 89 | $this->makeOption($value, $text) 90 | ->selectedUnless($this->hasSelection()) 91 | ->disabled($disabled) 92 | ); 93 | } 94 | 95 | /** 96 | * Set the value. 97 | * 98 | * @return $this 99 | */ 100 | public function value(mixed $value = null): static 101 | { 102 | return tap(clone $this, function (self $element) use ($value): void { 103 | $element->value = $value; 104 | })->applyValueToOptions(); 105 | } 106 | 107 | /** 108 | * Apply the selected value to the options. 109 | */ 110 | protected static function applyValueToElements(Collection $value, Collection $children): Collection 111 | { 112 | return $children->map(function (HtmlElement $child) use ($value) { 113 | if ($child instanceof Optgroup) { 114 | return $child->setNewChildren(static::applyValueToElements($value, $child->getChildren())); 115 | } 116 | 117 | if ($child instanceof Selectable) { 118 | return $child->selectedIf( 119 | $value->contains($child->getAttribute('value')->value()) 120 | ); 121 | } 122 | 123 | return $child; 124 | }); 125 | } 126 | 127 | /* ----------------------------------------------------------------- 128 | | Other Methods 129 | | ----------------------------------------------------------------- 130 | */ 131 | 132 | /** 133 | * Check if has a selected option. 134 | */ 135 | protected function hasSelection(): bool 136 | { 137 | return $this->getChildren()->contains( 138 | fn(HtmlElement $child) => $child->hasAttribute('selected') 139 | ); 140 | } 141 | 142 | /** 143 | * Make an option tag. 144 | */ 145 | protected function makeOption(mixed $value, ?string $text = null, array $attributes = []): Option 146 | { 147 | return Option::make() 148 | ->value($value) 149 | ->text($text ?: $value, false) 150 | ->selectedIf($value === $this->value) 151 | ->attributes($attributes); 152 | } 153 | 154 | /** 155 | * Make an options group. 156 | */ 157 | protected function makeOptionsGroup( 158 | string $label, 159 | array $options, 160 | array $attributes = [], 161 | array $groupAttributes = [] 162 | ): Optgroup { 163 | return Optgroup::make() 164 | ->label($label) 165 | ->attributes($groupAttributes) 166 | ->children( 167 | $options, 168 | fn($optionText, $optionValue) => $this->makeOption($optionValue, $optionText, $attributes[$optionValue] ?? []) 169 | ); 170 | } 171 | 172 | /** 173 | * Apply the selected value to the options. 174 | * 175 | * @return $this 176 | */ 177 | protected function applyValueToOptions(): static 178 | { 179 | $value = Collection::make($this->value); 180 | 181 | if ( ! $this->hasAttribute('multiple')) { 182 | $value = $value->take(1); 183 | } 184 | 185 | return $this->setNewChildren( 186 | static::applyValueToElements($value, $this->getChildren()) 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Elements/Span.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Span extends HtmlElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'span'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Elements/Textarea.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Textarea extends HtmlElement 21 | { 22 | /* ----------------------------------------------------------------- 23 | | Traits 24 | | ----------------------------------------------------------------- 25 | */ 26 | 27 | use HasAutofocusAttribute; 28 | use HasDisabledAttribute; 29 | use HasMinMaxLengthAttributes; 30 | use HasNameAttribute; 31 | use HasPlaceholderAttribute; 32 | use HasReadonlyAttribute; 33 | use HasRequiredAttribute; 34 | 35 | /* ----------------------------------------------------------------- 36 | | Properties 37 | | ----------------------------------------------------------------- 38 | */ 39 | 40 | protected string $tag = 'textarea'; 41 | 42 | /* ----------------------------------------------------------------- 43 | | Main Methods 44 | | ----------------------------------------------------------------- 45 | */ 46 | 47 | /** 48 | * Set the textarea value. 49 | * 50 | * @return $this 51 | */ 52 | public function value(?string $value): static 53 | { 54 | return $this->html($value); 55 | } 56 | 57 | /** 58 | * Set the textarea cols & rows sizes. 59 | * 60 | * @return $this 61 | */ 62 | public function size(string $size): static 63 | { 64 | list($cols, $rows) = explode('x', $size); 65 | 66 | return $this->attribute('cols', $cols)->attribute('rows', $rows); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Elements/Ul.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Ul extends ListElement 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | protected string $tag = 'ul'; 20 | 21 | /* ----------------------------------------------------------------- 22 | | Other Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Make an item. 28 | */ 29 | protected function makeItem(mixed $value, array $attributes): HtmlElement 30 | { 31 | return static::withTag('li') 32 | ->attributes($attributes) 33 | ->html($value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Entities/Attributes.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Attributes implements ArrayAccess, Arrayable 19 | { 20 | /* ----------------------------------------------------------------- 21 | | Properties 22 | | ----------------------------------------------------------------- 23 | */ 24 | 25 | protected array $items; 26 | 27 | /* ----------------------------------------------------------------- 28 | | Constructor 29 | | ----------------------------------------------------------------- 30 | */ 31 | 32 | /** 33 | * Attributes constructor. 34 | */ 35 | public function __construct(array $items = []) 36 | { 37 | $this->items = []; 38 | $this->setMany($items); 39 | } 40 | 41 | /* ----------------------------------------------------------------- 42 | | Main Methods 43 | | ----------------------------------------------------------------- 44 | */ 45 | 46 | /** 47 | * Make the attribute instance. 48 | */ 49 | public static function make(array $items = []): static 50 | { 51 | return new static($items); 52 | } 53 | 54 | /** 55 | * Set an attribute. 56 | * 57 | * @return $this 58 | */ 59 | public function set(string $name, mixed $value = null): static 60 | { 61 | $attribute = static::makeAttribute($name, $value); 62 | 63 | $this->offsetSet($attribute->name(), $attribute); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Get an attribute. 70 | * 71 | * @return $this|mixed 72 | */ 73 | public function get(string $key, mixed $default = null): mixed 74 | { 75 | return $this->offsetExists($key) 76 | ? $this->offsetGet($key) 77 | : value($default); 78 | } 79 | 80 | /** 81 | * Forget one or multiple attributes 82 | * 83 | * @return $this 84 | */ 85 | public function forget(array|string $keys): static 86 | { 87 | foreach ((array) $keys as $key) { 88 | $this->offsetUnset($key); 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Determine if an item exists in the collection by key. 96 | * 97 | * @param string|array ...$keys 98 | * 99 | * @return bool 100 | */ 101 | public function has(...$keys): bool 102 | { 103 | foreach ($keys as $value) { 104 | if ( ! $this->offsetExists($value)) { 105 | return false; 106 | } 107 | } 108 | 109 | return true; 110 | } 111 | 112 | /** 113 | * Set multiple attributes. 114 | * 115 | * @return $this 116 | */ 117 | public function setMany(iterable $attributes): static 118 | { 119 | foreach ($attributes as $attribute => $value) { 120 | if (is_int($attribute)) { 121 | $attribute = $value; 122 | $value = null; 123 | } 124 | 125 | $this->set($attribute, $value); 126 | } 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Add a class. 133 | * 134 | * @return $this 135 | */ 136 | public function addClass(iterable|string $class): static 137 | { 138 | return $this->set('class', $class); 139 | } 140 | 141 | /** 142 | * Render the attributes. 143 | */ 144 | public function render(): string 145 | { 146 | if ($this->isEmpty()) { 147 | return ''; 148 | } 149 | 150 | return implode(' ', array_map( 151 | fn(AbstractAttribute $attribute) => $attribute->render(), 152 | $this->items 153 | )); 154 | } 155 | 156 | /** 157 | * Convert the attributes to array. 158 | */ 159 | public function toArray(): array 160 | { 161 | return array_map( 162 | fn(AbstractAttribute $attribute) => $attribute->value(), 163 | $this->items 164 | ); 165 | } 166 | 167 | /** 168 | * Check if the container is empty. 169 | */ 170 | public function isEmpty(): bool 171 | { 172 | return empty($this->items); 173 | } 174 | 175 | /** 176 | * Check if the container is not empty. 177 | */ 178 | public function isNotEmpty(): bool 179 | { 180 | return ! $this->isEmpty(); 181 | } 182 | 183 | /** 184 | * Get the class attribute. 185 | */ 186 | public function classList(): ?ClassAttribute 187 | { 188 | return $this->get('class'); 189 | } 190 | 191 | /* ----------------------------------------------------------------- 192 | | Other Methods 193 | | ----------------------------------------------------------------- 194 | */ 195 | 196 | /** {@inheritDoc} */ 197 | public function offsetExists($offset): bool 198 | { 199 | return array_key_exists($offset, $this->items); 200 | } 201 | 202 | /** {@inheritDoc} */ 203 | public function offsetGet($offset): mixed 204 | { 205 | return $this->items[$offset]; 206 | } 207 | 208 | /** {@inheritDoc} */ 209 | public function offsetSet($offset, $value): void 210 | { 211 | $this->items[$offset] = $value; 212 | } 213 | 214 | /** {@inheritDoc} */ 215 | public function offsetUnset($offset): void 216 | { 217 | unset($this->items[$offset]); 218 | } 219 | 220 | /** 221 | * Make a new attribute. 222 | */ 223 | protected static function makeAttribute(string $name, mixed $value): ClassAttribute|MiscAttribute 224 | { 225 | return match ($name) { 226 | 'class' => new ClassAttribute($value), 227 | default => new MiscAttribute($name, $value), 228 | }; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Entities/Attributes/AbstractAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class AbstractAttribute 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * The attribute's name. 21 | */ 22 | protected string $name; 23 | 24 | /** 25 | * Get the attribute's value. 26 | * 27 | * @return mixed 28 | */ 29 | abstract public function value(): mixed; 30 | 31 | /* ----------------------------------------------------------------- 32 | | Main Methods 33 | | ----------------------------------------------------------------- 34 | */ 35 | 36 | /** 37 | * Render the attribute. 38 | */ 39 | abstract public function render(): string; 40 | 41 | /* ----------------------------------------------------------------- 42 | | Getters & Setters 43 | | ----------------------------------------------------------------- 44 | */ 45 | 46 | /** 47 | * Get the attribute name. 48 | */ 49 | public function name(): string 50 | { 51 | return $this->name; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Entities/Attributes/ClassAttribute.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ClassAttribute extends AbstractAttribute implements Countable 19 | { 20 | /* ----------------------------------------------------------------- 21 | | Properties 22 | | ----------------------------------------------------------------- 23 | */ 24 | 25 | /** 26 | * The attribute's name. 27 | */ 28 | protected string $name = 'class'; 29 | 30 | /** 31 | * The CSS classes. 32 | * 33 | * @var array 34 | */ 35 | protected array $classes = []; 36 | 37 | /* ----------------------------------------------------------------- 38 | | Constructor 39 | | ----------------------------------------------------------------- 40 | */ 41 | 42 | /** 43 | * ClassAttribute constructor. 44 | */ 45 | public function __construct(mixed $value = null) 46 | { 47 | $this->setValue($value); 48 | } 49 | 50 | /** 51 | * Render the attribute. 52 | * 53 | * @see render 54 | */ 55 | public function __toString(): string 56 | { 57 | return $this->render(); 58 | } 59 | 60 | /** 61 | * Make a new instance. 62 | * 63 | * @return $this 64 | */ 65 | public static function make(mixed $value = null): static 66 | { 67 | return new static($value); 68 | } 69 | 70 | /* ----------------------------------------------------------------- 71 | | Getters & Setters 72 | | ----------------------------------------------------------------- 73 | */ 74 | 75 | /** 76 | * Get the attribute's value. 77 | */ 78 | public function value(): string 79 | { 80 | return implode(' ', $this->all()); 81 | } 82 | 83 | /** 84 | * Get the attribute's value. 85 | * 86 | * @return $this 87 | */ 88 | public function setValue(mixed $value): static 89 | { 90 | if ($value !== null) { 91 | $this->classes = static::parseClasses($value); 92 | } 93 | 94 | return $this; 95 | } 96 | 97 | /* ----------------------------------------------------------------- 98 | | Main Methods 99 | | ----------------------------------------------------------------- 100 | */ 101 | 102 | /** 103 | * Get all the classes. 104 | */ 105 | public function all(): array 106 | { 107 | return $this->classes; 108 | } 109 | 110 | /** 111 | * Push the given class. 112 | * 113 | * @return $this 114 | */ 115 | public function push(string $class): static 116 | { 117 | foreach (explode(' ', $class) as $item) { 118 | if ( ! $this->has($item)) { 119 | $this->classes[] = $item; 120 | } 121 | } 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Remove the given class. 128 | * 129 | * @return $this 130 | */ 131 | public function remove(string $class): static 132 | { 133 | if ( ! $this->has($class)) { 134 | return $this; 135 | } 136 | 137 | $class = trim($class); 138 | 139 | return $this->setValue( 140 | array_diff($this->all(), [$class]) 141 | ); 142 | } 143 | 144 | /** 145 | * Toggle the given class. 146 | */ 147 | public function toggle(string $class): static 148 | { 149 | return $this->has($class) 150 | ? $this->remove($class) 151 | : $this->push($class); 152 | } 153 | 154 | /** 155 | * Check if contains the given class (alias). 156 | * 157 | * @see has 158 | */ 159 | public function contains(string $class): bool 160 | { 161 | return $this->has($class); 162 | } 163 | 164 | /** 165 | * Check if contains the given class. 166 | */ 167 | public function has(string $class): bool 168 | { 169 | return in_array(trim($class), $this->all()); 170 | } 171 | 172 | /** 173 | * Replaces an existing class with a new class. 174 | * 175 | * @return $this 176 | */ 177 | public function replace(string $oldClass, string $newClass): static 178 | { 179 | if ($this->has($oldClass)) { 180 | $this->remove($oldClass)->push($newClass); 181 | } 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Count the classes. 188 | */ 189 | public function count(): int 190 | { 191 | return count($this->all()); 192 | } 193 | 194 | /** 195 | * Check if is empty. 196 | */ 197 | public function isEmpty(): bool 198 | { 199 | return empty($this->all()); 200 | } 201 | 202 | /** 203 | * Check if is not empty. 204 | */ 205 | public function isNotEmpty(): bool 206 | { 207 | return ! $this->isEmpty(); 208 | } 209 | 210 | /** 211 | * Render the attribute. 212 | */ 213 | public function render(): string 214 | { 215 | if ($this->isEmpty()) { 216 | return ''; 217 | } 218 | 219 | return $this->name . '="' . e($this->value(), false) . '"'; 220 | } 221 | 222 | /* ----------------------------------------------------------------- 223 | | Other Methods 224 | | ----------------------------------------------------------------- 225 | */ 226 | 227 | /** 228 | * Parse the given value. 229 | */ 230 | private static function parseClasses(mixed $classes): array 231 | { 232 | if ($classes instanceof Arrayable) { 233 | $classes = $classes->toArray(); 234 | } 235 | 236 | if (is_string($classes)) { 237 | $classes = explode(' ', $classes); 238 | } 239 | 240 | if (Arr::isAssoc($classes)) { 241 | $classes = Collection::make($classes) 242 | ->transform(fn($value, $key) => is_numeric($key) ? $value : ($value ? $key : false)) 243 | ->filter() 244 | ->values() 245 | ->toArray(); 246 | } 247 | 248 | return array_unique($classes); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Entities/Attributes/MiscAttribute.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class MiscAttribute extends AbstractAttribute 13 | { 14 | /* ----------------------------------------------------------------- 15 | | Properties 16 | | ----------------------------------------------------------------- 17 | */ 18 | 19 | /** 20 | * The attribute's value. 21 | * 22 | * @var mixed 23 | */ 24 | protected mixed $value; 25 | 26 | /* ----------------------------------------------------------------- 27 | | Constructor 28 | | ----------------------------------------------------------------- 29 | */ 30 | 31 | /** 32 | * Attribute constructor. 33 | */ 34 | public function __construct(string $name, mixed $value) 35 | { 36 | $this->name = $name; 37 | $this->setValue($value); 38 | } 39 | 40 | /* ----------------------------------------------------------------- 41 | | Getters & Setters 42 | | ----------------------------------------------------------------- 43 | */ 44 | 45 | /** 46 | * Get the attribute's value. 47 | */ 48 | public function value(): mixed 49 | { 50 | return $this->value; 51 | } 52 | 53 | /* ----------------------------------------------------------------- 54 | | Main Methods 55 | | ----------------------------------------------------------------- 56 | */ 57 | 58 | /** 59 | * Render the attribute. 60 | */ 61 | public function render(): string 62 | { 63 | $value = $this->value(); 64 | 65 | return ($value === null || $value === '') 66 | ? $this->name 67 | : $this->name . '="' . e($value, false) . '"'; 68 | } 69 | 70 | /** 71 | * Set the value. 72 | * 73 | * @return $this 74 | */ 75 | protected function setValue(mixed $value): static 76 | { 77 | $this->value = trim((string) $value); 78 | 79 | return $this; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Entities/ChildrenCollection.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ChildrenCollection extends Collection implements Renderable 20 | { 21 | /* ----------------------------------------------------------------- 22 | | Main Methods 23 | | ----------------------------------------------------------------- 24 | */ 25 | 26 | /** 27 | * Parse the element's children. 28 | * 29 | * @return $this 30 | * 31 | * @throws InvalidChildException 32 | */ 33 | public static function parse(mixed $children, Closure|array|null $mapper = null): static 34 | { 35 | return static::make($children) 36 | ->unless($mapper === null, fn(ChildrenCollection $items) => $items->map($mapper)) 37 | ->each(function ($child): void { 38 | if ( ! static::isValidChild($child)) { 39 | throw new InvalidChildException(); 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Render the object as a string of HTML. 46 | */ 47 | public function render(): HtmlString 48 | { 49 | return new HtmlString($this->toHtml()); 50 | } 51 | 52 | /** 53 | * Get content as a string of HTML. 54 | */ 55 | public function toHtml(): string 56 | { 57 | $mapper = function ($child): string { 58 | if ($child === null) { 59 | return ''; 60 | } 61 | 62 | if ($child instanceof Htmlable) { 63 | return $child->toHtml(); 64 | } 65 | 66 | if (is_string($child) || is_numeric($child)) { 67 | return (string) $child; 68 | } 69 | 70 | throw new InvalidChildException(); 71 | }; 72 | 73 | return $this->map($mapper)->implode(''); 74 | } 75 | 76 | /** 77 | * Check if valid child. 78 | */ 79 | protected static function isValidChild(mixed $child): bool 80 | { 81 | return $child instanceof Htmlable 82 | || is_string($child) 83 | || is_numeric($child) 84 | || $child === null; 85 | } 86 | 87 | /* ----------------------------------------------------------------- 88 | | Other Methods 89 | | ----------------------------------------------------------------- 90 | */ 91 | 92 | /** 93 | * Results array of items from Collection or Arrayable. 94 | */ 95 | protected function getArrayableItems(mixed $items): array 96 | { 97 | if ($items instanceof HtmlElement || $items instanceof HtmlString) { 98 | return [$items]; 99 | } 100 | 101 | return parent::getArrayableItems($items); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidChildException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class InvalidChildException extends Exception {} 15 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidHtmlException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class InvalidHtmlException extends Exception 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | public static function onTag(string $tag): static 22 | { 23 | return new static( 24 | "Can't set inner contents on `{$tag}` because it's a void element" 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exceptions/MissingTagException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MissingTagException extends Exception 15 | { 16 | /* ----------------------------------------------------------------- 17 | | Main Methods 18 | | ----------------------------------------------------------------- 19 | */ 20 | 21 | public static function onClass(string $className): static 22 | { 23 | return new static( 24 | "Class {$className} has no `\$tag` property or empty." 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Exceptions/VoidElementException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class VoidElementException extends Exception {} 15 | -------------------------------------------------------------------------------- /src/Html.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | class Html implements HtmlContract 39 | { 40 | /* ----------------------------------------------------------------- 41 | | Constants 42 | | ----------------------------------------------------------------- 43 | */ 44 | 45 | public const HTML_DATE_FORMAT = 'Y-m-d'; 46 | public const HTML_TIME_FORMAT = 'H:i:s'; 47 | 48 | /* ----------------------------------------------------------------- 49 | | Main Methods 50 | | ----------------------------------------------------------------- 51 | */ 52 | 53 | /** 54 | * Make an `a` tag. 55 | */ 56 | public function a(?string $href = null, ?string $content = null): A 57 | { 58 | return A::make() 59 | ->attributeIfNotNull($href, 'href', $href) 60 | ->html($content); 61 | } 62 | 63 | /** 64 | * Make a `button` tag. 65 | */ 66 | public function button(mixed $content = null, ?string $type = null): Button 67 | { 68 | return Button::make() 69 | ->attributeIfNotNull($type, 'type', $type) 70 | ->html($content); 71 | } 72 | 73 | /** 74 | * Make a checkbox input. 75 | */ 76 | public function checkbox(?string $name = null, ?bool $checked = null, string|int $value = '1'): Input 77 | { 78 | return $this->input() 79 | ->attribute('type', 'checkbox') 80 | ->attributeIfNotNull($name, 'name', $name) 81 | ->attributeIfNotNull($name, 'id', $name) 82 | ->attributeIfNotNull($value, 'value', $value) 83 | ->attributeIf((bool) $checked, 'checked'); 84 | } 85 | 86 | /** 87 | * Parse and render `class` attribute. 88 | */ 89 | public function class(iterable|string $classes): string 90 | { 91 | return ClassAttribute::make($classes)->render(); 92 | } 93 | 94 | /** 95 | * Make a date input. 96 | */ 97 | public function date(?string $name = null, ?string $value = null, bool $format = true): Input 98 | { 99 | $input = $this->input('date', $name, $value); 100 | 101 | return $this->canFormatDateInput($input, $value, $format) 102 | ? $input->value(static::formatDateTime($value, static::HTML_DATE_FORMAT)) 103 | : $input; 104 | } 105 | 106 | /** 107 | * Make a datetime input. 108 | */ 109 | public function datetime(?string $name = null, ?string $value = null, ?bool $format = true): Input 110 | { 111 | $input = $this->input('datetime-local', $name, $value); 112 | 113 | return $this->canFormatDateInput($input, $value, $format) 114 | ? $input->value($this->formatDateTime($value, static::HTML_DATE_FORMAT . '\T' . static::HTML_TIME_FORMAT)) 115 | : $input; 116 | } 117 | 118 | /** 119 | * Make a div element. 120 | */ 121 | public function div(mixed $content = null): Div 122 | { 123 | return Div::make()->addChild($content); 124 | } 125 | 126 | /** 127 | * Make a description list. 128 | */ 129 | public function dl(array $attributes = []): Dl 130 | { 131 | return Dl::make()->attributes($attributes); 132 | } 133 | 134 | /** 135 | * Make a custom tag element. 136 | */ 137 | public function element(string $tag): HtmlElement 138 | { 139 | return HtmlElement::withTag($tag); 140 | } 141 | 142 | /** 143 | * Make an email input. 144 | */ 145 | public function email(?string $name = null, ?string $value = null): Input 146 | { 147 | return $this->input('email', $name, $value); 148 | } 149 | 150 | /** 151 | * Make a fieldset tag. 152 | */ 153 | public function fieldset(mixed $legend = null): Fieldset 154 | { 155 | return $legend === null 156 | ? Fieldset::make() 157 | : Fieldset::make()->legend($legend); 158 | } 159 | 160 | /** 161 | * Make a file input. 162 | */ 163 | public function file(?string $name = null): File 164 | { 165 | return File::make() 166 | ->attributeIfNotNull($name, 'name', $name) 167 | ->attributeIfNotNull($name, 'id', $name); 168 | } 169 | 170 | /** 171 | * Make a form input. 172 | */ 173 | public function form(string $method = 'POST', ?string $action = null): Form 174 | { 175 | return Form::make() 176 | ->method($method) 177 | ->attributeIfNotNull($action, 'action', $action); 178 | } 179 | 180 | /** 181 | * Make a hidden input. 182 | */ 183 | public function hidden(?string $name = null, ?string $value = null): Input 184 | { 185 | return $this->input('hidden', $name, $value); 186 | } 187 | 188 | /** 189 | * Make an i tag. 190 | */ 191 | public function i(?string $content = null): I 192 | { 193 | return I::make()->html($content); 194 | } 195 | 196 | /** 197 | * Make an input tag. 198 | */ 199 | public function input(?string $type = null, ?string $name = null, mixed $value = null): Input 200 | { 201 | $hasValue = $name && $value !== null && $type !== 'password'; 202 | 203 | return Input::make() 204 | ->attributeIfNotNull($type, 'type', $type) 205 | ->attributeIfNotNull($name, 'name', $name) 206 | ->attributeIfNotNull($name, 'id', $name) 207 | ->attributeIf($hasValue, 'value', $value); 208 | } 209 | 210 | /** 211 | * Make an image tag. 212 | */ 213 | public function img(?string $src = null, ?string $alt = null): Img 214 | { 215 | return Img::make() 216 | ->attributeIfNotNull($src, 'src', $src) 217 | ->attributeIfNotNull($alt, 'alt', $alt); 218 | } 219 | 220 | /** 221 | * Make a label tag. 222 | */ 223 | public function label(mixed $content = null, ?string $for = null): Label 224 | { 225 | return Label::make() 226 | ->attributeIfNotNull($for, 'for', $for) 227 | ->children($content); 228 | } 229 | 230 | /** 231 | * Make a legend tag. 232 | */ 233 | public function legend(HtmlElement|string|null $content = null): Legend 234 | { 235 | return Legend::make()->html($content); 236 | } 237 | 238 | /** 239 | * Make a mailto link. 240 | */ 241 | public function mailto(string $email, ?string $content = null): A 242 | { 243 | return $this->a("mailto:{$email}", $content ?: $email); 244 | } 245 | 246 | /** 247 | * Make a number input. 248 | */ 249 | public function number( 250 | ?string $name = null, 251 | mixed $value = null, 252 | mixed $min = null, 253 | mixed $max = null, 254 | mixed $step = null 255 | ): Input { 256 | return $this->input('number', $name, $value) 257 | ->attributeIfNotNull($min, 'min', $min) 258 | ->attributeIfNotNull($max, 'max', $max) 259 | ->attributeIfNotNull($step, 'step', $step); 260 | } 261 | 262 | /** 263 | * Make an ordered list. 264 | */ 265 | public function ol(array $attributes = []): Ol 266 | { 267 | return Ol::make()->attributes($attributes); 268 | } 269 | 270 | /** 271 | * Make an option tag. 272 | */ 273 | public function option(?string $text = null, mixed $value = null, bool $selected = false): Option 274 | { 275 | return Option::make() 276 | ->text($text) 277 | ->value($value) 278 | ->selectedIf($selected); 279 | } 280 | 281 | /** 282 | * Make a paragraph tag. 283 | */ 284 | public function p(HtmlElement|string|null $content = null): P 285 | { 286 | return P::make()->html($content); 287 | } 288 | 289 | /** 290 | * Make a password input. 291 | */ 292 | public function password(?string $name = null): Input 293 | { 294 | return $this->input('password', $name); 295 | } 296 | 297 | /** 298 | * Make a radio input. 299 | */ 300 | public function radio(?string $name = null, ?bool $checked = null, mixed $value = null): Input 301 | { 302 | return $this->input('radio', $name, $value) 303 | ->attributeIfNotNull($name, 'id', $value === null ? $name : ($name . '_' . Str::slug($value))) 304 | ->attributeIf(($value !== null) || $checked, 'checked'); 305 | } 306 | 307 | /** 308 | * Make a range input. 309 | */ 310 | public function range( 311 | ?string $name = null, 312 | mixed $value = null, 313 | mixed $min = null, 314 | mixed $max = null, 315 | mixed $step = null 316 | ): Input { 317 | return $this->input('range', $name, $value) 318 | ->attributeIfNotNull($min, 'min', $min) 319 | ->attributeIfNotNull($max, 'max', $max) 320 | ->attributeIfNotNull($step, 'step', $step); 321 | } 322 | 323 | /** 324 | * Make a reset button. 325 | */ 326 | public function reset(mixed $content = null): Button 327 | { 328 | return $this->button($content, 'reset'); 329 | } 330 | 331 | /** 332 | * Make a select tag. 333 | */ 334 | public function select(?string $name = null, iterable $options = [], mixed $value = null): Select 335 | { 336 | return Select::make() 337 | ->attributeIfNotNull($name, 'name', $name) 338 | ->attributeIfNotNull($name, 'id', $name) 339 | ->options($options) 340 | ->value($value); 341 | } 342 | 343 | /** 344 | * Make a span tag. 345 | */ 346 | public function span(mixed $content = null): Span 347 | { 348 | return Span::make()->children($content); 349 | } 350 | 351 | /** 352 | * Make a submit button. 353 | */ 354 | public function submit(?string $text = null): Button 355 | { 356 | return $this->button($text, 'submit'); 357 | } 358 | 359 | /** 360 | * Make a tel link. 361 | */ 362 | public function telLink(string $phoneNumber, mixed $text = null): A 363 | { 364 | return $this->a("tel:{$phoneNumber}", $text ?: $phoneNumber); 365 | } 366 | 367 | /** 368 | * Make a text input. 369 | */ 370 | public function text(string $name, ?string $value = null): Input 371 | { 372 | return $this->input('text', $name, $value); 373 | } 374 | 375 | /** 376 | * Make a textarea tag. 377 | */ 378 | public function textarea(?string $name = null, ?string $value = null): Textarea 379 | { 380 | return Textarea::make() 381 | ->attributeIfNotNull($name, 'name', $name) 382 | ->attributeIfNotNull($name, 'id', $name) 383 | ->value($value); 384 | } 385 | 386 | /** 387 | * Make a time input. 388 | */ 389 | public function time(?string $name = null, ?string $value = null, bool $format = true): Input 390 | { 391 | $input = $this->input('time', $name, $value); 392 | 393 | return $this->canFormatDateInput($input, $value, $format) 394 | ? $input->value(static::formatDateTime($value, self::HTML_TIME_FORMAT)) 395 | : $input; 396 | } 397 | 398 | /** 399 | * Make an unordered list. 400 | */ 401 | public function ul(array $attributes = []): Ul 402 | { 403 | return Ul::make()->attributes($attributes); 404 | } 405 | 406 | /* ----------------------------------------------------------------- 407 | | Other Methods 408 | | ----------------------------------------------------------------- 409 | */ 410 | 411 | /** 412 | * Format the date/time value. 413 | */ 414 | protected static function formatDateTime(string $value, string $format): string 415 | { 416 | try { 417 | if ( ! empty($value)) { 418 | $value = (new DateTimeImmutable($value))->format($format); 419 | } 420 | } catch (Exception $e) { 421 | // Do nothing... 422 | } 423 | 424 | return $value; 425 | } 426 | 427 | /** 428 | * Check if the input can be formatted. 429 | */ 430 | protected function canFormatDateInput(Input $input, ?string $value, bool $format): bool 431 | { 432 | return $format 433 | && $input->hasAttribute('value') 434 | && ! empty($value = $input->getAttribute('value')->value()); 435 | } 436 | } 437 | --------------------------------------------------------------------------------