├── resources ├── views │ ├── widgets │ │ └── bootstrap5 │ │ │ ├── hidden.blade.php │ │ │ ├── separator.blade.php │ │ │ ├── textarea.blade.php │ │ │ ├── wysiwyg.blade.php │ │ │ ├── toggle.blade.php │ │ │ ├── upload.blade.php │ │ │ ├── select.blade.php │ │ │ ├── range.blade.php │ │ │ ├── radio.blade.php │ │ │ ├── color.blade.php │ │ │ ├── text.blade.php │ │ │ ├── date.blade.php │ │ │ ├── checkboxes.blade.php │ │ │ └── select2.blade.php │ ├── directives │ │ ├── styles.blade.php │ │ └── scripts.blade.php │ └── form.blade.php └── assets │ ├── js │ ├── l10n │ │ └── it.js │ ├── formello.js │ └── pickr.min.js │ └── css │ ├── nano.min.css │ ├── flatpickr.min.css │ └── select2.min.css ├── src ├── Facades │ └── Formello.php ├── Interfaces │ └── WidgetInterface.php ├── Widgets │ ├── HiddenWidget.php │ ├── TextareaWidget.php │ ├── UploadWidget.php │ ├── SeparatorWidget.php │ ├── WysiwygWidget.php │ ├── CheckboxesWidget.php │ ├── RadioWidget.php │ ├── ToggleWidget.php │ ├── MaskWidget.php │ ├── RangeWidget.php │ ├── SelectWidget.php │ ├── TextWidget.php │ ├── BaseWidget.php │ ├── ColorWidget.php │ ├── DateTimeWidget.php │ ├── ColorSwatchWidget.php │ ├── DateWidget.php │ └── Select2Widget.php ├── Console │ ├── stubs │ │ └── formello.stub │ └── MakeFormelloCommand.php ├── FormelloManager.php ├── SchemaInspector.php ├── AssetManager.php ├── Traits │ └── HasSelect2Widget.php ├── WidgetFactory.php ├── FormelloServiceProvider.php └── Formello.php ├── .gitignore ├── config └── formello.php ├── LICENSE ├── phpunit.xml ├── composer.json ├── tests └── Unit │ ├── DateWidgetTest.php │ ├── ColorWidgetTest.php │ ├── RangeWidgetTest.php │ ├── HiddenWidgetTest.php │ ├── ToggleWidgetTest.php │ ├── DateTimeWidgetTest.php │ ├── TextareaWidgetTest.php │ ├── ColorSwatchWidgetTest.php │ ├── RadioWidgetTest.php │ ├── SelectWidgetTest.php │ ├── CheckboxesWidgetTest.php │ ├── UploadWidgetTest.php │ ├── SeparatorWidgetTest.php │ ├── TextWidgetTest.php │ ├── FormelloMultipartTest.php │ ├── CustomWidgetsOverrideTest.php │ ├── FormelloTest.php │ ├── WysiwygWidgetTest.php │ └── Select2WidgetTest.php ├── CHANGELOG.md └── README.md /resources/views/widgets/bootstrap5/hidden.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Facades/Formello.php: -------------------------------------------------------------------------------- 1 | 9 | @endforeach 10 | @endif 11 | -------------------------------------------------------------------------------- /src/Interfaces/WidgetInterface.php: -------------------------------------------------------------------------------- 1 | $value) 3 | {{ $attr }}="{{ $value }}" @endforeach> 4 | 5 | @if (!empty($label)) 6 |
{{ $label }}
7 | @endif 8 | 9 |
$value) {{ $attr }}="{{ $value }}" @endforeach> 10 | 11 | -------------------------------------------------------------------------------- /resources/views/directives/scripts.blade.php: -------------------------------------------------------------------------------- 1 | {{-- Formello Scripts --}} 2 | @php 3 | $scripts = \Metalogico\Formello\AssetManager::getScripts(); 4 | @endphp 5 | 6 | @if(!empty($scripts)) 7 | @foreach($scripts as $script) 8 | 9 | @endforeach 10 | @endif 11 | 12 | {{-- Always load formello.js --}} 13 | 14 | -------------------------------------------------------------------------------- /src/Widgets/HiddenWidget.php: -------------------------------------------------------------------------------- 1 | $name, 16 | 'value' => old($name, $value), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | 14 | # Laravel 5 & Lumen specific with changed public path 15 | public_html/storage 16 | public_html/hot 17 | 18 | storage/*.key 19 | .env 20 | Homestead.yaml 21 | Homestead.json 22 | /.vagrant 23 | .phpunit.result.cache 24 | .vscode 25 | .idea 26 | .zed 27 | .DS_Store -------------------------------------------------------------------------------- /src/Console/stubs/formello.stub: -------------------------------------------------------------------------------- 1 | 'POST', 13 | 'action' => route('DummyModel.store'), 14 | ]; 15 | } 16 | 17 | protected function edit(): array 18 | { 19 | return [ 20 | 'method' => 'PATCH', 21 | 'action' => route('DummyModel.update', $this->model->id), 22 | ]; 23 | } 24 | 25 | protected function fields(): array 26 | { 27 | return [ 28 | DummyFields 29 | ]; 30 | } 31 | } -------------------------------------------------------------------------------- /config/formello.php: -------------------------------------------------------------------------------- 1 | 'bootstrap5', 9 | 10 | /** 11 | * Custom widgets registered by the application. 12 | * Map alias => Fully Qualified Class Name. These override built-ins. 13 | */ 14 | 'custom_widgets' => [ 15 | // 'alias' => App\Formello\MyCustomWidget::class, 16 | ], 17 | 18 | /** 19 | * Asset loading configuration 20 | * Set to false any library you already have in your theme to avoid conflicts 21 | */ 22 | 'assets' => [ 23 | 'select2' => true, 24 | 'date' => true, 25 | 'datetime' => true, 26 | 'mask' => true, 27 | 'color' => true, 28 | 'colorswatch' => true, 29 | 'wysiwyg' => true, 30 | ], 31 | 32 | ]; 33 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/textarea.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | 9 | 10 | @if (isset($config['help'])) 11 |
{!! $config['help'] !!}
12 | @endif 13 | 14 | @if ($errors) 15 |
16 | 21 |
22 | @endif 23 | 24 |
25 | -------------------------------------------------------------------------------- /src/Widgets/TextareaWidget.php: -------------------------------------------------------------------------------- 1 | $name, 21 | 'value' => old($name, $value), 22 | 'label' => $fieldConfig['label'] ?? null, 23 | 'config' => $fieldConfig, 24 | 'errors' => $errors, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Widgets/UploadWidget.php: -------------------------------------------------------------------------------- 1 | $name, 21 | 'value' => $value, 22 | 'label' => $fieldConfig['label'] ?? null, 23 | 'config' => $fieldConfig, 24 | 'errors' => $errors, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Widgets/SeparatorWidget.php: -------------------------------------------------------------------------------- 1 | $name, 21 | 'label' => $fieldConfig['label'] ?? null, 22 | 'config' => $fieldConfig, 23 | ]; 24 | } 25 | 26 | public function getAssets(?array $fieldConfig = null): ?array 27 | { 28 | return null; // No assets required 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/wysiwyg.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | 9 | 10 | @if (isset($config['help'])) 11 |
{!! $config['help'] !!}
12 | @endif 13 | 14 | @if ($errors) 15 |
16 | 21 |
22 | @endif 23 | 24 |
25 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/toggle.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if (isset($config['label'])) 3 | 4 | @endif 5 |
6 | 7 | $attrValue) 9 | {{ $attr }}="{{ $attrValue }}" @endforeach> 10 |
11 | @if (isset($config['help'])) 12 |
{!! $config['help'] !!}
13 | @endif 14 | 15 | @if ($errors) 16 |
17 | 22 |
23 | @endif 24 | 25 |
26 | -------------------------------------------------------------------------------- /src/FormelloManager.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | @if (isset($label)) 5 | 6 | @endif 7 | 8 | $attrValue) {{ $attr }}="{{ $attrValue }}" @endforeach> 11 | 12 | @if (isset($config['help'])) 13 |
{!! $config['help'] !!}
14 | @endif 15 |
16 | 17 | @if ($errors) 18 |
19 | 24 |
25 | @endif 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Widgets/WysiwygWidget.php: -------------------------------------------------------------------------------- 1 | 'form-control', 16 | 'data-formello-wysiwyg' => json_encode($fieldConfig['jodit'] ?? []), 17 | ]; 18 | 19 | $fieldConfig = $this->mergeDefaultAttributes($fieldConfig, $defaults, $name); 20 | 21 | return [ 22 | 'name' => $name, 23 | 'value' => $value, 24 | 'config' => $fieldConfig, 25 | 'label' => $fieldConfig['label'] ?? null, 26 | 'errors' => $errors, 27 | ]; 28 | } 29 | 30 | public function getAssets(?array $fieldConfig = null): ?array 31 | { 32 | return [ 33 | 'scripts' => ['jodit.min.js'], 34 | 'styles' => ['jodit.min.css'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Widgets/CheckboxesWidget.php: -------------------------------------------------------------------------------- 1 | resolveChoices($fieldConfig['choices']) ?? []; 18 | 19 | return [ 20 | 'name' => $name, 21 | 'value' => old($name, $value), 22 | 'label' => $fieldConfig['label'] ?? null, 23 | 'config' => $fieldConfig, 24 | 'errors' => $errors, 25 | 'choices' => $choices, 26 | ]; 27 | } 28 | 29 | protected function resolveChoices($choices) 30 | { 31 | if (is_callable($choices)) { 32 | return call_user_func($choices); 33 | } 34 | 35 | return $choices; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 metalogico 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/form.blade.php: -------------------------------------------------------------------------------- 1 |
$value) 5 | {{ $attr }}="{{ $value }}" @endforeach> 6 | 7 | @csrf 8 | @if (isset($formConfig['method'])) 9 | @method($formConfig['method']) 10 | @endif 11 | 12 | {{-- Grid container per columnSpan --}} 13 |
14 | @foreach ($formello->getFields() as $name => $field) 15 |
16 | {!! $formello->renderField($name) !!} 17 |
18 | @endforeach 19 |
20 | 21 |
22 | 23 | {{ $formConfig['cancel_label'] ?? __('Cancel') }} 24 |
25 | 26 |
27 | 28 | @stack('formello-scripts') 29 | -------------------------------------------------------------------------------- /src/Widgets/RadioWidget.php: -------------------------------------------------------------------------------- 1 | $name, 19 | 'value' => old($name, $value), 20 | 'label' => $fieldConfig['label'] ?? null, 21 | 'config' => $fieldConfig, 22 | 'errors' => $errors, 23 | 'options' => $this->getOptions($fieldConfig), 24 | ]; 25 | } 26 | 27 | protected function getOptions(array $fieldConfig): array 28 | { 29 | $options = $fieldConfig['options'] ?? []; 30 | 31 | if (is_callable($options)) { 32 | $options = call_user_func($options); 33 | } 34 | 35 | return $options; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Widgets/ToggleWidget.php: -------------------------------------------------------------------------------- 1 | $name, 30 | 'value' => old($name, $value), 31 | 'config' => $fieldConfig, 32 | 'errors' => $errors, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/select.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | 20 | 21 | @if (isset($config['help'])) 22 |
{!! $config['help'] !!}
23 | @endif 24 | 25 | @if ($errors) 26 |
27 | 32 |
33 | @endif 34 |
-------------------------------------------------------------------------------- /src/Widgets/MaskWidget.php: -------------------------------------------------------------------------------- 1 | getCssFramework(); 16 | 17 | return "formello::widgets.{$framework}.text"; 18 | } 19 | 20 | public function getViewData($name, $value, array $fieldConfig, $errors = null): array 21 | { 22 | // Get base data from TextWidget 23 | $viewData = parent::getViewData($name, $value, $fieldConfig, $errors); 24 | 25 | // Add mask data attribute if mask is configured 26 | if (isset($fieldConfig['mask'])) { 27 | $viewData['config']['attributes']['data-formello-mask'] = json_encode($fieldConfig['mask']); 28 | } 29 | 30 | return $viewData; 31 | } 32 | 33 | /** 34 | * Get assets for MaskWidget - always returns IMask assets 35 | */ 36 | public function getAssets(?array $fieldConfig = null): ?array 37 | { 38 | return [ 39 | 'scripts' => ['imask.min.js'], 40 | 'styles' => [], 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Widgets/RangeWidget.php: -------------------------------------------------------------------------------- 1 | $name, 26 | 'value' => old($name, $value), 27 | 'label' => $fieldConfig['label'] ?? null, 28 | 'config' => $fieldConfig, 29 | 'errors' => $errors, 30 | 'showValue' => $fieldConfig['showValue'] ?? true, 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/SchemaInspector.php: -------------------------------------------------------------------------------- 1 | getCasts(); 14 | if (isset($casts[$field])) { 15 | return $this->normalizeCastType($casts[$field]); 16 | } 17 | 18 | // 2. Check fillable/guarded hints 19 | if (Str::endsWith($field, ['_id', 'Id'])) { 20 | return 'select'; 21 | } 22 | 23 | if (in_array($field, ['email'])) { 24 | return 'email'; 25 | } 26 | 27 | if (in_array($field, ['password', 'password_confirmation'])) { 28 | return 'password'; 29 | } 30 | 31 | // 3. Default fallback 32 | return 'text'; 33 | } 34 | 35 | private function normalizeCastType(string $cast): string 36 | { 37 | return match ($cast) { 38 | 'boolean' => 'toggle', 39 | 'date' => 'date', 40 | 'datetime' => 'datetime', 41 | 'timestamp' => 'datetime', 42 | 'array' => 'checkboxes', 43 | 'json' => 'textarea', 44 | default => 'text' 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/range.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 10 | @endif 11 | 12 | $attrValue) 17 | {{ $attr }}="{{ $attrValue }}" 18 | @endforeach 19 | > 20 | 21 | @if (isset($config['help'])) 22 |
{!! $config['help'] !!}
23 | @endif 24 | 25 | @if ($errors) 26 |
27 | 32 |
33 | @endif 34 |
35 | 36 | @if ($showValue) 37 | 42 | @endif -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metalogico/laravel-formello", 3 | "description": "A Laravel package for generating Bootstrap 5 forms based on models", 4 | "keywords": ["laravel", "forms", "form-builder", "bootstrap", "metalogico"], 5 | "type": "library", 6 | "license": "MIT", 7 | "homepage": "https://github.com/metalogico/laravel-formello", 8 | "minimum-stability": "stable", 9 | "autoload": { 10 | "psr-4": { 11 | "Metalogico\\Formello\\": "src/" 12 | } 13 | }, 14 | "autoload-dev": { 15 | "psr-4": { 16 | "Tests\\": "tests/" 17 | } 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Metalogico", 22 | "email": "michele.brandolin@gmail.com" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.0", 27 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0" 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Metalogico\\Formello\\FormelloServiceProvider" 33 | ], 34 | "aliases": { 35 | "Formello": "Metalogico\\Formello\\Facades\\Formello" 36 | } 37 | } 38 | }, 39 | "require-dev": { 40 | "pestphp/pest-plugin-laravel": "^3.2", 41 | "orchestra/testbench": "^10.4", 42 | "pestphp/pest": "^3.8" 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "pestphp/pest-plugin": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AssetManager.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | @foreach ($options as $optionValue => $optionLabel) 8 |
9 | $attrValue) 16 | {{ $attr }}="{{ $attrValue }}" 17 | @endforeach 18 | > 19 | 22 |
23 | @endforeach 24 | 25 | @if (isset($config['help'])) 26 |
{!! $config['help'] !!}
27 | @endif 28 | 29 | @if (isset($config['help'])) 30 |
{!! $config['help'] !!}
31 | @endif 32 | 33 | @if ($errors) 34 |
35 | 40 |
41 | @endif 42 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/color.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | @if (isset($config['icon']) || isset($config['prefix']) || isset($config['suffix'])) 8 |
9 | @if (isset($config['prefix'])) 10 | @if (isset($config['icon'])) @endif {{ $config['prefix'] }} 11 | @elseif (isset($config['icon'])) 12 | 13 | @endif 14 | $attrValue) {{ $attr }}="{{ $attrValue }}" @endforeach> 15 | @if (isset($config['suffix'])) 16 | {{ $config['suffix'] }} 17 | @endif 18 |
19 | @else 20 | $attrValue) {{ $attr }}="{{ $attrValue }}" @endforeach> 21 | @endif 22 | 23 | @if (isset($config['help'])) 24 |
{!! $config['help'] !!}
25 | @endif 26 | 27 | @if ($errors) 28 |
29 | 34 |
35 | @endif 36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Widgets/SelectWidget.php: -------------------------------------------------------------------------------- 1 | resolveChoices($fieldConfig['choices'] ?? []); 25 | 26 | return [ 27 | 'name' => $name, 28 | 'value' => old($name, $value), 29 | 'label' => $fieldConfig['label'] ?? null, 30 | 'config' => $fieldConfig, 31 | 'errors' => $errors, 32 | 'choices' => $choices, 33 | ]; 34 | } 35 | 36 | protected function resolveChoices($choices) 37 | { 38 | if (is_callable($choices)) { 39 | return call_user_func($choices); 40 | } 41 | 42 | return $choices; 43 | } 44 | 45 | public function getAssets(?array $fieldConfig = null): ?array 46 | { 47 | return [ 48 | 'scripts' => ['select2.min.js'], 49 | 'styles' => ['select2.min.css'], 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Widgets/TextWidget.php: -------------------------------------------------------------------------------- 1 | ['inputmode' => 'numeric', 'pattern' => '[0-9]*'], 21 | 'email' => ['autocomplete' => 'email'], 22 | 'password' => ['autocomplete' => 'new-password'], 23 | default => [] 24 | }; 25 | 26 | $fieldConfig['attributes'] = array_merge($fieldConfig['attributes'], $typeAttributes); 27 | 28 | $safeValue = $value; 29 | if ($fieldConfig['attributes']['type'] === 'password') { 30 | $safeValue = ''; 31 | } 32 | 33 | return [ 34 | 'name' => $name, 35 | 'value' => old($name, $safeValue), 36 | 'label' => $fieldConfig['label'] ?? null, 37 | 'config' => $fieldConfig, 38 | 'errors' => $errors, 39 | ]; 40 | } 41 | 42 | /** 43 | * Get assets for TextWidget - no assets needed 44 | */ 45 | public function getAssets(?array $fieldConfig = null): ?array 46 | { 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Traits/HasSelect2Widget.php: -------------------------------------------------------------------------------- 1 | newQuery() : $query; 28 | 29 | // Apply search term if provided 30 | if ($term && ! empty($searchFields)) { 31 | $queryBuilder->where(function ($q) use ($searchFields, $term) { 32 | foreach ($searchFields as $field) { 33 | $q->orWhere($field, 'LIKE', "%{$term}%"); 34 | } 35 | }); 36 | } 37 | 38 | // Execute query and format results 39 | $items = $queryBuilder 40 | ->limit($limit) 41 | ->get() 42 | ->map(fn ($item) => [ 43 | 'id' => $item->$valueField, 44 | 'text' => data_get($item, $labelField), // supports nested fields like 'user.name' 45 | ]); 46 | 47 | return response()->json(['results' => $items]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Widgets/BaseWidget.php: -------------------------------------------------------------------------------- 1 | getViewData($name, $value, $fieldConfig, $errors); 15 | 16 | return View::make($this->getTemplate(), $viewData)->render(); 17 | } 18 | 19 | public function getTemplate(): string 20 | { 21 | $framework = app('formello')->getCssFramework(); 22 | 23 | return "formello::widgets.{$framework}.".$this->getWidgetName(); 24 | } 25 | 26 | public function getWidgetName(): string 27 | { 28 | return strtolower(class_basename($this)); 29 | } 30 | 31 | protected function mergeDefaultAttributes(array $fieldConfig, array $defaults, string $name): array 32 | { 33 | $fieldConfig['attributes'] = array_merge( 34 | $defaults, 35 | $fieldConfig['attributes'] ?? [], 36 | ['id' => $fieldConfig['attributes']['id'] ?? $name] 37 | ); 38 | 39 | return $fieldConfig; 40 | } 41 | 42 | /** 43 | * Get assets required by this widget (optional) 44 | * Override in child classes to specify required assets 45 | * 46 | * @param array|null $fieldConfig Optional field configuration for conditional assets 47 | * @return array|null Array with 'scripts' and 'styles' keys, or null if no assets needed 48 | */ 49 | public function getAssets(?array $fieldConfig = null): ?array 50 | { 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/DateWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithDateWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new DateWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_date_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithDateWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(DateWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/ColorWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithColorWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new ColorWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_color_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithColorWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(ColorWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/RangeWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithRangeWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new RangeWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_range_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithRangeWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(RangeWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/HiddenWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithHiddenWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new HiddenWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_hidden_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithHiddenWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(HiddenWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/ToggleWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithToggleWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new ToggleWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_toggle_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithToggleWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(ToggleWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/DateTimeWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithDateTimeWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new DateTimeWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_datetime_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithDateTimeWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(DateTimeWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/TextareaWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithTextareaWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new TextareaWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_textarea_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithTextareaWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(TextareaWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/ColorSwatchWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithColorSwatchWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new ColorSwatchWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_colorswatch_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithColorSwatchWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(ColorSwatchWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/RadioWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithRadioWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new RadioWidget(), 39 | 'options' => ['x' => 'X', 'y' => 'Y'], 40 | ], 41 | ]; 42 | } 43 | protected function create(): array { return []; } 44 | protected function edit(): array { return []; } 45 | }; 46 | } 47 | 48 | public function test_radio_widget_is_instantiated_and_renders() 49 | { 50 | $form = $this->makeFormWithRadioWidget(); 51 | $fields = $form->getFields(); 52 | $this->assertArrayHasKey('field', $fields); 53 | $this->assertInstanceOf(RadioWidget::class, $fields['field']['widget']); 54 | $output = $form->renderField('field'); 55 | $this->assertIsString($output); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/SelectWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithSelectWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new SelectWidget(), 39 | 'choices' => ['a' => 'A', 'b' => 'B'], 40 | ], 41 | ]; 42 | } 43 | protected function create(): array { return []; } 44 | protected function edit(): array { return []; } 45 | }; 46 | } 47 | 48 | public function test_select_widget_is_instantiated_and_renders() 49 | { 50 | $form = $this->makeFormWithSelectWidget(); 51 | $fields = $form->getFields(); 52 | $this->assertArrayHasKey('field', $fields); 53 | $this->assertInstanceOf(SelectWidget::class, $fields['field']['widget']); 54 | $output = $form->renderField('field'); 55 | $this->assertIsString($output); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/CheckboxesWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithCheckboxesWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new CheckboxesWidget(), 39 | 'choices' => ['foo' => 'Foo'], 40 | ], 41 | ]; 42 | } 43 | protected function create(): array { return []; } 44 | protected function edit(): array { return []; } 45 | }; 46 | } 47 | 48 | public function test_checkboxes_widget_is_instantiated_and_renders() 49 | { 50 | $form = $this->makeFormWithCheckboxesWidget(); 51 | $fields = $form->getFields(); 52 | $this->assertArrayHasKey('field', $fields); 53 | $this->assertInstanceOf(CheckboxesWidget::class, $fields['field']['widget']); 54 | $output = $form->renderField('field'); 55 | $this->assertIsString($output); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/text.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | @if (isset($config['icon']) || isset($config['prefix']) || isset($config['suffix'])) 8 |
9 | @if (isset($config['prefix'])) 10 | @if (isset($config['icon'])) @endif {{ $config['prefix'] }} 11 | @elseif (isset($config['icon'])) 12 | 13 | @endif 14 | @endif 15 | $attrValue) 19 | @if (!in_array($attr, ['data-formello-mask'])) 20 | {{ $attr }}="{{ $attrValue }}" 21 | @endif 22 | @endforeach> 23 | 24 | @if (isset($config['icon']) || isset($config['prefix']) || isset($config['suffix'])) 25 | @if (isset($config['suffix'])) 26 | {{ $config['suffix'] }} 27 | @endif 28 |
29 | @endif 30 | 31 | @if (isset($config['help'])) 32 |
{!! $config['help'] !!}
33 | @endif 34 | 35 | @if ($errors) 36 |
37 | 42 |
43 | @endif 44 | 45 |
46 | -------------------------------------------------------------------------------- /src/Widgets/ColorWidget.php: -------------------------------------------------------------------------------- 1 | 'nano', 22 | 'default' => $value ?: '#3498db', 23 | 'components' => [ 24 | 'preview' => true, 25 | 'opacity' => true, 26 | 'hue' => true, 27 | 'interaction' => [ 28 | 'hex' => true, 29 | 'rgba' => true, 30 | 'input' => true, 31 | 'clear' => true, 32 | 'save' => true, 33 | ], 34 | ], 35 | ]; 36 | 37 | // Merge default options with user-provided options 38 | $userPickrOptions = $fieldConfig['pickr'] ?? []; 39 | $mergedOptions = array_merge($defaultPickrOptions, $userPickrOptions); 40 | 41 | // Pass the final options to the view 42 | $fieldConfig['attributes']['data-formello-colorpicker'] = json_encode($mergedOptions); 43 | 44 | return [ 45 | 'name' => $name, 46 | 'value' => old($name, $value), 47 | 'label' => $fieldConfig['label'] ?? null, 48 | 'config' => $fieldConfig, 49 | 'errors' => $errors, 50 | ]; 51 | } 52 | 53 | public function getAssets(?array $fieldConfig = null): ?array 54 | { 55 | return [ 56 | 'scripts' => ['pickr.min.js'], 57 | 'styles' => ['nano.min.css'], 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/WidgetFactory.php: -------------------------------------------------------------------------------- 1 | widgetMap = [ 15 | 'text' => Widgets\TextWidget::class, 16 | 'textarea' => Widgets\TextareaWidget::class, 17 | 'toggle' => Widgets\ToggleWidget::class, 18 | 'date' => Widgets\DateWidget::class, 19 | 'datetime' => Widgets\DateTimeWidget::class, 20 | 'timestamp' => Widgets\DateTimeWidget::class, 21 | 'select' => Widgets\SelectWidget::class, 22 | 'select2' => Widgets\Select2Widget::class, 23 | 'checkboxes' => Widgets\CheckboxesWidget::class, 24 | 'radio' => Widgets\RadioWidget::class, 25 | 'range' => Widgets\RangeWidget::class, 26 | 'upload' => Widgets\UploadWidget::class, 27 | 'hidden' => Widgets\HiddenWidget::class, 28 | 'color' => Widgets\ColorWidget::class, 29 | 'colorswatch' => Widgets\ColorSwatchWidget::class, 30 | 'wysiwyg' => Widgets\WysiwygWidget::class, 31 | 'mask' => Widgets\MaskWidget::class, 32 | 'separator' => Widgets\SeparatorWidget::class, 33 | ]; 34 | 35 | // Merge custom widgets from user config (overrides built-ins) 36 | $custom = config('formello.custom_widgets', []); 37 | if (is_array($custom) && ! empty($custom)) { 38 | $this->widgetMap = array_merge($this->widgetMap, $custom); 39 | } 40 | } 41 | 42 | public function make(string $type): WidgetInterface 43 | { 44 | $widgetClass = $this->widgetMap[$type] ?? Widgets\TextWidget::class; 45 | 46 | if (! class_exists($widgetClass)) { 47 | throw new \InvalidArgumentException("Widget class {$widgetClass} not found"); 48 | } 49 | 50 | $widget = new $widgetClass; 51 | 52 | if (! $widget instanceof WidgetInterface) { 53 | throw new \InvalidArgumentException('Widget must implement WidgetInterface'); 54 | } 55 | 56 | return $widget; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/UploadWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithUploadWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'file' => [ 38 | 'widget' => new UploadWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_upload_widget_is_instantiated_and_renders_and_sets_enctype() 48 | { 49 | $form = $this->makeFormWithUploadWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('file', $fields); 52 | $this->assertInstanceOf(UploadWidget::class, $fields['file']['widget']); 53 | $output = $form->renderField('file'); 54 | $this->assertIsString($output); 55 | $formConfig = (new \ReflectionClass($form))->getProperty('formConfig'); 56 | $formConfig->setAccessible(true); 57 | $config = $formConfig->getValue($form); 58 | $this->assertArrayHasKey('attributes', $config); 59 | $this->assertArrayHasKey('enctype', $config['attributes']); 60 | $this->assertEquals('multipart/form-data', $config['attributes']['enctype']); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/assets/js/l10n/it.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.it = {})); 5 | }(this, (function (exports) { 6 | 'use strict'; 7 | 8 | var fp = typeof window !== "undefined" && window.flatpickr !== undefined 9 | ? window.flatpickr 10 | : { 11 | l10ns: {}, 12 | }; 13 | var Italian = { 14 | weekdays: { 15 | shorthand: ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"], 16 | longhand: [ 17 | "Domenica", 18 | "Lunedì", 19 | "Martedì", 20 | "Mercoledì", 21 | "Giovedì", 22 | "Venerdì", 23 | "Sabato", 24 | ], 25 | }, 26 | months: { 27 | shorthand: [ 28 | "Gen", 29 | "Feb", 30 | "Mar", 31 | "Apr", 32 | "Mag", 33 | "Giu", 34 | "Lug", 35 | "Ago", 36 | "Set", 37 | "Ott", 38 | "Nov", 39 | "Dic", 40 | ], 41 | longhand: [ 42 | "Gennaio", 43 | "Febbraio", 44 | "Marzo", 45 | "Aprile", 46 | "Maggio", 47 | "Giugno", 48 | "Luglio", 49 | "Agosto", 50 | "Settembre", 51 | "Ottobre", 52 | "Novembre", 53 | "Dicembre", 54 | ], 55 | }, 56 | firstDayOfWeek: 1, 57 | ordinal: function () { return "°"; }, 58 | rangeSeparator: " al ", 59 | weekAbbreviation: "Se", 60 | scrollTitle: "Scrolla per aumentare", 61 | toggleTitle: "Clicca per cambiare", 62 | time_24hr: true, 63 | }; 64 | fp.l10ns.it = Italian; 65 | var it = fp.l10ns; 66 | 67 | exports.Italian = Italian; 68 | exports.default = it; 69 | 70 | Object.defineProperty(exports, '__esModule', { value: true }); 71 | 72 | }))); -------------------------------------------------------------------------------- /tests/Unit/SeparatorWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithSeparator(?string $label) 33 | { 34 | $labelConfig = is_null($label) ? [] : ['label' => $label]; 35 | 36 | return new class($this->makeDummyModel(), new ViewErrorBag(), $labelConfig) extends Formello { 37 | public function __construct($model, $errors, private array $labelConfig) 38 | { 39 | parent::__construct($model, $errors); 40 | } 41 | protected function fields(): array { 42 | return [ 43 | 'sep' => array_merge([ 44 | 'widget' => new SeparatorWidget(), 45 | ], $this->labelConfig), 46 | ]; 47 | } 48 | protected function create(): array { return []; } 49 | protected function edit(): array { return []; } 50 | }; 51 | } 52 | 53 | public function test_separator_widget_renders_with_label() 54 | { 55 | $form = $this->makeFormWithSeparator('Section Title'); 56 | $output = $form->renderField('sep'); 57 | $this->assertIsString($output); 58 | $this->assertStringContainsString('Section Title', $output); 59 | $this->assertStringContainsString('makeFormWithSeparator(null); 65 | $output = $form->renderField('sep'); 66 | $this->assertIsString($output); 67 | $this->assertStringNotContainsString('assertStringContainsString(' 2 | 3 | @if (isset($label)) 4 | 5 | @endif 6 | 7 | @if (isset($config['icon']) || isset($config['prefix']) || isset($config['suffix'])) 8 |
9 | @if (isset($config['prefix'])) 10 | @if (isset($config['icon'])) @endif {{ $config['prefix'] }} 11 | @elseif (isset($config['icon'])) 12 | 13 | @endif 14 | $attrValue) 17 | @if ($attr !== 'class') 18 | {{ $attr }}="{{ $attrValue }}" 19 | @endif 20 | @endforeach> 21 | @if (isset($config['suffix'])) 22 | {{ $config['suffix'] }} 23 | @endif 24 | @if ($errors) 25 |
26 |
    27 | @foreach ($errors as $error) 28 |
  • {{ $error }}
  • 29 | @endforeach 30 |
31 |
32 | @endif 33 |
34 | @else 35 | $attrValue) 38 | @if ($attr !== 'class') 39 | {{ $attr }}="{{ $attrValue }}" 40 | @endif 41 | @endforeach> 42 | @if ($errors) 43 |
44 |
    45 | @foreach ($errors as $error) 46 |
  • {{ $error }}
  • 47 | @endforeach 48 |
49 |
50 | @endif 51 | @endif 52 | 53 | @if (isset($config['help'])) 54 |
{!! $config['help'] !!}
55 | @endif 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Widgets/DateTimeWidget.php: -------------------------------------------------------------------------------- 1 | getCssFramework(); 16 | 17 | return "formello::widgets.{$framework}.date"; 18 | } 19 | 20 | public function getViewData($name, $value, array $fieldConfig, $errors = null): array 21 | { 22 | // Get base data from parent DateWidget 23 | $data = parent::getViewData($name, $value, $fieldConfig, $errors); 24 | 25 | // Override Flatpickr options for datetime 26 | $defaultFlatpickrOptions = [ 27 | 'altInput' => true, 28 | 'altFormat' => 'd F Y H:i', 29 | 'dateFormat' => 'Y-m-d H:i', 30 | 'locale' => 'it', 31 | 'enableTime' => true, 32 | 'time_24hr' => true, 33 | ]; 34 | 35 | // Merge with user options 36 | $userFlatpickrOptions = $fieldConfig['flatpickr'] ?? []; 37 | $mergedOptions = array_merge($defaultFlatpickrOptions, $userFlatpickrOptions); 38 | 39 | // Propagate validation state to Flatpickr's alt input 40 | $hasErrors = !empty($errors); 41 | if (!empty($mergedOptions['altInput'])) { 42 | $existingAltClass = $mergedOptions['altInputClass'] 43 | ?? ($data['config']['attributes']['class'] ?? 'form-control'); 44 | $mergedOptions['altInputClass'] = trim($existingAltClass . ($hasErrors ? ' is-invalid' : '')); 45 | } 46 | 47 | // Update the data-formello-datepicker attribute 48 | $data['config']['attributes']['data-formello-datepicker'] = json_encode($mergedOptions); 49 | 50 | // Override format for datetime 51 | $format = $fieldConfig['format'] ?? 'Y-m-d H:i'; 52 | $data['format'] = $format; 53 | 54 | // Handle datetime value formatting 55 | if ($value instanceof \DateTime) { 56 | $data['value'] = old($name, $value->format($format)); 57 | } elseif (is_string($value) && $format !== 'Y-m-d H:i') { 58 | $date = \DateTime::createFromFormat($format, $value); 59 | if ($date) { 60 | $data['value'] = old($name, $date->format('Y-m-d H:i')); 61 | } 62 | } 63 | 64 | return $data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Widgets/ColorSwatchWidget.php: -------------------------------------------------------------------------------- 1 | getCssFramework(); 16 | 17 | return "formello::widgets.{$framework}.color"; 18 | } 19 | 20 | public function getViewData($name, $value, array $fieldConfig, $errors = null): array 21 | { 22 | // Get base data from parent ColorWidget 23 | $data = parent::getViewData($name, $value, $fieldConfig, $errors); 24 | 25 | // Override Pickr options for swatches-only mode 26 | $defaultPickrOptions = [ 27 | 'theme' => 'nano', 28 | 'default' => $value ?: '#3498db', 29 | 'closeOnScroll' => true, 30 | 'autoReposition' => true, 31 | 32 | // Disable picker components but keep minimal interaction 33 | 'components' => [ 34 | 'preview' => false, 35 | 'opacity' => false, 36 | 'hue' => false, 37 | 'interaction' => [ 38 | 'hex' => false, 39 | 'rgba' => false, 40 | 'hsla' => false, 41 | 'hsva' => false, 42 | 'cmyk' => false, 43 | 'input' => false, 44 | 'clear' => false, 45 | 'save' => false, 46 | ], 47 | ], 48 | 49 | // Default swatches (can be overridden by user) 50 | 'swatches' => [ 51 | '#f44336', '#e91e63', '#9c27b0', '#673ab7', 52 | '#3f51b5', '#2196f3', '#03a9f4', '#00bcd4', 53 | '#009688', '#4caf50', '#8bc34a', '#cddc39', 54 | '#ffeb3b', '#ffc107', '#ff9800', '#ff5722', 55 | '#795548', '#9e9e9e', '#607d8b', '#000000', 56 | ], 57 | ]; 58 | 59 | // Merge with user options (user can override swatches) 60 | $userPickrOptions = $fieldConfig['pickr'] ?? []; 61 | $mergedOptions = array_merge($defaultPickrOptions, $userPickrOptions); 62 | 63 | // Update the data-formello-colorpicker attribute 64 | $data['config']['attributes']['data-formello-colorpicker'] = json_encode($mergedOptions); 65 | 66 | return $data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/TextWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithTextWidget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new TextWidget(), 39 | ], 40 | ]; 41 | } 42 | protected function create(): array { return []; } 43 | protected function edit(): array { return []; } 44 | }; 45 | } 46 | 47 | public function test_text_widget_is_instantiated_and_renders() 48 | { 49 | $form = $this->makeFormWithTextWidget(); 50 | $fields = $form->getFields(); 51 | $this->assertArrayHasKey('field', $fields); 52 | $this->assertInstanceOf(TextWidget::class, $fields['field']['widget']); 53 | $output = $form->renderField('field'); 54 | $this->assertIsString($output); 55 | } 56 | 57 | public function test_text_widget_renders_with_icon() 58 | { 59 | $form = new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 60 | protected function fields(): array { 61 | return [ 62 | 'field_with_icon' => [ 63 | 'widget' => new TextWidget(), 64 | 'icon' => '', 65 | ], 66 | ]; 67 | } 68 | protected function create(): array { return []; } 69 | protected function edit(): array { return []; } 70 | }; 71 | 72 | $output = $form->renderField('field_with_icon'); 73 | 74 | $this->assertStringContainsString('
', $output); 75 | $this->assertStringContainsString('', $output); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Widgets/DateWidget.php: -------------------------------------------------------------------------------- 1 | true, 22 | 'altFormat' => 'd F Y', 23 | 'dateFormat' => 'Y-m-d', 24 | 'locale' => 'it', 25 | ]; 26 | 27 | // Merge default options with user-provided options 28 | $userFlatpickrOptions = $fieldConfig['flatpickr'] ?? []; 29 | $mergedOptions = array_merge($defaultFlatpickrOptions, $userFlatpickrOptions); 30 | 31 | // Propagate validation state to Flatpickr's alt input 32 | $hasErrors = !empty($errors); 33 | if (!empty($mergedOptions['altInput'])) { 34 | $existingAltClass = $mergedOptions['altInputClass'] 35 | ?? ($fieldConfig['attributes']['class'] ?? 'form-control'); 36 | $mergedOptions['altInputClass'] = trim($existingAltClass . ($hasErrors ? ' is-invalid' : '')); 37 | } 38 | 39 | // Pass the final options to the view 40 | $fieldConfig['attributes']['data-formello-datepicker'] = json_encode($mergedOptions); 41 | 42 | $format = $fieldConfig['format'] ?? 'Y-m-d'; 43 | 44 | if ($value instanceof \DateTime) { 45 | $value = $value->format($format); 46 | } elseif (is_string($value) && $format !== 'Y-m-d') { 47 | $date = \DateTime::createFromFormat($format, $value); 48 | if ($date) { 49 | $value = $date->format('Y-m-d'); 50 | } 51 | } 52 | 53 | return [ 54 | 'name' => $name, 55 | 'value' => old($name, $value), 56 | 'label' => $fieldConfig['label'] ?? null, 57 | 'config' => $fieldConfig, 58 | 'errors' => $errors, 59 | 'format' => $format, 60 | ]; 61 | } 62 | 63 | public function getAssets(?array $fieldConfig = null): ?array 64 | { 65 | return [ 66 | 'scripts' => ['flatpickr.min.js', 'l10n/it.js'], 67 | 'styles' => ['flatpickr.min.css'], 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Widgets/Select2Widget.php: -------------------------------------------------------------------------------- 1 | 'form-control', 17 | ]; 18 | 19 | $fieldConfig = $this->mergeDefaultAttributes($fieldConfig, $defaults, $name); 20 | if(isset($fieldConfig['multiple']) && $fieldConfig['multiple'] == true){ 21 | $fieldConfig['multiple'] = true; 22 | $name .= '[]'; 23 | }else{ 24 | $fieldConfig['multiple'] = false; 25 | } 26 | 27 | // Estrai la configurazione specifica di select2 28 | $select2Config = $fieldConfig['select2'] ?? []; 29 | $usesAjax = ! empty($select2Config['route']); 30 | 31 | $currentValue = old($name, $value); 32 | $choices = []; 33 | 34 | // Se usiamo AJAX e c'è un valore, dobbiamo caricare l'opzione iniziale 35 | if ($usesAjax && ! empty($currentValue)) { 36 | $modelClass = $select2Config['model'] ?? null; 37 | $labelField = $select2Config['label_field'] ?? 'name'; 38 | $valueField = $select2Config['value_field'] ?? 'id'; 39 | 40 | if ($modelClass) { 41 | $initialItems = $modelClass::whereIn($valueField, (array) $currentValue)->get(); 42 | foreach ($initialItems as $item) { 43 | $choices[$item->$valueField] = data_get($item, $labelField); 44 | } 45 | } 46 | } elseif (! $usesAjax) { 47 | // Altrimenti, se non usiamo AJAX, risolviamo le choices come prima 48 | $choices = $this->resolveChoices($fieldConfig['choices'] ?? []); 49 | } 50 | 51 | return [ 52 | 'name' => $name, 53 | 'value' => $currentValue, 54 | 'label' => $fieldConfig['label'] ?? null, 55 | 'config' => $fieldConfig, 56 | 'errors' => $errors, 57 | 'choices' => $choices, 58 | 'usesAjax' => $usesAjax, 59 | ]; 60 | } 61 | 62 | protected function resolveChoices($choices): array 63 | { 64 | if (is_callable($choices)) { 65 | return call_user_func($choices); 66 | } 67 | 68 | return $choices; 69 | } 70 | 71 | public function getAssets(?array $fieldConfig = null): ?array 72 | { 73 | return [ 74 | 'scripts' => ['select2.min.js'], 75 | 'styles' => ['select2.min.css', 'select2-bootstrap-5-theme.min.css'], 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Unit/FormelloMultipartTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { 29 | return 'dummy'; 30 | } 31 | }; 32 | } 33 | 34 | private function makeFormWithUpload() 35 | { 36 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 37 | protected function fields(): array { 38 | return [ 39 | 'file' => [ 40 | 'widget' => new UploadWidget(), 41 | ], 42 | ]; 43 | } 44 | protected function create(): array { return []; } 45 | protected function edit(): array { return []; } 46 | }; 47 | } 48 | 49 | private function makeFormWithNoUpload() 50 | { 51 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 52 | protected function fields(): array { 53 | return [ 54 | 'name' => [ 55 | 'widget' => 'TextWidget', 56 | ], 57 | ]; 58 | } 59 | protected function create(): array { return []; } 60 | protected function edit(): array { return []; } 61 | }; 62 | } 63 | 64 | public function test_form_with_upload_has_multipart_enctype() 65 | { 66 | $form = $this->makeFormWithUpload(); 67 | $formConfig = (new \ReflectionClass($form))->getProperty('formConfig'); 68 | $formConfig->setAccessible(true); 69 | $config = $formConfig->getValue($form); 70 | $this->assertArrayHasKey('attributes', $config); 71 | $this->assertArrayHasKey('enctype', $config['attributes']); 72 | $this->assertEquals('multipart/form-data', $config['attributes']['enctype']); 73 | } 74 | 75 | public function test_form_without_upload_does_not_have_multipart_enctype() 76 | { 77 | $form = $this->makeFormWithNoUpload(); 78 | $formConfig = (new \ReflectionClass($form))->getProperty('formConfig'); 79 | $formConfig->setAccessible(true); 80 | $config = $formConfig->getValue($form); 81 | $this->assertTrue(!isset($config['attributes']) || !isset($config['attributes']['enctype'])); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Unit/CustomWidgetsOverrideTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 18 | } 19 | 20 | protected function getPackageProviders($app) 21 | { 22 | return [ 23 | \Metalogico\Formello\FormelloServiceProvider::class, 24 | ]; 25 | } 26 | 27 | private function makeDummyModel() 28 | { 29 | return new class extends Model { 30 | public function getTable() { return 'dummy'; } 31 | }; 32 | } 33 | 34 | public function test_custom_widgets_override_built_in_alias() 35 | { 36 | // Register a custom widget that overrides the 'text' alias 37 | config()->set('formello.custom_widgets', [ 38 | 'text' => \Tests\Unit\TestTextWidget::class, 39 | ]); 40 | 41 | // Build a simple form that uses the 'text' alias 42 | $form = new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 43 | protected function fields(): array { 44 | return [ 45 | 'field' => [ 46 | 'widget' => 'text', 47 | ], 48 | ]; 49 | } 50 | protected function create(): array { return []; } 51 | protected function edit(): array { return []; } 52 | }; 53 | 54 | $fields = $form->getFields(); 55 | $this->assertArrayHasKey('field', $fields); 56 | $this->assertInstanceOf(TestTextWidget::class, $fields['field']['widget']); 57 | $output = $form->renderField('field'); 58 | $this->assertIsString($output); 59 | $this->assertStringContainsString('data-test-text-widget', $output); 60 | } 61 | } 62 | 63 | // Simple test widget that replaces the built-in TextWidget 64 | class TestTextWidget extends BaseWidget implements WidgetInterface 65 | { 66 | public function getWidgetName(): string 67 | { 68 | return 'text'; 69 | } 70 | 71 | public function getViewData($name, $value, array $fieldConfig, $errors = null): array 72 | { 73 | return [ 74 | 'name' => $name, 75 | 'value' => $value, 76 | 'label' => $fieldConfig['label'] ?? null, 77 | 'config' => $fieldConfig, 78 | 'errors' => $errors, 79 | ]; 80 | } 81 | 82 | public function render($name, $value, array $fieldConfig, $errors = null): string 83 | { 84 | // Render a minimal recognizable markup for assertion 85 | return '
overridden
'; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/checkboxes.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if (isset($label)) 3 | 4 | @endif 5 | 6 | 7 | @if(isset($config['select-all']['enabled']) ) 8 |
9 | 13 |
14 | @endif 15 | 16 | @foreach($choices as $optionValue => $optionLabel) 17 |
18 | $attrValue) {{ $attr }}="{{ $attrValue }}" @endforeach 20 | type="checkbox" 21 | name="{{ $name }}[]" 22 | value="{{ $optionValue }}" 23 | class="form-check-input {{ $name }}-checkbox {{ $config['attributes']['class'] ?? '' }} @if ($errors) is-invalid @endif" 24 | id="{{ $name }}_{{ $optionValue }}" 25 | {{ in_array($optionValue, (array)old($name, $value)) ? 'checked' : '' }} 26 | > 27 | 30 |
31 | @endforeach 32 | 33 | @if (isset($config['help'])) 34 |
{!! $config['help'] !!}
35 | @endif 36 | 37 | @if ($errors) 38 |
39 |
    40 | @foreach ($errors as $error) 41 |
  • {{ $error }}
  • 42 | @endforeach 43 |
44 |
45 | @endif 46 |
47 | 48 | 49 | @if(isset($config['select-all']['enabled'])) 50 | 76 | @endif -------------------------------------------------------------------------------- /src/FormelloServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/formello.php', 'formello'); 14 | 15 | $this->app->singleton('formello', FormelloManager::class); 16 | $this->app->bind(Formello::class, FormelloManager::class); 17 | 18 | // Register factory and inspector 19 | $this->app->singleton(WidgetFactory::class); 20 | $this->app->singleton(SchemaInspector::class); 21 | } 22 | 23 | public function boot(): void 24 | { 25 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'formello'); 26 | 27 | $this->publishes([ 28 | __DIR__.'/../config/formello.php' => config_path('formello.php'), 29 | ], 'formello-config'); 30 | 31 | $this->publishes([ 32 | __DIR__.'/../resources/views' => resource_path('views/vendor/formello'), 33 | ], 'formello-views'); 34 | 35 | $this->publishes([ 36 | // Main script 37 | __DIR__.'/../resources/assets/js/formello.js' => public_path('vendor/formello/js/formello.js'), 38 | // IMask 39 | __DIR__.'/../resources/assets/js/imask.min.js' => public_path('vendor/formello/js/imask.min.js'), 40 | // Flatpickr 41 | __DIR__.'/../resources/assets/js/flatpickr.min.js' => public_path('vendor/formello/js/flatpickr.min.js'), 42 | __DIR__.'/../resources/assets/css/flatpickr.min.css' => public_path('vendor/formello/css/flatpickr.min.css'), 43 | __DIR__.'/../resources/assets/js/l10n/it.js' => public_path('vendor/formello/js/l10n/it.js'), 44 | // Pickr 45 | __DIR__.'/../resources/assets/js/pickr.min.js' => public_path('vendor/formello/js/pickr.min.js'), 46 | __DIR__.'/../resources/assets/css/nano.min.css' => public_path('vendor/formello/css/nano.min.css'), 47 | // Quill.js 48 | __DIR__.'/../resources/assets/js/jodit.min.js' => public_path('vendor/formello/js/jodit.min.js'), 49 | __DIR__.'/../resources/assets/css/jodit.min.css' => public_path('vendor/formello/css/jodit.min.css'), 50 | // Select2 51 | __DIR__.'/../resources/assets/js/select2.min.js' => public_path('vendor/formello/js/select2.min.js'), 52 | __DIR__.'/../resources/assets/css/select2.min.css' => public_path('vendor/formello/css/select2.min.css'), 53 | __DIR__.'/../resources/assets/css/select2-bootstrap-5-theme.min.css' => public_path('vendor/formello/css/select2-bootstrap-5-theme.min.css'), 54 | ], 'formello-assets'); 55 | 56 | if ($this->app->runningInConsole()) { 57 | $this->commands([MakeFormelloCommand::class]); 58 | } 59 | 60 | $this->registerBladeDirectives(); 61 | } 62 | 63 | /** 64 | * Register Blade directives for Formello assets 65 | */ 66 | protected function registerBladeDirectives(): void 67 | { 68 | Blade::directive('formelloStyles', function () { 69 | return "render(); ?>"; 70 | }); 71 | 72 | Blade::directive('formelloScripts', function () { 73 | return "render(); ?>"; 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Unit/FormelloTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model 28 | { 29 | public function getTable() 30 | { 31 | return 'dummy'; 32 | } 33 | }; 34 | } 35 | 36 | public function test_formello_can_be_instantiated_and_renders() 37 | { 38 | $form = new class($this->makeDummyModel(), new ViewErrorBag) extends Formello 39 | { 40 | protected function fields(): array 41 | { 42 | return [ 43 | 'field' => [ 44 | 'name' => 'test', 45 | 'label' => 'Test', 46 | 'widget' => new TextWidget, 47 | ], 48 | ]; 49 | } 50 | 51 | protected function create(): array 52 | { 53 | return []; 54 | } 55 | 56 | protected function edit(): array 57 | { 58 | return []; 59 | } 60 | }; 61 | $this->assertInstanceOf(Formello::class, $form); 62 | try { 63 | $output = $form->render(); 64 | $this->assertIsString($output); 65 | } catch (\Throwable $e) { 66 | $this->fail('Render exception: '.$e->getMessage()."\n".$e->getTraceAsString()); 67 | } 68 | } 69 | 70 | public function test_is_creating_returns_true_for_new_model() 71 | { 72 | $model = $this->makeDummyModel(); 73 | 74 | $form = new class($model, new ViewErrorBag) extends Formello 75 | { 76 | protected function fields(): array 77 | { 78 | return []; 79 | } 80 | 81 | protected function create(): array 82 | { 83 | return []; 84 | } 85 | 86 | protected function edit(): array 87 | { 88 | return []; 89 | } 90 | }; 91 | 92 | $this->assertTrue($form->isCreating()); 93 | $this->assertFalse($form->isEditing()); 94 | } 95 | 96 | public function test_is_editing_returns_true_for_existing_model() 97 | { 98 | $model = $this->makeDummyModel(); 99 | $model->exists = true; // Simulate an existing model 100 | 101 | $form = new class($model, new ViewErrorBag) extends Formello 102 | { 103 | protected function fields(): array 104 | { 105 | return []; 106 | } 107 | 108 | protected function create(): array 109 | { 110 | return []; 111 | } 112 | 113 | protected function edit(): array 114 | { 115 | return []; 116 | } 117 | }; 118 | 119 | $this->assertTrue($form->isEditing()); 120 | $this->assertFalse($form->isCreating()); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/Unit/WysiwygWidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model 28 | { 29 | public function getTable() 30 | { 31 | return 'dummy'; 32 | } 33 | }; 34 | } 35 | 36 | private function makeFormWithWysiwygWidget() 37 | { 38 | return new class($this->makeDummyModel(), new ViewErrorBag) extends Formello 39 | { 40 | protected function fields(): array 41 | { 42 | return [ 43 | 'content' => [ 44 | 'widget' => new WysiwygWidget, 45 | 'jodit' => [ 46 | 'toolbar' => ['bold', 'italic', 'link'], 47 | 'language' => 'it', 48 | ], 49 | ], 50 | ]; 51 | } 52 | 53 | protected function create(): array 54 | { 55 | return []; 56 | } 57 | 58 | protected function edit(): array 59 | { 60 | return []; 61 | } 62 | }; 63 | } 64 | 65 | public function test_wysiwyg_widget_is_instantiated_and_renders() 66 | { 67 | $form = $this->makeFormWithWysiwygWidget(); 68 | $fields = $form->getFields(); 69 | 70 | $this->assertArrayHasKey('content', $fields); 71 | $this->assertInstanceOf(WysiwygWidget::class, $fields['content']['widget']); 72 | 73 | $output = $form->renderField('content'); 74 | $this->assertIsString($output); 75 | $this->assertStringContainsString('data-formello-wysiwyg', $output); 76 | } 77 | 78 | public function test_wysiwyg_widget_includes_configuration() 79 | { 80 | $widget = new WysiwygWidget; 81 | $viewData = $widget->getViewData('content', 'test value', [ 82 | 'jodit' => [ 83 | 'toolbar' => ['bold', 'italic'], 84 | 'language' => 'en', 85 | ], 86 | ]); 87 | 88 | $this->assertArrayHasKey('config', $viewData); 89 | $this->assertArrayHasKey('attributes', $viewData['config']); 90 | $this->assertArrayHasKey('data-formello-wysiwyg', $viewData['config']['attributes']); 91 | 92 | $fieldConfig = json_decode($viewData['config']['attributes']['data-formello-wysiwyg'], true); 93 | $this->assertEquals(['bold', 'italic'], $fieldConfig['toolbar']); 94 | $this->assertEquals('en', $fieldConfig['language']); 95 | } 96 | 97 | public function test_wysiwyg_widget_with_empty_configuration() 98 | { 99 | $widget = new WysiwygWidget; 100 | $viewData = $widget->getViewData('content', 'test value', []); 101 | 102 | $this->assertArrayHasKey('config', $viewData); 103 | $this->assertArrayHasKey('attributes', $viewData['config']); 104 | $this->assertArrayHasKey('data-formello-wysiwyg', $viewData['config']['attributes']); 105 | 106 | $fieldConfig = json_decode($viewData['config']['attributes']['data-formello-wysiwyg'], true); 107 | $this->assertEquals([], $fieldConfig); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Console/MakeFormelloCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 21 | } 22 | 23 | public function handle() 24 | { 25 | $model = $this->option('model'); 26 | 27 | if (! $model) { 28 | $this->error('The --model option is required.'); 29 | 30 | return; 31 | } 32 | 33 | $modelClass = $this->qualifyModel($model); 34 | 35 | if (! class_exists($modelClass)) { 36 | $this->error("Model {$modelClass} does not exist."); 37 | 38 | return; 39 | } 40 | 41 | $name = $this->option('name'); 42 | $formName = $name ? $name : class_basename($model).'Form'; 43 | $formPath = app_path("Forms/{$formName}.php"); 44 | 45 | if ($this->files->exists($formPath)) { 46 | $this->error("Form {$formName} already exists!"); 47 | 48 | return; 49 | } 50 | 51 | $this->makeDirectory($formPath); 52 | 53 | // compiling the stub file 54 | $stubPath = __DIR__.'/stubs/formello.stub'; 55 | $stub = $this->files->get($stubPath); 56 | $content = $this->replaceNamespace($stub, $formName); 57 | $content = $this->replaceClass($content, $formName); 58 | $content = $this->replaceModel($content, $model); 59 | $content = $this->replaceFields($content, $modelClass); 60 | $this->files->put($formPath, $content); 61 | 62 | $this->info("Form {$formName} created successfully."); 63 | } 64 | 65 | protected function qualifyModel($model) 66 | { 67 | $model = ltrim($model, '\\/'); 68 | $model = str_replace('/', '\\', $model); 69 | $rootNamespace = $this->laravel->getNamespace(); 70 | 71 | if (Str::startsWith($model, $rootNamespace)) { 72 | return $model; 73 | } 74 | 75 | return is_dir(app_path('Models')) 76 | ? $rootNamespace.'Models\\'.$model 77 | : $rootNamespace.$model; 78 | } 79 | 80 | protected function makeDirectory($path) 81 | { 82 | if (! $this->files->isDirectory(dirname($path))) { 83 | $this->files->makeDirectory(dirname($path), 0777, true, true); 84 | } 85 | } 86 | 87 | protected function replaceNamespace($stub, $name) 88 | { 89 | $stub = str_replace( 90 | ['DummyNamespace', 'DummyRootNamespace'], 91 | [$this->getNamespace($name), $this->rootNamespace()], 92 | $stub 93 | ); 94 | 95 | return $stub; 96 | } 97 | 98 | protected function replaceClass($stub, $name) 99 | { 100 | $class = str_replace($this->getNamespace($name).'\\', '', $name); 101 | $stub = str_replace('DummyClass', $class, $stub); 102 | 103 | return $stub; 104 | } 105 | 106 | protected function replaceModel($stub, $model) 107 | { 108 | $stub = str_replace('DummyModel', Str::plural(strtolower($model)), $stub); 109 | 110 | return $stub; 111 | } 112 | 113 | protected function replaceFields($stub, $modelClass) 114 | { 115 | $model = new $modelClass; 116 | $fillable = $model->getFillable(); 117 | 118 | $fields = ''; 119 | foreach ($fillable as $field) { 120 | $fields .= " '{$field}' => [\n"; 121 | $fields .= " 'label' => __('".Str::title(str_replace('_', ' ', $field))."'),\n"; 122 | $fields .= " ],\n"; 123 | } 124 | 125 | $stub = str_replace('DummyFields', $fields, $stub); 126 | 127 | return $stub; 128 | } 129 | 130 | protected function getNamespace($name) 131 | { 132 | return 'App\\Forms'; 133 | } 134 | 135 | protected function rootNamespace() 136 | { 137 | return $this->laravel->getNamespace(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Unit/Select2WidgetTest.php: -------------------------------------------------------------------------------- 1 | set('formello.css_framework', 'bootstrap5'); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return [ 21 | \Metalogico\Formello\FormelloServiceProvider::class, 22 | ]; 23 | } 24 | 25 | private function makeDummyModel() 26 | { 27 | return new class extends Model { 28 | public function getTable() { return 'dummy'; } 29 | }; 30 | } 31 | 32 | private function makeFormWithSelect2Widget() 33 | { 34 | return new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 35 | protected function fields(): array { 36 | return [ 37 | 'field' => [ 38 | 'widget' => new Select2Widget(), 39 | 'choices' => ['a' => 'A', 'b' => 'B'], 40 | ], 41 | ]; 42 | } 43 | protected function create(): array { return []; } 44 | protected function edit(): array { return []; } 45 | }; 46 | } 47 | 48 | public function test_select2_widget_is_instantiated_and_renders() 49 | { 50 | $form = $this->makeFormWithSelect2Widget(); 51 | $fields = $form->getFields(); 52 | $this->assertArrayHasKey('field', $fields); 53 | $this->assertInstanceOf(Select2Widget::class, $fields['field']['widget']); 54 | $output = $form->renderField('field'); 55 | $this->assertIsString($output); 56 | } 57 | 58 | public function test_select2_does_not_render_multiple_attribute_by_default() 59 | { 60 | $form = new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 61 | protected function fields(): array { 62 | return [ 63 | 'field' => [ 64 | 'widget' => new Select2Widget(), 65 | 'choices' => ['a' => 'A', 'b' => 'B'], 66 | // 'multiple' omitted 67 | ], 68 | ]; 69 | } 70 | protected function create(): array { return []; } 71 | protected function edit(): array { return []; } 72 | }; 73 | 74 | $output = $form->renderField('field'); 75 | // Ensure the HTML boolean attribute 'multiple' is not present 76 | $this->assertStringNotContainsString(' multiple', $output); 77 | } 78 | 79 | public function test_select2_does_not_render_multiple_attribute_when_config_false() 80 | { 81 | $form = new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 82 | protected function fields(): array { 83 | return [ 84 | 'field' => [ 85 | 'widget' => new Select2Widget(), 86 | 'choices' => ['a' => 'A', 'b' => 'B'], 87 | 'multiple' => false, 88 | ], 89 | ]; 90 | } 91 | protected function create(): array { return []; } 92 | protected function edit(): array { return []; } 93 | }; 94 | 95 | $output = $form->renderField('field'); 96 | // Ensure the HTML boolean attribute 'multiple' is not present 97 | $this->assertStringNotContainsString(' multiple', $output); 98 | } 99 | 100 | public function test_select2_renders_multiple_attribute_when_config_true() 101 | { 102 | $form = new class($this->makeDummyModel(), new ViewErrorBag()) extends Formello { 103 | protected function fields(): array { 104 | return [ 105 | 'field' => [ 106 | 'widget' => new Select2Widget(), 107 | 'choices' => ['a' => 'A', 'b' => 'B'], 108 | 'multiple' => true, 109 | ], 110 | ]; 111 | } 112 | protected function create(): array { return []; } 113 | protected function edit(): array { return []; } 114 | }; 115 | 116 | $output = $form->renderField('field'); 117 | // HTML boolean attribute present 118 | $this->assertStringContainsString('multiple', $output); 119 | // Name should be suffixed with [] for multiple selects 120 | $this->assertStringContainsString('name="field[]"', $output); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.2.8] - 2025-09-02 9 | 10 | ### Changed 11 | - Separator widget for dividing sections in your forms. 12 | - config/formello.php removed "default_widgets" and added "custom_widgets" to alias your custom widgets. 13 | 14 | ## [1.2.7] - 2025-09-01 15 | 16 | ### Fixed 17 | - Date and DateTime: error messages now appear below the field and the red highlight works correctly, including with icons/prefix/suffix. 18 | - Select2: fields remain single-select unless the multiple option is enabled. 19 | 20 | ## [1.2.6] - 2025-08-06 21 | 22 | ### Changed 23 | - 'boolean' widget is now 'toggle' 24 | - 'select2' widget now uses a custom data-formello-select2 trigger to avoid collisions 25 | 26 | ## [1.2.5] - 2025-08-01 27 | 28 | ### Changed 29 | - README corrections for the custom widgets section. 30 | 31 | ### Added 32 | - Added --name option to make command to create forms with a custom name. 33 | 34 | ## [1.2.4] - 2025-07-30 35 | 36 | ### Fixed 37 | - Fixed an issue where the Select2 widget would incorrectly get the `multiple` attribute even when not specified in the field configuration. The logic now correctly passes the `multiple` state from the PHP backend to the JavaScript initialization via a `data-multiple` attribute, ensuring the widget behaves as expected. 38 | 39 | ## [1.2.2] - 2025-07-24 40 | 41 | ### Added 42 | - **Select2 Bootstrap 5 Theme**: it's possible to define a theme for select2 using the 'theme' config option. 43 | 44 | 45 | ## [1.2.0] - 2025-07-24 46 | 47 | ### Added 48 | - **WYSIWYG Editor Widget**: New `WysiwygWidget` using Jodit Editor for rich text editing with comprehensive features including tables, images, links, and formatting. 49 | - **Dedicated Mask Widget**: New `MaskWidget` extending `TextWidget` specifically for input masking with IMask.js, providing better separation of concerns. 50 | - **Enhanced Asset Management System**: Completely redesigned asset configuration system allowing users to disable specific widget libraries to prevent conflicts with existing theme assets. 51 | 52 | ### Changed 53 | - **Improved Asset Configuration**: Asset configuration now uses widget names directly (`'wysiwyg' => false`) instead of library names, making it more intuitive and maintainable. 54 | - **TextWidget Simplification**: Removed mask logic from `TextWidget` as it's now handled by the dedicated `MaskWidget`. 55 | - **Streamlined Asset Loading**: Simplified asset registration logic with direct widget-to-config mapping, eliminating hardcoded mappings. 56 | 57 | ### Fixed 58 | - **Asset Configuration Bug**: Fixed issue where setting libraries to `false` in config didn't prevent asset loading. 59 | - **Widget Type Detection**: Improved widget type detection in asset management system for more reliable asset filtering. 60 | 61 | 62 | ## [1.1.0] - 2025-07-22 63 | 64 | ### Added 65 | - **Icon Support for Text Fields**: Added a new `icon` option to the text widget to display an icon inside the input field using Bootstrap's input groups. 66 | - **Input Masking for Text Fields**: Integrated IMask.js to add input masking capabilities. Added a new `mask` option to the text widget to define custom input masks. 67 | - **Flatpickr Date/DateTime Widgets**: Replaced native HTML5 date inputs with Flatpickr for better UX and cross-browser compatibility. 68 | - **Italian Localization**: Added Italian locale support for Flatpickr date/datetime pickers with proper month and day names. 69 | - **Color Picker Widget**: New `ColorWidget` using Pickr nano library for full color selection with preview, opacity, and multiple format support. 70 | - **Color Swatch Widget**: New `ColorSwatchWidget` for predefined color selection from customizable swatches, perfect for brand colors and design systems. 71 | - **Enhanced Asset Management**: Added Pickr library assets (JS/CSS) with automatic publishing via ServiceProvider. 72 | 73 | ### Changed 74 | - **DateTimeWidget Refactoring**: DateTimeWidget now extends DateWidget for better code reuse and consistency. 75 | - **Template Optimization**: Eliminated duplicate datetime.blade.php template by reusing the date template. 76 | - **Improved JavaScript Integration**: Enhanced formello.js with proper Pickr initialization and event handling. 77 | 78 | 79 | 80 | ## [1.0.0] - 2025-07-21 81 | 82 | ### Added 83 | 84 | - Initial release of Laravel Formello. 85 | - Automatic form generation from Eloquent models. 86 | - Declarative form definition using simple PHP classes. 87 | - Built-in support for Bootstrap 5 CSS framework. 88 | - A comprehensive set of widgets: `Text`, `Textarea`, `Select`, `Select2`, `Radio`, `Checkboxes`, `Toggle`, `Range`, `Date`, `DateTime`, `Upload`, and `Hidden`. 89 | - Automatic rendering of form fields, labels, help text, and validation errors. 90 | - `isCreating()` and `isEditing()` methods for conditional logic within form classes. 91 | - Support for custom, user-defined widgets. 92 | - Artisan command `php artisan make:formello` to quickly scaffold form classes from models. 93 | - Publishable configuration file for easy customization. 94 | - Publishable views for full control over the rendered HTML. 95 | - Unit and Feature tests to ensure reliability. 96 | 97 | ### Fixed 98 | 99 | - Correctly detect form mode (`create` vs `edit`) based on the model's existence in the database (`$model->exists`). 100 | -------------------------------------------------------------------------------- /resources/assets/js/formello.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function () { 2 | 3 | // IMask initialization 4 | const maskElements = document.querySelectorAll('[data-formello-mask]'); 5 | maskElements.forEach(function (el) { 6 | try { 7 | const maskOptions = JSON.parse(el.getAttribute('data-formello-mask')); 8 | IMask(el, maskOptions); 9 | } catch (e) { 10 | console.error('Error parsing Formello mask options:', e); 11 | } 12 | }); 13 | 14 | // Flatpickr initialization 15 | const datepickerElements = document.querySelectorAll('[data-formello-datepicker]'); 16 | datepickerElements.forEach(function (el) { 17 | try { 18 | const flatpickrOptions = JSON.parse(el.getAttribute('data-formello-datepicker')); 19 | flatpickr(el, flatpickrOptions); 20 | } catch (e) { 21 | console.error('Error parsing Formello datepicker options:', e); 22 | } 23 | }); 24 | 25 | // Pickr color picker initialization 26 | const colorpickerElements = document.querySelectorAll('[data-formello-colorpicker]'); 27 | colorpickerElements.forEach(function (el) { 28 | try { 29 | const pickrOptions = JSON.parse(el.getAttribute('data-formello-colorpicker')); 30 | 31 | // Create Pickr instance 32 | const pickr = Pickr.create({ 33 | el: el, 34 | ...pickrOptions 35 | }); 36 | 37 | // Update input value when color changes 38 | pickr.on('change', (color) => { 39 | el.value = color.toHEXA().toString(); 40 | // Trigger change event for form validation 41 | el.dispatchEvent(new Event('change', { bubbles: true })); 42 | }); 43 | 44 | // Handle swatch clicks for swatches-only mode 45 | pickr.on('swatchselect', (color) => { 46 | const hexColor = color.toHEXA().toString(); 47 | el.value = hexColor; 48 | 49 | // Update the visual representation 50 | pickr.setColor(hexColor); 51 | 52 | // Trigger change event for form validation 53 | el.dispatchEvent(new Event('change', { bubbles: true })); 54 | 55 | // Close the picker automatically for swatches 56 | pickr.hide(); 57 | }); 58 | 59 | } catch (e) { 60 | console.error('Error parsing Formello colorpicker options:', e); 61 | } 62 | }); 63 | 64 | // Jodit WYSIWYG initialization 65 | const wysiwygElements = document.querySelectorAll('[data-formello-wysiwyg]'); 66 | if (wysiwygElements.length > 0 && typeof Jodit !== 'undefined') { 67 | wysiwygElements.forEach(function (el) { 68 | try { 69 | const joditOptions = JSON.parse(el.getAttribute('data-formello-wysiwyg')); 70 | 71 | // Default configuration - simple and stable 72 | const defaultConfig = { 73 | minHeight: 300, 74 | maxHeight: 300, 75 | iframe: true, 76 | toolbarSticky: false, 77 | showCharsCounter: false, 78 | showWordsCounter: false, 79 | showXPathInStatusbar: false, 80 | buttons: [ 81 | 'bold', 82 | 'italic', 83 | 'underline', 84 | '|', 85 | 'ul', 86 | 'ol', 87 | '|', 88 | 'fontsize', 89 | 'brush', 90 | '|', 91 | 'left', 92 | 'center', 93 | 'right', 94 | '|', 95 | 'undo', 96 | 'redo', 97 | '|', 98 | 'hr', 99 | '|', 100 | 'fullsize' 101 | ], 102 | removeButtons: ['about', 'print'], 103 | }; 104 | 105 | // Merge user options with defaults 106 | const finalConfig = { ...defaultConfig, ...joditOptions }; 107 | 108 | // Initialize Jodit 109 | const editor = new Jodit(el, finalConfig); 110 | 111 | // Update textarea value when content changes 112 | editor.events.on('change', function () { 113 | // Trigger change event for form validation 114 | el.dispatchEvent(new Event('change', { bubbles: true })); 115 | }); 116 | 117 | } catch (e) { 118 | console.error('Error parsing Formello WYSIWYG options:', e); 119 | } 120 | }); 121 | } else if (wysiwygElements.length > 0) { 122 | console.warn('Jodit not loaded but WYSIWYG elements found. Make sure to include Jodit script before formello.js'); 123 | } 124 | 125 | // Select2 initialization 126 | const select2Elements = document.querySelectorAll('[data-formello-select2]'); 127 | if (typeof $ !== 'undefined' && $.fn.select2) { 128 | select2Elements.forEach(function (el) { 129 | try { 130 | 131 | $(el).select2({ 132 | theme: 'bootstrap-5', 133 | }); 134 | 135 | } catch (e) { 136 | console.error('Error initializing Formello Select2:', e); 137 | } 138 | }); 139 | } else if (select2Elements.length > 0) { 140 | console.warn('Select2 not loaded but Select2 elements found. Make sure to include Select2 script and jQuery before formello.js'); 141 | } 142 | }); 143 | -------------------------------------------------------------------------------- /resources/views/widgets/bootstrap5/select2.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if (isset($label)) 3 | 4 | @endif 5 | 6 |
7 | 8 | 9 | 10 |
11 | 42 |
43 |
44 | 45 | @if (!empty(data_get($config, 'select2.depends_on'))) 46 | 118 | @endif 119 | 120 | @if (isset($config['help'])) 121 |
{!! $config['help'] !!}
122 | @endif 123 | 124 | @if ($errors) 125 |
126 |
    127 | @foreach ($errors as $error) 128 |
  • {{ $error }}
  • 129 | @endforeach 130 |
131 |
132 | @endif 133 |
134 | 135 | -------------------------------------------------------------------------------- /resources/assets/css/nano.min.css: -------------------------------------------------------------------------------- 1 | /*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ 2 | .pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;border-radius:.15em;background:url("data:image/svg+xml;utf8, ") no-repeat center;background-size:0;transition:all .3s}.pickr .pcr-button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pickr .pcr-button::before{z-index:initial}.pickr .pcr-button::after{position:absolute;content:"";top:0;left:0;height:100%;width:100%;transition:background .3s;background:var(--pcr-color);border-radius:.15em}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear::before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-button.disabled{cursor:not-allowed}.pickr *,.pcr-app *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr input:focus,.pickr input.pcr-active,.pickr button:focus,.pickr button.pcr-active,.pcr-app input:focus,.pcr-app input.pcr-active,.pcr-app button:focus,.pcr-app button.pcr-active{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-palette,.pickr .pcr-slider,.pcr-app .pcr-palette,.pcr-app .pcr-slider{transition:box-shadow .3s}.pickr .pcr-palette:focus,.pickr .pcr-slider:focus,.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(0,0,0,.25)}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);left:0;top:0}.pcr-app.visible{transition:opacity .3s;visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports(display: grid){.pcr-app .pcr-swatches{display:grid;align-items:center;grid-template-columns:repeat(auto-fit, 1.75em)}}.pcr-app .pcr-swatches>button{font-size:1em;position:relative;width:calc(1.75em - 5px);height:calc(1.75em - 5px);border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:rgba(0,0,0,0);z-index:1}.pcr-app .pcr-swatches>button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button::after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:var(--pcr-color);border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{filter:brightness(1.05)}.pcr-app .pcr-swatches>button:not(.pcr-active){box-shadow:none}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -0.2em 0 -0.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{filter:brightness(0.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::-moz-selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff;width:auto}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff}.pcr-app .pcr-interaction .pcr-save:hover,.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover{filter:brightness(0.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-cancel{background:#f44250}.pcr-app .pcr-interaction .pcr-clear:focus,.pcr-app .pcr-interaction .pcr-cancel:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pcr-app .pcr-selection .pcr-color-palette,.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity{position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-palette:active,.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active{cursor:grabbing;cursor:-webkit-grabbing}.pcr-app[data-theme=nano]{width:14.25em;max-width:95vw}.pcr-app[data-theme=nano] .pcr-swatches{margin-top:.6em;padding:0 .6em}.pcr-app[data-theme=nano] .pcr-interaction{padding:0 .6em .6em .6em}.pcr-app[data-theme=nano] .pcr-selection{display:grid;grid-gap:.6em;grid-template-columns:1fr 4fr;grid-template-rows:5fr auto auto;align-items:center;height:10.5em;width:100%;align-self:flex-start}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview{grid-area:2/1/4/1;height:100%;width:100%;display:flex;flex-direction:row;justify-content:center;margin-left:.6em}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-last-color{display:none}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-current-color{position:relative;background:var(--pcr-color);width:2em;height:2em;border-radius:50em;overflow:hidden}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-current-color::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette{grid-area:1/1/2/3;width:100%;height:100%;z-index:1}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette .pcr-palette{border-radius:.15em;width:100%;height:100%}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette .pcr-palette::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser{grid-area:2/2/2/2}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity{grid-area:3/2/3/2}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser,.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity{height:.5em;margin:0 .6em}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-picker{top:50%;transform:translateY(-50%)}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-slider{flex-grow:1;border-radius:50em}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(to right, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%))}.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(to right, transparent, black),url("data:image/svg+xml;utf8, ");background-size:100%,.25em} 3 | -------------------------------------------------------------------------------- /src/Formello.php: -------------------------------------------------------------------------------- 1 | model = new $model; 44 | } else { 45 | $this->model = $model; 46 | } 47 | $this->errors = $errors ?? session()->get('errors', new ViewErrorBag); 48 | $this->widgetFactory = $widgetFactory ?? new WidgetFactory; 49 | $this->schemaInspector = $schemaInspector ?? new SchemaInspector; 50 | 51 | // Set the form mode based on the model's existence 52 | if ($this->model->exists) { 53 | $this->setFormMode('edit'); 54 | } else { 55 | $this->setFormMode('create'); 56 | } 57 | 58 | $this->initializeFields(); 59 | $this->initializeForm(); 60 | } 61 | 62 | abstract protected function fields(): array; 63 | 64 | abstract protected function create(): array; 65 | 66 | abstract protected function edit(): array; 67 | 68 | /** 69 | * Returns the model instance associated with the form. 70 | */ 71 | public function getModel(): Model 72 | { 73 | return $this->model; 74 | } 75 | 76 | /** 77 | * Initialize the form 78 | */ 79 | protected function initializeForm() 80 | { 81 | if (method_exists($this, 'create') && ! $this->model->exists) { 82 | $this->formConfig = $this->create(); 83 | } elseif (method_exists($this, 'edit') && $this->model->exists) { 84 | $this->formConfig = $this->edit(); 85 | } else { 86 | throw new \RuntimeException('No form configuration method found.'); 87 | } 88 | 89 | // if there's an upload widget in the form add the multipart form attribute 90 | if ($this->hasUploadWidget()) { 91 | if (! isset($this->formConfig['attributes'])) { 92 | $this->formConfig['attributes'] = []; 93 | } 94 | $this->formConfig['attributes']['enctype'] = 'multipart/form-data'; 95 | } 96 | } 97 | 98 | protected function hasUploadWidget(): bool 99 | { 100 | foreach ($this->fields as $field) { 101 | if ($field['widget'] instanceof UploadWidget || $field['widget'] == 'upload') { 102 | return true; 103 | } 104 | } 105 | 106 | return false; 107 | } 108 | 109 | /** 110 | * Initialize the fields 111 | */ 112 | protected function initializeFields(): void 113 | { 114 | $definedFields = $this->fields(); 115 | 116 | foreach ($definedFields as $name => $fieldConfig) { 117 | 118 | $widget = $this->resolveWidget($fieldConfig, $name); 119 | $this->fields[$name] = [ 120 | 'widget' => $widget, 121 | 'config' => $fieldConfig, 122 | ]; 123 | 124 | // Register assets for this widget 125 | $this->registerWidgetAssets($widget, $fieldConfig); 126 | } 127 | } 128 | 129 | protected function resolveWidget(array $fieldConfig, string $fieldName): WidgetInterface 130 | { 131 | // Se widget specificato esplicitamente 132 | if (isset($fieldConfig['widget'])) { 133 | // Se è un alias (stringa breve, es: 'text', 'select2', ecc.) 134 | if (is_string($fieldConfig['widget'])) { 135 | // Usa la factory per risolvere l'alias 136 | return $this->widgetFactory->make($fieldConfig['widget']); 137 | } 138 | // Se è una classe completa 139 | if (is_string($fieldConfig['widget']) && class_exists($fieldConfig['widget'])) { 140 | return new $fieldConfig['widget']; 141 | } 142 | // Se è già un oggetto widget 143 | if ($fieldConfig['widget'] instanceof WidgetInterface) { 144 | return $fieldConfig['widget']; 145 | } 146 | // Se arriva qui, il valore non è valido 147 | throw new \InvalidArgumentException("Invalid widget definition for field '$fieldName'"); 148 | } 149 | 150 | // Auto-detect dal database schema 151 | $columnType = $this->schemaInspector->getColumnType($this->model, $fieldName); 152 | 153 | return $this->widgetFactory->make($columnType); 154 | } 155 | 156 | protected function getDefaultFields() 157 | { 158 | $defaults = []; 159 | foreach ($this->fields() as $field => $config) { 160 | $defaults[$field] = $this->getDefaultWidgetForField($field); 161 | } 162 | 163 | return $defaults; 164 | } 165 | 166 | /** 167 | * Map database field types to default widgets 168 | */ 169 | protected function getDefaultWidgetForField($field) 170 | { 171 | // checks if the column exists and gets its type 172 | $schema = $this->model->getConnection()->getSchemaBuilder(); 173 | if ($schema->hasColumn($this->model->getTable(), $field)) { 174 | $columnType = $schema->getColumnType($this->model->getTable(), $field); 175 | } else { 176 | $columnType = 'text'; 177 | } 178 | 179 | switch ($columnType) { 180 | case 'char': 181 | case 'varchar': 182 | case 'text': 183 | return new Widgets\TextWidget; 184 | case 'textarea': 185 | return new Widgets\TextareaWidget; 186 | case 'boolean': 187 | case 'tinyint': 188 | return new Widgets\ToggleWidget; 189 | case 'date': 190 | return new Widgets\DateWidget; 191 | case 'datetime': 192 | case 'timestamp': 193 | return new Widgets\DateTimeWidget; 194 | default: 195 | return new Widgets\TextWidget; 196 | } 197 | } 198 | 199 | public function renderForm() 200 | { 201 | return view('formello::form', [ 202 | 'formello' => $this, 203 | 'formConfig' => $this->formConfig, 204 | ])->render(); 205 | } 206 | 207 | public function render() 208 | { 209 | return view('formello::form', [ 210 | 'formello' => $this, 211 | 'formConfig' => $this->formConfig, 212 | ])->render(); 213 | } 214 | 215 | public function renderField(string $name): string 216 | { 217 | if (! isset($this->fields[$name])) { 218 | throw new \InvalidArgumentException("Field '{$name}' not found"); 219 | } 220 | 221 | $fieldConfig = $this->fields[$name]; 222 | $widget = $fieldConfig['widget']; 223 | $config = $fieldConfig['config']; 224 | 225 | $value = old($name, $config['value'] ?? $this->model->{$name} ?? null); 226 | $errors = $this->errors->get($name); 227 | 228 | return $widget->render($name, $value, $config, $errors); 229 | } 230 | 231 | public function getCssFramework() 232 | { 233 | return config('formello.css_framework', 'bootstrap5'); 234 | } 235 | 236 | public function getFields() 237 | { 238 | return $this->fields; 239 | } 240 | 241 | public function isCreating(): bool 242 | { 243 | return $this->formMode === 'create'; 244 | } 245 | 246 | public function isEditing(): bool 247 | { 248 | return $this->formMode === 'edit'; 249 | } 250 | 251 | public function setFormMode(string $mode): self 252 | { 253 | $this->formMode = $mode; 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * Register assets for a widget 260 | */ 261 | protected function registerWidgetAssets(WidgetInterface $widget, array $fieldConfig): void 262 | { 263 | $type = $widget->getWidgetName(); 264 | $assetConfig = config('formello.assets', []); 265 | 266 | // Check if assets are enabled for this widget type 267 | if (! ($assetConfig[$type] ?? true)) { 268 | return; 269 | } 270 | 271 | // Get assets from widget, passing field configuration for conditional assets 272 | $assets = $widget->getAssets($fieldConfig); 273 | 274 | if ($assets) { 275 | $this->registerAssets($assets); 276 | } 277 | } 278 | 279 | /** 280 | * Register an array of assets 281 | */ 282 | protected function registerAssets(array $assets): void 283 | { 284 | if (isset($assets['scripts'])) { 285 | foreach ($assets['scripts'] as $script) { 286 | AssetManager::addScript($script); 287 | } 288 | } 289 | 290 | if (isset($assets['styles'])) { 291 | foreach ($assets['styles'] as $style) { 292 | AssetManager::addStyle($style); 293 | } 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/user-attachments/assets/5f54ca42-1aa9-44a5-92b0-79862d4f3f27) 2 | 3 | # Formello 4 | 5 | A Laravel package for generating Bootstrap 5 forms based on models. Laravel 9+ 6 | 7 | Formello is a comprehensive form generation and handling tool for Laravel applications, inspired by Django forms. 8 | 9 | SCR-20250730-kida 10 | 11 | ## 🎉 Motivation 12 | 13 | The Laravel ecosystem offers powerful tools for building applications, from full-featured admin panels like Nova and Filament to complex form-handling libraries. However, I felt there was a need for a tool that sits in the "sweet spot" between these solutions. 14 | 15 | Formello was created for developers who need to generate forms quickly without the overhead of a complete admin panel, but who also want a simpler, more intuitive API than more complex form libraries. It's designed to automate the repetitive aspects of form creation while giving you full control over the final output. 16 | 17 | Currently, Formello ships with built-in support for **Bootstrap 5**, and support for **Tailwind CSS** is coming soon™! 18 | 19 | If you use this project, please consider giving it a ⭐. 20 | 21 | ## ✨ Features 22 | 23 | - Easy form definition using Laravel classes 24 | - Automatic form rendering 25 | - Support for various field types: 26 | - Text 27 | - Textarea 28 | - Select (with multiple) 29 | - Select2 30 | - Radio 31 | - Checkboxes 32 | - Toggle 33 | - Range 34 | - Date 35 | - DateTime 36 | - Upload 37 | - Hidden 38 | - Customizable widgets 39 | - Automatic error handling and display 40 | - Form validation integration 41 | 42 | ## 🛠️ How to install 43 | 44 | 1. Install the package via Composer: 45 | 46 | ```bash 47 | composer require metalogico/laravel-formello 48 | ``` 49 | 50 | 2. Publish the assets: 51 | 52 | ```bash 53 | php artisan vendor:publish --tag=formello-assets 54 | ``` 55 | 56 | 3. (Optional) Auto-publish assets on update 57 | 58 | To ensure that Formello's assets are automatically updated every time you run `composer update`, you can add a command to the `post-update-cmd` script in your project's `composer.json` file. 59 | 60 | ```json 61 | "scripts": { 62 | "post-update-cmd": [ 63 | "@php artisan vendor:publish --tag=formello-assets --force" 64 | ] 65 | } 66 | ``` 67 | 68 | This will overwrite the existing assets with the latest ones from the package. 69 | 70 | 71 | ## 😎 How to use 72 | 73 | Creating a Form 74 | Create a new form class that extends `Metalogico\Formello\Formello`. 75 | 76 | Here's a simple example for a product form. 77 | 78 | ```php 79 | 'POST', 92 | 'action' => route('products.store'), 93 | ]; 94 | } 95 | 96 | protected function edit(): array 97 | { 98 | return [ 99 | 'method' => 'POST', 100 | 'action' => route('products.update', $this->model->id), 101 | ]; 102 | } 103 | 104 | protected function fields(): array 105 | { 106 | return [ 107 | 'name' => [ 108 | 'label' => __('Product Name'), 109 | 'help' => 'Enter the name of the product', 110 | ], 111 | 'description' => [ 112 | 'label' => __('Description'), 113 | ], 114 | 'category_id' => [ 115 | 'label' => __('Category'), 116 | 'widget' => 'select', 117 | 'choices' => Category::pluck('name', 'id')->toArray(); 118 | ], 119 | 'in_stock' => [ 120 | 'label' => __('In Stock'), 121 | ], 122 | ]; 123 | } 124 | } 125 | ``` 126 | 127 | Remember to add these fields to your model's `$fillable` array otherwise Formello will not render them. 128 | 129 | ```php 130 | 131 | class Product extends Model 132 | { 133 | // ... 134 | protected $fillable = [ 135 | 'name', 136 | 'category_id', 137 | 'description', 138 | 'in_stock', 139 | ]; 140 | 141 | } 142 | ``` 143 | 144 | ## Using the provided artisan command 145 | 146 | You can generate a basic formello file using this command: 147 | 148 | ```bash 149 | php artisan make:formello --model=Product 150 | ``` 151 | 152 | The script will generate a skeleton file that contains a basic field definition for each fillable field found in your model. 153 | 154 | 155 | ## Rendering the Form 156 | 157 | In your controller for an empty form (create action): 158 | 159 | ```php 160 | public function create() 161 | { 162 | // create the form 163 | $formello = new ProductForm(Product::class); 164 | // pass it to the view 165 | return view('products.create', [ 166 | 'formello' => $formello 167 | ]); 168 | } 169 | ``` 170 | 171 | or, for an edit form: 172 | 173 | ```php 174 | public function edit(Product $product) 175 | { 176 | // pass the model to the form 177 | $formello = new ProductForm($product); 178 | // pass it to the view 179 | return view('products.edit', [ 180 | 'formello' => $formello 181 | ]); 182 | } 183 | ``` 184 | 185 | Then in you blade template: 186 | 187 | ```php 188 | {!! $formello->render() !!} 189 | ``` 190 | 191 | If you want to render only the fields (without the \ tag) you can use: 192 | 193 | ```php 194 | @foreach ($formello->getFields() as $name => $field) 195 | {!! $formello->renderField($name) !!} 196 | @endforeach 197 | ``` 198 | 199 | ## Conditional Logic 200 | 201 | You can use `isCreating()` and `isEditing()` methods in your form class to dynamically change fields, labels, rules, or other options based on the form's mode. 202 | 203 | Here's an example of how to use these methods to change a field's behavior: 204 | 205 | ```php 206 | protected function fields(): array 207 | { 208 | $fields = [ 209 | 'name' => [ 210 | 'label' => __('User Name'), 211 | 'help' => 'Enter the name of the user', 212 | ], 213 | 'password' => [ 214 | 'label' => __('Password'), 215 | 'type' => 'password', 216 | 'required' => $this->isCreating(), 217 | 'help' => $this->isEditing() ? 'Leave the field empty to keep the current password' : '', 218 | ], 219 | ]; 220 | 221 | return $fields; 222 | } 223 | ``` 224 | 225 | ## Creating Custom Widgets 226 | 227 | Formello is designed to be extensible, allowing you to create your own custom widgets. This is useful when you need a specific form control that isn't included in the default set. 228 | 229 | To create a custom widget, you need to follow these steps: 230 | 231 | ### First of all, the config file! 232 | 233 | You need to have the config file to add the widget in the Formello list of available widgets. 234 | 235 | ```bash 236 | php artisan vendor:publish --tag=formello-config 237 | ``` 238 | In this file you will see a list of all the available widgets. 239 | Add your widget to the list with a name, for example: `star-rating`. 240 | 241 | 242 | ### 1. Create a Widget Class 243 | 244 | Then, create a new PHP class for your widget. This class must extend the `Metalogico\Formello\Widgets\BaseWidget` class. 245 | 246 | You can place this class anywhere in your project, for example, in `app/Widgets`. 247 | 248 | To maintain a clean separation of concerns, the `getViewData` method should return an array of data that will be used to render the widget. 249 | 250 | To set the template you can use the `getTemplate` method. 251 | 252 | Here is an example of a `StarRatingWidget` class: 253 | 254 | ```php 255 | $name, 267 | 'value' => $value, 268 | 'config' => $config, 269 | 'errors' => $errors, 270 | ]; 271 | } 272 | 273 | public function getTemplate(): string 274 | { 275 | return "widgets.star-rating"; 276 | } 277 | } 278 | ``` 279 | 280 | ### 2. Create the Widget's Blade Template 281 | 282 | Next, create the Blade template that will render the widget's HTML. For instance, you can create the file `resources/views/widgets/star-rating.blade.php`: 283 | 284 | ```php 285 |
286 | 287 | 294 | 295 | @if ($errors) 296 |
297 |
    298 | @foreach ($errors as $error) 299 |
  • {{ $error }}
  • 300 | @endforeach 301 |
302 |
303 | @endif 304 |
305 | ``` 306 | 307 | ### 3. Use the Custom Widget in Your Form 308 | 309 | Once you have created your widget class, you can use it in your Formello form by specifying the fully qualified class name in the `widget` option for a field. 310 | 311 | ```php 312 | [ 327 | 'label' => __('Product Rating'), 328 | 'widget' => 'star-rating', 329 | ], 330 | ]; 331 | } 332 | } 333 | ``` 334 | 335 | Formello will automatically instantiate your widget class and call its `render` method to generate the HTML for the form field. 336 | 337 | ## 🎨 Asset Management - Modular System 338 | 339 | Formello uses a modular and flexible system for loading JavaScript and CSS assets, avoiding conflicts with themes that already include the same libraries. 340 | 341 | ### How It Works 342 | 343 | #### 1. Widget-Based Asset Management 344 | 345 | Each widget defines its own assets through the `getAssets()` method (optional): 346 | 347 | ```php 348 | public function getAssets(?array $fieldConfig = null): ?array 349 | { 350 | return [ 351 | 'scripts' => ['flatpickr.min.js'], 352 | 'styles' => ['flatpickr.min.css'], 353 | ]; 354 | } 355 | ``` 356 | 357 | #### 2. Blade Directives 358 | 359 | Use the new blade directives in your layout: 360 | 361 | ```blade 362 | 363 | 364 | 365 | 366 | Your App 367 | 368 | 369 | 370 | 371 | 372 | @formelloStyles 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | @formelloScripts 382 | 383 | 384 | ``` 385 | 386 | #### 3. Custom Widgets 387 | 388 | Custom widgets can optionally implement `getAssets()`: 389 | 390 | ```php 391 | class CustomWidget extends BaseWidget 392 | { 393 | // The getAssets() method is OPTIONAL 394 | public function getAssets(?array $fieldConfig = null): ?array 395 | { 396 | return [ 397 | 'scripts' => ['my-custom-lib.js'], 398 | 'styles' => ['my-custom-styles.css'], 399 | ]; 400 | } 401 | 402 | // If you don't implement getAssets(), the widget still works 403 | } 404 | ``` 405 | 406 | ### Supported Libraries 407 | 408 | | Library | Widgets that use it | Assets loaded | 409 | |----------|-------------------|----------------| 410 | | `select2` | SelectWidget, Select2Widget | select2.min.js, select2.min.css, select2-bootstrap-5-theme.min.css | 411 | | `flatpickr` | DateWidget, DateTimeWidget | flatpickr.min.js, l10n/it.js, flatpickr.min.css | 412 | | `imask` | TextWidget (with mask) | imask.min.js | 413 | | `pickr` | ColorWidget, ColorSwatchWidget | pickr.min.js, nano.min.css | 414 | | `jodit` | WysiwygWidget | jodit.min.js, jodit.min.css | 415 | 416 | 417 | ## ⚖️ License 418 | 419 | Laravel Formello is open-sourced software licensed under the [MIT license](LICENSE.md). 420 | 421 | 422 | ## 🍺 Donations 423 | If you really like this project and you want to help me please consider [buying me a beer 🍺](https://www.buymeacoffee.com/metalogico 424 | ) 425 | -------------------------------------------------------------------------------- /resources/assets/css/flatpickr.min.css: -------------------------------------------------------------------------------- 1 | .flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/* 2 | /*rtl:begin:ignore*/left:0/* 3 | /*rtl:end:ignore*/}/* 4 | /*rtl:begin:ignore*/ 5 | /* 6 | /*rtl:end:ignore*/ 7 | .flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{/* 8 | /*rtl:begin:ignore*/right:0/* 9 | /*rtl:end:ignore*/}/* 10 | /*rtl:begin:ignore*/ 11 | /* 12 | /*rtl:end:ignore*/ 13 | .flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover{color:#959ea9}.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:#f64747}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,0.15);-webkit-box-sizing:border-box;box-sizing:border-box}.numInputWrapper span:hover{background:rgba(0,0,0,0.1)}.numInputWrapper span:active{background:rgba(0,0,0,0.2)}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,0.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,0.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:rgba(0,0,0,0.5)}.numInputWrapper:hover{background:rgba(0,0,0,0.05)}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{font-size:135%;line-height:inherit;font-weight:300;color:inherit;position:absolute;width:75%;left:12.5%;padding:7.48px 0 0 0;line-height:1;height:34px;display:inline-block;text-align:center;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:rgba(0,0,0,0.05)}.flatpickr-current-month .numInputWrapper{width:6ch;width:7ch\0;display:inline-block}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:rgba(0,0,0,0.9)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:rgba(0,0,0,0.9)}.flatpickr-current-month input.cur-year{background:transparent;-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;cursor:text;padding:0 0 0 .5ch;margin:0;display:inline-block;font-size:inherit;font-family:inherit;font-weight:300;line-height:inherit;height:auto;border:0;border-radius:0;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{font-size:100%;color:rgba(0,0,0,0.5);background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;background:transparent;border:none;border-radius:0;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;font-family:inherit;font-weight:300;height:auto;line-height:inherit;margin:-1px 0 0 0;outline:none;padding:0 0 0 .5ch;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:rgba(0,0,0,0.05)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{cursor:default;font-size:90%;background:transparent;color:rgba(0,0,0,0.54);line-height:1;margin:0;text-align:center;display:block;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;font-weight:bolder}.dayContainer,.flatpickr-weeks{padding:1px 0 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;width:307.875px}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:307.875px;min-width:307.875px;max-width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;display:inline-block;display:-ms-flexbox;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-wrap:wrap;-ms-flex-pack:justify;-webkit-justify-content:space-around;justify-content:space-around;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}.dayContainer + .dayContainer{-webkit-box-shadow:-1px 0 0 #e6e6e6;box-shadow:-1px 0 0 #e6e6e6}.flatpickr-day{background:none;border:1px solid transparent;border-radius:150px;-webkit-box-sizing:border-box;box-sizing:border-box;color:#393939;cursor:pointer;font-weight:400;width:14.2857143%;-webkit-flex-basis:14.2857143%;-ms-flex-preferred-size:14.2857143%;flex-basis:14.2857143%;max-width:39px;height:39px;line-height:39px;margin:0;display:inline-block;position:relative;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:#e6e6e6;border-color:#e6e6e6}.flatpickr-day.today{border-color:#959ea9}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:#959ea9;background:#959ea9;color:#fff}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:#569ff7;-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:#569ff7}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange + .endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange + .endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 #569ff7;box-shadow:-10px 0 0 #569ff7}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;-webkit-box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-5px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:rgba(57,57,57,0.3);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:rgba(57,57,57,0.1)}.flatpickr-day.week.selected{border-radius:0;-webkit-box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7;box-shadow:-5px 0 0 #569ff7,5px 0 0 #569ff7}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 #e6e6e6;box-shadow:1px 0 0 #e6e6e6}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:rgba(57,57,57,0.3);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:block;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.flatpickr-rContainer{display:inline-block;padding:0;-webkit-box-sizing:border-box;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:#393939}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:#393939}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;-webkit-box-shadow:none;box-shadow:none;border:0;border-radius:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:#393939;font-size:14px;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:bold}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:#393939;font-weight:bold;width:2%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:#eee}.flatpickr-input[readonly]{cursor:pointer}@-webkit-keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes fpFadeInDown{from{opacity:0;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:1;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}} -------------------------------------------------------------------------------- /resources/assets/css/select2.min.css: -------------------------------------------------------------------------------- 1 | .select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline;list-style:none;padding:0}.select2-container .select2-selection--multiple .select2-selection__clear{background-color:transparent;border:none;font-size:1em}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;margin-left:5px;padding:0;max-width:100%;resize:none;height:18px;vertical-align:bottom;font-family:sans-serif;overflow:hidden;word-break:keep-all}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option--selectable{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px;padding-right:0px}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;padding-bottom:5px;padding-right:5px;position:relative}.select2-container--default .select2-selection--multiple.select2-selection--clearable{padding-right:25px}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;font-weight:bold;height:20px;margin-right:10px;margin-top:5px;position:absolute;right:0;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:inline-block;margin-left:5px;margin-top:5px;padding:0;padding-left:20px;position:relative;max-width:100%;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap}.select2-container--default .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-right:1px solid #aaa;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#999;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px;position:absolute;left:0;top:0}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus{background-color:#f1f1f1;color:#333;outline:none}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-left:1px solid #aaa;border-right:none;border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__clear{float:left;margin-left:10px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--group{padding:0}.select2-container--default .select2-results__option--disabled{color:#999}.select2-container--default .select2-results__option--selected{background-color:#ddd}.select2-container--default .select2-results__option--highlighted.select2-results__option--selectable{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;height:26px;margin-right:20px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;padding-bottom:5px;padding-right:5px}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;display:inline-block;margin-left:5px;margin-top:5px;padding:0}.select2-container--classic .select2-selection--multiple .select2-selection__choice__display{cursor:default;padding-left:2px;padding-right:5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{background-color:transparent;border:none;border-top-left-radius:4px;border-bottom-left-radius:4px;color:#888;cursor:pointer;font-size:1em;font-weight:bold;padding:0 4px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;outline:none}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__display{padding-left:5px;padding-right:2px}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{border-top-left-radius:0;border-bottom-left-radius:0;border-top-right-radius:4px;border-bottom-right-radius:4px}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option--group{padding:0}.select2-container--classic .select2-results__option--disabled{color:grey}.select2-container--classic .select2-results__option--highlighted.select2-results__option--selectable{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} 2 | -------------------------------------------------------------------------------- /resources/assets/js/pickr.min.js: -------------------------------------------------------------------------------- 1 | /*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ 2 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Pickr=e():t.Pickr=e()}(self,(()=>(()=>{"use strict";var t={d:(e,o)=>{for(var n in o)t.o(o,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:o[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.d(e,{default:()=>E});var o={};function n(t,e,o,n,i={}){e instanceof HTMLCollection||e instanceof NodeList?e=Array.from(e):Array.isArray(e)||(e=[e]),Array.isArray(o)||(o=[o]);for(const s of e)for(const e of o)s[t](e,n,{capture:!1,...i});return Array.prototype.slice.call(arguments,1)}t.r(o),t.d(o,{adjustableInputNumbers:()=>p,createElementFromString:()=>r,createFromTemplate:()=>a,eventPath:()=>l,off:()=>s,on:()=>i,resolveElement:()=>c});const i=n.bind(null,"addEventListener"),s=n.bind(null,"removeEventListener");function r(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstElementChild}function a(t){const e=(t,e)=>{const o=t.getAttribute(e);return t.removeAttribute(e),o},o=(t,n={})=>{const i=e(t,":obj"),s=e(t,":ref"),r=i?n[i]={}:n;s&&(n[s]=t);for(const n of Array.from(t.children)){const t=e(n,":arr"),i=o(n,t?{}:r);t&&(r[t]||(r[t]=[])).push(Object.keys(i).length?i:n)}return n};return o(r(t))}function l(t){let e=t.path||t.composedPath&&t.composedPath();if(e)return e;let o=t.target.parentElement;for(e=[t.target,o];o=o.parentElement;)e.push(o);return e.push(document,window),e}function c(t){return t instanceof Element?t:"string"==typeof t?t.split(/>>/g).reduce(((t,e,o,n)=>(t=t.querySelector(e),ot)){function o(o){const n=[.001,.01,.1][Number(o.shiftKey||2*o.ctrlKey)]*(o.deltaY<0?1:-1);let i=0,s=t.selectionStart;t.value=t.value.replace(/[\d.]+/g,((t,o)=>o<=s&&o+t.length>=s?(s=o,e(Number(t),n,i)):(i++,t))),t.focus(),t.setSelectionRange(s,s),o.preventDefault(),t.dispatchEvent(new Event("input"))}i(t,"focus",(()=>i(window,"wheel",o,{passive:!1}))),i(t,"blur",(()=>s(window,"wheel",o)))}const{min:u,max:h,floor:d,round:m}=Math;function f(t,e,o){e/=100,o/=100;const n=d(t=t/360*6),i=t-n,s=o*(1-e),r=o*(1-i*e),a=o*(1-(1-i)*e),l=n%6;return[255*[o,r,s,s,a,o][l],255*[a,o,o,r,s,s][l],255*[s,s,a,o,o,r][l]]}function v(t,e,o){const n=(2-(e/=100))*(o/=100)/2;return 0!==n&&(e=1===n?0:n<.5?e*o/(2*n):e*o/(2-2*n)),[t,100*e,100*n]}function b(t,e,o){const n=u(t/=255,e/=255,o/=255),i=h(t,e,o),s=i-n;let r,a;if(0===s)r=a=0;else{a=s/i;const n=((i-t)/6+s/2)/s,l=((i-e)/6+s/2)/s,c=((i-o)/6+s/2)/s;t===i?r=c-l:e===i?r=1/3+n-c:o===i&&(r=2/3+l-n),r<0?r+=1:r>1&&(r-=1)}return[360*r,100*a,100*i]}function y(t,e,o,n){e/=100,o/=100;return[...b(255*(1-u(1,(t/=100)*(1-(n/=100))+n)),255*(1-u(1,e*(1-n)+n)),255*(1-u(1,o*(1-n)+n)))]}function g(t,e,o){e/=100;const n=2*(e*=(o/=100)<.5?o:1-o)/(o+e)*100,i=100*(o+e);return[t,isNaN(n)?0:n,i]}function _(t){return b(...t.match(/.{2}/g).map((t=>parseInt(t,16))))}function w(t){t=t.match(/^[a-zA-Z]+$/)?function(t){if("black"===t.toLowerCase())return"#000";const e=document.createElement("canvas").getContext("2d");return e.fillStyle=t,"#000"===e.fillStyle?null:e.fillStyle}(t):t;const e={cmyk:/^cmyk\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)/i,rgba:/^rgba?\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D*?(([\d.]+)(%?)|$)/i,hsla:/^hsla?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hsva:/^hsva?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hexa:/^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i},o=t=>t.map((t=>/^(|\d+)\.\d+|\d+$/.test(t)?Number(t):void 0));let n;t:for(const i in e)if(n=e[i].exec(t))switch(i){case"cmyk":{const[,t,e,s,r]=o(n);if(t>100||e>100||s>100||r>100)break t;return{values:y(t,e,s,r),type:i}}case"rgba":{let[,t,,e,,s,,,r]=o(n);if(t="%"===n[2]?t/100*255:t,e="%"===n[4]?e/100*255:e,s="%"===n[6]?s/100*255:s,r="%"===n[9]?r/100:r,t>255||e>255||s>255||r<0||r>1)break t;return{values:[...b(t,e,s),r],a:r,type:i}}case"hexa":{let[,t]=n;4!==t.length&&3!==t.length||(t=t.split("").map((t=>t+t)).join(""));const e=t.substring(0,6);let o=t.substring(6);return o=o?parseInt(o,16)/255:void 0,{values:[..._(e),o],a:o,type:i}}case"hsla":{let[,t,e,s,,r]=o(n);if(r="%"===n[6]?r/100:r,t>360||e>100||s>100||r<0||r>1)break t;return{values:[...g(t,e,s),r],a:r,type:i}}case"hsva":{let[,t,e,s,,r]=o(n);if(r="%"===n[6]?r/100:r,t>360||e>100||s>100||r<0||r>1)break t;return{values:[t,e,s,r],a:r,type:i}}}return{values:null,type:null}}function A(t=0,e=0,o=0,n=1){const i=(t,e)=>(o=-1)=>e(~o?t.map((t=>Number(t.toFixed(o)))):t),s={h:t,s:e,v:o,a:n,toHSVA(){const t=[s.h,s.s,s.v,s.a];return t.toString=i(t,(t=>`hsva(${t[0]}, ${t[1]}%, ${t[2]}%, ${s.a})`)),t},toHSLA(){const t=[...v(s.h,s.s,s.v),s.a];return t.toString=i(t,(t=>`hsla(${t[0]}, ${t[1]}%, ${t[2]}%, ${s.a})`)),t},toRGBA(){const t=[...f(s.h,s.s,s.v),s.a];return t.toString=i(t,(t=>`rgba(${t[0]}, ${t[1]}, ${t[2]}, ${s.a})`)),t},toCMYK(){const t=function(t,e,o){const n=f(t,e,o),i=n[0]/255,s=n[1]/255,r=n[2]/255,a=u(1-i,1-s,1-r);return[100*(1===a?0:(1-i-a)/(1-a)),100*(1===a?0:(1-s-a)/(1-a)),100*(1===a?0:(1-r-a)/(1-a)),100*a]}(s.h,s.s,s.v);return t.toString=i(t,(t=>`cmyk(${t[0]}%, ${t[1]}%, ${t[2]}%, ${t[3]}%)`)),t},toHEXA(){const t=function(t,e,o){return f(t,e,o).map((t=>m(t).toString(16).padStart(2,"0")))}(s.h,s.s,s.v),e=s.a>=1?"":Number((255*s.a).toFixed(0)).toString(16).toUpperCase().padStart(2,"0");return e&&t.push(e),t.toString=()=>`#${t.join("").toUpperCase()}`,t},clone:()=>A(s.h,s.s,s.v,s.a)};return s}const $=t=>Math.max(Math.min(t,1),0);function C(t){const e={options:Object.assign({lock:null,onchange:()=>0,onstop:()=>0},t),_keyboard(t){const{options:o}=e,{type:n,key:i}=t;if(document.activeElement===o.wrapper){const{lock:o}=e.options,s="ArrowUp"===i,r="ArrowRight"===i,a="ArrowDown"===i,l="ArrowLeft"===i;if("keydown"===n&&(s||r||a||l)){let n=0,i=0;"v"===o?n=s||r?1:-1:"h"===o?n=s||r?-1:1:(i=s?-1:a?1:0,n=l?-1:r?1:0),e.update($(e.cache.x+.01*n),$(e.cache.y+.01*i)),t.preventDefault()}else i.startsWith("Arrow")&&(e.options.onstop(),t.preventDefault())}},_tapstart(t){i(document,["mouseup","touchend","touchcancel"],e._tapstop),i(document,["mousemove","touchmove"],e._tapmove),t.cancelable&&t.preventDefault(),e._tapmove(t)},_tapmove(t){const{options:o,cache:n}=e,{lock:i,element:s,wrapper:r}=o,a=r.getBoundingClientRect();let l=0,c=0;if(t){const e=t&&t.touches&&t.touches[0];l=t?(e||t).clientX:0,c=t?(e||t).clientY:0,la.left+a.width&&(l=a.left+a.width),ca.top+a.height&&(c=a.top+a.height),l-=a.left,c-=a.top}else n&&(l=n.x*a.width,c=n.y*a.height);"h"!==i&&(s.style.left=`calc(${l/a.width*100}% - ${s.offsetWidth/2}px)`),"v"!==i&&(s.style.top=`calc(${c/a.height*100}% - ${s.offsetHeight/2}px)`),e.cache={x:l/a.width,y:c/a.height};const p=$(l/a.width),u=$(c/a.height);switch(i){case"v":return o.onchange(p);case"h":return o.onchange(u);default:return o.onchange(p,u)}},_tapstop(){e.options.onstop(),s(document,["mouseup","touchend","touchcancel"],e._tapstop),s(document,["mousemove","touchmove"],e._tapmove)},trigger(){e._tapmove()},update(t=0,o=0){const{left:n,top:i,width:s,height:r}=e.options.wrapper.getBoundingClientRect();"h"===e.options.lock&&(o=t),e._tapmove({clientX:n+s*t,clientY:i+r*o})},destroy(){const{options:t,_tapstart:o,_keyboard:n}=e;s(document,["keydown","keyup"],n),s([t.wrapper,t.element],"mousedown",o),s([t.wrapper,t.element],"touchstart",o,{passive:!1})}},{options:o,_tapstart:n,_keyboard:r}=e;return i([o.wrapper,o.element],"mousedown",n),i([o.wrapper,o.element],"touchstart",n,{passive:!1}),i(document,["keydown","keyup"],r),e}function k(t={}){t=Object.assign({onchange:()=>0,className:"",elements:[]},t);const e=i(t.elements,"click",(e=>{t.elements.forEach((o=>o.classList[e.target===o?"add":"remove"](t.className))),t.onchange(e),e.stopPropagation()}));return{destroy:()=>s(...e)}}const S={variantFlipOrder:{start:"sme",middle:"mse",end:"ems"},positionFlipOrder:{top:"tbrl",right:"rltb",bottom:"btrl",left:"lrbt"},position:"bottom",margin:8,padding:0},O=(t,e,o)=>{const n="object"!=typeof t||t instanceof HTMLElement?{reference:t,popper:e,...o}:t;return{update(t=n){const{reference:e,popper:o}=Object.assign(n,t);if(!o||!e)throw new Error("Popper- or reference-element missing.");return((t,e,o)=>{const{container:n,arrow:i,margin:s,padding:r,position:a,variantFlipOrder:l,positionFlipOrder:c}={container:document.documentElement.getBoundingClientRect(),...S,...o},{left:p,top:u}=e.style;e.style.left="0",e.style.top="0";const h=t.getBoundingClientRect(),d=e.getBoundingClientRect(),m={t:h.top-d.height-s,b:h.bottom+s,r:h.right+s,l:h.left-d.width-s},f={vs:h.left,vm:h.left+h.width/2-d.width/2,ve:h.left+h.width-d.width,hs:h.top,hm:h.bottom-h.height/2-d.height/2,he:h.bottom-d.height},[v,b="middle"]=a.split("-"),y=c[v],g=l[b],{top:_,left:w,bottom:A,right:$}=n;for(const t of y){const o="t"===t||"b"===t;let n=m[t];const[s,a]=o?["top","left"]:["left","top"],[l,c]=o?[d.height,d.width]:[d.width,d.height],[p,u]=o?[A,$]:[$,A],[v,b]=o?[_,w]:[w,_];if(!(np))for(const p of g){let m=f[(o?"v":"h")+p];if(!(mu)){if(m-=d[a],n-=d[s],e.style[a]=`${m}px`,e.style[s]=`${n}px`,i){const e=o?h.width/2:h.height/2,r=c/2,u=e>r,d=m+{s:u?r:e,m:r,e:u?r:c-e}[p],f=n+{t:l,b:0,r:0,l}[t];i.style[a]=`${d}px`,i.style[s]=`${f}px`}return t+p}}}return e.style.left=p,e.style.top=u,null})(e,o,n)}}};class E{static utils=o;static version="1.9.1";static I18N_DEFAULTS={"ui:dialog":"color picker dialog","btn:toggle":"toggle color picker dialog","btn:swatch":"color swatch","btn:last-color":"use previous color","btn:save":"Save","btn:cancel":"Cancel","btn:clear":"Clear","aria:btn:save":"save and close","aria:btn:cancel":"cancel and close","aria:btn:clear":"clear and close","aria:input":"color input field","aria:palette":"color selection area","aria:hue":"hue selection slider","aria:opacity":"selection slider"};static DEFAULT_OPTIONS={appClass:null,theme:"classic",useAsButton:!1,padding:8,disabled:!1,comparison:!0,closeOnScroll:!1,outputPrecision:0,lockOpacity:!1,autoReposition:!0,container:"body",components:{interaction:{}},i18n:{},swatches:null,inline:!1,sliders:null,default:"#42445a",defaultRepresentation:null,position:"bottom-middle",adjustableNumbers:!0,showAlways:!1,closeWithKey:"Escape"};_initializingActive=!0;_recalc=!0;_nanopop=null;_root=null;_color=A();_lastColor=A();_swatchColors=[];_setupAnimationFrame=null;_eventListener={init:[],save:[],hide:[],show:[],clear:[],change:[],changestop:[],cancel:[],swatchselect:[]};constructor(t){this.options=t=Object.assign({...E.DEFAULT_OPTIONS},t);const{swatches:e,components:o,theme:n,sliders:i,lockOpacity:s,padding:r}=t;["nano","monolith"].includes(n)&&!i&&(t.sliders="h"),o.interaction||(o.interaction={});const{preview:a,opacity:l,hue:c,palette:p}=o;o.opacity=!s&&l,o.palette=p||a||l||c,this._preBuild(),this._buildComponents(),this._bindEvents(),this._finalBuild(),e&&e.length&&e.forEach((t=>this.addSwatch(t)));const{button:u,app:h}=this._root;this._nanopop=O(u,h,{margin:r}),u.setAttribute("role","button"),u.setAttribute("aria-label",this._t("btn:toggle"));const d=this;this._setupAnimationFrame=requestAnimationFrame((function e(){if(!h.offsetWidth)return requestAnimationFrame(e);d.setColor(t.default),d._rePositioningPicker(),t.defaultRepresentation&&(d._representation=t.defaultRepresentation,d.setColorRepresentation(d._representation)),t.showAlways&&d.show(),d._initializingActive=!1,d._emit("init")}))}static create=t=>new E(t);_preBuild(){const{options:t}=this;for(const e of["el","container"])t[e]=c(t[e]);this._root=(t=>{const{components:e,useAsButton:o,inline:n,appClass:i,theme:s,lockOpacity:r}=t.options,l=t=>t?"":'style="display:none" hidden',c=e=>t._t(e),p=a(`\n
\n\n ${o?"":''}\n\n
\n
\n
\n \n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n\n
\n\n
\n \n\n \n \n \n \n \n\n \n \n \n
\n
\n
\n `),u=p.interaction;return u.options.find((t=>!t.hidden&&!t.classList.add("active"))),u.type=()=>u.options.find((t=>t.classList.contains("active"))),p})(this),t.useAsButton&&(this._root.button=t.el),t.container.appendChild(this._root.root)}_finalBuild(){const t=this.options,e=this._root;if(t.container.removeChild(e.root),t.inline){const o=t.el.parentElement;t.el.nextSibling?o.insertBefore(e.app,t.el.nextSibling):o.appendChild(e.app)}else t.container.appendChild(e.app);t.useAsButton?t.inline&&t.el.remove():t.el.parentNode.replaceChild(e.root,t.el),t.disabled&&this.disable(),t.comparison||(e.button.style.transition="none",t.useAsButton||(e.preview.lastColor.style.transition="none")),this.hide()}_buildComponents(){const t=this,e=this.options.components,o=(t.options.sliders||"v").repeat(2),[n,i]=o.match(/^[vh]+$/g)?o:[],s=()=>this._color||(this._color=this._lastColor.clone()),r={palette:C({element:t._root.palette.picker,wrapper:t._root.palette.palette,onstop:()=>t._emit("changestop","slider",t),onchange(o,n){if(!e.palette)return;const i=s(),{_root:r,options:a}=t,{lastColor:l,currentColor:c}=r.preview;t._recalc&&(i.s=100*o,i.v=100-100*n,i.v<0&&(i.v=0),t._updateOutput("slider"));const p=i.toRGBA().toString(0);this.element.style.background=p,this.wrapper.style.background=`\n linear-gradient(to top, rgba(0, 0, 0, ${i.a}), transparent),\n linear-gradient(to left, hsla(${i.h}, 100%, 50%, ${i.a}), rgba(255, 255, 255, ${i.a}))\n `,a.comparison?a.useAsButton||t._lastColor||l.style.setProperty("--pcr-color",p):(r.button.style.setProperty("--pcr-color",p),r.button.classList.remove("clear"));const u=i.toHEXA().toString();for(const{el:e,color:o}of t._swatchColors)e.classList[u===o.toHEXA().toString()?"add":"remove"]("pcr-active");c.style.setProperty("--pcr-color",p)}}),hue:C({lock:"v"===i?"h":"v",element:t._root.hue.picker,wrapper:t._root.hue.slider,onstop:()=>t._emit("changestop","slider",t),onchange(o){if(!e.hue||!e.palette)return;const n=s();t._recalc&&(n.h=360*o),this.element.style.backgroundColor=`hsl(${n.h}, 100%, 50%)`,r.palette.trigger()}}),opacity:C({lock:"v"===n?"h":"v",element:t._root.opacity.picker,wrapper:t._root.opacity.slider,onstop:()=>t._emit("changestop","slider",t),onchange(o){if(!e.opacity||!e.palette)return;const n=s();t._recalc&&(n.a=Math.round(100*o)/100),this.element.style.background=`rgba(0, 0, 0, ${n.a})`,r.palette.trigger()}}),selectable:k({elements:t._root.interaction.options,className:"active",onchange(e){t._representation=e.target.getAttribute("data-type").toUpperCase(),t._recalc&&t._updateOutput("swatch")}})};this._components=r}_bindEvents(){const{_root:t,options:e}=this,o=[i(t.interaction.clear,"click",(()=>this._clearColor())),i([t.interaction.cancel,t.preview.lastColor],"click",(()=>{this.setHSVA(...(this._lastColor||this._color).toHSVA(),!0),this._emit("cancel")})),i(t.interaction.save,"click",(()=>{!this.applyColor()&&!e.showAlways&&this.hide()})),i(t.interaction.result,["keyup","input"],(t=>{this.setColor(t.target.value,!0)&&!this._initializingActive&&(this._emit("change",this._color,"input",this),this._emit("changestop","input",this)),t.stopImmediatePropagation()})),i(t.interaction.result,["focus","blur"],(t=>{this._recalc="blur"===t.type,this._recalc&&this._updateOutput(null)})),i([t.palette.palette,t.palette.picker,t.hue.slider,t.hue.picker,t.opacity.slider,t.opacity.picker],["mousedown","touchstart"],(()=>this._recalc=!0),{passive:!0})];if(!e.showAlways){const n=e.closeWithKey;o.push(i(t.button,"click",(()=>this.isOpen()?this.hide():this.show())),i(document,"keyup",(t=>this.isOpen()&&(t.key===n||t.code===n)&&this.hide())),i(document,["touchstart","mousedown"],(e=>{this.isOpen()&&!l(e).some((e=>e===t.app||e===t.button))&&this.hide()}),{capture:!0}))}if(e.adjustableNumbers){const e={rgba:[255,255,255,1],hsva:[360,100,100,1],hsla:[360,100,100,1],cmyk:[100,100,100,100]};p(t.interaction.result,((t,o,n)=>{const i=e[this.getColorRepresentation().toLowerCase()];if(i){const e=i[n],s=t+(e>=100?1e3*o:o);return s<=0?0:Number((s{n.isOpen()&&(e.closeOnScroll&&n.hide(),null===t?(t=setTimeout((()=>t=null),100),requestAnimationFrame((function e(){n._rePositioningPicker(),null!==t&&requestAnimationFrame(e)}))):(clearTimeout(t),t=setTimeout((()=>t=null),100)))}),{capture:!0}))}this._eventBindings=o}_rePositioningPicker(){const{options:t}=this;if(!t.inline){if(!this._nanopop.update({container:document.body.getBoundingClientRect(),position:t.position})){const t=this._root.app,e=t.getBoundingClientRect();t.style.top=(window.innerHeight-e.height)/2+"px",t.style.left=(window.innerWidth-e.width)/2+"px"}}}_updateOutput(t){const{_root:e,_color:o,options:n}=this;if(e.interaction.type()){const t=`to${e.interaction.type().getAttribute("data-type")}`;e.interaction.result.value="function"==typeof o[t]?o[t]().toString(n.outputPrecision):""}!this._initializingActive&&this._recalc&&this._emit("change",o,t,this)}_clearColor(t=!1){const{_root:e,options:o}=this;o.useAsButton||e.button.style.setProperty("--pcr-color","rgba(0, 0, 0, 0.15)"),e.button.classList.add("clear"),o.showAlways||this.hide(),this._lastColor=null,this._initializingActive||t||(this._emit("save",null),this._emit("clear"))}_parseLocalColor(t){const{values:e,type:o,a:n}=w(t),{lockOpacity:i}=this.options,s=void 0!==n&&1!==n;return e&&3===e.length&&(e[3]=void 0),{values:!e||i&&s?null:e,type:o}}_t(t){return this.options.i18n[t]||E.I18N_DEFAULTS[t]}_emit(t,...e){this._eventListener[t].forEach((t=>t(...e,this)))}on(t,e){return this._eventListener[t].push(e),this}off(t,e){const o=this._eventListener[t]||[],n=o.indexOf(e);return~n&&o.splice(n,1),this}addSwatch(t){const{values:e}=this._parseLocalColor(t);if(e){const{_swatchColors:t,_root:o}=this,n=A(...e),s=r(`