🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,190 @@
<?php
namespace Livewire\Features\SupportTesting {
use Closure;
class Testable {
public function fillForm(array | Closure $state = [], ?string $form = null): static {}
/**
* @deprecated Use `assertSchemaStateSet()` instead.
*/
public function assertFormSet(array | Closure $state, string $form = 'form'): static {}
public function assertHasFormErrors(array $keys = [], ?string $form = null): static {}
public function assertHasNoFormErrors(array $keys = [], ?string $form = null): static {}
public function assertFormFieldExists(string $key, string | Closure | null $form = null, ?Closure $checkFieldUsing = null): static {}
public function assertFormFieldDoesNotExist(string $key, ?string $form = null): static {}
public function assertFormFieldDisabled(string $key, ?string $form = null): static {}
public function assertFormFieldEnabled(string $key, ?string $form = null): static {}
public function assertFormFieldReadOnly(string $key, ?string $form = null): static {}
/**
* @deprecated Use `assertSchemaExists()` instead.
*/
public function assertFormExists(string $name = 'form'): static {}
/**
* @deprecated Use `assertSchemaComponentHidden()` instead.
*/
public function assertFormFieldHidden(string $key, string $form = 'form'): static {}
/**
* @deprecated Use `assertSchemaComponentVisible()` instead.
*/
public function assertFormFieldVisible(string $key, string $form = 'form'): static {}
/**
* @deprecated Use `assertSchemaComponentExists()` instead.
*/
public function assertFormComponentExists(string $componentKey, string | Closure $form = 'form', ?Closure $checkComponentUsing = null): static {}
/**
* @deprecated Use `assertSchemaComponentDoesNotExist()` instead.
*/
public function assertFormComponentDoesNotExist(string $componentKey, string $form = 'form'): static {}
/**
* @deprecated Use `mountAction()` instead.
*/
public function mountFormComponentAction(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `unmountAction()` instead.
*/
public function unmountFormComponentAction(): static {}
/**
* @deprecated Use `fillForm()` instead.
*/
public function setFormComponentActionData(array $data): static {}
/**
* @deprecated Use `assertSchemaStateSet()` instead.
*/
public function assertFormComponentActionDataSet(array $data): static {}
/**
* @deprecated Use `callAction()` instead.
*/
public function callFormComponentAction(string | array $component, string | array $name, array $data = [], array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `callMountedAction()` instead.
*/
public function callMountedFormComponentAction(array $arguments = []): static {}
/**
* @deprecated Use `assertActionExists()` instead.
*/
public function assertFormComponentActionExists(string | array $component, string | array $name, string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDoesNotExist()` instead.
*/
public function assertFormComponentActionDoesNotExist(string | array $component, string | array $name, string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionVisible()` instead.
*/
public function assertFormComponentActionVisible(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionHidden()` instead.
*/
public function assertFormComponentActionHidden(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionEnabled()` instead.
*/
public function assertFormComponentActionEnabled(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDisabled()` instead.
*/
public function assertFormComponentActionDisabled(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionMounted()` instead.
*/
public function assertFormComponentActionMounted(string | array $component, string | array $name, string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionNotMounted()` instead.
*/
public function assertFormComponentActionNotMounted(string | array $component, string | array $name, string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionHalted()` instead.
*/
public function assertFormComponentActionHalted(string | array $component, string | array $name, string $formName = 'form'): static {}
/**
* @deprecated Use `assertHasFormErrors()` instead.
*/
public function assertHasFormComponentActionErrors(array $keys = []): static {}
/**
* @deprecated Use `assertHasNoFormErrors()` instead.
*/
public function assertHasNoFormComponentActionErrors(array $keys = []): static {}
/**
* @deprecated Use `assertActionHasIcon()` instead.
*/
public function assertFormComponentActionHasIcon(string | array $component, string | array $name, string $icon, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveIcon()` instead.
*/
public function assertFormComponentActionDoesNotHaveIcon(string | array $component, string | array $name, string $icon, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionHasLabel()` instead.
*/
public function assertFormComponentActionHasLabel(string | array $component, string | array $name, string $label, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveLabel()` instead.
*/
public function assertFormComponentActionDoesNotHaveLabel(string | array $component, string | array $name, string $label, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionHasColor()` instead.
*/
public function assertFormComponentActionHasColor(string | array $component, string | array $name, string | array $color, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveColor()` instead.
*/
public function assertFormComponentActionDoesNotHaveColor(string | array $component, string | array $name, string | array $color, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionHasUrl()` instead.
*/
public function assertFormComponentActionHasUrl(string | array $component, string | array $name, string $url, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionDoesNotHaveUrl()` instead.
*/
public function assertFormComponentActionDoesNotHaveUrl(string | array $component, string | array $name, string $url, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionShouldOpenUrlInNewTab()` instead.
*/
public function assertFormComponentActionShouldOpenUrlInNewTab(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
/**
* @deprecated Use `assertActionShouldNotOpenUrlInNewTab()` instead.
*/
public function assertFormComponentActionShouldNotOpenUrlInNewTab(string | array $component, string | array $name, array $arguments = [], string $formName = 'form'): static {}
}
}
@@ -0,0 +1,38 @@
{
"name": "filament/forms",
"description": "Easily add beautiful forms to any Livewire component.",
"license": "MIT",
"homepage": "https://github.com/filamentphp/filament",
"support": {
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"require": {
"php": "^8.2",
"danharrin/date-format-converter": "^0.3",
"filament/actions": "self.version",
"filament/schemas": "self.version",
"filament/support": "self.version",
"ueberdosis/tiptap-php": "^2.0"
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Filament\\Forms\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Filament\\Forms\\FormsServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1 @@
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
function h({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{h as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
function s({state:n,splitKeys:a}){return{newTag:"",state:n,createTag(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag(t){this.state=this.state.filter(e=>e!==t)},reorderTags(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...a].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(a.length===0){this.createTag();return}let t=a.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{s as default};
@@ -0,0 +1 @@
function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
(()=>{})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,408 @@
---
title: Text input
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The text input allows you to interact with a string:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
```
<AutoScreenshot name="forms/fields/text-input/simple" alt="Text input" version="4.x" />
## Setting the HTML input type
You may set the type of string using a set of methods. Some, such as `email()`, also provide validation:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('text')
->email() // or
->numeric() // or
->integer() // or
->password() // or
->tel() // or
->url()
```
You may instead use the `type()` method to pass another [HTML input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types):
```php
use Filament\Forms\Components\TextInput;
TextInput::make('backgroundColor')
->type('color')
```
The individual type methods also allow you to pass in a boolean value to control if the field should be that or not:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('text')
->email(FeatureFlag::active()) // or
->numeric(FeatureFlag::active()) // or
->integer(FeatureFlag::active()) // or
->password(FeatureFlag::active()) // or
->tel(FeatureFlag::active()) // or
->url(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value these methods also accept a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting the HTML input mode
You may set the [`inputmode` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#inputmode) of the input using the `inputMode()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('text')
->numeric()
->inputMode('decimal')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `inputMode()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting the numeric step
You may set the [`step` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#step) of the input using the `step()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('number')
->numeric()
->step(100)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `step()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Autocompleting text
You may allow the text to be [autocompleted by the browser](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocomplete) using the `autocomplete()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('password')
->password()
->autocomplete('new-password')
```
As a shortcut for `autocomplete="off"`, you may use `autocomplete(false)`:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('password')
->password()
->autocomplete(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `autocomplete()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
For more complex autocomplete options, text inputs also support [datalists](#autocompleting-text-with-a-datalist).
### Autocompleting text with a datalist
You may specify [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist) options for a text input using the `datalist()` method:
```php
TextInput::make('manufacturer')
->datalist([
'BMW',
'Ford',
'Mercedes-Benz',
'Porsche',
'Toyota',
'Volkswagen',
])
```
Datalists provide autocomplete options to users when they use a text input. However, these are purely recommendations, and the user is still able to type any value into the input. If you're looking to strictly limit users to a set of predefined options, check out the [select field](select).
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `datalist()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Autocapitalizing text
You may allow the text to be [autocapitalized by the browser](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#autocapitalize) using the `autocapitalize()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->autocapitalize('words')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `autocapitalize()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding affix text aside the field
You may place text before and after the input using the `prefix()` and `suffix()` methods:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('domain')
->prefix('https://')
->suffix('.com')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefix()` and `suffix()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/text-input/affix" alt="Text input with affixes" version="4.x" />
### Using icons as affixes
You may place an [icon](../styling/icons) before and after the input using the `prefixIcon()` and `suffixIcon()` methods:
```php
use Filament\Forms\Components\TextInput;
use Filament\Support\Icons\Heroicon;
TextInput::make('domain')
->url()
->suffixIcon(Heroicon::GlobeAlt)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefixIcon()` and `suffixIcon()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/text-input/suffix-icon" alt="Text input with suffix icon" version="4.x" />
#### Setting the affix icon's color
Affix icons are gray by default, but you may set a different color using the `prefixIconColor()` and `suffixIconColor()` methods:
```php
use Filament\Forms\Components\TextInput;
use Filament\Support\Icons\Heroicon;
TextInput::make('domain')
->url()
->suffixIcon(Heroicon::CheckCircle)
->suffixIconColor('success')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefixIconColor()` and `suffixIconColor()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
## Revealable password inputs
When using `password()`, you can also make the input `revealable()`, so that the user can see a plain text version of the password they're typing by clicking a button:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('password')
->password()
->revealable()
```
<AutoScreenshot name="forms/fields/text-input/revealable-password" alt="Text input with revealable password" version="4.x" />
Optionally, you may pass a boolean value to control if the input should be revealable or not:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('password')
->password()
->revealable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `revealable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Allowing the text to be copied to the clipboard
You may make the text copyable, such that clicking on a button next to the input copies the text to the clipboard, and optionally specify a custom confirmation message and duration in milliseconds:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('apiKey')
->label('API key')
->copyable(copyMessage: 'Copied!', copyMessageDuration: 1500)
```
Optionally, you may pass a boolean value to control if the text should be copyable or not:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('apiKey')
->label('API key')
->copyable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `copyable()` method parameters also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="warning">
This feature only works when SSL is enabled for the app.
</Aside>
## Input masking
Input masking is the practice of defining a format that the input value must conform to.
In Filament, you may use the `mask()` method to configure an [Alpine.js mask](https://alpinejs.dev/plugins/mask#x-mask):
```php
use Filament\Forms\Components\TextInput;
TextInput::make('birthday')
->mask('99/99/9999')
->placeholder('MM/DD/YYYY')
```
To use a [dynamic mask](https://alpinejs.dev/plugins/mask#mask-functions), wrap the JavaScript in a `RawJs` object:
```php
use Filament\Forms\Components\TextInput;
use Filament\Support\RawJs;
TextInput::make('cardNumber')
->mask(RawJs::make(<<<'JS'
$input.startsWith('34') || $input.startsWith('37') ? '9999 999999 99999' : '9999 9999 9999 9999'
JS))
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `mask()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
Alpine.js will send the entire masked value to the server, so you may need to strip certain characters from the state before validating the field and saving it. You can do this with the `stripCharacters()` method, passing in a character or an array of characters to remove from the masked value:
```php
use Filament\Forms\Components\TextInput;
use Filament\Support\RawJs;
TextInput::make('amount')
->mask(RawJs::make('$money($input)'))
->stripCharacters(',')
->numeric()
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `stripCharacters()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Trimming whitespace
You can automatically trim whitespace from the beginning and end of the input value using the `trim()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->trim()
```
You may want to enable trimming globally for all text inputs, similar to Laravel's `TrimStrings` middleware. You can do this in a service provider using the `configureUsing()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::configureUsing(function (TextInput $component): void {
$component->trim();
});
```
## Making the field read-only
Not to be confused with [disabling the field](overview#disabling-a-field), you may make the field "read-only" using the `readOnly()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->readOnly()
```
There are a few differences, compared to [`disabled()`](overview#disabling-a-field):
- When using `readOnly()`, the field will still be sent to the server when the form is submitted. It can be mutated with the browser console, or via JavaScript. You can use [`dehydrated(false)`](overview#preventing-a-field-from-being-dehydrated) to prevent this.
- There are no styling changes, such as less opacity, when using `readOnly()`.
- The field is still focusable when using `readOnly()`.
Optionally, you may pass a boolean value to control if the field should be read-only or not:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->readOnly(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `readOnly()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Text input validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to text inputs.
### Length validation
You may limit the length of the input by setting the `minLength()` and `maxLength()` methods. These methods add both frontend and backend validation:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->minLength(2)
->maxLength(255)
```
You can also specify the exact length of the input by setting the `length()`. This method adds both frontend and backend validation:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('code')
->length(8)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minLength()`, `maxLength()` and `length()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Size validation
You may validate the minimum and maximum value of a numeric input by setting the `minValue()` and `maxValue()` methods:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('number')
->numeric()
->minValue(1)
->maxValue(100)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minValue()` and `maxValue()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Phone number validation
When using a `tel()` field, the value will be validated using: `/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\.\/0-9]*$/`.
If you wish to change that, then you can use the `telRegex()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('phone')
->tel()
->telRegex('/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\.\/0-9]*$/')
```
Alternatively, to customize the `telRegex()` across all fields, use a service provider:
```php
use Filament\Forms\Components\TextInput;
TextInput::configureUsing(function (TextInput $component): void {
$component->telRegex('/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\.\/0-9]*$/');
});
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `telRegex()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,114 @@
---
title: Checkbox
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The checkbox component, similar to a [toggle](toggle), allows you to interact a boolean value.
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_admin')
```
<AutoScreenshot name="forms/fields/checkbox/simple" alt="Checkbox" version="4.x" />
If you're saving the boolean value using Eloquent, you should be sure to add a `boolean` [cast](https://laravel.com/docs/eloquent-mutators#attribute-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_admin' => 'boolean',
];
}
// ...
}
```
## Positioning the label above
Checkbox fields have two layout modes, inline and stacked. By default, they are inline.
When the checkbox is inline, its label is adjacent to it:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_admin')
->inline()
```
<AutoScreenshot name="forms/fields/checkbox/inline" alt="Checkbox with its label inline" version="4.x" />
When the checkbox is stacked, its label is above it:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_admin')
->inline(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `inline()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/checkbox/not-inline" alt="Checkbox with its label above" version="4.x" />
## Checkbox validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to checkboxes.
### Accepted validation
You may ensure that the checkbox is checked using the `accepted()` method:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('terms_of_service')
->accepted()
```
Optionally, you may pass a boolean value to control if the validation rule should be applied or not:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('terms_of_service')
->accepted(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `accepted()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Declined validation
You may ensure that the checkbox is not checked using the `declined()` method:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_under_18')
->declined()
```
Optionally, you may pass a boolean value to control if the validation rule should be applied or not:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_under_18')
->declined(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `declined()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,150 @@
---
title: Toggle
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The toggle component, similar to a [checkbox](checkbox), allows you to interact a boolean value.
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_admin')
```
<AutoScreenshot name="forms/fields/toggle/simple" alt="Toggle" version="4.x" />
If you're saving the boolean value using Eloquent, you should be sure to add a `boolean` [cast](https://laravel.com/docs/eloquent-mutators#attribute-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_admin' => 'boolean',
];
}
// ...
}
```
## Adding icons to the toggle button
Toggles may also use an [icon](../styling/icons) to represent the "on" and "off" state of the button. To add an icon to the "on" state, use the `onIcon()` method. To add an icon to the "off" state, use the `offIcon()` method:
```php
use Filament\Forms\Components\Toggle;
use Filament\Support\Icons\Heroicon;
Toggle::make('is_admin')
->onIcon(Heroicon::Bolt)
->offIcon(Heroicon::User)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `onIcon()` and `offIcon()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle/icons" alt="Toggle icons" version="4.x" />
## Customizing the color of the toggle button
You may also customize the [color](../styling/colors) representing the "on" or "off" state of the toggle. To add a color to the "on" state, use the `onColor()` method. To add a color to the "off" state, use the `offColor()` method:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_admin')
->onColor('success')
->offColor('danger')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `onColor()` and `offColor()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle/off-color" alt="Toggle off color" version="4.x" />
<AutoScreenshot name="forms/fields/toggle/on-color" alt="Toggle on color" version="4.x" />
## Positioning the label above
Toggle fields have two layout modes, inline and stacked. By default, they are inline.
When the toggle is inline, its label is adjacent to it:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_admin')
->inline()
```
<AutoScreenshot name="forms/fields/toggle/inline" alt="Toggle with its label inline" version="4.x" />
When the toggle is stacked, its label is above it:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_admin')
->inline(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `inline()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle/not-inline" alt="Toggle with its label above" version="4.x" />
## Toggle validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to toggles.
### Accepted validation
You may ensure that the toggle is "on" using the `accepted()` method:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('terms_of_service')
->accepted()
```
Optionally, you may pass a boolean value to control if the validation rule should be applied or not:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('terms_of_service')
->accepted(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `accepted()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Declined validation
You may ensure that the toggle is "off" using the `declined()` method:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_under_18')
->declined()
```
Optionally, you may pass a boolean value to control if the validation rule should be applied or not:
```php
use Filament\Forms\Components\Toggle;
Toggle::make('is_under_18')
->declined(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `declined()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,420 @@
---
title: Checkbox list
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The checkbox list component allows you to select multiple values from a list of predefined options:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `options()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/checkbox-list/simple" alt="Checkbox list" version="4.x" />
These options are returned in JSON format. If you're saving them using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class App extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'technologies' => 'array',
];
}
// ...
}
```
## Setting option descriptions
You can optionally provide descriptions to each option using the `descriptions()` method. This method accepts an array of plain text strings, or instances of `Illuminate\Support\HtmlString` or `Illuminate\Contracts\Support\Htmlable`. This allows you to render HTML, or even markdown, in the descriptions:
```php
use Filament\Forms\Components\CheckboxList;
use Illuminate\Support\HtmlString;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
->descriptions([
'tailwind' => 'A utility-first CSS framework for rapidly building modern websites without ever leaving your HTML.',
'alpine' => new HtmlString('A rugged, minimal tool for composing behavior <strong>directly in your markup</strong>.'),
'laravel' => str('A **web application** framework with expressive, elegant syntax.')->inlineMarkdown()->toHtmlString(),
'livewire' => 'A full-stack framework for Laravel building dynamic interfaces simple, without leaving the comfort of Laravel.',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `descriptions()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/checkbox-list/option-descriptions" alt="Checkbox list with option descriptions" version="4.x" />
<Aside variant="info">
Be sure to use the same `key` in the descriptions array as the `key` in the option array so the right description matches the right option.
</Aside>
## Splitting options into columns
You may split options into columns by using the `columns()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->columns(2)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `columns()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/checkbox-list/columns" alt="Checkbox list with 2 columns" version="4.x" />
This method accepts the same options as the `columns()` method of the [grid](../schemas/layouts#grid-system). This allows you to responsively customize the number of columns at various breakpoints.
### Setting the grid direction
By default, when you arrange checkboxes into columns, they will be listed in order vertically. If you'd like to list them horizontally, you may use the `gridDirection(GridDirection::Row)` method:
```php
use Filament\Forms\Components\CheckboxList;
use Filament\Support\Enums\GridDirection;
CheckboxList::make('technologies')
->options([
// ...
])
->columns(2)
->gridDirection(GridDirection::Row)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `gridDirection()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/checkbox-list/rows" alt="Checkbox list with 2 rows" version="4.x" />
## Searching options
You may enable a search input to allow easier access to many options, using the `searchable()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->searchable()
```
<AutoScreenshot name="forms/fields/checkbox-list/searchable" alt="Searchable checkbox list" version="4.x" />
Optionally, you may pass a boolean value to control if the options should be searchable or not:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->searchable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `searchable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Bulk toggling checkboxes
You may allow users to toggle all checkboxes at once using the `bulkToggleable()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->bulkToggleable()
```
<AutoScreenshot name="forms/fields/checkbox-list/bulk-toggleable" alt="Bulk toggleable checkbox list" version="4.x" />
Optionally, you may pass a boolean value to control if the checkboxes should be bulk toggleable or not:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->bulkToggleable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `bulkToggleable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Disabling specific options
You can disable specific options using the `disableOptionWhen()` method. It accepts a closure, in which you can check if the option with a specific `$value` should be disabled:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
->disableOptionWhen(fn (string $value): bool => $value === 'livewire')
```
<UtilityInjection set="formFields" version="4.x" extras="Option value;;mixed;;$value;;The value of the option to disable.||Option label;;string | Illuminate\Contracts\Support\Htmlable;;$label;;The label of the option to disable.">You can inject various utilities into the function as parameters.</UtilityInjection>
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
'heroicons' => 'SVG icons',
])
->disableOptionWhen(fn (string $value): bool => $value === 'heroicons')
->in(fn (CheckboxList $component): array => array_keys($component->getEnabledOptions()))
```
For more information about the `in()` function, please see the [Validation documentation](validation#in).
## Allowing HTML in the option labels
By default, Filament will escape any HTML in the option labels. If you'd like to allow HTML, you can use the `allowHtml()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technology')
->options([
'tailwind' => '<span class="text-blue-500">Tailwind</span>',
'alpine' => '<span class="text-green-500">Alpine</span>',
'laravel' => '<span class="text-red-500">Laravel</span>',
'livewire' => '<span class="text-pink-500">Livewire</span>',
])
->searchable()
->allowHtml()
```
<Aside variant="danger">
Be aware that you will need to ensure that the HTML is safe to render, otherwise your application will be vulnerable to XSS attacks.
</Aside>
Optionally, you may pass a boolean value to control if the options should allow HTML or not:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technology')
->options([
'tailwind' => '<span class="text-blue-500">Tailwind</span>',
'alpine' => '<span class="text-green-500">Alpine</span>',
'laravel' => '<span class="text-red-500">Laravel</span>',
'livewire' => '<span class="text-pink-500">Livewire</span>',
])
->searchable()
->allowHtml(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `allowHtml()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Integrating with an Eloquent relationship
> If you're building a form inside your Livewire component, make sure you have set up the [form's model](../components/form#setting-a-form-model). Otherwise, Filament doesn't know which model to use to retrieve the relationship from.
You may employ the `relationship()` method of the `CheckboxList` to point to a `BelongsToMany` relationship. Filament will load the options from the relationship, and save them back to the relationship's pivot table when the form is submitted. The `titleAttribute` is the name of a column that will be used to generate a label for each option:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->relationship(titleAttribute: 'name')
```
<Aside variant="warning">
When using `disabled()` with `relationship()`, ensure that `disabled()` is called before `relationship()`. This ensures that the `dehydrated()` call from within `relationship()` is not overridden by the call from `disabled()`:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->disabled()
->relationship(titleAttribute: 'name')
```
</Aside>
### Customizing the relationship query
You may customize the database query that retrieves options using the `modifyOptionsQueryUsing` parameter of the `relationship()` method:
```php
use Filament\Forms\Components\CheckboxList;
use Illuminate\Database\Eloquent\Builder;
CheckboxList::make('technologies')
->relationship(
titleAttribute: 'name',
modifyQueryUsing: fn (Builder $query) => $query->withTrashed(),
)
```
<UtilityInjection set="formFields" version="4.x" extras="Query;;Illuminate\Database\Eloquent\Builder;;$query;;The Eloquent query builder to modify.">The `modifyQueryUsing` argument can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing the relationship option labels
If you'd like to customize the label of each option, maybe to be more descriptive, or to concatenate a first and last name, you could use a virtual column in your database migration:
```php
$table->string('full_name')->virtualAs('concat(first_name, \' \', last_name)');
```
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('authors')
->relationship(titleAttribute: 'full_name')
```
Alternatively, you can use the `getOptionLabelFromRecordUsing()` method to transform an option's Eloquent model into a label:
```php
use Filament\Forms\Components\CheckboxList;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
CheckboxList::make('authors')
->relationship(
modifyQueryUsing: fn (Builder $query) => $query->orderBy('first_name')->orderBy('last_name'),
)
->getOptionLabelFromRecordUsing(fn (Model $record) => "{$record->first_name} {$record->last_name}")
```
<UtilityInjection set="formFields" version="4.x" extras="Eloquent record;;Illuminate\Database\Eloquent\Model;;$record;;The Eloquent record to get the option label for.">The `getOptionLabelFromRecordUsing()` method can inject various utilities into the function as parameters.</UtilityInjection>
### Saving pivot data to the relationship
If your pivot table has additional columns, you can use the `pivotData()` method to specify the data that should be saved in them:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('primaryTechnologies')
->relationship(name: 'technologies', titleAttribute: 'name')
->pivotData([
'is_primary' => true,
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `pivotData()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting a custom no search results message
When you're using a searchable checkbox list, you may want to display a custom message when no search results are found. You can do this using the `noSearchResultsMessage()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->searchable()
->noSearchResultsMessage('No technologies found.')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `noSearchResultsMessage()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting a custom search prompt
When you're using a searchable checkbox list, you may want to tweak the search input's placeholder when the user has not yet entered a search term. You can do this using the `searchPrompt()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->searchable()
->searchPrompt('Search for a technology')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `searchPrompt()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Tweaking the search debounce
By default, Filament will wait 1000 milliseconds (1 second) before searching for options when the user types in a searchable checkbox list. It will also wait 1000 milliseconds between searches if the user is continuously typing into the search input. You can change this using the `searchDebounce()` method:
```php
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->searchable()
->searchDebounce(500)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `searchDebounce()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Customizing the checkbox list action objects
This field uses action objects for easy customization of buttons within it. You can customize these buttons by passing a function to an action registration method. The function has access to the `$action` object, which you can use to [customize it](../actions/overview). The following methods are available to customize the actions:
- `selectAllAction()`
- `deselectAllAction()`
Here is an example of how you might customize an action:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList;
CheckboxList::make('technologies')
->options([
// ...
])
->selectAllAction(
fn (Action $action) => $action->label('Select all technologies'),
)
```
<UtilityInjection set="formFields" version="4.x" extras="Action;;Filament\Actions\Action;;$action;;The action object to customize.">The action registration methods can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,152 @@
---
title: Radio
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The radio input provides a radio button group for selecting a single value from a list of predefined options:
```php
use Filament\Forms\Components\Radio;
Radio::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published'
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `options()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/radio/simple" alt="Radio" version="4.x" />
## Setting option descriptions
You can optionally provide descriptions to each option using the `descriptions()` method:
```php
use Filament\Forms\Components\Radio;
Radio::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published'
])
->descriptions([
'draft' => 'Is not visible.',
'scheduled' => 'Will be visible.',
'published' => 'Is visible.'
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `descriptions()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/radio/option-descriptions" alt="Radio with option descriptions" version="4.x" />
<Aside variant="info">
Be sure to use the same `key` in the descriptions array as the `key` in the option array so the right description matches the right option.
</Aside>
## Positioning the options inline with each other
You may wish to display the options `inline()` with each other:
```php
use Filament\Forms\Components\Radio;
Radio::make('feedback')
->label('Like this post?')
->boolean()
->inline()
```
<AutoScreenshot name="forms/fields/radio/inline" alt="Inline toggle buttons" version="4.x" />
Optionally, you may pass a boolean value to control if the options should be inline or not:
```php
use Filament\Forms\Components\Radio;
Radio::make('feedback')
->label('Like this post?')
->boolean()
->inline(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `inline()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Disabling specific options
You can disable specific options using the `disableOptionWhen()` method. It accepts a closure, in which you can check if the option with a specific `$value` should be disabled:
```php
use Filament\Forms\Components\Radio;
Radio::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published',
])
->disableOptionWhen(fn (string $value): bool => $value === 'published')
```
<UtilityInjection set="formFields" version="4.x" extras="Option value;;mixed;;$value;;The value of the option to disable.||Option label;;string | Illuminate\Contracts\Support\Htmlable;;$label;;The label of the option to disable.">You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/radio/disabled-option" alt="Radio with disabled option" version="4.x" />
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\Radio;
Radio::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published',
])
->disableOptionWhen(fn (string $value): bool => $value === 'published')
->in(fn (Radio $component): array => array_keys($component->getEnabledOptions()))
```
For more information about the `in()` function, please see the [Validation documentation](validation#in).
## Boolean options
If you want a simple boolean radio button group, with "Yes" and "No" options, you can use the `boolean()` method:
```php
use Filament\Forms\Components\Radio;
Radio::make('feedback')
->label('Like this post?')
->boolean()
```
<AutoScreenshot name="forms/fields/radio/boolean" alt="Boolean radio" version="4.x" />
To customize the "Yes" label, you can use the `trueLabel` argument on the `boolean()` method:
```php
use Filament\Forms\Components\Radio;
Radio::make('feedback')
->label('Like this post?')
->boolean(trueLabel: 'Absolutely!')
```
To customize the "No" label, you can use the `falseLabel` argument on the `boolean()` method:
```php
use Filament\Forms\Components\Radio;
Radio::make('feedback')
->label('Like this post?')
->boolean(falseLabel: 'Not at all!')
```
@@ -0,0 +1,350 @@
---
title: Date-time picker
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The date-time picker provides an interactive interface for selecting a date and/or a time.
```php
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TimePicker;
DateTimePicker::make('published_at')
DatePicker::make('date_of_birth')
TimePicker::make('alarm_at')
```
<AutoScreenshot name="forms/fields/date-time-picker/simple" alt="Date time pickers" version="4.x" />
## Customizing the storage format
You may customize the format of the field when it is saved in your database, using the `format()` method. This accepts a string date format, using [PHP date formatting tokens](https://www.php.net/manual/en/datetime.format.php):
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->format('d/m/Y')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `format()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Disabling the seconds input
When using the time picker, you may disable the seconds input using the `seconds(false)` method:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->seconds(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `seconds()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/without-seconds" alt="Date time picker without seconds" version="4.x" />
## Timezones
If you'd like users to be able to manage dates in their own timezone, you can use the `timezone()` method:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->timezone('America/New_York')
```
While dates will still be stored using the app's configured timezone, the date will now load in the new timezone, and it will be converted back when the form is saved.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `timezone()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
If you do not pass a `timezone()` to the component, it will use Filament's default timezone. You can set Filament's default timezone using the `FilamentTimezone::set()` method in the `boot()` method of a service provider such as `AppServiceProvider`:
```php
use Filament\Support\Facades\FilamentTimezone;
public function boot(): void
{
FilamentTimezone::set('America/New_York');
}
```
This is useful if you want to set a default timezone for all date-time pickers in your application. It is also used in other places where timezones are used in Filament.
<Aside variant="warning">
Filament's default timezone will only apply when the field stores a time. If the field stores a date only (`DatePicker` instead of `DateTimePicker` or `TimePicker`), the timezone will not be applied. This is to prevent timezone shifts when storing dates without times.
</Aside>
## Enabling the JavaScript date picker
By default, Filament uses the native HTML5 date picker. You may enable a more customizable JavaScript date picker using the `native(false)` method:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->native(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `native()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/javascript" alt="JavaScript-based date time picker" version="4.x" />
<Aside variant="info">
The JavaScript date picker does not support full keyboard input in the same way that the native date picker does. If you require full keyboard input, you should use the native date picker.
</Aside>
### Customizing the display format
You may customize the display format of the field, separately from the format used when it is saved in your database. For this, use the `displayFormat()` method, which also accepts a string date format, using [PHP date formatting tokens](https://www.php.net/manual/en/datetime.format.php):
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->native(false)
->displayFormat('d/m/Y')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `displayFormat()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/display-format" alt="Date time picker with custom display format" version="4.x" />
You may also configure the locale that is used when rendering the display, if you want to use different locale from your app config. For this, you can use the `locale()` method:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->native(false)
->displayFormat('d F Y')
->locale('fr')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `locale()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Configuring the time input intervals
You may customize the input interval for increasing/decreasing the hours/minutes /seconds using the `hoursStep()` , `minutesStep()` or `secondsStep()` methods:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->native(false)
->hoursStep(2)
->minutesStep(15)
->secondsStep(10)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `hoursStep()`, `minutesStep()`, and `secondsStep()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
### Configuring the first day of the week
In some countries, the first day of the week is not Monday. To customize the first day of the week in the date picker, use the `firstDayOfWeek()` method on the component. 0 to 7 are accepted values, with Monday as 1 and Sunday as 7 or 0:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->native(false)
->firstDayOfWeek(7)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `firstDayOfWeek()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/week-starts-on-sunday" alt="Date time picker where the week starts on Sunday" version="4.x" />
There are additionally convenient helper methods to set the first day of the week more semantically:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->native(false)
->weekStartsOnMonday()
DateTimePicker::make('published_at')
->native(false)
->weekStartsOnSunday()
```
### Disabling specific dates
To prevent specific dates from being selected:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('date')
->native(false)
->disabledDates(['2000-01-03', '2000-01-15', '2000-01-20'])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `disabledDates()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/disabled-dates" alt="Date time picker where dates are disabled" version="4.x" />
### Closing the picker when a date is selected
To close the picker when a date is selected, you can use the `closeOnDateSelection()` method:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('date')
->native(false)
->closeOnDateSelection()
```
Optionally, you may pass a boolean value to control if the input should close when a date is selected or not:
```php
use Filament\Forms\Components\DateTimePicker;
DateTimePicker::make('date')
->native(false)
->closeOnDateSelection(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `closeOnDateSelection()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Autocompleting dates with a datalist
Unless you're using the [JavaScript date picker](#enabling-the-javascript-date-picker), you may specify [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist) options for a date picker using the `datalist()` method:
```php
use Filament\Forms\Components\TimePicker;
TimePicker::make('appointment_at')
->datalist([
'09:00',
'09:30',
'10:00',
'10:30',
'11:00',
'11:30',
'12:00',
])
```
Datalists provide autocomplete options to users when they use the picker. However, these are purely recommendations, and the user is still able to type any value into the input. If you're looking to strictly limit users to a set of predefined options, check out the [select field](select).
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `datalist()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Focusing a default calendar date
By default, if the field has no state, opening the calendar panel will open the calendar at the current date. This might not be convenient for situations where you want to open the calendar on a specific date instead. You can use the `defaultFocusedDate()` to set a default focused date on the calendar:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('custom_starts_at')
->native(false)
->placeholder(now()->startOfMonth())
->defaultFocusedDate(now()->startOfMonth())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `defaultFocusedDate()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding affix text aside the field
You may place text before and after the input using the `prefix()` and `suffix()` methods:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date')
->prefix('Starts')
->suffix('at midnight')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefix()` and `suffix()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/affix" alt="Date time picker with affixes" version="4.x" />
### Using icons as affixes
You may place an [icon](../styling/icons) before and after the input using the `prefixIcon()` and `suffixIcon()` methods:
```php
use Filament\Forms\Components\TimePicker;
use Filament\Support\Icons\Heroicon;
TimePicker::make('at')
->prefixIcon(Heroicon::Play)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefixIcon()` and `suffixIcon()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/date-time-picker/prefix-icon" alt="Date time picker with prefix icon" version="4.x" />
#### Setting the affix icon's color
Affix icons are gray by default, but you may set a different color using the `prefixIconColor()` and `suffixIconColor()` methods:
```php
use Filament\Forms\Components\TimePicker;
use Filament\Support\Icons\Heroicon;
TimePicker::make('at')
->prefixIcon(Heroicon::CheckCircle)
->prefixIconColor('success')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `prefixIconColor()` and `suffixIconColor()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
## Making the field read-only
Not to be confused with [disabling the field](overview#disabling-a-field), you may make the field "read-only" using the `readonly()` method:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->readonly()
```
Please note that this setting is only enforced on native date pickers. If you're using the [JavaScript date picker](#enabling-the-javascript-date-picker), you'll need to use [`disabled()`](overview#disabling-a-field).
There are a few differences, compared to [`disabled()`](overview#disabling-a-field):
- When using `readOnly()`, the field will still be sent to the server when the form is submitted. It can be mutated with the browser console, or via JavaScript. You can use [`dehydrated(false)`](overview#preventing-a-field-from-being-dehydrated) to prevent this.
- There are no styling changes, such as less opacity, when using `readOnly()`.
- The field is still focusable when using `readOnly()`.
Optionally, you may pass a boolean value to control if the field should be read-only or not:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->readOnly(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `readOnly()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Date-time picker validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to date-time pickers.
### Max date / min date validation
You may restrict the minimum and maximum date that can be selected with the picker. The `minDate()` and `maxDate()` methods accept a `DateTime` instance (e.g. `Carbon`), or a string:
```php
use Filament\Forms\Components\DatePicker;
DatePicker::make('date_of_birth')
->native(false)
->minDate(now()->subYears(150))
->maxDate(now())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minDate()` and `maxDate()` methods also accept functions to dynamically calculate them. If the functions return `null`, the validation rule is not applied. You can inject various utilities into the functions as parameters. </UtilityInjection>
@@ -0,0 +1,694 @@
---
title: File upload
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The file upload field is based on [Filepond](https://pqina.nl/filepond).
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
```
<AutoScreenshot name="forms/fields/file-upload/simple" alt="File upload" version="4.x" />
<Aside variant="tip">
Filament also supports [`spatie/laravel-medialibrary`](https://github.com/spatie/laravel-medialibrary). See our [plugin documentation](/plugins/filament-spatie-media-library) for more information.
</Aside>
## Configuring the storage disk and directory
By default, files will be uploaded to the storage disk defined in the [configuration file](../introduction/installation#publishing-configuration). You can also set the `FILESYSTEM_DISK` environment variable to change this.
<Aside variant="tip">
To correctly preview images and other files, FilePond requires files to be served from the same domain as the app, or the appropriate CORS headers need to be present. Ensure that the `APP_URL` environment variable is correct, or modify the [filesystem](https://laravel.com/docs/filesystem) driver to set the correct URL. If you're hosting files on a separate domain like S3, ensure that CORS headers are set up.
</Aside>
To change the disk and directory for a specific field, and the visibility of files, use the `disk()`, `directory()` and `visibility()` methods. By default, files are uploaded with `private` visibility to your storage disk, unless the disk is set to `public`:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->disk('s3')
->directory('form-attachments')
->visibility('public')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `disk()`, `directory()` and `visibility()` methods accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters. </UtilityInjection>
<Aside variant="info">
It is the responsibility of the developer to delete these files from the disk if they are removed, as Filament is unaware if they are depended on elsewhere. One way to do this automatically is observing a [model event](https://laravel.com/docs/eloquent#events).
</Aside>
## Uploading multiple files
You may also upload multiple files. This stores URLs in JSON:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
```
Optionally, you may pass a boolean value to control if multiple files can be uploaded at once:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `multiple()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
If you're saving the file URLs using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'attachments' => 'array',
];
}
// ...
}
```
### Controlling the maximum parallel uploads
You can control the maximum number of parallel uploads using the `maxParallelUploads()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->maxParallelUploads(1)
```
This will limit the number of parallel uploads to `1`. If unset, we'll use the [default FilePond value](https://pqina.nl/filepond/docs/api/instance/properties/#core-properties) which is `2`.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `maxParallelUploads()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Controlling file names
By default, a random file name will be generated for newly-uploaded files. This is to ensure that there are never any conflicts with existing files.
### Security implications of controlling file names
Before using the `preserveFilenames()` or `getUploadedFileNameForStorageUsing()` methods, please be aware of the security implications. If you allow users to upload files with their own file names, there are ways that they can exploit this to upload malicious files. **This applies even if you use the [`acceptedFileTypes()`](#file-type-validation) method** to restrict the types of files that can be uploaded, since it uses Laravel's `mimetypes` rule which does not validate the extension of the file, only its mime type, which could be manipulated.
This is specifically an issue with the `getClientOriginalName()` method on the `TemporaryUploadedFile` object, which the `preserveFilenames()` method uses. By default, Livewire generates a random file name for each file uploaded, and uses the mime type of the file to determine the file extension.
Using these methods **with the `local` or `public` filesystem disks** will make your app vulnerable to remote code execution if the attacker uploads a PHP file with a deceptive mime type. **Using an S3 disk protects you from this specific attack vector**, as S3 will not execute PHP files in the same way that your server might when serving files from local storage.
If you are using the `local` or `public` disk, you should consider using the [`storeFileNamesIn()` method](#storing-original-file-names-independently) to store the original file names in a separate column in your database, and keep the randomly generated file names in the file system. This way, you can still display the original file names to users, while keeping the file system secure.
On top of this security issue, you should also be aware that allowing users to upload files with their own file names can lead to conflicts with existing files, and can make it difficult to manage your storage. Users could upload files with the same name and overwrite the other's content if you do not scope them to a specific directory, so these features should in all cases only be accessible to trusted users.
### Preserving original file names
<Aside variant="danger">
Before using this feature, please ensure that you have read the [security implications](#security-implications-of-controlling-file-names).
</Aside>
To preserve the original filenames of the uploaded files, use the `preserveFilenames()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->preserveFilenames()
```
Optionally, you may pass a boolean value to control if the original file names should be preserved:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->preserveFilenames(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `preserveFilenames()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Generating custom file names
<Aside variant="danger">
Before using this feature, please ensure that you have read the [security implications](#security-implications-of-controlling-file-names).
</Aside>
You may completely customize how file names are generated using the `getUploadedFileNameForStorageUsing()` method, and returning a string from the closure based on the `$file` that was uploaded:
```php
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
FileUpload::make('attachment')
->getUploadedFileNameForStorageUsing(
fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
->prepend('custom-prefix-'),
)
```
<UtilityInjection set="formFields" version="4.x" extras="File;;Livewire\Features\SupportFileUploads\TemporaryUploadedFile;;$file;;The temporary file object being uploaded.">You can inject various utilities into the function passed to `getUploadedFileNameForStorageUsing()` as parameters.</UtilityInjection>
### Storing original file names independently
You can keep the randomly generated file names, while still storing the original file name, using the `storeFileNamesIn()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->storeFileNamesIn('attachment_file_names')
```
`attachment_file_names` will now store the original file names of your uploaded files, so you can save them to the database when the form is submitted. If you're uploading `multiple()` files, make sure that you add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to this Eloquent model property too.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `storeFileNamesIn()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Avatar mode
You can enable avatar mode for your file upload field using the `avatar()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('avatar')
->avatar()
```
This will only allow images to be uploaded, and when they are, it will display them in a compact circle layout that is perfect for avatars.
This feature pairs well with the [circle cropper](#allowing-users-to-crop-images-as-a-circle).
## Image editor
You can enable an image editor for your file upload field using the `imageEditor()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
```
You can open the editor once you upload an image by clicking the pencil icon. You can also open the editor by clicking the pencil icon on an existing image, which will remove and re-upload it on save.
Optionally, you may pass a boolean value to control if the image editor is enabled:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `imageEditor()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Allowing users to crop images to aspect ratios
You can allow users to crop images to a set of specific aspect ratios using the `imageEditorAspectRatios()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
->imageEditorAspectRatios([
'16:9',
'4:3',
'1:1',
])
```
You can also allow users to choose no aspect ratio, "free cropping", by passing `null` as an option:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
->imageEditorAspectRatios([
null,
'16:9',
'4:3',
'1:1',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `imageEditorAspectRatios()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Setting the image editor's mode
You can change the mode of the image editor using the `imageEditorMode()` method, which accepts either `1`, `2` or `3`. These options are explained in the [Cropper.js documentation](https://github.com/fengyuanchen/cropperjs#viewmode):
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
->imageEditorMode(2)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `imageEditorMode()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing the image editor's empty fill color
By default, the image editor will make the empty space around the image transparent. You can customize this using the `imageEditorEmptyFillColor()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
->imageEditorEmptyFillColor('#000000')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `imageEditorEmptyFillColor()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Setting the image editor's viewport size
You can change the size of the image editor's viewport using the `imageEditorViewportWidth()` and `imageEditorViewportHeight()` methods, which generate an aspect ratio to use across device sizes:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageEditor()
->imageEditorViewportWidth('1920')
->imageEditorViewportHeight('1080')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `imageEditorViewportWidth()` and `imageEditorViewportHeight()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
### Allowing users to crop images as a circle
You can allow users to crop images as a circle using the `circleCropper()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->avatar()
->imageEditor()
->circleCropper()
```
This is perfectly accompanied by the [`avatar()` method](#avatar-mode), which renders the images in a compact circle layout.
Optionally, you may pass a boolean value to control if the circle cropper is enabled:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->avatar()
->imageEditor()
->circleCropper(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `circleCropper()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Cropping and resizing images without the editor
Filepond allows you to crop and resize images before they are uploaded, without the need for a separate editor. You can customize this behavior using the `imageCropAspectRatio()`, `imageResizeTargetHeight()` and `imageResizeTargetWidth()` methods. `imageResizeMode()` should be set for these methods to have an effect - either [`force`, `cover`, or `contain`](https://pqina.nl/filepond/docs/api/plugins/image-resize).
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
->imageResizeMode('cover')
->imageCropAspectRatio('16:9')
->imageResizeTargetWidth('1920')
->imageResizeTargetHeight('1080')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `imageResizeMode()`, `imageCropAspectRatio()`, `imageResizeTargetHeight()` and `imageResizeTargetWidth()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
## Altering the appearance of the file upload area
You may also alter the general appearance of the Filepond component. Available options for these methods are available on the [Filepond website](https://pqina.nl/filepond/docs/api/instance/properties/#styles).
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->imagePreviewHeight('250')
->loadingIndicatorPosition('left')
->panelAspectRatio('2:1')
->panelLayout('integrated')
->removeUploadedFileButtonPosition('right')
->uploadButtonPosition('left')
->uploadProgressIndicatorPosition('left')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, these methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
### Displaying files in a grid
You can use the [Filepond `grid` layout](https://pqina.nl/filepond/docs/api/style/#grid-layout) by setting the `panelLayout()`:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->panelLayout('grid')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `panelLayout()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Reordering files
You can also allow users to re-order uploaded files using the `reorderable()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->reorderable()
```
When using this method, FilePond may add newly-uploaded files to the beginning of the list, instead of the end. To fix this, use the `appendFiles()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->reorderable()
->appendFiles()
```
Optionally, the `reorderable()` and `appendFiles()` methods accept a boolean value to control if the files can be reordered and if new files should be appended to the end of the list:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->reorderable(FeatureFlag::active())
->appendFiles(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderable()` and `appendFiles()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
## Opening files in a new tab
You can add a button to open each file in a new tab with the `openable()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->openable()
```
Optionally, you may pass a boolean value to control if the files can be opened in a new tab:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->openable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `openable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Downloading files
If you wish to add a download button to each file instead, you can use the `downloadable()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->downloadable()
```
Optionally, you may pass a boolean value to control if the files can be downloaded:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->downloadable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `downloadable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Previewing files
By default, some file types can be previewed in FilePond. If you wish to disable the preview for all files, you can use the `previewable(false)` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->previewable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `previewable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Moving files instead of copying when the form is submitted
By default, files are initially uploaded to Livewire's temporary storage directory, and then copied to the destination directory when the form is submitted. If you wish to move the files instead, providing that temporary uploads are stored on the same disk as permanent files, you can use the `moveFiles()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->moveFiles()
```
Optionally, you may pass a boolean value to control if the files should be moved:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->moveFiles(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `moveFiles()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Preventing files from being stored permanently
If you wish to prevent files from being stored permanently when the form is submitted, you can use the `storeFiles(false)` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->storeFiles(false)
```
When the form is submitted, a temporary file upload object will be returned instead of a permanently stored file path. This is perfect for temporary files like imported CSVs.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `storeFiles()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="warning">
Images, video and audio files will not show the stored file name in the form's preview, unless you use [`previewable(false)`](#previewing-files). This is due to a limitation with the FilePond preview plugin.
</Aside>
## Orienting images from their EXIF data
By default, FilePond will automatically orient images based on their EXIF data. If you wish to disable this behavior, you can use the `orientImagesFromExif(false)` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->orientImagesFromExif(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `orientImagesFromExif()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Hiding the remove file button
It is also possible to hide the remove uploaded file button by using `deletable(false)`:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->deletable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `deletable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Preventing pasting files
You can disable the ability to paste files via the clipboard using the `pasteable(false)` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->pasteable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `pasteable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Preventing file information fetching
While the form is loaded, it will automatically detect whether the files exist, what size they are, and what type of files they are. This is all done on the backend. When using remote storage with many files, this can be time-consuming. You can use the `fetchFileInformation(false)` method to disable this feature:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->fetchFileInformation(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `fetchFileInformation()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Customizing the uploading message
You may customize the uploading message that is displayed in the form's submit button using the `uploadingMessage()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->uploadingMessage('Uploading attachment...')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `uploadingMessage()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## File upload validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to file uploads.
Since Filament is powered by Livewire and uses its file upload system, you will want to refer to the default Livewire file upload validation rules in the `config/livewire.php` file as well. This also controls the 12MB file size maximum.
### File type validation
You may restrict the types of files that may be uploaded using the `acceptedFileTypes()` method, and passing an array of MIME types.
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('document')
->acceptedFileTypes(['application/pdf'])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `acceptedFileTypes()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
You may also use the `image()` method as shorthand to allow all image MIME types.
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('image')
->image()
```
#### Custom MIME type mapping
Some file formats may not be recognized correctly by the browser when uploading files. Filament allows you to manually define MIME types for specific file extensions using the `mimeTypeMap()` method:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('designs')
->acceptedFileTypes([
'x-world/x-3dmf',
'application/vnd.sketchup.skp',
])
->mimeTypeMap([
'3dm' => 'x-world/x-3dmf',
'skp' => 'application/vnd.sketchup.skp',
]);
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `mimeTypeMap()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### File size validation
You may also restrict the size of uploaded files in kilobytes:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachment')
->minSize(512)
->maxSize(1024)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minSize()` and `maxSize()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
#### Uploading large files
If you experience issues when uploading large files, such as HTTP requests failing with a response status of 422 in the browser's console, you may need to tweak your configuration.
In the `php.ini` file for your server, increasing the maximum file size may fix the issue:
```ini
post_max_size = 120M
upload_max_filesize = 120M
```
Livewire also validates file size before uploading. To publish the Livewire config file, run:
```bash
php artisan livewire:publish --config
```
The [max upload size can be adjusted in the `rules` key of `temporary_file_upload`](https://livewire.laravel.com/docs/uploads#global-validation). In this instance, KB are used in the rule, and 120MB is 122880KB:
```php
'temporary_file_upload' => [
// ...
'rules' => ['required', 'file', 'max:122880'],
// ...
],
```
### Number of files validation
You may customize the number of files that may be uploaded, using the `minFiles()` and `maxFiles()` methods:
```php
use Filament\Forms\Components\FileUpload;
FileUpload::make('attachments')
->multiple()
->minFiles(2)
->maxFiles(5)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minFiles()` and `maxFiles()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
@@ -0,0 +1,933 @@
---
title: Rich editor
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The rich editor allows you to edit and preview HTML content, as well as upload images. It uses [TipTap](https://tiptap.dev) as the underlying editor.
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
```
<AutoScreenshot name="forms/fields/rich-editor/simple" alt="Rich editor" version="4.x" />
## Storing content as JSON
By default, the rich editor stores content as HTML. If you would like to store the content as JSON instead, you can use the `json()` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->json()
```
The JSON is in [TipTap's](https://tiptap.dev) format, which is a structured representation of the content.
If you're saving the JSON tags using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'content' => 'array',
];
}
// ...
}
```
## Customizing the toolbar buttons
You may set the toolbar buttons for the editor using the `toolbarButtons()` method. The options shown here are the defaults:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->toolbarButtons([
['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'link'],
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
['table', 'attachFiles'], // The `customBlocks` and `mergeTags` tools are also added here if those features are used.
['undo', 'redo'],
])
```
Each nested array in the main array represents a group of buttons in the toolbar.
Additional tools available in the toolbar include:
- `h1` - Applies the "h1" tag to the text.
- `alignJustify` - Justifies the text.
- `clearFormatting` - Clears all formatting from the selected text.
- `details` - Inserts a `<details>` tag, which allows users to create collapsible sections in their content.
- `grid` - Inserts a grid layout into the editor, allowing users to create responsive columns of content.
- `gridDelete` - Deletes the current grid layout.
- `highlight` - Highlights the selected text with a `<mark>` tag around it.
- `horizontalRule` - Inserts a horizontal rule.
- `lead` - Applies a `lead` class around the text, which is typically used for the first paragraph of an article.
- `small` - Applies the `<small>` tag to the text, which is typically used for small print or disclaimers.
- `code` - Format the selected text as inline code.
- `textColor` - Changes the [text color](#customizing-text-colors) of the selected text.
- `table` - Creates a table in the editor with a default layout of 3 columns and 2 rows, with the first row configured as a header row.
- `tableAddColumnBefore` - Adds a new column before the current column.
- `tableAddColumnAfter` - Adds a new column after the current column.
- `tableDeleteColumn` - Deletes the current column.
- `tableAddRowBefore` - Adds a new row above the current row.
- `tableAddRowAfter` - Adds a new row below the current row.
- `tableDeleteRow` - Deletes the current row.
- `tableMergeCells` - Merges the selected cells into one cell.
- `tableSplitCell` - Splits the selected cell into multiple cells.
- `tableToggleHeaderRow` - Toggles the header row of the table.
- `tableDelete` - Deletes the table.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `toolbarButtons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Customizing floating toolbars
If your toolbar is too full, you can use a floating toolbar to show certain tools in a toolbar below the cursor, only when the user is inside a specific node type. This allows you to keep the main toolbar clean while still providing access to additional tools when needed.
You can customize the floating toolbars that appear when your cursor is placed inside a specific node by using the `floatingToolbars()` method.
In the example below, a floating toolbar appears when the cursor is inside a paragraph node. It shows bold, italic, and similar buttons. When the cursor is in a heading node, it displays heading-related buttons, and when inside a table, it shows table-specific controls.
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->floatingToolbars([
'paragraph' => [
'bold', 'italic', 'underline', 'strike', 'subscript', 'superscript',
],
'heading' => [
'h1', 'h2', 'h3',
],
'table' => [
'tableAddColumnBefore', 'tableAddColumnAfter', 'tableDeleteColumn',
'tableAddRowBefore', 'tableAddRowAfter', 'tableDeleteRow',
'tableMergeCells', 'tableSplitCell',
'tableToggleHeaderRow',
'tableDelete',
],
])
```
## Customizing text colors
The rich editor includes a text color tool for styling inline text. By default, it uses the [Tailwind CSS color palette](https://tailwindcss.com/docs/colors). In light mode, the 600 shades are applied to text, and in dark mode, the 400 shades are used.
You can customize which colors are available in the picker using the `textColors()` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->textColors([
'#ef4444' => 'Red',
'#10b981' => 'Green',
'#0ea5e9' => 'Sky',
])
```
If you would like to define different colors for light and dark mode, you can use the a `TextColor` object to define the color:
```php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\TextColor;
RichEditor::make('content')
->textColors([
'brand' => TextColor::make('Brand', '#0ea5e9'),
'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
])
```
If you would like to add new colors onto the existing Tailwind palette, you can merge your colors into the `TextColor::getDefaults()` array:
```php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\TextColor;
RichEditor::make('content')
->textColors([
'brand' => TextColor::make('Brand', '#0ea5e9'),
'warning' => TextColor::make('Warning', '#f59e0b', darkColor: '#fbbf24'),
...TextColor::getDefaults(),
])
```
When you use a `TextColor` object, the key of the array becomes the stored `data-color` attribute on the `<span>` tag, allowing you to reference the color in your CSS if needed. When you use the color as the array values, the actual color value (e.g., a HEX string) is stored as the `data-color` attribute.
You can also pass `textColors()` to the [content renderer](#rendering-rich-content) and [rich content attribute](#registering-rich-content-attributes) so that server-side rendering matches your editor configuration.
You can also allow users to pick custom colors that aren't in the predefined list by using the `customTextColors()` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->textColors([
// ...
])
->customTextColors()
```
You do not need to use `customTextColors()` on the [content renderer](#rendering-rich-content), as it will automatically render any custom colors that are used in the content.
## Rendering rich content
If you're [storing content as JSON](#storing-content-as-json) instead of HTML, or your content requires processing to inject [private image URLs](#using-private-images-in-the-editor) or similar, you'll need to use the `RichContentRenderer` tool in Filament to output HTML:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)->toHtml()
```
The `toHtml()` method returns a string. If you would like to output HTML in a Blade view without escaping the HTML, you can echo the `RichContentRender` object without calling `toHtml()`:
```blade
{{ \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) }}
```
If you have configured the [file attachments behavior](#uploading-images-to-the-editor) of the editor to change the disk or visibility of the uploaded files, you must also pass these settings to the renderer to ensure that the correct URLs are generated:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->fileAttachmentsDisk('s3')
->fileAttachmentsVisibility('private')
->toHtml()
```
If you are using [custom blocks](#using-custom-blocks) in the rich editor, you can pass an array of custom blocks to the renderer to ensure that they are rendered correctly:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class => [
'categoryUrl' => $record->category->getUrl(),
],
CallToActionBlock::class,
])
->toHtml()
```
If you are using [merge tags](#using-merge-tags), you can pass an array of values to replace the merge tags with:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
```
If you are using [custom text colors](#customizing-text-colors), you can pass an array of colors to the renderer to ensure that the colors are rendered correctly:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Filament\Forms\Components\RichEditor\TextColor;
RichContentRenderer::make($record->content)
->textColors([
'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
])
->toHtml();
```
### Styling the rendered content
The rich editor HTML uses a combination of HTML elements, CSS classes, and inline styles to style the content, depending on the features used in the editor. If you render the content in a Filament table column or infolist entry with `prose()`, Filament will automatically apply the necessary styles for you. If you are outputting the content in your own Blade view, you may need to add some additional styles to ensure that the content is styled correctly.
One way of styling the content is to use [Tailwind CSS Typography](https://tailwindcss.com/docs/typography-plugin). This plugin provides a set of pre-defined styles for common HTML elements, such as headings, paragraphs, lists, and tables. You can apply these styles to a container element using the `prose` class:
```blade
<div class="prose dark:prose-invert">
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>
```
However, some features, such as the grid layout and text colors, require additional styles that are not included in the Tailwind CSS Typography plugin. Filament also includes its own `fi-prose` CSS class that adds these additional styles. Any app that loads Filament's `vendor/filament/support/resources/css/index.css` CSS will have access to this class. The styling is different to the `prose` class, but fits with Filament's design system better:
```blade
<div class="fi-prose">
{!! \Filament\Forms\Components\RichEditor\RichContentRenderer::make($record->content) !!}
</div>
```
## Security
By default, the editor outputs raw HTML, and sends it to the backend. Attackers are able to intercept the value of the component and send a different raw HTML string to the backend. As such, it is important that when outputting the HTML from a rich editor, it is sanitized; otherwise your site may be exposed to Cross-Site Scripting (XSS) vulnerabilities.
When Filament outputs raw HTML from the database in components such as `TextColumn` and `TextEntry`, it sanitizes it to remove any dangerous JavaScript. However, if you are outputting the HTML from a rich editor in your own Blade view, this is your responsibility. One option is to use Filament's `sanitizeHtml()` helper to do this, which is the same tool we use to sanitize HTML in the components mentioned above:
```blade
{!! str($record->content)->sanitizeHtml() !!}
```
If you're [storing content as JSON](#storing-content-as-json) instead of HTML, or your content requires processing to inject [private image URLs](#using-private-images-in-the-editor) or similar, you can use the [content renderer](#rendering-rich-content) to output HTML. This will automatically sanitize the HTML for you, so you don't need to worry about it.
## Uploading images to the editor
By default, uploaded images are stored publicly on your storage disk, so that the rich content stored in the database can be output easily anywhere. You may customize how images are uploaded using configuration methods:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsDisk('s3')
->fileAttachmentsDirectory('attachments')
->fileAttachmentsVisibility('private')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `fileAttachmentsDisk()`, `fileAttachmentsDirectory()`, and `fileAttachmentsVisibility()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="tip">
Filament also supports [`spatie/laravel-medialibrary`](https://github.com/spatie/laravel-medialibrary) for storing rich editor file attachments. See our [plugin documentation](/plugins/filament-spatie-media-library#using-media-library-for-rich-editor-file-attachments) for more information.
</Aside>
### Using private images in the editor
Using private images in the editor adds a layer of complexity to the process, since private images cannot be accessed directly via a permanent URL. Each time the editor is loaded or its content is rendered, temporary URLs need to be generated for each image, which are never stored in the database. Instead, Filament adds a `data-id` attribute to the image tags, which contains an identifier for the image in the storage disk, so that a temporary URL can be generated on demand.
When rendering the content using private images, ensure that you are using the [`RichContentRenderer` tool](#rendering-rich-content) in Filament to output HTML:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->fileAttachmentsDisk('s3')
->fileAttachmentsVisibility('private')
->toHtml()
```
### Validating uploaded images
You may use the `fileAttachmentsAcceptedFileTypes()` method to control a list of accepted mime types for uploaded images. By default, `image/png`, `image/jpeg`, `image/gif`, and `image/webp` are accepted:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsAcceptedFileTypes(['image/png', 'image/jpeg'])
```
You may use the `fileAttachmentsMaxSize()` method to control the maximum file size for uploaded images. The size is specified in kilobytes. By default, the maximum size is 12288 KB (12 MB):
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->fileAttachmentsMaxSize(5120) // 5 MB
```
## Using custom blocks
Custom blocks are elements that users can drag and drop into the rich editor. You can define custom blocks that user can insert into the rich editor using the `customBlocks()` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
```
To create a custom block, you can use the following command:
```bash
php artisan make:filament-rich-content-custom-block HeroBlock
```
Each block needs a corresponding class that extends the `Filament\Forms\Components\RichEditor\RichContentCustomBlock` class. The `getId()` method should return a unique identifier for the block, and the `getLabel()` method should return the label that will be displayed in the editor's side panel:
```php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeroBlock extends RichContentCustomBlock
{
public static function getId(): string
{
return 'hero';
}
public static function getLabel(): string
{
return 'Hero section';
}
}
```
When a user drags a custom block into the editor, you can choose to open a modal to collect additional information from the user before inserting the block. To do this, you can use the `configureEditorAction()` method to configure the [modal](../actions/modals) that will be opened when the block is inserted:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeroBlock extends RichContentCustomBlock
{
// ...
public static function configureEditorAction(Action $action): Action
{
return $action
->modalDescription('Configure the hero section')
->schema([
TextInput::make('heading')
->required(),
TextInput::make('subheading'),
]);
}
}
```
The `schema()` method on the action can define form fields that will be displayed in the modal. When the user submits the form, the form data will be saved as "configuration" for that block.
### Rendering a preview for a custom block
Once a block is inserted into the editor, you may define a "preview" for it using the `toPreviewHtml()` method. This method should return a string of HTML that will be displayed in the editor when the block is inserted, allowing users to see what the block will look like before they save it. You can access the `$config` for the block in this method, which contains the data that was submitted in the modal when the block was inserted:
```php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeroBlock extends RichContentCustomBlock
{
// ...
/**
* @param array<string, mixed> $config
*/
public static function toPreviewHtml(array $config): string
{
return view('filament.forms.components.rich-editor.rich-content-custom-blocks.hero.preview', [
'heading' => $config['heading'],
'subheading' => $config['subheading'] ?? 'Default subheading',
])->render();
}
}
```
The `getPreviewLabel()` can be defined if you would like to customize the label that is displayed above the preview in the editor. By default, it will use the label defined in the `getLabel()` method, but the `getPreviewLabel()` is able to access the `$config` for the block, allowing you to display dynamic information in the label:
```php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeroBlock extends RichContentCustomBlock
{
// ...
/**
* @param array<string, mixed> $config
*/
public static function getPreviewLabel(array $config): string
{
return "Hero section: {$config['heading']}";
}
}
```
### Rendering content with custom blocks
When rendering the rich content, you can pass the array of custom blocks to the `RichContentRenderer` to ensure that the blocks are rendered correctly:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
->toHtml()
```
Each block class may have a `toHtml()` method that returns the HTML that should be rendered for that block:
```php
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
class HeroBlock extends RichContentCustomBlock
{
// ...
/**
* @param array<string, mixed> $config
* @param array<string, mixed> $data
*/
public static function toHtml(array $config, array $data): string
{
return view('filament.forms.components.rich-editor.rich-content-custom-blocks.hero.index', [
'heading' => $config['heading'],
'subheading' => $config['subheading'],
'buttonLabel' => 'View category',
'buttonUrl' => $data['categoryUrl'],
])->render();
}
}
```
As seen above, the `toHtml()` method receives two parameters: `$config`, which contains the configuration data submitted in the modal when the block was inserted, and `$data`, which contains any additional data that may be needed to render the block. This allows you to access the configuration data and render the block accordingly. The data can be passed in the `customBlocks()` method:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->customBlocks([
HeroBlock::class => [
'categoryUrl' => $record->category->getUrl(),
],
CallToActionBlock::class,
])
->toHtml()
```
### Opening the custom blocks panel by default
If you want the custom blocks panel to be open by default when the rich editor is loaded, you can use the `activePanel('customBlocks')` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->customBlocks([
HeroBlock::class,
CallToActionBlock::class,
])
->activePanel('customBlocks')
```
## Using merge tags
Merge tags allow the user to insert "placeholders" into their rich content, which can be replaced with dynamic values when the content is rendered. This is useful for inserting things like the current user's name, or the current date.
To register merge tags on an editor, use the `mergeTags()` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name',
'today',
])
```
Merge tags are surrounded by double curly braces, like `{{ name }}`. When the content is rendered, these tags will be replaced with the corresponding values.
To insert a merge tag into the content, users can start typing `{{` to search for a tag to insert. Alternatively, they can click on the "merge tags" tool in the editor's toolbar, which opens a panel containing all the merge tags. They can then drag a merge tag from the editor's side panel into the content or click to insert it.
### Rendering content with merge tags
When rendering the rich content, you can pass an array of values to replace the merge tags with:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
```
If you have many merge tags or you need to run some logic to determine the values, you can use a function as the value of each merge tag. This function will be called when a merge tag is first encountered in the content, and its result is cached for subsequent tags of the same name:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichContentRenderer::make($record->content)
->mergeTags([
'name' => fn (): string => $record->user->name,
'today' => now()->toFormattedDateString(),
])
->toHtml()
```
#### Using HTML content in merge tags
By default, merge tags render their values as plain text. However, you can render HTML content in merge tags by providing values that implement Laravel's `Htmlable` interface. This is useful for inserting formatted content, links, or other HTML elements:
```php
use Filament\Forms\Components\RichEditor\RichContentRenderer;
use Illuminate\Support\HtmlString;
RichContentRenderer::make($record->content)
->mergeTags([
'user_name' => $record->user->name, // Plain text
'user_profile_link' => new HtmlString('<a href="' . route('profile', $record->user) . '">View Profile</a>'),
])
->toHtml()
```
When a merge tag value implements the `Htmlable` interface (such as `HtmlString`), the system automatically detects this and renders the HTML content without escaping it. Non-`Htmlable` values continue to be rendered as plain text for security.
### Using custom merge tag labels
You may provide custom labels for merge tags that will be displayed in the editor's side panel and content preview using an associative array where the keys are the merge tag names and the values are the labels:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name' => 'Full name',
'today' => 'Today\'s date',
])
```
The labels aren't saved in the content of the editor and are only used for display purposes.
### Opening the merge tags panel by default
If you want the merge tags panel to be open by default when the rich editor is loaded, you can use the `activePanel('mergeTags')` method:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
->mergeTags([
'name',
'today',
])
->activePanel('mergeTags')
```
## Registering rich content attributes
There are elements of the rich editor configuration that apply to both the editor and the renderer. For example, if you are using [private images](#using-private-images-in-the-editor), [custom blocks](#using-custom-blocks), [merge tags](#using-merge-tags), or [plugins](#extending-the-rich-editor), you need to ensure that the same configuration is used in both places. To do this, Filament provides you with a way to register rich content attributes that can be used in both the editor and the renderer.
To register rich content attributes on an Eloquent model, you should use the `InteractsWithRichContent` trait and implement the `HasRichContent` interface. This allows you to register the attributes in the `setUpRichContent()` method:
```php
use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent;
use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent;
use Illuminate\Database\Eloquent\Model;
class Post extends Model implements HasRichContent
{
use InteractsWithRichContent;
public function setUpRichContent(): void
{
$this->registerRichContent('content')
->fileAttachmentsDisk('s3')
->fileAttachmentsVisibility('private')
->customBlocks([
HeroBlock::class => [
'categoryUrl' => fn (): string => $this->category->getUrl(),
],
CallToActionBlock::class,
])
->mergeTags([
'name' => fn (): string => $this->user->name,
'today' => now()->toFormattedDateString(),
])
->mergeTagLabels([
'name' => 'Full name',
'today' => 'Today\'s date',
])
->textColors(
'brand' => TextColor::make('Brand', '#0ea5e9', darkColor: '#38bdf8'),
)
->customTextColors()
->plugins([
HighlightRichContentPlugin::make(),
]);
}
}
```
Whenever you use the `RichEditor` component, the configuration registered for the corresponding attribute will be used:
```php
use Filament\Forms\Components\RichEditor;
RichEditor::make('content')
```
To easily render the rich content HTML from a model with the given configuration, you can call the `renderRichContent()` method on the model, passing the name of the attribute:
```blade
{!! $record->renderRichContent('content') !!}
```
Alternatively, you can get an `Htmlable` object to render without escaping the HTML:
```blade
{{ $record->getRichContentAttribute('content') }}
```
When using a [text column](../tables/columns/text) in a table or a [text entry](../infolists/text-entry) in an infolist, you don't need to manually render the rich content. Filament will do this for you automatically:
```php
use Filament\Infolists\Components\TextEntry;
use Filament\Tables\Columns\TextColumn;
TextColumn::make('content')
TextEntry::make('content')
```
## Extending the rich editor
You can create plugins for the rich editor, which allow you to add custom TipTap extensions to the editor and renderer, as well as custom toolbar buttons. Create a new class that implements the `RichContentPlugin` interface:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\EditorCommand;
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
use Filament\Forms\Components\RichEditor\RichEditorTool;
use Filament\Support\Enums\Width;
use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Icons\Heroicon;
use Tiptap\Core\Extension;
use Tiptap\Marks\Highlight;
class HighlightRichContentPlugin implements RichContentPlugin
{
public static function make(): static
{
return app(static::class);
}
/**
* @return array<Extension>
*/
public function getTipTapPhpExtensions(): array
{
// This method should return an array of PHP TipTap extension objects.
// See: https://github.com/ueberdosis/tiptap-php
return [
app(Highlight::class, [
'options' => ['multicolor' => true],
]),
];
}
/**
* @return array<string>
*/
public function getTipTapJsExtensions(): array
{
// This method should return an array of URLs to JavaScript files containing
// TipTap extensions that should be asynchronously loaded into the editor
// when the plugin is used.
return [
FilamentAsset::getScriptSrc('rich-content-plugins/highlight'),
];
}
/**
* @return array<RichEditorTool>
*/
public function getEditorTools(): array
{
// This method should return an array of `RichEditorTool` objects, which can then be
// used in the `toolbarButtons()` of the editor.
// The `jsHandler()` method allows you to access the TipTap editor instance
// through `$getEditor()`, and `chain()` any TipTap commands to it.
// See: https://tiptap.dev/docs/editor/api/commands
// The `action()` method allows you to run an action (registered in the `getEditorActions()`
// method) when the toolbar button is clicked. This allows you to open a modal to
// collect additional information from the user before running a command.
return [
RichEditorTool::make('highlight')
->jsHandler('$getEditor()?.chain().focus().toggleHighlight().run()')
->icon(Heroicon::CursorArrowRays),
RichEditorTool::make('highlightWithCustomColor')
->action(arguments: '{ color: $getEditor().getAttributes(\'highlight\')?.[\'data-color\'] }')
->icon(Heroicon::CursorArrowRipple),
];
}
/**
* @return array<Action>
*/
public function getEditorActions(): array
{
// This method should return an array of `Action` objects, which can be used by the tools
// registered in the `getEditorTools()` method. The name of the action should match
// the name of the tool that uses it.
// The `runCommands()` method allows you to run TipTap commands on the editor instance.
// It accepts an array of `EditorCommand` objects that define the command to run,
// as well as any arguments to pass to the command. You should also pass in the
// `editorSelection` argument, which is the current selection in the editor
// to apply the commands to.
return [
Action::make('highlightWithCustomColor')
->modalWidth(Width::Large)
->fillForm(fn (array $arguments): array => [
'color' => $arguments['color'] ?? null,
])
->schema([
ColorPicker::make('color'),
])
->action(function (array $arguments, array $data, RichEditor $component): void {
$component->runCommands(
[
EditorCommand::make(
'toggleHighlight',
arguments: [[
'color' => $data['color'],
]],
),
],
editorSelection: $arguments['editorSelection'],
);
}),
];
}
}
```
You can use the `plugins()` method to register your plugin with the rich editor and [rich content renderer](#rendering-rich-content):
```php
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\RichContentRenderer;
RichEditor::make('content')
->toolbarButtons([
['bold', 'highlight', 'highlightWithCustomColor'],
['h2', 'h3'],
['bulletList', 'orderedList'],
])
->plugins([
HighlightRichContentPlugin::make(),
])
RichContentRenderer::make($record->content)
->plugins([
HighlightRichContentPlugin::make(),
])
```
### Setting up a TipTap JavaScript extension
Filament is able to asynchronously load JavaScript extensions for TipTap. To do this, you need to create a JavaScript file that contains the extension, and register it in the `getTipTapJsExtensions()` method of your [plugin](#extending-the-rich-editor).
For instance, if you wanted to use the [TipTap highlight extension](https://tiptap.dev/docs/editor/extensions/marks/highlight), make sure it is installed first:
```bash
npm install @tiptap/extension-highlight --save-dev
```
Then, create a JavaScript file that imports the extension. In this example, create a file called `highlight.js` in the `resources/js/filament/rich-content-plugins` directory, and add the following code to it:
```javascript
import Highlight from '@tiptap/extension-highlight'
export default Highlight.configure({
multicolor: true,
})
```
One way to compile this file is to use [esbuild](https://esbuild.github.io). You can install it using `npm`:
```bash
npm install esbuild --save-dev
```
You must create an [esbuild](https://esbuild.github.io) script to compile the file. You can put this anywhere, for example `bin/build.js`:
```js
import * as esbuild from 'esbuild'
async function compile(options) {
const context = await esbuild.context(options)
await context.rebuild()
await context.dispose()
}
compile({
define: {
'process.env.NODE_ENV': `'production'`,
},
bundle: true,
mainFields: ['module', 'main'],
platform: 'neutral',
sourcemap: false,
sourcesContent: false,
treeShaking: true,
target: ['es2020'],
minify: true,
entryPoints: ['./resources/js/filament/rich-content-plugins/highlight.js'],
outfile: './resources/js/dist/filament/rich-content-plugins/highlight.js',
})
```
As you can see at the bottom of the script, we are compiling a file called `resources/js/filament/rich-content-plugins/highlight.js` into `resources/js/dist/filament/rich-content-plugins/highlight.js`. You can change these paths to suit your needs. You can compile as many files as you want.
To run the script and compile this file into `resources/js/dist/filament/rich-content-plugins/highlight.js` run the following command:
```bash
node bin/build.js
```
You should register it in the `boot()` method of a service provider, like `AppServiceProvider`, and use `loadedOnRequest()` so that it is not downloaded until the rich editor is loaded on a page:
```php
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Js::make('rich-content-plugins/highlight', __DIR__ . '/../../resources/js/dist/filament/rich-content-plugins/highlight.js')->loadedOnRequest(),
]);
```
To publish this new JavaScript file into the `/public` directory of your app so that it can be served, you can use the `filament:assets` command:
```bash
php artisan filament:assets
```
In the [plugin object](#extending-the-rich-editor), the `getTipTapJsExtensions()` method should return the path to the JavaScript file you just created. Now that it's registered with `FilamentAsset`, you can use the `getScriptSrc()` method to get the URL to the file:
```php
use Filament\Support\Facades\FilamentAsset;
/**
* @return array<string>
*/
public function getTipTapJsExtensions(): array
{
return [
FilamentAsset::getScriptSrc('rich-content-plugins/highlight'),
];
}
```
@@ -0,0 +1,82 @@
---
title: Markdown editor
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The markdown editor allows you to edit and preview markdown content, as well as upload images using drag and drop.
```php
use Filament\Forms\Components\MarkdownEditor;
MarkdownEditor::make('content')
```
<AutoScreenshot name="forms/fields/markdown-editor/simple" alt="Markdown editor" version="4.x" />
## Security
By default, the editor outputs raw Markdown and HTML, and sends it to the backend. Attackers are able to intercept the value of the component and send a different raw HTML string to the backend. As such, it is important that when outputting the HTML from a Markdown editor, it is sanitized; otherwise your site may be exposed to Cross-Site Scripting (XSS) vulnerabilities.
When Filament outputs raw HTML from the database in components such as `TextColumn` and `TextEntry`, it sanitizes it to remove any dangerous JavaScript. However, if you are outputting the HTML from a Markdown editor in your own Blade view, this is your responsibility. One option is to use Filament's `sanitizeHtml()` helper to do this, which is the same tool we use to sanitize HTML in the components mentioned above:
```blade
{!! str($record->content)->markdown()->sanitizeHtml() !!}
```
## Customizing the toolbar buttons
You may set the toolbar buttons for the editor using the `toolbarButtons()` method. The options shown here are the defaults:
```php
use Filament\Forms\Components\MarkdownEditor;
MarkdownEditor::make('content')
->toolbarButtons([
['bold', 'italic', 'strike', 'link'],
['heading'],
['blockquote', 'codeBlock', 'bulletList', 'orderedList'],
['table', 'attachFiles'],
['undo', 'redo'],
])
```
Each nested array in the main array represents a group of buttons in the toolbar.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `toolbarButtons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Uploading images to the editor
Images may be uploaded to the editor. They will always be uploaded to a publicly available URL with public storage permissions, since generating temporary file upload URLs is not supported in static content. You may customize where images are uploaded using configuration methods:
```php
use Filament\Forms\Components\MarkdownEditor;
MarkdownEditor::make('content')
->fileAttachmentsDisk('s3')
->fileAttachmentsDirectory('attachments')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `fileAttachmentsDisk()` and `fileAttachmentsDirectory()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Validating uploaded images
You may use the `fileAttachmentsAcceptedFileTypes()` method to control a list of accepted mime types for uploaded images. By default, `image/png`, `image/jpeg`, `image/gif`, and `image/webp` are accepted:
```php
use Filament\Forms\Components\MarkdownEditor;
MarkdownEditor::make('content')
->fileAttachmentsAcceptedFileTypes(['image/png', 'image/jpeg'])
```
You may use the `fileAttachmentsMaxSize()` method to control the maximum file size for uploaded images. The size is specified in kilobytes. By default, the maximum size is 12288 KB (12 MB):
```php
use Filament\Forms\Components\MarkdownEditor;
MarkdownEditor::make('content')
->fileAttachmentsMaxSize(5120) // 5 MB
```
@@ -0,0 +1,951 @@
---
title: Repeater
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The repeater component allows you to output a JSON array of repeated form components.
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
Repeater::make('members')
->schema([
TextInput::make('name')->required(),
Select::make('role')
->options([
'member' => 'Member',
'administrator' => 'Administrator',
'owner' => 'Owner',
])
->required(),
])
->columns(2)
```
<AutoScreenshot name="forms/fields/repeater/simple" alt="Repeater" version="4.x" />
We recommend that you store repeater data with a `JSON` column in your database. Additionally, if you're using Eloquent, make sure that column has an `array` cast.
As evident in the above example, the component schema can be defined within the `schema()` method of the component:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
Repeater::make('members')
->schema([
TextInput::make('name')->required(),
// ...
])
```
If you wish to define a repeater with multiple schema blocks that can be repeated in any order, please use the [builder](builder).
## Setting empty default items
Repeaters may have a certain number of empty items created by default. The default is only used when a schema is loaded with no data. In a standard [panel resource](../resources), defaults are used on the Create page, not the Edit page. To use default items, pass the number of items to the `defaultItems()` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->defaultItems(3)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `defaultItems()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding items
An action button is displayed below the repeater to allow the user to add a new item.
## Setting the add action button's label
You may set a label to customize the text that should be displayed in the button for adding a repeater item, using the `addActionLabel()` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->addActionLabel('Add member')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addActionLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Aligning the add action button
By default, the add action is aligned in the center. You may adjust this using the `addActionAlignment()` method, passing an `Alignment` option of `Alignment::Start` or `Alignment::End`:
```php
use Filament\Forms\Components\Repeater;
use Filament\Support\Enums\Alignment;
Repeater::make('members')
->schema([
// ...
])
->addActionAlignment(Alignment::Start)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addActionAlignment()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing the user from adding items
You may prevent the user from adding items to the repeater using the `addable(false)` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->addable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Deleting items
An action button is displayed on each item to allow the user to delete it.
### Preventing the user from deleting items
You may prevent the user from deleting items from the repeater using the `deletable(false)` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->deletable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `deletable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Reordering items
A button is displayed on each item to allow the user to drag and drop to reorder it in the list.
### Preventing the user from reordering items
You may prevent the user from reordering items from the repeater using the `reorderable(false)` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->reorderable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Reordering items with buttons
You may use the `reorderableWithButtons()` method to enable reordering items with buttons to move the item up and down:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->reorderableWithButtons()
```
<AutoScreenshot name="forms/fields/repeater/reorderable-with-buttons" alt="Repeater that is reorderable with buttons" version="4.x" />
Optionally, you may pass a boolean value to control if the repeater should be ordered with buttons or not:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->reorderableWithButtons(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderableWithButtons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing reordering with drag and drop
You may use the `reorderableWithDragAndDrop(false)` method to prevent items from being ordered with drag and drop:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->reorderableWithDragAndDrop(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderableWithDragAndDrop()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Collapsing items
The repeater may be `collapsible()` to optionally hide content in long forms:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->collapsible()
```
You may also collapse all items by default:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->collapsed()
```
<AutoScreenshot name="forms/fields/repeater/collapsed" alt="Collapsed repeater" version="4.x" />
Optionally, the `collapsible()` and `collapsed()` methods accept a boolean value to control if the repeater should be collapsible and collapsed or not:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->collapsible(FeatureFlag::active())
->collapsed(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `collapsible()` and `collapsed()` methods also accept functions to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Cloning items
You may allow repeater items to be duplicated using the `cloneable()` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->cloneable()
```
<AutoScreenshot name="forms/fields/repeater/cloneable" alt="Cloneable repeater" version="4.x" />
Optionally, the `cloneable()` method accepts a boolean value to control if the repeater should be cloneable or not:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->cloneable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `cloneable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Integrating with an Eloquent relationship
You may employ the `relationship()` method of the `Repeater` to configure a `HasMany` relationship. Filament will load the item data from the relationship, and save it back to the relationship when the form is submitted. If a custom relationship name is not passed to `relationship()`, Filament will use the field name as the relationship name:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
```
<Aside variant="warning">
When using `disabled()` with `relationship()`, ensure that `disabled()` is called before `relationship()`. This ensures that the `dehydrated()` call from within `relationship()` is not overridden by the call from `disabled()`:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->disabled()
->relationship()
->schema([
// ...
])
```
</Aside>
### Reordering items in a relationship
By default, [reordering](#reordering-items) relationship repeater items is disabled. This is because your related model needs a `sort` column to store the order of related records. To enable reordering, you may use the `orderColumn()` method, passing in a name of the column on your related model to store the order in:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
->orderColumn('sort')
```
If you use something like [`spatie/eloquent-sortable`](https://github.com/spatie/eloquent-sortable) with an order column such as `order_column`, you may pass this in to `orderColumn()`:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
->orderColumn('order_column')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `orderColumn()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Integrating with a `BelongsToMany` Eloquent relationship
There is a common misconception that using a `BelongsToMany` relationship with a repeater is as simple as using a `HasMany` relationship. This is not the case, as a `BelongsToMany` relationship requires a pivot table to store the relationship data. The repeater saves its data to the related model, not the pivot table. Therefore, if you want to map each repeater item to a row in the pivot table, you must use a `HasMany` relationship with a pivot model to use a repeater with a `BelongsToMany` relationship.
Imagine you have a form to create a new `Order` model. Each order belongs to many `Product` models, and each product belongs to many orders. You have a `order_product` pivot table to store the relationship data. Instead of using the `products` relationship with the repeater, you should create a new relationship called `orderProducts` on the `Order` model, and use that with the repeater:
```php
use Illuminate\Database\Eloquent\Relations\HasMany;
public function orderProducts(): HasMany
{
return $this->hasMany(OrderProduct::class);
}
```
If you don't already have an `OrderProduct` pivot model, you should create that, with inverse relationships to `Order` and `Product`:
```php
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
class OrderProduct extends Pivot
{
public $incrementing = true;
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
```
<Aside variant="info">
Please ensure that your pivot model has a primary key column, like `id`, to allow Filament to keep track of which repeater items have been created, updated and deleted. To make sure that Filament keeps track of the primary key, the pivot model needs to have the `$incrementing` property set to `true`.
</Aside>
Now you can use the `orderProducts` relationship with the repeater, and it will save the data to the `order_product` pivot table:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
Repeater::make('orderProducts')
->relationship()
->schema([
Select::make('product_id')
->relationship('product', 'name')
->required(),
// ...
])
```
### Mutating related item data before filling the field
You may mutate the data for a related item before it is filled into the field using the `mutateRelationshipDataBeforeFillUsing()` method. This method accepts a closure that receives the current item's data in a `$data` variable. You must return the modified array of data:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
->mutateRelationshipDataBeforeFillUsing(function (array $data): array {
$data['user_id'] = auth()->id();
return $data;
})
```
<UtilityInjection set="formFields" version="4.x" extras="Data;;array<array<string, mixed>>;;$data;;The data that is being filled into the repeater.">You can inject various utilities into the function passed to `mutateRelationshipDataBeforeFillUsing()` as parameters.</UtilityInjection>
### Mutating related item data before creating
You may mutate the data for a new related item before it is created in the database using the `mutateRelationshipDataBeforeCreateUsing()` method. This method accepts a closure that receives the current item's data in a `$data` variable. You can choose to return either the modified array of data, or `null` to prevent the item from being created:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['user_id'] = auth()->id();
return $data;
})
```
<UtilityInjection set="formFields" version="4.x" extras="Data;;array<string, mixed>;;$data;;The data that is being saved by the repeater.">You can inject various utilities into the function passed to `mutateRelationshipDataBeforeCreateUsing()` as parameters.</UtilityInjection>
### Mutating related item data before saving
You may mutate the data for an existing related item before it is saved in the database using the `mutateRelationshipDataBeforeSaveUsing()` method. This method accepts a closure that receives the current item's data in a `$data` variable. You can choose to return either the modified array of data, or `null` to prevent the item from being saved:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->relationship()
->schema([
// ...
])
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['user_id'] = auth()->id();
return $data;
})
```
<UtilityInjection set="formFields" version="4.x" extras="Data;;array<string, mixed>;;$data;;The data that is being saved by the repeater.">You can inject various utilities into the function passed to `mutateRelationshipDataBeforeSaveUsing()` as parameters.</UtilityInjection>
### Modifying related records after retrieval
You may filter or modify the related records of a repeater after they are retrieved from the database using the `modifyRecordsUsing` argument. This method accepts a function that receives a `Collection` of related records. You should return the modified collection.
This can be particularly useful to restrict records to a specific group or category without doing this in the database query itself, which would trigger an extra query if the records are already eager loaded:
```php
use Filament\Forms\Components\Repeater;
use Illuminate\Database\Eloquent\Collection;
Repeater::make('startItems')
->relationship(name: 'items', modifyRecordsUsing: fn (Collection $records): Collection => $records->where('group', 'start')),
Repeater::make('endItems')
->relationship(name: 'items', modifyRecordsUsing: fn (Collection $records): Collection => $records->where('group', 'end')),
```
<UtilityInjection set="formFields" version="4.x" extras="Records;;Illuminate\Database\Eloquent\Collection;;$records;;The collection of related records.">You can inject various utilities into the function passed to `modifyRecordsUsing` as parameters.</UtilityInjection>
## Grid layout
You may organize repeater items into columns by using the `grid()` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('qualifications')
->schema([
// ...
])
->grid(2)
```
<AutoScreenshot name="forms/fields/repeater/grid" alt="Repeater with a 2 column grid of items" version="4.x" />
This method accepts the same options as the `columns()` method of the [grid](../schemas/layouts#grid-system). This allows you to responsively customize the number of grid columns at various breakpoints.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `grid()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding a label to repeater items based on their content
You may add a label for repeater items using the `itemLabel()` method. This method accepts a closure that receives the current item's data in a `$state` variable. You must return a string to be used as the item label:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
Repeater::make('members')
->schema([
TextInput::make('name')
->required()
->live(onBlur: true),
Select::make('role')
->options([
'member' => 'Member',
'administrator' => 'Administrator',
'owner' => 'Owner',
])
->required(),
])
->columns(2)
->itemLabel(fn (array $state): ?string => $state['name'] ?? null),
```
<Aside variant="tip">
Any fields that you use from `$state` should be `live()` if you wish to see the item label update live as you use the form.
</Aside>
<UtilityInjection set="formFields" version="4.x" extras="Item;;Filament\Schemas\Schema;;$item;;The schema object for the current repeater item.||Key;;string;;$key;;The key for the current repeater item.||State;;array<string, mixed>;;$state;;The raw unvalidated data for the current repeater item.">You can inject various utilities into the function passed to `itemLabel()` as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/repeater/labelled" alt="Repeater with item labels" version="4.x" />
## Numbering repeater items
You can add the repeater item's number next to its label using the `itemNumbers()` method:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->itemNumbers()
```
## Simple repeaters with one field
You can use the `simple()` method to create a repeater with a single field, using a minimal design
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
Repeater::make('invitations')
->simple(
TextInput::make('email')
->email()
->required(),
)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `simple()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/repeater/simple-one-field" alt="Simple repeater design with only one field" version="4.x" />
Instead of using a nested array to store data, simple repeaters use a flat array of values. This means that the data structure for the above example could look like this:
```php
[
'invitations' => [
'dan@filamentphp.com',
'ryan@filamentphp.com',
],
],
```
## Using `$get()` to access parent field values
All form components are able to [use `$get()` and `$set()`](overview#injecting-the-state-of-another-field) to access another field's value. However, you might experience unexpected behavior when using this inside the repeater's schema.
This is because `$get()` and `$set()`, by default, are scoped to the current repeater item. This means that you are able to interact with another field inside that repeater item easily without knowing which repeater item the current form component belongs to.
The consequence of this is that you may be confused when you are unable to interact with a field outside the repeater. We use `../` syntax to solve this problem - `$get('../parent_field_name')`.
Consider your form has this data structure:
```php
[
'client_id' => 1,
'repeater' => [
'item1' => [
'service_id' => 2,
],
],
]
```
You are trying to retrieve the value of `client_id` from inside the repeater item.
`$get()` is relative to the current repeater item, so `$get('client_id')` is looking for `$get('repeater.item1.client_id')`.
You can use `../` to go up a level in the data structure, so `$get('../client_id')` is `$get('repeater.client_id')` and `$get('../../client_id')` is `$get('client_id')`.
The special case of `$get()` with no arguments, or `$get('')` or `$get('./')`, will always return the full data array for the current repeater item.
## Table repeaters
You can present repeater items in a table format using the `table()` method, which accepts an array of `TableColumn` objects. These objects represent the columns of the table, which correspond to any components in the schema of the repeater:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Repeater\TableColumn;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
Repeater::make('members')
->table([
TableColumn::make('Name'),
TableColumn::make('Role'),
])
->schema([
TextInput::make('name')
->required(),
Select::make('role')
->options([
'member' => 'Member',
'administrator' => 'Administrator',
'owner' => 'Owner',
])
->required(),
])
```
<AutoScreenshot name="forms/fields/repeater/table" alt="Repeater with a table layout" version="4.x" />
The labels displayed in the header of the table are passed to the `TableColumn::make()` method. If you want to provide an accessible label for a column but do not wish to display it, you can use the `hiddenHeaderLabel()` method:
```php
use Filament\Forms\Components\Repeater\TableColumn;
TableColumn::make('Name')
->hiddenHeaderLabel()
```
If you would like to mark a column as "required" with a red asterisk, you can use the `markAsRequired()` method:
```php
use Filament\Forms\Components\Repeater\TableColumn;
TableColumn::make('Name')
->markAsRequired()
```
You can enable wrapping of the column header using the `wrapHeader()` method:
```php
use Filament\Forms\Components\Repeater\TableColumn;
TableColumn::make('Name')
->wrapHeader()
```
You can also adjust the alignment of the column header using the `alignment()` method, passing an `Alignment` option of `Alignment::Start`, `Alignment::Center`, or `Alignment::End`:
```php
use Filament\Forms\Components\Repeater\TableColumn;
use Filament\Support\Enums\Alignment;
TableColumn::make('Name')
->alignment(Alignment::Start)
```
You can set a fixed column width using the `width()` method, passing a string value that represents the width of the column. This value is passed directly to the `style` attribute of the column header:
```php
use Filament\Forms\Components\Repeater\TableColumn;
TableColumn::make('Name')
->width('200px')
```
### Compact table repeaters
You can make table repeaters more compact by using the `compact()` method, to fit more data in a smaller space:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->table([
// ...
])
->compact()
->schema([
// ...
])
```
Optionally, you may pass a boolean value to control if the table repeater should be compact or not:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->table([
// ...
])
->compact(FeatureFlag::active())
->schema([
// ...
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `compact()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/repeater/table-compact" alt="Repeater with a compact table layout" version="4.x" />
## Repeater validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to repeaters.
### Number of items validation
You can validate the minimum and maximum number of items that you can have in a repeater by setting the `minItems()` and `maxItems()` methods:
```php
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->minItems(2)
->maxItems(5)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minItems()` and `maxItems()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Distinct state validation
In many cases, you will want to ensure some sort of uniqueness between repeater items. A couple of common examples could be:
- Ensuring that only one [checkbox](checkbox) or [toggle](toggle) is activated at once across items in the repeater.
- Ensuring that an option may only be selected once across [select](select), [radio](radio), [checkbox list](checkbox-list), or [toggle buttons](toggle-buttons) fields in a repeater.
You can use the `distinct()` method to validate that the state of a field is unique across all items in the repeater:
```php
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Repeater;
Repeater::make('answers')
->schema([
// ...
Checkbox::make('is_correct')
->distinct(),
])
```
The behavior of the `distinct()` validation depends on the data type that the field handles
- If the field returns a boolean, like a [checkbox](checkbox) or [toggle](toggle), the validation will ensure that only one item has a value of `true`. There may be many fields in the repeater that have a value of `false`.
- Otherwise, for fields like a [select](select), [radio](radio), [checkbox list](checkbox-list), or [toggle buttons](toggle-buttons), the validation will ensure that each option may only be selected once across all items in the repeater.
Optionally, you may pass a boolean value to the `distinct()` method to control if the field should be distinct or not:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_correct')
->distinct(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `distinct()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Automatically fixing indistinct state
If you'd like to automatically fix indistinct state, you can use the `fixIndistinctState()` method:
```php
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Repeater;
Repeater::make('answers')
->schema([
// ...
Checkbox::make('is_correct')
->fixIndistinctState(),
])
```
This method will automatically enable the `distinct()` and `live()` methods on the field.
Depending on the data type that the field handles, the behavior of the `fixIndistinctState()` adapts:
- If the field returns a boolean, like a [checkbox](checkbox) or [toggle](toggle), and one of the fields is enabled, Filament will automatically disable all other enabled fields on behalf of the user.
- Otherwise, for fields like a [select](select), [radio](radio), [checkbox list](checkbox-list), or [toggle buttons](toggle-buttons), when a user selects an option, Filament will automatically deselect all other usages of that option on behalf of the user.
Optionally, you may pass a boolean value to the `fixIndistinctState()` method to control if the field should fix indistinct state or not:
```php
use Filament\Forms\Components\Checkbox;
Checkbox::make('is_correct')
->fixIndistinctState(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `fixIndistinctState()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
#### Disabling options when they are already selected in another item
If you'd like to disable options in a [select](select), [radio](radio), [checkbox list](checkbox-list), or [toggle buttons](toggle-buttons) when they are already selected in another item, you can use the `disableOptionsWhenSelectedInSiblingRepeaterItems()` method:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
Repeater::make('members')
->schema([
Select::make('role')
->options([
// ...
])
->disableOptionsWhenSelectedInSiblingRepeaterItems(),
])
```
This method will automatically enable the `distinct()` and `live()` methods on the field.
<Aside variant="warning">
In case you want to add another condition to [disable options](../select#disabling-specific-options) with, you can chain `disableOptionWhen()` with the `merge: true` argument:
```php
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
Repeater::make('members')
->schema([
Select::make('role')
->options([
// ...
])
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->disableOptionWhen(fn (string $value): bool => $value === 'super_admin', merge: true),
])
```
</Aside>
## Customizing the repeater item actions
This field uses action objects for easy customization of buttons within it. You can customize these buttons by passing a function to an action registration method. The function has access to the `$action` object, which you can use to [customize it](../actions/overview). The following methods are available to customize the actions:
- `addAction()`
- `cloneAction()`
- `collapseAction()`
- `collapseAllAction()`
- `deleteAction()`
- `expandAction()`
- `expandAllAction()`
- `moveDownAction()`
- `moveUpAction()`
- `reorderAction()`
Here is an example of how you might customize an action:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->collapseAllAction(
fn (Action $action) => $action->label('Collapse all members'),
)
```
<UtilityInjection set="formFields" version="4.x" extras="Action;;Filament\Actions\Action;;$action;;The action object to customize.">The action registration methods can inject various utilities into the function as parameters.</UtilityInjection>
### Confirming repeater actions with a modal
You can confirm actions with a modal by using the `requiresConfirmation()` method on the action object. You may use any [modal customization method](../actions/modals) to change its content and behavior:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Repeater;
Repeater::make('members')
->schema([
// ...
])
->deleteAction(
fn (Action $action) => $action->requiresConfirmation(),
)
```
<Aside variant="info">
The `collapseAction()`, `collapseAllAction()`, `expandAction()`, `expandAllAction()` and `reorderAction()` methods do not support confirmation modals, as clicking their buttons does not make the network request that is required to show the modal.
</Aside>
### Adding extra item actions to a repeater
You may add new [action buttons](../actions) to the header of each repeater item by passing `Action` objects into `extraItemActions()`:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Mail;
Repeater::make('members')
->schema([
TextInput::make('email')
->label('Email address')
->email(),
// ...
])
->extraItemActions([
Action::make('sendEmail')
->icon(Heroicon::Envelope)
->action(function (array $arguments, Repeater $component): void {
$itemData = $component->getItemState($arguments['item']);
Mail::to($itemData['email'])
->send(
// ...
);
}),
])
```
In this example, `$arguments['item']` gives you the ID of the current repeater item. You can validate the data in that repeater item using the `getItemState()` method on the repeater component. This method returns the validated data for the item. If the item is not valid, it will cancel the action and show an error message for that item in the form.
If you want to get the raw data from the current item without validating it, you can use `$component->getRawItemState($arguments['item'])` instead.
If you want to manipulate the raw data for the entire repeater, for example, to add, remove or modify items, you can use `$component->getState()` to get the data, and `$component->state($state)` to set it again:
```php
use Illuminate\Support\Str;
// Get the raw data for the entire repeater
$state = $component->getState();
// Add an item, with a random UUID as the key
$state[Str::uuid()] = [
'email' => auth()->user()->email,
];
// Set the new data for the repeater
$component->state($state);
```
@@ -0,0 +1,663 @@
---
title: Builder
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
Similar to a [repeater](repeater), the builder component allows you to output a JSON array of repeated form components. Unlike the repeater, which only defines one form schema to repeat, the builder allows you to define different schema "blocks", which you can repeat in any order. This makes it useful for building more advanced array structures.
The primary use of the builder component is to build web page content using predefined blocks. This could be content for a marketing website, or maybe even fields in an online form. The example below defines multiple blocks for different elements in the page content. On the frontend of your website, you could loop through each block in the JSON and format it how you wish.
```php
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Builder\Block;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
Builder::make('content')
->blocks([
Block::make('heading')
->schema([
TextInput::make('content')
->label('Heading')
->required(),
Select::make('level')
->options([
'h1' => 'Heading 1',
'h2' => 'Heading 2',
'h3' => 'Heading 3',
'h4' => 'Heading 4',
'h5' => 'Heading 5',
'h6' => 'Heading 6',
])
->required(),
])
->columns(2),
Block::make('paragraph')
->schema([
Textarea::make('content')
->label('Paragraph')
->required(),
]),
Block::make('image')
->schema([
FileUpload::make('url')
->label('Image')
->image()
->required(),
TextInput::make('alt')
->label('Alt text')
->required(),
]),
])
```
<AutoScreenshot name="forms/fields/builder/simple" alt="Builder" version="4.x" />
We recommend that you store builder data with a `JSON` column in your database. Additionally, if you're using Eloquent, make sure that column has an `array` cast.
As evident in the above example, blocks can be defined within the `blocks()` method of the component. Blocks are `Builder\Block` objects, and require a unique name, and a component schema:
```php
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Builder\Block;
use Filament\Forms\Components\TextInput;
Builder::make('content')
->blocks([
Block::make('heading')
->schema([
TextInput::make('content')->required(),
// ...
]),
// ...
])
```
## Setting a block's label
By default, the label of the block will be automatically determined based on its name. To override the block's label, you may use the `label()` method. Customizing the label in this way is useful if you wish to use a [translation string for localization](https://laravel.com/docs/localization#retrieving-translation-strings):
```php
use Filament\Forms\Components\Builder\Block;
Block::make('heading')
->label(__('blocks.heading'))
```
### Labelling builder items based on their content
You may add a label for a builder item using the same `label()` method. This method accepts a closure that receives the item's data in a `$state` variable. If `$state` is null, you should return the block label that should be displayed in the block picker. Otherwise, you should return a string to be used as the item label:
```php
use Filament\Forms\Components\Builder\Block;
use Filament\Forms\Components\TextInput;
Block::make('heading')
->schema([
TextInput::make('content')
->live(onBlur: true)
->required(),
// ...
])
->label(function (?array $state): string {
if ($state === null) {
return 'Heading';
}
return $state['content'] ?? 'Untitled heading';
})
```
Any fields that you use from `$state` should be `live()` if you wish to see the item label update live as you use the form.
<UtilityInjection set="formFields" version="4.x" extras="Key;;string;;$key;;The key for the current block.||State;;array<string, mixed>;;$state;;The raw unvalidated data for the current block.">You can inject various utilities into the function passed to `label()` as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/builder/labelled" alt="Builder with labelled blocks based on the content" version="4.x" />
### Numbering builder items
By default, items in the builder have a number next to their label. You may disable this using the `blockNumbers(false)` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->blockNumbers(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `blockNumbers()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Setting a block's icon
Blocks may also have an [icon](../styling/icons), which is displayed next to the label. You can add an icon by passing its name to the `icon()` method:
```php
use Filament\Forms\Components\Builder\Block;
use Filament\Support\Icons\Heroicon;
Block::make('paragraph')
->icon(Heroicon::Bars3BottomLeft)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `icon()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/builder/icons" alt="Builder with block icons in the dropdown" version="4.x" />
### Adding icons to the header of blocks
By default, blocks in the builder don't have an icon next to the header label, just in the dropdown to add new blocks. You may enable this using the `blockIcons()` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->blockIcons()
```
Optionally, you may pass a boolean value to the `blockIcons()` method to control if the icons are displayed in the block headers:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->blockIcons(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `blockIcons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Previewing blocks
If you prefer to render read-only previews in the builder instead of the blocks' forms, you can use the `blockPreviews()` method. This will render each block's `preview()` instead of the form. Block data will be passed to the preview Blade view in a variable with the same name:
```php
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Builder\Block;
use Filament\Forms\Components\TextInput;
Builder::make('content')
->blockPreviews()
->blocks([
Block::make('heading')
->schema([
TextInput::make('text')
->placeholder('Default heading'),
])
->preview('filament.content.block-previews.heading'),
])
```
In `/resources/views/filament/content/block-previews/heading.blade.php`, you can access the block data like so:
```blade
<h1>
{{ $text ?? 'Default heading' }}
</h1>
```
Optionally, the `blockPreviews()` method accepts a boolean value to control if the builder should render block previews or not:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->blockPreviews(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `blockPreviews()` and `preview()` methods also accept functions to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
### Interactive block previews
By default, preview content is not interactive, and clicking it will open the Edit modal for that block to manage its settings. If you have links and buttons that you'd like to remain interactive in the block previews, you can use the `areInteractive: true` argument of the `blockPreviews()` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blockPreviews(areInteractive: true)
->blocks([
//
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `areInteractive` argument also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding items
An action button is displayed below the builder to allow the user to add a new item.
## Setting the add action button's label
You may set a label to customize the text that should be displayed in the button for adding a builder item, using the `addActionLabel()` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->addActionLabel('Add a new block')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addActionLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Aligning the add action button
By default, the add action is aligned in the center. You may adjust this using the `addActionAlignment()` method, passing an `Alignment` option of `Alignment::Start` or `Alignment::End`:
```php
use Filament\Forms\Components\Builder;
use Filament\Support\Enums\Alignment;
Builder::make('content')
->schema([
// ...
])
->addActionAlignment(Alignment::Start)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addActionAlignment()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing the user from adding items
You may prevent the user from adding items to the builder using the `addable(false)` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->addable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Deleting items
An action button is displayed on each item to allow the user to delete it.
### Preventing the user from deleting items
You may prevent the user from deleting items from the builder using the `deletable(false)` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->deletable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `deletable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Reordering items
A button is displayed on each item to allow the user to drag and drop to reorder it in the list.
### Preventing the user from reordering items
You may prevent the user from reordering items from the builder using the `reorderable(false)` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->reorderable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Reordering items with buttons
You may use the `reorderableWithButtons()` method to enable reordering items with buttons to move the item up and down:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->reorderableWithButtons()
```
<AutoScreenshot name="forms/fields/builder/reorderable-with-buttons" alt="Builder that is reorderable with buttons" version="4.x" />
Optionally, you may pass a boolean value to control if the builder should be ordered with buttons or not:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->reorderableWithButtons(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderableWithButtons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing reordering with drag and drop
You may use the `reorderableWithDragAndDrop(false)` method to prevent items from being ordered with drag and drop:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->reorderableWithDragAndDrop(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderableWithDragAndDrop()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Collapsing items
The builder may be `collapsible()` to optionally hide content in long forms:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->collapsible()
```
You may also collapse all items by default:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->collapsed()
```
<AutoScreenshot name="forms/fields/builder/collapsed" alt="Collapsed builder" version="4.x" />
Optionally, the `collapsible()` and `collapsed()` methods accept a boolean value to control if the builder should be collapsible and collapsed or not:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->collapsible(FeatureFlag::active())
->collapsed(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `collapsible()` and `collapsed()` methods also accept functions to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Cloning items
You may allow builder items to be duplicated using the `cloneable()` method:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->cloneable()
```
<AutoScreenshot name="forms/fields/builder/cloneable" alt="Cloneable repeater" version="4.x" />
## Customizing the block picker
### Changing the number of columns in the block picker
The block picker has only 1 column. You may customize it by passing a number of columns to `blockPickerColumns()`:
```php
use Filament\Forms\Components\Builder;
Builder::make()
->blockPickerColumns(2)
->blocks([
// ...
])
```
This method can be used in a couple of different ways:
- You can pass an integer like `blockPickerColumns(2)`. This integer is the number of columns used on the `lg` breakpoint and higher. All smaller devices will have just 1 column.
- You can pass an array, where the key is the breakpoint and the value is the number of columns. For example, `blockPickerColumns(['md' => 2, 'xl' => 4])` will create a 2 column layout on medium devices, and a 4 column layout on extra large devices. The default breakpoint for smaller devices uses 1 column, unless you use a `default` array key.
Breakpoints (`sm`, `md`, `lg`, `xl`, `2xl`) are defined by Tailwind, and can be found in the [Tailwind documentation](https://tailwindcss.com/docs/responsive-design#overview).
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `blockPickerColumns()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Increasing the width of the block picker
When you [increase the number of columns](#changing-the-number-of-columns-in-the-block-picker), the width of the dropdown should increase incrementally to handle the additional columns. If you'd like more control, you can manually set a maximum width for the dropdown using the `blockPickerWidth()` method. Options correspond to [Tailwind's max-width scale](https://tailwindcss.com/docs/max-width). The options are `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `4xl`, `5xl`, `6xl`, `7xl`:
```php
use Filament\Forms\Components\Builder;
Builder::make()
->blockPickerColumns(3)
->blockPickerWidth('2xl')
->blocks([
// ...
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `blockPickerWidth()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Limiting the number of times a block can be used
By default, each block can be used in the builder an unlimited number of times. You may limit this using the `maxItems()` method on a block:
```php
use Filament\Forms\Components\Builder\Block;
Block::make('heading')
->schema([
// ...
])
->maxItems(1)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `maxItems()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Using `$get()` to access parent field values
All form components are able to [use `$get()` and `$set()`](overview#injecting-the-state-of-another-field) to access another field's value. However, you might experience unexpected behavior when using this inside the builder's schema.
This is because `$get()` and `$set()`, by default, are scoped to the current builder item. This means that you are able to interact with another field inside that builder item easily without knowing which builder item the current form component belongs to.
The consequence of this is that you may be confused when you are unable to interact with a field outside the builder. We use `../` syntax to solve this problem - `$get('../parent_field_name')`.
Consider your form has this data structure:
```php
[
'client_id' => 1,
'builder' => [
'item1' => [
'service_id' => 2,
],
],
]
```
You are trying to retrieve the value of `client_id` from inside the builder item.
`$get()` is relative to the current builder item, so `$get('client_id')` is looking for `$get('builder.item1.client_id')`.
You can use `../` to go up a level in the data structure, so `$get('../client_id')` is `$get('builder.client_id')` and `$get('../client_id')` is `$get('client_id')`.
The special case of `$get()` with no arguments, or `$get('')` or `$get('./')`, will always return the full data array for the current builder item.
## Builder validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to builders.
### Number of items validation
You can validate the minimum and maximum number of items that you can have in a builder by setting the `minItems()` and `maxItems()` methods:
```php
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->minItems(1)
->maxItems(5)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minItems()` and `maxItems()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
## Customizing the builder item actions
This field uses action objects for easy customization of buttons within it. You can customize these buttons by passing a function to an action registration method. The function has access to the `$action` object, which you can use to [customize it](../actions/overview). The following methods are available to customize the actions:
- `addAction()`
- `addBetweenAction()`
- `cloneAction()`
- `collapseAction()`
- `collapseAllAction()`
- `deleteAction()`
- `expandAction()`
- `expandAllAction()`
- `moveDownAction()`
- `moveUpAction()`
- `reorderAction()`
Here is an example of how you might customize an action:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->collapseAllAction(
fn (Action $action) => $action->label('Collapse all content'),
)
```
<UtilityInjection set="formFields" version="4.x" extras="Action;;Filament\Actions\Action;;$action;;The action object to customize.">The action registration methods can inject various utilities into the function as parameters.</UtilityInjection>
### Confirming builder actions with a modal
You can confirm actions with a modal by using the `requiresConfirmation()` method on the action object. You may use any [modal customization method](../actions/modals) to change its content and behavior:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Builder;
Builder::make('content')
->blocks([
// ...
])
->deleteAction(
fn (Action $action) => $action->requiresConfirmation(),
)
```
<Aside variant="info">
The `addAction()`, `addBetweenAction()`, `collapseAction()`, `collapseAllAction()`, `expandAction()`, `expandAllAction()` and `reorderAction()` methods do not support confirmation modals, as clicking their buttons does not make the network request that is required to show the modal.
</Aside>
### Adding extra item actions to a builder
You may add new [action buttons](../actions) to the header of each builder item by passing `Action` objects into `extraItemActions()`:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\Builder;
use Filament\Forms\Components\Builder\Block;
use Filament\Forms\Components\TextInput;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Mail;
Builder::make('content')
->blocks([
Block::make('contactDetails')
->schema([
TextInput::make('email')
->label('Email address')
->email()
->required(),
// ...
]),
// ...
])
->extraItemActions([
Action::make('sendEmail')
->icon(Heroicon::Square2Stack)
->action(function (array $arguments, Builder $component): void {
$itemData = $component->getItemState($arguments['item']);
Mail::to($itemData['email'])
->send(
// ...
);
}),
])
```
In this example, `$arguments['item']` gives you the ID of the current builder item. You can validate the data in that builder item using the `getItemState()` method on the builder component. This method returns the validated data for the item. If the item is not valid, it will cancel the action and show an error message for that item in the form.
If you want to get the raw data from the current item without validating it, you can use `$component->getRawItemState($arguments['item'])` instead.
If you want to manipulate the raw data for the entire builder, for example, to add, remove or modify items, you can use `$component->getState()` to get the data, and `$component->state($state)` to set it again:
```php
use Illuminate\Support\Str;
// Get the raw data for the entire builder
$state = $component->getState();
// Add an item, with a random UUID as the key
$state[Str::uuid()] = [
'type' => 'contactDetails',
'data' => [
'email' => auth()->user()->email,
],
];
// Set the new data for the builder
$component->state($state);
```
@@ -0,0 +1,174 @@
---
title: Tags input
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The tags input component allows you to interact with a list of tags.
By default, tags are stored in JSON:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
```
<AutoScreenshot name="forms/fields/tags-input/simple" alt="Tags input" version="4.x" />
If you're saving the JSON tags using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'tags' => 'array',
];
}
// ...
}
```
<Aside variant="tip">
Filament also supports [`spatie/laravel-tags`](https://github.com/spatie/laravel-tags). See our [plugin documentation](/plugins/filament-spatie-tags) for more information.
</Aside>
## Comma-separated tags
You may allow the tags to be stored in a separated string, instead of JSON. To set this up, pass the separating character to the `separator()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->separator(',')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `separator()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Autocompleting tag suggestions
Tags inputs may have autocomplete suggestions. To enable this, pass an array of suggestions to the `suggestions()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->suggestions([
'tailwindcss',
'alpinejs',
'laravel',
'livewire',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `suggestions()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Defining split keys
Split keys allow you to map specific buttons on your user's keyboard to create a new tag. By default, when the user presses "Enter", a new tag is created in the input. You may also define other keys to create new tags, such as "Tab" or " ". To do this, pass an array of keys to the `splitKeys()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->splitKeys(['Tab', ' '])
```
You can [read more about possible options for keys](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key).
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `splitKeys()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding a prefix and suffix to individual tags
You can add prefix and suffix to tags without modifying the real state of the field. This can be useful if you need to show presentational formatting to users without saving it. This is done with the `tagPrefix()` or `tagSuffix()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('percentages')
->tagSuffix('%')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `tagPrefix()` and `tagSuffix()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
## Reordering tags
You can allow the user to reorder tags within the field using the `reorderable()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->reorderable()
```
Optionally, you may pass a boolean value to control if the tags should be reorderable or not:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->reorderable(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Changing the color of tags
You can change the color of the tags by passing a [color](../styling/colors) to the `color()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->color('danger')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `color()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Trimming whitespace
You can automatically trim whitespace from the beginning and end of each tag using the `trim()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->trim()
```
You may want to enable trimming globally for all tags inputs, similar to Laravel's `TrimStrings` middleware. You can do this in a service provider using the `configureUsing()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::configureUsing(function (TagsInput $component): void {
$component->trim();
});
```
## Tags validation
You may add validation rules for each tag by passing an array of rules to the `nestedRecursiveRules()` method:
```php
use Filament\Forms\Components\TagsInput;
TagsInput::make('tags')
->nestedRecursiveRules([
'min:3',
'max:255',
])
```
@@ -0,0 +1,151 @@
---
title: Textarea
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The textarea allows you to interact with a multi-line string:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
```
<AutoScreenshot name="forms/fields/textarea/simple" alt="Textarea" version="4.x" />
## Resizing the textarea
You may change the size of the textarea by defining the `rows()` and `cols()` methods:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->rows(10)
->cols(20)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `rows()` and `cols()` methods also accept functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
### Autosizing the textarea
You may allow the textarea to automatically resize to fit its content by setting the `autosize()` method:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->autosize()
```
Optionally, you may pass a boolean value to control if the textarea should be autosizeable or not:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->autosize(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `autosize()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Making the field read-only
Not to be confused with [disabling the field](overview#disabling-a-field), you may make the field "read-only" using the `readOnly()` method:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->readOnly()
```
There are a few differences, compared to [`disabled()`](overview#disabling-a-field):
- When using `readOnly()`, the field will still be sent to the server when the form is submitted. It can be mutated with the browser console, or via JavaScript. You can use [`dehydrated(false)`](overview#preventing-a-field-from-being-dehydrated) to prevent this.
- There are no styling changes, such as less opacity, when using `readOnly()`.
- The field is still focusable when using `readOnly()`.
Optionally, you may pass a boolean value to control if the field should be read-only or not:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->readOnly(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `readOnly()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Disabling Grammarly checks
If the user has Grammarly installed and you would like to prevent it from analyzing the contents of the textarea, you can use the `disableGrammarly()` method:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->disableGrammarly()
```
Optionally, you may pass a boolean value to control if the field should disable Grammarly checks or not:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->disableGrammarly(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `disableGrammarly()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Trimming whitespace
You can automatically trim whitespace from the beginning and end of the textarea value using the `trim()` method:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->trim()
```
You may want to enable trimming globally for all textareas, similar to Laravel's `TrimStrings` middleware. You can do this in a service provider using the `configureUsing()` method:
```php
use Filament\Forms\Components\Textarea;
Textarea::configureUsing(function (Textarea $component): void {
$component->trim();
});
```
## Textarea validation
As well as all rules listed on the [validation](validation) page, there are additional rules that are specific to textareas.
### Length validation
You may limit the length of the textarea by setting the `minLength()` and `maxLength()` methods. These methods add both frontend and backend validation:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('description')
->minLength(2)
->maxLength(1024)
```
You can also specify the exact length of the textarea by setting the `length()`. This method adds both frontend and backend validation:
```php
use Filament\Forms\Components\Textarea;
Textarea::make('question')
->length(100)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `minLength()`, `maxLength()` and `length()` methods also accept a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,205 @@
---
title: Key-value
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The key-value field allows you to interact with one-dimensional JSON object:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
```
<AutoScreenshot name="forms/fields/key-value/simple" alt="Key-value" version="4.x" />
If you're saving the data in Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'meta' => 'array',
];
}
// ...
}
```
## Adding rows
An action button is displayed below the field to allow the user to add a new row.
## Setting the add action button's label
You may set a label to customize the text that should be displayed in the button for adding a row, using the `addActionLabel()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->addActionLabel('Add property')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addActionLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing the user from adding rows
You may prevent the user from adding rows using the `addable(false)` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->addable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `addable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Deleting rows
An action button is displayed on each item to allow the user to delete it.
### Preventing the user from deleting rows
You may prevent the user from deleting rows using the `deletable(false)` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->deletable(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `deletable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Editing keys
### Customizing the key fields' label
You may customize the label for the key fields using the `keyLabel()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->keyLabel('Property name')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `keyLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Adding key field placeholders
You may also add placeholders for the key fields using the `keyPlaceholder()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->keyPlaceholder('Property name')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `keyPlaceholder()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing the user from editing keys
You may prevent the user from editing keys using the `editableKeys(false)` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->editableKeys(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `editableKeys()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Editing values
### Customizing the value fields' label
You may customize the label for the value fields using the `valueLabel()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->valueLabel('Property value')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `valueLabel()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Adding value field placeholders
You may also add placeholders for the value fields using the `valuePlaceholder()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->valuePlaceholder('Property value')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `valuePlaceholder()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Preventing the user from editing values
You may prevent the user from editing values using the `editableValues(false)` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->editableValues(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `editableValues()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Reordering rows
You can allow the user to reorder rows within the table using the `reorderable()` method:
```php
use Filament\Forms\Components\KeyValue;
KeyValue::make('meta')
->reorderable()
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `reorderable()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/key-value/reorderable" alt="Key-value with reorderable rows" version="4.x" />
## Customizing the key-value action objects
This field uses action objects for easy customization of buttons within it. You can customize these buttons by passing a function to an action registration method. The function has access to the `$action` object, which you can use to [customize it](../actions/overview). The following methods are available to customize the actions:
- `addAction()`
- `deleteAction()`
- `reorderAction()`
Here is an example of how you might customize an action:
```php
use Filament\Actions\Action;
use Filament\Forms\Components\KeyValue;
use Filament\Support\Icons\Heroicon;
KeyValue::make('meta')
->deleteAction(
fn (Action $action) => $action->icon(Heroicon::XMark),
)
```
<UtilityInjection set="formFields" version="4.x" extras="Action;;Filament\Actions\Action;;$action;;The action object to customize.">The action registration methods can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,58 @@
---
title: Color picker
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
## Introduction
The color picker component allows you to pick a color in a range of formats.
By default, the component uses HEX format:
```php
use Filament\Forms\Components\ColorPicker;
ColorPicker::make('color')
```
<AutoScreenshot name="forms/fields/color-picker/simple" alt="Color picker" version="4.x" />
## Setting the color format
While HEX format is used by default, you can choose which color format to use:
```php
use Filament\Forms\Components\ColorPicker;
ColorPicker::make('hsl_color')
->hsl()
ColorPicker::make('rgb_color')
->rgb()
ColorPicker::make('rgba_color')
->rgba()
```
## Color picker validation
You may use Laravel's validation rules to validate the values of the color picker:
```php
use Filament\Forms\Components\ColorPicker;
ColorPicker::make('hex_color')
->regex('/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\b$/')
ColorPicker::make('hsl_color')
->hsl()
->regex('/^hsl\(\s*(\d+)\s*,\s*(\d*(?:\.\d+)?%)\s*,\s*(\d*(?:\.\d+)?%)\)$/')
ColorPicker::make('rgb_color')
->rgb()
->regex('/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/')
ColorPicker::make('rgba_color')
->rgba()
->regex('/^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*(?:\.\d+)?)\)$/')
```
@@ -0,0 +1,305 @@
---
title: Toggle buttons
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The toggle buttons input provides a group of buttons for selecting a single value, or multiple values, from a list of predefined options:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published'
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `options()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle-buttons/simple" alt="Toggle buttons" version="4.x" />
## Changing the color of option buttons
You can change the [color](../styling/colors) of the option buttons using the `colors()` method. Each key in the array should correspond to an option value:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published'
])
->colors([
'draft' => 'info',
'scheduled' => 'warning',
'published' => 'success',
])
```
If you are using an enum for the options, you can use the [`HasColor` interface](../advanced/enums#enum-colors) to define colors instead.
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `colors()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle-buttons/colors" alt="Toggle buttons with different colors" version="4.x" />
## Adding icons to option buttons
You can add [icon](../styling/icons) to the option buttons using the `icons()` method. Each key in the array should correspond to an option value, and the value may be any valid [icon](../styling/icons):
```php
use Filament\Forms\Components\ToggleButtons;
use Filament\Support\Icons\Heroicon;
ToggleButtons::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published'
])
->icons([
'draft' => Heroicon::OutlinedPencil,
'scheduled' => Heroicon::OutlinedClock,
'published' => Heroicon::OutlinedCheckCircle,
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static array, the `icons()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
If you are using an enum for the options, you can use the [`HasIcon` interface](../advanced/enums#enum-icons) to define icons instead.
<AutoScreenshot name="forms/fields/toggle-buttons/icons" alt="Toggle buttons with icons" version="4.x" />
If you want to display only icons, you can use `hiddenButtonLabels()` to hide the option labels.
## Boolean options
If you want a simple boolean toggle button group, with "Yes" and "No" options, you can use the `boolean()` method:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean()
```
The options will have [colors](#changing-the-color-of-option-buttons) and [icons](#adding-icons-to-option-buttons) set up automatically, but you can override these with `colors()` or `icons()`.
<AutoScreenshot name="forms/fields/toggle-buttons/boolean" alt="Boolean toggle buttons" version="4.x" />
To customize the "Yes" label, you can use the `trueLabel` argument on the `boolean()` method:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean(trueLabel: 'Absolutely!')
```
To customize the "No" label, you can use the `falseLabel` argument on the `boolean()` method:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean(falseLabel: 'Not at all!')
```
## Positioning the options inline with each other
You may wish to display the buttons `inline()` with each other:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean()
->inline()
```
<AutoScreenshot name="forms/fields/toggle-buttons/inline" alt="Inline toggle buttons" version="4.x" />
Optionally, you may pass a boolean value to control if the buttons should be inline or not:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean()
->inline(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `inline()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Grouping option buttons
You may wish to group option buttons together so they are more compact, using the `grouped()` method. This also makes them appear horizontally inline with each other:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean()
->grouped()
```
<AutoScreenshot name="forms/fields/toggle-buttons/grouped" alt="Grouped toggle buttons" version="4.x" />
Optionally, you may pass a boolean value to control if the buttons should be grouped or not:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('feedback')
->label('Like this post?')
->boolean()
->grouped(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `grouped()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Selecting multiple buttons
The `multiple()` method on the `ToggleButtons` component allows you to select multiple values from the list of options:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('technologies')
->multiple()
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
```
<AutoScreenshot name="forms/fields/toggle-buttons/multiple" alt="Multiple toggle buttons selected" version="4.x" />
These options are returned in JSON format. If you're saving them using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class App extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'technologies' => 'array',
];
}
// ...
}
```
Optionally, you may pass a boolean value to control if the buttons should allow multiple selections or not:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('technologies')
->multiple(FeatureFlag::active())
->options([
'tailwind' => 'Tailwind CSS',
'alpine' => 'Alpine.js',
'laravel' => 'Laravel',
'livewire' => 'Laravel Livewire',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `multiple()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Splitting options into columns
You may split options into columns by using the `columns()` method:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('technologies')
->options([
// ...
])
->columns(2)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `columns()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle-buttons/columns" alt="Toggle buttons with 2 columns" version="4.x" />
This method accepts the same options as the `columns()` method of the [grid](../schemas/layouts#grid-system). This allows you to responsively customize the number of columns at various breakpoints.
### Setting the grid direction
By default, when you arrange buttons into columns, they will be listed in order vertically. If you'd like to list them horizontally, you may use the `gridDirection(GridDirection::Row)` method:
```php
use Filament\Forms\Components\ToggleButtons;
use Filament\Support\Enums\GridDirection;
ToggleButtons::make('technologies')
->options([
// ...
])
->columns(2)
->gridDirection(GridDirection::Row)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `gridDirection()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle-buttons/rows" alt="Toggle buttons with 2 rows" version="4.x" />
## Disabling specific options
You can disable specific options using the `disableOptionWhen()` method. It accepts a closure, in which you can check if the option with a specific `$value` should be disabled:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published',
])
->disableOptionWhen(fn (string $value): bool => $value === 'published')
```
<UtilityInjection set="formFields" version="4.x" extras="Option value;;mixed;;$value;;The value of the option to disable.||Option label;;string | Illuminate\Contracts\Support\Htmlable;;$label;;The label of the option to disable.">You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/toggle-buttons/disabled-option" alt="Toggle buttons with disabled option" version="4.x" />
If you want to retrieve the options that have not been disabled, e.g. for validation purposes, you can do so using `getEnabledOptions()`:
```php
use Filament\Forms\Components\ToggleButtons;
ToggleButtons::make('status')
->options([
'draft' => 'Draft',
'scheduled' => 'Scheduled',
'published' => 'Published',
])
->disableOptionWhen(fn (string $value): bool => $value === 'published')
->in(fn (ToggleButtons $component): array => array_keys($component->getEnabledOptions()))
```
For more information about the `in()` function, please see the [Validation documentation](validation#in).
@@ -0,0 +1,600 @@
---
title: Slider
---
import Aside from "@components/Aside.astro"
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The slider component allows you to drag a handle across a track to select one or more numeric values:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
```
<AutoScreenshot name="forms/fields/slider/simple" alt="Slider" version="4.x" />
The [noUiSlider](https://refreshless.com/nouislider) package is used for this component, and much of its API is based upon that library.
<Aside variant="warning">
Due to their nature, slider fields can never be empty. The value of the field can never be `null` or an empty array. If a slider field is empty, the user will not have a handle to drag across the track.
Because of this, slider fields have a default value set out of the box, which is set to the minimum value allowed in the [range](#controlling-the-range-of-the-slider) of the slider. The default value is used when a form is empty, for example on the Create page of a resource. To learn more about default values, check out the [`default()` documentation](overview#setting-the-default-value-of-a-field).
</Aside>
## Controlling the range of the slider
The minimum and maximum values that can be selected by the slider are 0 and 100 by default. Filament will automatically apply validation rules to ensure that users cannot exceed these values. You can adjust these with the `range()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 40, maxValue: 80)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `range()` method also accepts functions to dynamically calculate them. You can inject various utilities into the functions as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/range" alt="Slider with a customized range" version="4.x" />
### Controlling the step size
By default, users can select any decimal value between the minimum and maximum. You can restrict the values to a specific step size using the `step()` method. Filament will automatically apply validation rules to ensure that users cannot deviate from this step size:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->step(10)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `step()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Limiting the number of decimal places
If you would rather allow the user to select any decimal value between the minimum and maximum instead of restricting them to a certain step size, you can define a number of decimal places that their selected values will be rounded to using the `decimalPlaces()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->decimalPlaces(2)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `decimalPlaces()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Controlling the behavioral padding of the track
By default, users can select any value across the entire length of the track. You can add behavioral padding to the track using the `rangePadding()` method. This will ensure that the selected value is always at least a certain distance from the edges of the track. The minimum and maximum value validation that Filament applies to the slider by default will take the padding into account to ensure that users cannot exceed the padded range:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->rangePadding(10)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `rangePadding()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
In this example, even though the minimum value is 0 and the maximum value is 100, the user will only be able to select values between 10 and 90. The padding will be applied to both ends of the track, so the selected value will always be at least 10 units away from the edges of the track.
If you would like to control the padding on each side of the track separately, you can pass an array of two values to the `rangePadding()` method. The first value will be applied to the start of the track, and the second value will be applied to the end of the track:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->rangePadding([10, 20])
```
### Right-to-left tracks
By default, a track operates left-to-right. If the user is using a right-to-left locale (e.g. Arabic), the track will be displayed right-to-left automatically. You can also force the track to be displayed right-to-left using the `rtl()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->rtl()
```
Optionally, you may pass a boolean value to control if the slider should be right-to-left or not:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->rtl(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `rtl()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Adding multiple values to a slider
If the slider is set to an array of values, the user will be able to drag multiple handles across the track within the allowed range. Make sure that the slider has a [`default()` value](overview#setting-the-default-value-of-a-field) set to an array of values to use when a form is empty:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->default([20, 70])
```
<AutoScreenshot name="forms/fields/slider/multiple" alt="Slider with multiple values" version="4.x" />
If you're saving multiple slider values using Eloquent, you should be sure to add an `array` [cast](https://laravel.com/docs/eloquent-mutators#array-and-json-casting) to the model property:
```php
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'slider' => 'array',
];
}
// ...
}
```
## Using a vertical track
You can display the slider as a vertical track instead of horizontal, you can use the `vertical()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->vertical()
```
<AutoScreenshot name="forms/fields/slider/vertical" alt="Vertical slider" version="4.x" />
Optionally, you may pass a boolean value to control if the slider should be vertical or not:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->vertical(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `vertical()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
### Top-to-bottom tracks
By default, a vertical track operates bottom-to-top. In [noUiSlider](https://refreshless.com/nouislider), this is the [right-to-left behavior](#right-to-left-tracks). To assign the minimum value to the top of the track, you can use the `rtl(false)` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->vertical()
->rtl(false)
```
## Adding tooltips to handles
You can add tooltips to the handles of the slider using the `tooltips()` method. The tooltip will display the current value of the handle:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->tooltips()
```
<AutoScreenshot name="forms/fields/slider/tooltips" alt="Slider with tooltips" version="4.x" />
Optionally, you may pass a boolean value to control if the slider should have tooltips or not:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->tooltips(FeatureFlag::active())
```
When using multiple handles, multiple tooltips will be displayed, one for each handle. Tooltips also work with [vertical tracks](#using-a-vertical-track).
<AutoScreenshot name="forms/fields/slider/tooltips-vertical" alt="Vertical slider with tooltips" version="4.x" />
### Custom tooltip formatting
You can use JavaScript to customize the formatting of a tooltip. Pass a `RawJs` object to the `tooltips()` method, containing a JavaScript string expression to use. The current value to format will be available in the `$value` variable:
```php
use Filament\Forms\Components\Slider;
use Filament\Support\RawJs;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->tooltips(RawJs::make(<<<'JS'
`$${$value.toFixed(2)}`
JS))
```
<AutoScreenshot name="forms/fields/slider/tooltips-formatting" alt="Slider with custom tooltip formatting" version="4.x" />
### Controlling tooltips for multiple handles individually
If the slider is set to an array of values, you can control the tooltips for each handle separately by passing an array of values to the `tooltips()` method. The first value will be applied to the first handle, and the second value will be applied to the second handle, and so on:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->tooltips([true, false])
```
<AutoScreenshot name="forms/fields/slider/tooltips-multiple" alt="Slider with multiple tooltips" version="4.x" />
## Filling a track with color
By default, the color of the track is not affected by the position of any handles it has. When using an individual handle, you can fill the part of the track before the handle with color using the `fillTrack()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->fillTrack()
```
<AutoScreenshot name="forms/fields/slider/fill" alt="Slider with fill" version="4.x" />
Optionally, you may pass a boolean value to control if the slider should be filled or not:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->fillTrack(FeatureFlag::active())
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `fillTrack()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
When using multiple handles, you must manually specify which parts of the track should be filled by passing an array of `true` and `false` values, one for each section. The total number of values should be one more than the number of handles. The first value will be applied to the section before the first handle, the second value will be applied to the section between the first and second handles, and so on:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->fillTrack([false, true, false])
```
<AutoScreenshot name="forms/fields/slider/fill-multiple" alt="Slider with multiple fills" version="4.x" />
Filling also works on [vertical tracks](#using-a-vertical-track):
<AutoScreenshot name="forms/fields/slider/fill-vertical" alt="Vertical slider with fill" version="4.x" />
## Adding pips to tracks
You can add pips to tracks, which are small markers that indicate the position of the handles. You can add pips to the track using the `pips()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips()
```
<AutoScreenshot name="forms/fields/slider/pips" alt="Slider with pips" version="4.x" />
Pips also work when there are multiple handles:
<AutoScreenshot name="forms/fields/slider/pips-multiple" alt="Slider with multiple pips" version="4.x" />
You can also add pips to [vertical tracks](#using-a-vertical-track):
<AutoScreenshot name="forms/fields/slider/pips-vertical" alt="Vertical slider with pips" version="4.x" />
### Adjusting the density of pips
By default, pips are displayed at a density of `10`. This means that for every 10 units of the track, a pip will be displayed. You can adjust this density using the `density` parameter of the `pips()` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(density: 5)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `density` parameter also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/pips-density" alt="Slider with a custom pips density" version="4.x" />
### Custom pip label formatting
You can use JavaScript to customize the formatting of a pip label. Pass a `RawJs` object to the `pipsFormatter()` method, containing a JavaScript string expression to use. The current value to format will be available in the `$value` variable:
```php
use Filament\Forms\Components\Slider;
use Filament\Support\RawJs;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips()
->pipsFormatter(RawJs::make(<<<'JS'
`$${$value.toFixed(2)}`
JS))
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `pipsFormatter()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/pips-formatting" alt="Slider with custom pips formatting" version="4.x" />
### Adding pip labels to steps of the track
If you are using [steps](#controlling-the-step-size) to restrict the movement of the track, you can add a pip label to each step. To do this, pass a `PipsMode::Steps` object to the `pips()` method:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->step(10)
->pips(PipsMode::Steps)
```
<AutoScreenshot name="forms/fields/slider/pips-steps" alt="Slider with pips on steps" version="4.x" />
If you would like to add additional pips to the track without labels, you can [adjust the density](#adjusting-the-density-of-pips) of the pips as well:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->step(10)
->pips(PipsMode::Steps, density: 5)
```
<AutoScreenshot name="forms/fields/slider/pips-steps-density" alt="Slider with pips on steps and a custom density" version="4.x" />
### Adding pip labels to percentage positions of the track
If you would like to add pip labels to the track at specific percentage positions, you can pass a `PipsMode::Positions` object to the `pips()` method. The percentage positions need to be defined in the `pipsValues()` method in an array of numbers:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(PipsMode::Positions)
->pipsValues([0, 25, 50, 75, 100])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `pipsValues()` method also accepts a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/pips-positions" alt="Slider with pips on positions" version="4.x" />
The [density](#adjusting-the-density-of-pips) still controls the spacing of the pips without labels.
### Adding a set number of pip labels to the track
If you would like to add a set number of pip labels to the track, you can pass a `PipsMode::Count` object to the `pips()` method. The number of pips need to be defined in the `pipsValues()` method:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(PipsMode::Count)
->pipsValues(5)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `pipsValues()` method also accepts a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/pips-count" alt="Slider with a set number of pips" version="4.x" />
The [density](#adjusting-the-density-of-pips) still controls the spacing of the pips without labels.
### Adding pip labels to set values on the track
Instead of defining [percentage positions](#adding-pip-labels-to-percentage-positions-of-the-track) or a [set number](#adding-a-set-number-of-pip-labels-to-the-track) of pip labels, you can also define a set of values to use for the pip labels. To do this, pass a `PipsMode::Values` object to the `pips()` method. The values need to be defined in the `pipsValues()` method in an array of numbers:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(PipsMode::Values)
->pipsValues([5, 15, 25, 35, 45, 55, 65, 75, 85, 95])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `pipsValues()` method also accepts a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/slider/pips-values" alt="Slider with pips on values" version="4.x" />
The [density](#adjusting-the-density-of-pips) still controls the spacing of the pips without labels:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(PipsMode::Values, density: 5)
->pipsValues([5, 15, 25, 35, 45, 55, 65, 75, 85, 95])
```
<AutoScreenshot name="forms/fields/slider/pips-values-density" alt="Slider with pips on values and a custom density" version="4.x" />
### Manually filtering pips
For even more control over how pips are displayed on a track, you can use a JavaScript expression. The JavaScript expression should return different numbers based on the pip's appearance:
- The expression should return `1` if a large pip label should be displayed.
- The expression should return `2` if a small pip label should be displayed.
- The expression should return `0` if a pip should be displayed without a label.
- The expression should return `-1` if a pip should not be displayed at all.
The [density](#adjusting-the-density-of-pips) of the pips will control which values get passed to the JavaScript expression. The expression should use the `$value` variable to access the current value of the pip. The expression should be defined in a `RawJs` object and passed to the `pipsFilter()` method:
```php
use Filament\Forms\Components\Slider;
use Filament\Support\RawJs;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->pips(density: 5)
->pipsFilter(RawJs::make(<<<'JS'
($value % 50) === 0
? 1
: ($value % 10) === 0
? 2
: ($value % 25) === 0
? 0
: -1
JS))
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `pipsFilter()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
In this example the `%` operator is used to determine the divisibility of the pip value. The expression will return `1` for every 50 units, `2` for every 10 units, `0` for every 25 units, and `-1` for all other values:
<AutoScreenshot name="forms/fields/slider/pips-filter" alt="Slider with pips filter" version="4.x" />
## Setting a minimum difference between handles
To set a minimum distance between handles, you can use the `minDifference()` method, passing a number to it. This represents the real difference between the values of both handles, not their visual distance:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->minDifference(10)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `minDifference()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="warning">
The `minDifference()` method does not impact the validation of the slider. A skilled user could manipulate the value of the slider using JavaScript to select a value that does not align with the minimum difference. They will still be prevented from selecting a value outside the range of the slider.
</Aside>
## Setting a maximum difference between handles
To set a maximum distance between handles, you can use the `maxDifference()` method, passing a number to it. This represents the real difference between the values of both handles, not their visual distance:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->maxDifference(40)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `maxDifference()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<Aside variant="warning">
The `maxDifference()` method does not impact the validation of the slider. A skilled user could manipulate the value of the slider using JavaScript to select a value that does not align with the maximum difference. They will still be prevented from selecting a value outside the range of the slider.
</Aside>
## Controlling the general behavior of the slider
The `behavior()` method of the slider allows you to pass one or more `Behavior` objects to control the behavior of the slider. The available options are:
- `Behavior::Tap` - The slider will smoothly move to the position of the tap when the user clicks on the track. This is a default behavior, so when applying another behavior, you should also include this one in the array if you want to preserve it.
- `Behavior::Drag` - When there are two handles on the track, the user can drag the track to move both handles at the same time. The [`fillTrack([false, true, false])`](#filling-a-track-with-color) method must be used to ensure that the user has something to drag.
- `Behavior::Drag` and `Behavior::Fixed` - When there are two handles on the track, the user can drag the track to move both handles at the same time, but they cannot change the distance between them. The [`fillTrack([false, true, false])`](#filling-a-track-with-color) method must be used to ensure that the user has something to drag. Be aware that the distance between the handles is not automatically validated on the backend, so a skilled user could manipulate the value of the slider using JavaScript to select a value with a different distance. They will still be prevented from selecting a value outside the range of the slider.
- `Behavior::Unconstrained` - When there are multiple handles on the track, they can be dragged past each other. The [`minDifference()`](#setting-a-minimum-difference-between-handles) and [`maxDifference()`](#setting-a-maximum-difference-between-handles) methods will not work with this behavior.
- `Behavior::SmoothSteps` - While dragging handles, they will not snap to the [steps](#controlling-the-step-size) of the track. Once the user releases the handle, it will snap to the nearest step.
For example, to use `Behavior::Tap`, `Behavior::Drag` and `Behavior::SmoothSteps` all at once:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\Behavior;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->behavior([Behavior::Tap, Behavior::Drag, Behavior::SmoothSteps])
```
To disable all behavior, including the default `Behavior::Tap`, you can use the `behavior(null)` method:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->behavior(null)
```
## Non-linear tracks
You can create non-linear tracks, where certain parts of the track are compressed or expanded, by defining an array of percentage points in the `nonLinearPoints()` method of the slider. Each percentage key of the array should have a corresponding real value, which will be used to calculate the position of the handle on the track. The example below features [pips](#adding-pips-to-tracks) to demonstrate the non-linear behavior of the track:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->nonLinearPoints(['20%' => 50, '50%' => 75])
->pips()
```
<UtilityInjection set="formFields" version="4.x">As well as allowing static values, the `nonLinearPoints()` method also accepts a function to dynamically calculate them. You can inject various utilities into the function as parameters.</UtilityInjection>
When using a non-linear track, you can also control the stepping for each section. By defining an array of two numbers for each percentage point, the first number will be used as the real value for percentage position, and the second number will be used as the step size for that section, active until the next percentage point:
```php
use Filament\Forms\Components\Slider;
Slider::make('slider')
->range(minValue: 0, maxValue: 100)
->nonLinearPoints(['20%' => [50, 5], '50%' => [75, 1]])
->pips()
```
<Aside variant="warning">
When using a non-linear track, the step values of certain track sections do not affect the validation of the slider. A skilled user could manipulate the value of the slider using JavaScript to select a value that does not align with a step value in the track. They will still be prevented from selecting a value outside the range of the slider.
</Aside>
When using [pips](#adding-pips-to-tracks) with a non-linear track, you can ensure that pip labels are rounded and only displayed at selectable positions on the track. Otherwise, the stepping of a non-linear portion of the track could add labels to positions that are not selectable. To do this, use the `steppedPips()` method:
```php
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
Slider::make('slider')
->range(minValue: 0, maxValue: 10000)
->nonLinearPoints(['10%' => [500, 500], '50%' => [4000, 1000]])
->pips(PipsMode::Positions, density: 4)
->pipsValues([0, 25, 50, 75, 100])
->steppedPips()
```
@@ -0,0 +1,49 @@
---
title: Code editor
---
import AutoScreenshot from "@components/AutoScreenshot.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
The code editor component allows you to write code in a textarea with line numbers. By default, no syntax highlighting is applied.
```php
use Filament\Forms\Components\CodeEditor;
CodeEditor::make('code')
```
<AutoScreenshot name="forms/fields/code-editor/simple" alt="Code editor" version="4.x" />
## Using language syntax highlighting
You may change the language syntax highlighting of the code editor using the `language()` method. The editor supports the following languages:
- C++
- CSS
- Go
- HTML
- Java
- JavaScript
- JSON
- Markdown
- PHP
- Python
- SQL
- XML
- YAML
You can open the `Filament\Forms\Components\CodeEditor\Enums\Language` enum class to see this list. To switch to using JavaScript syntax highlighting, you can use the `Language::JavaScript` enum value:
```php
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\CodeEditor\Enums\Language;
CodeEditor::make('code')
->language(Language::JavaScript)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `language()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
<AutoScreenshot name="forms/fields/code-editor/language" alt="Code editor with syntax highlighting" version="4.x" />
@@ -0,0 +1,15 @@
---
title: Hidden
---
## Introduction
The hidden component allows you to create a hidden field in your form that holds a value.
```php
use Filament\Forms\Components\Hidden;
Hidden::make('token')
```
Please be aware that the value of this field is still editable by the user if they decide to use the browser's developer tools. You should not use this component to store sensitive or read-only information.
@@ -0,0 +1,263 @@
---
title: Custom fields
---
import Aside from "@components/Aside.astro"
## Introduction
Livewire components are PHP classes that have their state stored in the user's browser. When a network request is made, the state is sent to the server, and filled into public properties on the Livewire component class, where it can be accessed in the same way as any other class property in PHP can be.
Imagine you had a Livewire component with a public property called `$name`. You could bind that property to an input field in the HTML of the Livewire component in one of two ways: with the [`wire:model` attribute](https://livewire.laravel.com/docs/properties#data-binding), or by [entangling](https://livewire.laravel.com/docs/javascript#the-wire-object) it with an Alpine.js property:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<input wire:model="name" />
<!-- Or -->
<div x-data="{ state: $wire.$entangle('name') }">
<input x-model="state" />
</div>
</x-dynamic-component>
```
When the user types into the input field, the `$name` property is updated in the Livewire component class. When the user submits the form, the `$name` property is sent to the server, where it can be saved.
This is the basis of how fields work in Filament. Each field is assigned to a public property in the Livewire component class, which is where the state of the field is stored. We call the name of this property the "state path" of the field. You can access the state path of a field using the `$getStatePath()` function in the field's view:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<input wire:model="{{ $getStatePath() }}" />
<!-- Or -->
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<input x-model="state" />
</div>
</x-dynamic-component>
```
If your component heavily relies on third party libraries, we advise that you asynchronously load the Alpine.js component using the Filament asset system. This ensures that the Alpine.js component is only loaded when it's needed, and not on every page load. To find out how to do this, check out our [Assets documentation](../assets#asynchronous-alpinejs-components).
### Custom field classes
You may create your own custom field classes and views, which you can reuse across your project, and even release as a plugin to the community.
To create a custom field class and view, you may use the following command:
```bash
php artisan make:filament-form-field LocationPicker
```
This will create the following component class:
```php
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
}
```
It will also create a view file at `resources/views/filament/forms/components/location-picker.blade.php`.
<Aside variant="info">
Filament form fields are **not** Livewire components. Defining public properties and methods on a form field class will not make them accessible in the Blade view.
</Aside>
## Accessing the state of another component in the Blade view
Inside the Blade view, you may access the state of another component in the schema using the `$get()` function:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $get('email') }}
</x-dynamic-component>
```
<Aside variant="tip">
Unless a form field is [reactive](../forms/overview#the-basics-of-reactivity), the Blade view will not refresh when the value of the field changes, only when the next user interaction occurs that makes a request to the server. If you need to react to changes in a field's value, it should be `live()`.
</Aside>
## Accessing the Eloquent record in the Blade view
Inside the Blade view, you may access the current Eloquent record using the `$record` variable:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $record->name }}
</x-dynamic-component>
```
## Accessing the current operation in the Blade view
Inside the Blade view, you may access the current operation, usually `create`, `edit` or `view`, using the `$operation` variable:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($operation === 'create')
This is a new conference.
@else
This is an existing conference.
@endif
</x-dynamic-component>
```
## Accessing the current Livewire component instance in the Blade view
Inside the Blade view, you may access the current Livewire component instance using `$this`:
```blade
@php
use Filament\Resources\Users\RelationManagers\ConferencesRelationManager;
@endphp
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($this instanceof ConferencesRelationManager)
You are editing conferences the of a user.
@endif
</x-dynamic-component>
```
## Accessing the current field instance in the Blade view
Inside the Blade view, you may access the current field instance using `$field`. You can call public methods on this object to access other information that may not be available in variables:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($field->getState())
This is a new conference.
@endif
</x-dynamic-component>
```
## Adding a configuration method to a custom field class
You may add a public method to the custom field class that accepts a configuration value, stores it in a protected property, and returns it again from another public method:
```php
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
protected ?float $zoom = null;
public function zoom(?float $zoom): static
{
$this->zoom = $zoom;
return $this;
}
public function getZoom(): ?float
{
return $this->zoom;
}
}
```
Now, in the Blade view for the custom field, you may access the zoom using the `$getZoom()` function:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $getZoom() }}
</x-dynamic-component>
```
Any public method that you define on the custom field class can be accessed in the Blade view as a variable function in this way.
To pass the configuration value to the custom field class, you may use the public method:
```php
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
->zoom(0.5)
```
## Allowing utility injection in a custom field configuration method
[Utility injection](overview#field-utility-injection) is a powerful feature of Filament that allows users to configure a component using functions that can access various utilities. You can allow utility injection by ensuring that the parameter type and property type of the configuration allows the user to pass a `Closure`. In the getter method, you should pass the configuration value to the `$this->evaluate()` method, which will inject utilities into the user's function if they pass one, or return the value if it is static:
```php
use Closure;
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
protected float | Closure | null $zoom = null;
public function zoom(float | Closure | null $zoom): static
{
$this->zoom = $zoom;
return $this;
}
public function getZoom(): ?float
{
return $this->evaluate($this->zoom);
}
}
```
Now, you can pass a static value or a function to the `zoom()` method, and [inject any utility](overview#component-utility-injection) as a parameter:
```php
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
->zoom(fn (Conference $record): float => $record->isGlobal() ? 1 : 0.5)
```
## Obeying state binding modifiers
When you bind a field to a state path, you may use the `defer` modifier to ensure that the state is only sent to the server when the user submits the form, or whenever the next Livewire request is made. This is the default behavior.
However, you may use the [`live()`](overview#the-basics-of-reactivity) on a field to ensure that the state is sent to the server immediately when the user interacts with the field. This allows for lots of advanced use cases as explained in the [reactivity](overview#the-basics-of-reactivity) section of the documentation.
Filament provides a `$applyStateBindingModifiers()` function that you may use in your view to apply any state binding modifiers to a `wire:model` or `$wire.$entangle()` binding:
```blade
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<input {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" />
<!-- Or -->
<div x-data="{ state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$getStatePath()}')") }} }">
<input x-model="state" />
</div>
</x-dynamic-component>
```
@@ -0,0 +1,689 @@
---
title: Validation
---
import Aside from "@components/Aside.astro"
import UtilityInjection from "@components/UtilityInjection.astro"
## Introduction
Validation rules may be added to any [field](overview#available-fields).
In Laravel, validation rules are usually defined in arrays like `['required', 'max:255']` or a combined string like `required|max:255`. This is fine if you're exclusively working in the backend with simple form requests. But Filament is also able to give your users frontend validation, so they can fix their mistakes before any backend requests are made.
Filament includes many [dedicated validation methods](#available-rules), but you can also use any [other Laravel validation rules](#other-rules), including [custom validation rules](#custom-rules).
<Aside variant="warning">
Some default Laravel validation rules rely on the correct attribute names and won't work when passed via `rule()`/`rules()`. Use the dedicated validation methods whenever you can.
</Aside>
## Available rules
### Active URL
The field must have a valid A or AAAA record according to the `dns_get_record()` PHP function. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-active-url)
```php
Field::make('name')->activeUrl()
```
### After (date)
The field value must be a value after a given date. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-after)
```php
Field::make('start_date')->after('tomorrow')
```
Alternatively, you may pass the name of another field to compare against:
```php
Field::make('start_date')
Field::make('end_date')->after('start_date')
```
### After or equal to (date)
The field value must be a date after or equal to the given date. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-after-or-equal)
```php
Field::make('start_date')->afterOrEqual('tomorrow')
```
Alternatively, you may pass the name of another field to compare against:
```php
Field::make('start_date')
Field::make('end_date')->afterOrEqual('start_date')
```
### Alpha
The field must be entirely alphabetic characters. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-alpha)
```php
Field::make('name')->alpha()
```
### Alpha Dash
The field may have alphanumeric characters, as well as dashes and underscores. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-alpha-dash)
```php
Field::make('name')->alphaDash()
```
### Alpha Numeric
The field must be entirely alphanumeric characters. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-alpha-num)
```php
Field::make('name')->alphaNum()
```
### ASCII
The field must be entirely 7-bit ASCII characters. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-ascii)
```php
Field::make('name')->ascii()
```
### Before (date)
The field value must be a date before a given date. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-before)
```php
Field::make('start_date')->before('first day of next month')
```
Alternatively, you may pass the name of another field to compare against:
```php
Field::make('start_date')->before('end_date')
Field::make('end_date')
```
### Before or equal to (date)
The field value must be a date before or equal to the given date. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-before-or-equal)
```php
Field::make('start_date')->beforeOrEqual('end of this month')
```
Alternatively, you may pass the name of another field to compare against:
```php
Field::make('start_date')->beforeOrEqual('end_date')
Field::make('end_date')
```
### Confirmed
The field must have a matching field of `{field}_confirmation`. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-confirmed)
```php
Field::make('password')->confirmed()
Field::make('password_confirmation')
```
### Different
The field value must be different to another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-different)
```php
Field::make('backup_email')->different('email')
```
### Doesn't Start With
The field must not start with one of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-doesnt-start-with)
```php
Field::make('name')->doesntStartWith(['admin'])
```
### Doesn't End With
The field must not end with one of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-doesnt-end-with)
```php
Field::make('name')->doesntEndWith(['admin'])
```
### Ends With
The field must end with one of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-ends-with)
```php
Field::make('name')->endsWith(['bot'])
```
### Enum
The field must contain a valid enum value. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-enum)
```php
Field::make('status')->enum(MyStatus::class)
```
### Exists
The field value must exist in the database. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-exists)
```php
Field::make('invitation')->exists()
```
By default, the form's model will be searched, [if it is registered](../components/form#setting-a-form-model). You may specify a custom table name or model to search:
```php
use App\Models\Invitation;
Field::make('invitation')->exists(table: Invitation::class)
```
By default, the field name will be used as the column to search. You may specify a custom column to search:
```php
Field::make('invitation')->exists(column: 'id')
```
You can further customize the rule by passing a [closure](overview#closure-customization) to the `modifyRuleUsing` parameter:
```php
use Illuminate\Validation\Rules\Exists;
Field::make('invitation')
->exists(modifyRuleUsing: function (Exists $rule) {
return $rule->where('is_active', 1);
})
```
Laravel's `exists` validation rule does not use the Eloquent model to query the database by default, so it will not use any global scopes defined on the model, including for soft-deletes. As such, even if there is a soft-deleted record with the same value, the validation will pass.
Since global scopes are not applied, Filament's multi-tenancy feature also does not scope the query to the current tenant by default.
To do this, you should use the `scopedExists()` method instead, which replaces Laravel's `exists` implementation with one that uses the model to query the database, applying any global scopes defined on the model, including for soft-deletes and multi-tenancy:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('email')
->scopedExists()
```
If you would like to modify the Eloquent query used to check for presence, including to remove a global scope, you can pass a function to the `modifyQueryUsing` parameter:
```php
use Filament\Forms\Components\TextInput;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
TextInput::make('email')
->scopedExists(modifyQueryUsing: function (Builder $query) {
return $query->withoutGlobalScope(SoftDeletingScope::class);
})
```
### Filled
The field must not be empty when it is present. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-filled)
```php
Field::make('name')->filled()
```
### Greater than
The field value must be greater than another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-gt)
```php
Field::make('newNumber')->gt('oldNumber')
```
### Greater than or equal to
The field value must be greater than or equal to another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-gte)
```php
Field::make('newNumber')->gte('oldNumber')
```
### Hex color
The field value must be a valid color in hexadecimal format. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-hex-color)
```php
Field::make('color')->hexColor()
```
### In
The field must be included in the given list of values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-in)
```php
Field::make('status')->in(['pending', 'completed'])
```
The [toggle buttons](toggle-buttons), [checkbox list](checkbox-list), [radio](radio) and [select](select#valid-options-validation-in-rule) fields automatically apply the `in()` rule based on their available options, so you do not need to add it manually.
### Ip Address
The field must be an IP address. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-ip)
```php
Field::make('ip_address')->ip()
Field::make('ip_address')->ipv4()
Field::make('ip_address')->ipv6()
```
### JSON
The field must be a valid JSON string. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-json)
```php
Field::make('ip_address')->json()
```
### Less than
The field value must be less than another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-lt)
```php
Field::make('newNumber')->lt('oldNumber')
```
### Less than or equal to
The field value must be less than or equal to another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-lte)
```php
Field::make('newNumber')->lte('oldNumber')
```
### Mac Address
The field must be a MAC address. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-mac)
```php
Field::make('mac_address')->macAddress()
```
### Multiple Of
The field must be a multiple of value. [See the Laravel documentation.](https://laravel.com/docs/validation#multiple-of)
```php
Field::make('number')->multipleOf(2)
```
### Not In
The field must not be included in the given list of values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-not-in)
```php
Field::make('status')->notIn(['cancelled', 'rejected'])
```
### Not Regex
The field must not match the given regular expression. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-not-regex)
```php
Field::make('email')->notRegex('/^.+$/i')
```
### Nullable
The field value can be empty. This rule is applied by default if the `required` rule is not present. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-nullable)
```php
Field::make('name')->nullable()
```
### Prohibited
The field value must be empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-prohibited)
```php
Field::make('name')->prohibited()
```
### Prohibited If
The field must be empty *only if* the other specified field has any of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-prohibited-if)
```php
Field::make('name')->prohibitedIf('field', 'value')
```
### Prohibited Unless
The field must be empty *unless* the other specified field has any of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-prohibited-unless)
```php
Field::make('name')->prohibitedUnless('field', 'value')
```
### Prohibits
If the field is not empty, all other specified fields must be empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-prohibits)
```php
Field::make('name')->prohibits('field')
Field::make('name')->prohibits(['field', 'another_field'])
```
### Required
The field value must not be empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required)
```php
Field::make('name')->required()
```
#### Marking a field as required
By default, required fields will show an asterisk `*` next to their label. You may want to hide the asterisk on forms where all fields are required, or where it makes sense to add a [hint](#adding-a-hint-next-to-the-label) to optional fields instead:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->required() // Adds validation to ensure the field is required
->markAsRequired(false) // Removes the asterisk
```
If your field is not `required()`, but you still wish to show an asterisk `*` you can use `markAsRequired()` too:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->markAsRequired()
```
### Required If
The field value must not be empty _only if_ the other specified field has any of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-if)
```php
Field::make('name')->requiredIf('field', 'value')
```
### Required If Accepted
The field value must not be empty _only if_ the other specified field is equal to "yes", "on", 1, "1", true, or "true". [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-if-accepted)
```php
Field::make('name')->requiredIfAccepted('field')
```
### Required Unless
The field value must not be empty _unless_ the other specified field has any of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-unless)
```php
Field::make('name')->requiredUnless('field', 'value')
```
### Required With
The field value must not be empty _only if_ any of the other specified fields are not empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-with)
```php
Field::make('name')->requiredWith('field,another_field')
```
### Required With All
The field value must not be empty _only if_ all the other specified fields are not empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-with-all)
```php
Field::make('name')->requiredWithAll('field,another_field')
```
### Required Without
The field value must not be empty _only when_ any of the other specified fields are empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-without)
```php
Field::make('name')->requiredWithout('field,another_field')
```
### Required Without All
The field value must not be empty _only when_ all the other specified fields are empty. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-required-without-all)
```php
Field::make('name')->requiredWithoutAll('field,another_field')
```
### Regex
The field must match the given regular expression. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-regex)
```php
Field::make('email')->regex('/^.+@.+$/i')
```
### Same
The field value must be the same as another. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-same)
```php
Field::make('password')->same('passwordConfirmation')
```
### Starts With
The field must start with one of the given values. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-starts-with)
```php
Field::make('name')->startsWith(['a'])
```
### String
The field must be a string. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-string)
```php
Field::make('name')->string()
```
### Unique
The field value must not exist in the database. [See the Laravel documentation.](https://laravel.com/docs/validation#rule-unique)
```php
Field::make('email')->unique()
```
If your Filament form already has an Eloquent model associated with it, such as in a [panel resource](../resources), Filament will use that. You may also specify a custom table name or model to search:
```php
use App\Models\User;
Field::make('email')->unique(table: User::class)
```
By default, the field name will be used as the column to search. You may specify a custom column to search:
```php
Field::make('email')->unique(column: 'email_address')
```
Usually, you wish to ignore a given model during unique validation. For example, consider an "update profile" form that includes the user's name, email address, and location. You will probably want to verify that the email address is unique. However, if the user only changes the name field and not the email field, you do not want a validation error to be thrown because the user is already the owner of the email address in question. If your Filament form already has an Eloquent model associated with it, such as in a [panel resource](../resources), Filament will ignore it.
To prevent Filament from ignoring the current Eloquent record, you can pass `false` to the `ignoreRecord` parameter:
```php
Field::make('email')->unique(ignoreRecord: false)
```
Alternatively, to ignore an Eloquent record of your choice, you can pass it to the `ignorable` parameter:
```php
Field::make('email')->unique(ignorable: $ignoredUser)
```
You can further customize the rule by passing a [closure](overview#closure-customization) to the `modifyRuleUsing` parameter:
```php
use Illuminate\Validation\Rules\Unique;
Field::make('email')
->unique(modifyRuleUsing: function (Unique $rule) {
return $rule->where('is_active', 1);
})
```
Laravel's `unique` validation rule does not use the Eloquent model to query the database by default, so it will not use any global scopes defined on the model, including for soft-deletes. As such, even if there is a soft-deleted record with the same value, the validation will fail.
Since global scopes are not applied, Filament's multi-tenancy feature also does not scope the query to the current tenant by default.
To do this, you should use the `scopedUnique()` method instead, which replaces Laravel's `unique` implementation with one that uses the model to query the database, applying any global scopes defined on the model, including for soft-deletes and multi-tenancy:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('email')
->scopedUnique()
```
If you would like to modify the Eloquent query used to check for uniqueness, including to remove a global scope, you can pass a function to the `modifyQueryUsing` parameter:
```php
use Filament\Forms\Components\TextInput;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
TextInput::make('email')
->scopedUnique(modifyQueryUsing: function (Builder $query) {
return $query->withoutGlobalScope(SoftDeletingScope::class);
})
```
### ULID
The field under validation must be a valid [Universally Unique Lexicographically Sortable Identifier](https://github.com/ulid/spec) (ULID). [See the Laravel documentation.](https://laravel.com/docs/validation#rule-ulid)
```php
Field::make('identifier')->ulid()
```
### UUID
The field must be a valid RFC 4122 (version 1, 3, 4, or 5) universally unique identifier (UUID). [See the Laravel documentation.](https://laravel.com/docs/validation#rule-uuid)
```php
Field::make('identifier')->uuid()
```
## Other rules
You may add other validation rules to any field using the `rules()` method:
```php
TextInput::make('slug')->rules(['alpha_dash'])
```
A full list of validation rules may be found in the [Laravel documentation](https://laravel.com/docs/validation#available-validation-rules).
## Custom rules
You may use any custom validation rules as you would do in [Laravel](https://laravel.com/docs/validation#custom-validation-rules):
```php
TextInput::make('slug')->rules([new Uppercase()])
```
You may also use [closure rules](https://laravel.com/docs/validation#using-closures):
```php
use Closure;
TextInput::make('slug')->rules([
fn (): Closure => function (string $attribute, $value, Closure $fail) {
if ($value === 'foo') {
$fail('The :attribute is invalid.');
}
},
])
```
You may [inject utilities](overview#field-utility-injection) like [`$get`](overview#injecting-the-state-of-another-field) into your custom rules, for example if you need to reference other field values in your form. To do this, wrap the closure rule in another function that returns it:
```php
use Filament\Schemas\Components\Utilities\Get;
TextInput::make('slug')->rules([
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
if ($get('other_field') === 'foo' && $value !== 'bar') {
$fail("The {$attribute} is invalid.");
}
},
])
```
## Customizing validation attributes
When fields fail validation, their label is used in the error message. To customize the label used in field error messages, use the `validationAttribute()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->validationAttribute('full name')
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `validationAttribute()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Validation messages
By default Laravel's validation error message is used. To customize the error messages, use the `validationMessages()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('email')
->unique(// ...)
->validationMessages([
'unique' => 'The :attribute has already been registered.',
])
```
<UtilityInjection set="formFields" version="4.x">As well as allowing an array of static value, the `validationMessages()` method also accepts a function for each message. You can inject various utilities into the functions as parameters.</UtilityInjection>
### Allowing HTML in validation messages
By default, validation messages are rendered as plain text to prevent XSS attacks. However, you may need to render HTML in your validation messages, such as when displaying lists or links. To enable HTML rendering for validation messages, use the `allowHtmlValidationMessages()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('password')
->required()
->rules([
new CustomRule(), // Custom rule that returns a validation message that contains HTML
])
->allowHtmlValidationMessages()
```
Be aware that you will need to ensure that the HTML in all validation messages is safe to render, otherwise your application will be vulnerable to XSS attacks.
## Disabling validation when fields are not dehydrated
When a field is [not dehydrated](overview#preventing-a-field-from-being-dehydrated), it is still validated. To disable validation for fields that are not dehydrated, use the `validatedWhenNotDehydrated()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->required()
->dehydrated(false)
->validatedWhenNotDehydrated(false)
```
<UtilityInjection set="formFields" version="4.x">As well as allowing a static value, the `validatedWhenNotDehydrated()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
@@ -0,0 +1,174 @@
.fi-fo-builder {
@apply grid grid-cols-1 gap-y-4;
& .fi-fo-builder-actions {
@apply flex gap-x-3;
&.fi-hidden {
@apply hidden;
}
}
& .fi-fo-builder-items {
@apply grid grid-cols-1;
& > * + * {
@apply mt-4;
}
}
& .fi-fo-builder-item {
@apply rounded-xl bg-white shadow-xs ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10;
&.fi-collapsed {
& .fi-fo-builder-item-header-collapsible-actions {
@apply -rotate-180;
}
& .fi-fo-builder-item-header-collapse-action {
@apply pointer-events-none opacity-0;
}
}
&:not(.fi-collapsed) {
& .fi-fo-builder-item-header-expand-action {
@apply pointer-events-none opacity-0;
}
}
}
&.fi-fo-builder-not-contained > .fi-fo-builder-items {
& > .fi-fo-builder-item {
@apply rounded-none bg-transparent shadow-none ring-0;
& > .fi-fo-builder-item-content {
@apply p-0;
}
}
& > .fi-fo-builder-label-between-items-ctn {
& > .fi-fo-builder-label-between-items-divider-before {
@apply w-0;
}
& > .fi-fo-builder-label-between-items {
@apply ps-0;
}
}
}
& .fi-fo-builder-item-header {
@apply flex items-center gap-x-3 overflow-hidden px-4 py-3;
}
&.fi-collapsible {
& .fi-fo-builder-item-header {
@apply cursor-pointer select-none;
}
}
& .fi-fo-builder-item-header-start-actions {
@apply flex items-center gap-x-3;
}
& .fi-fo-builder-item-header-icon {
@apply text-gray-400 dark:text-gray-500;
}
& .fi-fo-builder-item-header-label {
@apply text-sm font-medium text-gray-950 dark:text-white;
&.fi-truncated {
@apply truncate;
}
}
& .fi-fo-builder-item-header-end-actions {
@apply ms-auto flex items-center gap-x-3;
}
& .fi-fo-builder-item-header-collapsible-actions {
@apply relative transition;
}
& .fi-fo-builder-item-header-collapse-action {
@apply transition;
}
& .fi-fo-builder-item-header-expand-action {
@apply absolute inset-0 rotate-180 transition;
}
& .fi-fo-builder-item-content {
&:not(.fi-fo-builder-item-content-has-preview) {
@apply p-4;
}
&.fi-fo-builder-item-content-has-preview {
@apply relative;
}
}
& .fi-fo-builder-item-has-header > .fi-fo-builder-item-content {
@apply border-t border-gray-100 dark:border-white/10;
}
& .fi-fo-builder-item-preview {
&:not(.fi-interactive) {
@apply pointer-events-none;
}
}
& .fi-fo-builder-item-preview-edit-overlay {
@apply absolute inset-0 z-1 cursor-pointer;
}
& .fi-fo-builder-block-picker-ctn {
@apply rounded-lg bg-white dark:bg-gray-900;
}
& .fi-fo-builder-add-between-items-ctn {
@apply pointer-events-none invisible relative mt-0 flex h-0 w-full justify-center overflow-visible opacity-0 transition-opacity;
}
& .fi-fo-builder-item:hover + .fi-fo-builder-add-between-items-ctn,
& .fi-fo-builder-add-between-items-ctn:has(+ .fi-fo-builder-item:hover),
& .fi-fo-builder-add-between-items-ctn:hover,
& .fi-fo-builder-add-between-items-ctn:focus-within {
@apply pointer-events-auto visible opacity-100;
}
& .fi-fo-builder-add-between-items {
@apply absolute top-1/2 z-10 translate-y-[calc(-50%+0.5rem)] rounded-lg bg-white dark:bg-gray-900;
}
& .fi-fo-builder-label-between-items-ctn {
@apply relative mt-1 -mb-3 flex items-center;
}
& .fi-fo-builder-label-between-items-divider-before {
@apply w-3 shrink-0 border-t border-gray-200 dark:border-white/10;
}
& .fi-fo-builder-label-between-items {
@apply shrink-0 px-1 text-sm font-medium text-gray-500 dark:text-gray-400;
}
& .fi-fo-builder-label-between-items-divider-after {
@apply flex-1 border-t border-gray-200 dark:border-white/10;
}
& .fi-fo-builder-block-picker {
@apply flex justify-center;
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
}
}
@@ -0,0 +1,45 @@
.fi-fo-checkbox-list {
& .fi-fo-checkbox-list-search-input-wrp {
@apply mb-4;
}
& .fi-fo-checkbox-list-actions {
@apply mb-2;
}
& .fi-fo-checkbox-list-options {
@apply gap-4;
&.fi-grid-direction-col {
@apply -mt-4;
& .fi-fo-checkbox-list-option-ctn {
@apply break-inside-avoid pt-4;
}
}
}
& .fi-fo-checkbox-list-option {
@apply flex gap-x-3;
& .fi-checkbox-input {
@apply mt-1 shrink-0;
}
& .fi-fo-checkbox-list-option-text {
@apply grid text-sm leading-6;
}
& .fi-fo-checkbox-list-option-label {
@apply overflow-hidden font-medium break-words text-gray-950 dark:text-white;
}
& .fi-fo-checkbox-list-option-description {
@apply text-gray-500 dark:text-gray-400;
}
}
& .fi-fo-checkbox-list-no-search-results-message {
@apply text-sm text-gray-500 dark:text-gray-400;
}
}
@@ -0,0 +1,71 @@
.fi-fo-code-editor {
@apply overflow-hidden;
& .cm-editor {
&.cm-focused {
@apply outline-none!;
}
& .cm-gutters {
@apply min-h-48! border-e-gray-300! bg-gray-100! dark:border-e-gray-800! dark:bg-gray-950!;
& .cm-gutter {
&.cm-lineNumbers {
& .cm-gutterElement {
@apply ms-1 rounded-s-md;
&.cm-activeLineGutter {
@apply bg-gray-200! dark:bg-gray-800!;
}
}
}
&.cm-foldGutter {
& .cm-gutterElement {
&.cm-activeLineGutter {
@apply bg-gray-200! dark:bg-gray-800!;
}
}
}
}
}
& .cm-scroller {
@apply min-h-48!;
}
& .cm-line {
@apply me-1 rounded-e-md;
}
}
&.fi-disabled {
& .cm-editor {
& .cm-gutters {
& .cm-gutter {
&.cm-lineNumbers {
& .cm-gutterElement {
&.cm-activeLineGutter {
@apply bg-transparent!;
}
}
}
&.cm-foldGutter {
& .cm-gutterElement {
&.cm-activeLineGutter {
@apply bg-transparent!;
}
}
}
}
}
& .cm-line {
&.cm-activeLine {
@apply bg-transparent!;
}
}
}
}
}
@@ -0,0 +1,17 @@
.fi-fo-color-picker {
& .fi-input-wrp-content {
@apply flex;
}
& .fi-fo-color-picker-preview {
@apply my-auto me-3 size-5 shrink-0 rounded-full select-none;
&.fi-empty {
@apply ring-1 ring-gray-200 ring-inset dark:ring-white/10;
}
}
& .fi-fo-color-picker-panel {
@apply absolute z-10 hidden rounded-lg shadow-lg;
}
}
@@ -0,0 +1,88 @@
.fi-fo-date-time-picker {
/* Prevent excessive date input height in WebKit */
/* https://github.com/twbs/bootstrap/issues/34433 */
& input::-webkit-datetime-edit {
display: block;
padding: 0;
}
& .fi-fo-date-time-picker-trigger {
@apply w-full;
}
& .fi-fo-date-time-picker-display-text-input {
@apply w-full border-none bg-transparent px-3 py-1.5 text-sm leading-6 text-gray-950 outline-hidden transition duration-75 placeholder:text-gray-400 focus:ring-0 disabled:text-gray-500 disabled:[-webkit-text-fill-color:var(--color-gray-500)] dark:text-white dark:placeholder:text-gray-500 dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:var(--color-gray-400)];
}
& .fi-fo-date-time-picker-panel {
@apply absolute z-10 space-y-3 rounded-lg bg-white p-4 shadow-lg ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10;
& .fi-fo-date-time-picker-panel-header {
@apply flex items-center justify-between;
}
}
& .fi-fo-date-time-picker-month-select {
@apply grow cursor-pointer border-none bg-transparent p-0 text-sm font-medium text-gray-950 focus:ring-0 dark:bg-gray-900 dark:text-white;
}
& .fi-fo-date-time-picker-year-input {
@apply w-16 border-none bg-transparent p-0 text-right text-sm text-gray-950 focus:ring-0 dark:text-white;
}
& .fi-fo-date-time-picker-calendar-header {
@apply grid grid-cols-7 gap-1;
& .fi-fo-date-time-picker-calendar-header-day {
@apply text-center text-xs font-medium text-gray-500 dark:text-gray-400;
}
}
& .fi-fo-date-time-picker-calendar {
@apply grid grid-cols-[repeat(7,minmax(--spacing(7),1fr))] gap-1;
& .fi-fo-date-time-picker-calendar-day {
@apply rounded-full text-center text-sm leading-loose transition duration-75;
&.fi-disabled {
@apply pointer-events-none opacity-50;
}
&:not(.fi-disabled) {
@apply cursor-pointer;
}
&.fi-selected {
@apply text-primary-600 dark:text-primary-400 bg-gray-50 dark:bg-white/5;
}
&.fi-focused:not(.fi-selected):not(.fi-disabled) {
@apply bg-gray-100 dark:bg-white/10;
}
&.fi-fo-date-time-picker-calendar-day-today:not(.fi-focused):not(
.fi-selected
):not(.fi-disabled) {
@apply text-primary-600 dark:text-primary-400;
}
&:not(.fi-fo-date-time-picker-calendar-day-today):not(
.fi-selected
) {
@apply text-gray-950 dark:text-white;
}
}
}
& .fi-fo-date-time-picker-time-inputs {
@apply flex items-center justify-center rtl:flex-row-reverse;
& input {
@apply me-1 w-10 border-none bg-transparent p-0 text-center text-sm text-gray-950 focus:ring-0 dark:text-white;
}
& .fi-fo-date-time-picker-time-input-separator {
@apply text-sm font-medium text-gray-500 dark:text-gray-400;
}
}
}
@@ -0,0 +1,76 @@
.fi-fo-field {
@apply grid gap-y-2;
&.fi-fo-field-has-inline-label {
@apply sm:grid-cols-3 sm:items-start sm:gap-x-4;
& .fi-fo-field-content-col {
@apply sm:col-span-2;
}
}
& .fi-fo-field-label-ctn,
& .fi-fo-field-label {
@apply flex items-start gap-x-3;
& > .fi-checkbox-input {
@apply mt-0.5 shrink-0;
}
& > .fi-toggle {
@apply -my-0.5;
}
& > .fi-sc:nth-child(1) {
@apply grow-0;
}
&.fi-hidden {
@apply hidden;
}
}
& .fi-fo-field-label-content {
@apply text-sm font-medium text-gray-950 dark:text-white;
& .fi-fo-field-label-required-mark {
@apply text-danger-600 dark:text-danger-400 font-medium;
}
}
& .fi-fo-field-label-col {
@apply grid h-full auto-cols-fr gap-y-2;
&.fi-vertical-align-start {
@apply sm:items-start;
}
&.fi-vertical-align-center {
@apply sm:items-center;
}
&.fi-vertical-align-end {
@apply sm:items-end;
}
}
& .fi-fo-field-content-col {
@apply grid auto-cols-fr gap-y-2;
}
& .fi-fo-field-content-ctn {
@apply flex w-full items-center gap-x-3;
}
& .fi-fo-field-content {
@apply w-full;
}
& .fi-fo-field-wrp-error-message {
@apply text-danger-600 dark:text-danger-400 text-sm;
}
& .fi-fo-field-wrp-error-list {
@apply list-inside list-disc space-y-0.5;
}
}
@@ -0,0 +1,158 @@
.fi-fo-file-upload {
@apply flex flex-col gap-y-2;
&.fi-align-start,
&.fi-align-left {
@apply items-start;
}
&.fi-align-center {
@apply items-center;
}
&.fi-align-end,
&.fi-align-right {
@apply items-end;
}
& .fi-fo-file-upload-input-ctn {
@apply h-full w-full;
}
&.fi-fo-file-upload-avatar {
& .fi-fo-file-upload-input-ctn {
@apply h-full w-32;
}
}
& .fi-fo-file-upload-error-message {
@apply text-danger-600 dark:text-danger-400 text-sm;
}
& .filepond--root {
@apply mb-0 overflow-hidden rounded-lg bg-white font-sans shadow-sm ring-1 ring-gray-950/10 dark:bg-white/5 dark:ring-white/20;
}
& .filepond--root[data-disabled='disabled'] {
@apply bg-gray-50 dark:bg-transparent dark:ring-white/10;
}
& .filepond--root[data-style-panel-layout='compact circle'] {
@apply rounded-full;
}
& .filepond--panel-root {
@apply bg-transparent;
}
& .filepond--drop-label label {
@apply p-3! text-sm text-gray-600 dark:text-gray-400;
}
& .filepond--label-action {
@apply text-primary-600 hover:text-primary-500 dark:hover:text-primary-500 font-medium no-underline transition duration-75 dark:text-white;
}
& .filepond--drip-blob {
@apply bg-gray-400 dark:bg-gray-500;
}
& .filepond--root[data-style-panel-layout='grid'] .filepond--item {
@apply inline;
width: calc(50% - 0.5rem);
}
@variant lg {
& .filepond--root[data-style-panel-layout='grid'] .filepond--item {
width: calc(33.33% - 0.5rem);
}
}
& .filepond--download-icon {
@apply pointer-events-auto me-1 inline-block size-4 bg-white align-bottom hover:bg-white/70;
-webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZG93bmxvYWQiPjxwYXRoIGQ9Ik0yMSAxNXY0YTIgMiAwIDAgMS0yIDJINWEyIDIgMCAwIDEtMi0ydi00Ij48L3BhdGg+PHBvbHlsaW5lIHBvaW50cz0iNyAxMCAxMiAxNSAxNyAxMCI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIxNSIgeDI9IjEyIiB5Mj0iMyI+PC9saW5lPjwvc3ZnPg==');
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItZG93bmxvYWQiPjxwYXRoIGQ9Ik0yMSAxNXY0YTIgMiAwIDAgMS0yIDJINWEyIDIgMCAwIDEtMi0ydi00Ij48L3BhdGg+PHBvbHlsaW5lIHBvaW50cz0iNyAxMCAxMiAxNSAxNyAxMCI+PC9wb2x5bGluZT48bGluZSB4MT0iMTIiIHkxPSIxNSIgeDI9IjEyIiB5Mj0iMyI+PC9saW5lPjwvc3ZnPg==');
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100%;
mask-size: 100%;
}
& .filepond--open-icon {
@apply pointer-events-auto me-1 inline-block size-4 bg-white align-bottom hover:bg-white/70;
-webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJoLTYgdy02IiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiPgogIDxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTEwIDZINmEyIDIgMCAwMC0yIDJ2MTBhMiAyIDAgMDAyIDJoMTBhMiAyIDAgMDAyLTJ2LTRNMTQgNGg2bTAgMHY2bTAtNkwxMCAxNCIgLz4KPC9zdmc+Cg==);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJoLTYgdy02IiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiPgogIDxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTEwIDZINmEyIDIgMCAwMC0yIDJ2MTBhMiAyIDAgMDAyIDJoMTBhMiAyIDAgMDAyLTJ2LTRNMTQgNGg2bTAgMHY2bTAtNkwxMCAxNCIgLz4KPC9zdmc+Cg==);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100%;
mask-size: 100%;
}
& .filepond--file-action-button.filepond--action-edit-item {
@apply bg-black/50;
}
& .fi-fo-file-upload-editor {
@apply fixed inset-0 isolate z-50 h-[100dvh] w-screen p-2 sm:p-10 md:p-20;
& .fi-fo-file-upload-editor-overlay {
@apply fixed inset-0 h-full w-full cursor-pointer bg-gray-950/50 dark:bg-gray-950/75;
will-change: transform;
}
& .fi-fo-file-upload-editor-window {
@apply isolate mx-auto flex h-full w-full flex-col overflow-hidden rounded-xl bg-white ring-1 ring-gray-900/10 lg:flex-row dark:bg-gray-800 dark:ring-gray-50/10;
}
& .fi-fo-file-upload-editor-image-ctn {
@apply m-4 max-h-full max-w-full flex-1;
}
& .fi-fo-file-upload-editor-image {
@apply h-full w-auto;
}
& .fi-fo-file-upload-editor-control-panel {
@apply flex h-full w-full flex-1 flex-col overflow-y-auto bg-gray-50 lg:max-w-xs dark:bg-gray-900/30;
& .fi-fo-file-upload-editor-control-panel-main {
@apply flex-1 space-y-6 overflow-auto p-4;
}
& .fi-fo-file-upload-editor-control-panel-group {
@apply grid gap-3;
& .fi-btn-group {
@apply w-full;
}
& .fi-btn.fi-active {
@apply bg-gray-50 dark:bg-gray-700;
}
& .fi-fo-file-upload-editor-control-panel-group-title {
@apply text-xs text-gray-950 dark:text-white;
}
}
& .fi-fo-file-upload-editor-control-panel-footer {
@apply flex items-center gap-3 px-4 py-3;
}
& .fi-fo-file-upload-editor-control-panel-reset-action {
@apply ml-auto;
}
}
& .cropper-drag-box.cropper-crop.cropper-modal {
@apply bg-gray-100/50 opacity-100 dark:bg-gray-900/80;
}
&.fi-fo-file-upload-editor-circle-cropper {
& .cropper-view-box,
& .cropper-face {
border-radius: 50%;
}
}
}
}
@@ -0,0 +1,57 @@
.fi-fo-key-value {
& .fi-fo-key-value-table-ctn {
@apply divide-y divide-gray-200 dark:divide-white/10;
}
& .fi-fo-key-value-table {
@apply w-full table-auto divide-y divide-gray-200 dark:divide-white/5;
& > thead {
& > tr {
& > th {
@apply px-3 py-2 text-start text-sm font-medium text-gray-700 dark:text-gray-200;
&.fi-has-action {
@apply w-9 p-0;
}
}
}
}
& > tbody {
@apply divide-y divide-gray-200 dark:divide-white/5;
& > tr {
@apply divide-x divide-gray-200 rtl:divide-x-reverse dark:divide-white/5;
& > td {
@apply w-1/2 p-0;
&.fi-has-action {
@apply w-auto p-0.5;
& .fi-fo-key-value-table-row-sortable-handle {
@apply flex;
}
}
& .fi-input {
@apply font-mono;
}
}
}
}
}
& .fi-fo-key-value-add-action-ctn {
@apply flex justify-center px-3 py-2;
}
}
.fi-fo-key-value-wrp {
&.fi-fo-field-has-inline-label {
& .fi-fo-field-label-col {
@apply sm:pt-1.5;
}
}
}
@@ -0,0 +1,317 @@
.fi-fo-markdown-editor {
--color-cm-red: #991b1b;
--color-cm-orange: #9a3412;
--color-cm-amber: #92400e;
--color-cm-yellow: #854d0e;
--color-cm-lime: #3f6212;
--color-cm-green: #166534;
--color-cm-emerald: #065f46;
--color-cm-teal: #115e59;
--color-cm-cyan: #155e75;
--color-cm-sky: #075985;
--color-cm-blue: #1e40af;
--color-cm-indigo: #3730a3;
--color-cm-violet: #5b21b6;
--color-cm-purple: #6b21a8;
--color-cm-fuchsia: #86198f;
--color-cm-pink: #9d174d;
--color-cm-rose: #9f1239;
--color-cm-gray: #18181b;
--color-cm-gray-muted: #71717a;
--color-cm-gray-background: #e4e4e7;
&:not(.fi-disabled) {
@apply max-w-full overflow-hidden font-mono text-sm text-gray-950 dark:text-white;
}
&.fi-disabled {
@apply block w-full rounded-lg bg-gray-50 px-3 py-3 text-sm text-gray-500 shadow-xs ring-1 ring-gray-950/10 dark:bg-transparent dark:text-gray-400 dark:ring-white/10;
}
& .EasyMDEContainer .CodeMirror {
@apply px-4! py-3!;
}
& .cm-s-easymde .cm-comment {
background-color: transparent;
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .CodeMirror-cursor {
border-color: currentColor;
}
& .EasyMDEContainer .cm-s-easymde .cm-keyword {
color: var(--color-cm-violet);
}
& .EasyMDEContainer .cm-s-easymde .cm-atom {
color: var(--color-cm-blue);
}
& .EasyMDEContainer .cm-s-easymde .cm-number {
color: var(--color-cm-green);
}
& .EasyMDEContainer .cm-s-easymde .cm-def {
color: var(--color-cm-blue);
}
& .EasyMDEContainer .cm-s-easymde .cm-variable {
color: var(--color-cm-yellow);
}
& .EasyMDEContainer .cm-s-easymde .cm-variable-2 {
color: var(--color-cm-blue);
}
& .EasyMDEContainer .cm-s-easymde .cm-variable-3 {
color: var(--color-cm-emerald);
}
& .EasyMDEContainer .cm-s-easymde .cm-property {
color: var(--color-cm-gray);
}
& .EasyMDEContainer .cm-s-easymde .cm-operator {
color: var(--color-cm-gray);
}
& .EasyMDEContainer .cm-s-easymde .cm-string {
color: var(--color-cm-rose);
}
& .EasyMDEContainer .cm-s-easymde .cm-string-2 {
color: var(--color-cm-rose);
}
& .EasyMDEContainer .cm-s-easymde .cm-meta {
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .cm-s-easymde .cm-error {
color: var(--color-cm-red);
}
& .EasyMDEContainer .cm-s-easymde .cm-qualifier {
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .cm-s-easymde .cm-builtin {
color: var(--color-cm-violet);
}
& .EasyMDEContainer .cm-s-easymde .cm-bracket {
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .cm-s-easymde .cm-tag {
color: var(--color-cm-green);
}
& .EasyMDEContainer .cm-s-easymde .cm-attribute {
color: var(--color-cm-blue);
}
& .EasyMDEContainer .cm-s-easymde .cm-hr {
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .cm-s-easymde .cm-formatting-quote {
color: var(--color-cm-sky);
}
& .EasyMDEContainer .cm-s-easymde .cm-formatting-quote + .cm-quote {
color: var(--color-cm-gray-muted);
}
& .EasyMDEContainer .cm-s-easymde .cm-formatting-list,
& .EasyMDEContainer .cm-s-easymde .cm-formatting-list + .cm-variable-2,
& .EasyMDEContainer .cm-s-easymde .cm-tab + .cm-variable-2 {
color: var(--color-cm-gray);
}
& .EasyMDEContainer .cm-s-easymde .cm-link {
color: var(--color-cm-blue);
}
& .EasyMDEContainer .cm-s-easymde .cm-tag {
color: var(--color-cm-red);
}
& .EasyMDEContainer .cm-s-easymde .cm-attribute {
color: var(--color-cm-amber);
}
& .EasyMDEContainer .cm-s-easymde .cm-attribute + .cm-string {
color: var(--color-cm-green);
}
&
.EasyMDEContainer
.cm-s-easymde
.cm-formatting-code
+ .cm-comment:not(.cm-formatting-code) {
background-color: var(--color-cm-gray-background);
color: var(--color-cm-gray);
}
& .EasyMDEContainer .cm-s-easymde .cm-header-1 {
@apply text-3xl;
}
& .EasyMDEContainer .cm-s-easymde .cm-header-2 {
@apply text-2xl;
}
& .EasyMDEContainer .cm-s-easymde .cm-header-3 {
@apply text-xl;
}
& .EasyMDEContainer .cm-s-easymde .cm-header-4 {
@apply text-lg;
}
& .EasyMDEContainer .cm-s-easymde .cm-header-5 {
@apply text-base;
}
& .EasyMDEContainer .cm-s-easymde .cm-header-6 {
@apply text-sm;
}
& .EasyMDEContainer .cm-s-easymde .cm-comment {
@apply bg-none;
}
& .EasyMDEContainer .cm-s-easymde .cm-formatting-code-block,
& .EasyMDEContainer .cm-s-easymde .cm-tab + .cm-comment {
@apply bg-transparent text-inherit;
}
& .EasyMDEContainer .CodeMirror {
@apply border-none bg-transparent px-3 py-1.5 text-inherit;
}
& .EasyMDEContainer .CodeMirror-scroll {
@apply h-auto;
}
& .EasyMDEContainer .editor-toolbar {
@apply flex flex-wrap gap-1 rounded-none border-0 border-b border-gray-200 px-2.5 py-2 dark:border-white/10;
}
& .EasyMDEContainer .editor-toolbar button {
@apply grid size-8 place-content-center rounded-lg border-none p-0 transition duration-75 hover:bg-gray-50 focus-visible:bg-gray-50 dark:hover:bg-white/5 dark:focus-visible:bg-white/5;
}
& .EasyMDEContainer .editor-toolbar button.active {
@apply bg-gray-50 dark:bg-white/5;
}
& .EasyMDEContainer .editor-toolbar button::before {
@apply block size-5 bg-gray-700;
content: '';
mask-position: center;
mask-repeat: no-repeat;
}
& .EasyMDEContainer .editor-toolbar button.active::before {
@apply bg-primary-600;
}
& .EasyMDEContainer .editor-toolbar .separator {
@apply m-0! w-1 border-none;
}
& .EasyMDEContainer .editor-toolbar .bold::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M4 3a1 1 0 0 1 1-1h6a4.5 4.5 0 0 1 3.274 7.587A4.75 4.75 0 0 1 11.25 18H5a1 1 0 0 1-1-1V3Zm2.5 5.5v-4H11a2 2 0 1 1 0 4H6.5Zm0 2.5v4.5h4.75a2.25 2.25 0 0 0 0-4.5H6.5Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .italic::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .strikethrough::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M11.617 3.963c-1.186-.318-2.418-.323-3.416.015-.992.336-1.49.91-1.642 1.476-.152.566-.007 1.313.684 2.1.528.6 1.273 1.1 2.128 1.446h7.879a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5h3.813a5.976 5.976 0 0 1-.447-.456C5.18 7.479 4.798 6.231 5.11 5.066c.312-1.164 1.268-2.055 2.61-2.509 1.336-.451 2.877-.42 4.286-.043.856.23 1.684.592 2.409 1.074a.75.75 0 1 1-.83 1.25 6.723 6.723 0 0 0-1.968-.875Zm1.909 8.123a.75.75 0 0 1 1.015.309c.53.99.607 2.062.18 3.01-.421.94-1.289 1.648-2.441 2.038-1.336.452-2.877.42-4.286.043-1.409-.377-2.759-1.121-3.69-2.18a.75.75 0 1 1 1.127-.99c.696.791 1.765 1.403 2.952 1.721 1.186.318 2.418.323 3.416-.015.853-.288 1.34-.756 1.555-1.232.21-.467.205-1.049-.136-1.69a.75.75 0 0 1 .308-1.014Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .link::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath d='M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.138 2.5 2.5 0 0 1-.142-3.667l3-3Z' /%3E%3Cpath d='M11.603 7.963a.75.75 0 0 0-.977 1.138 2.5 2.5 0 0 1 .142 3.667l-3 3a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 1 0 5.656 5.656l3-3a4 4 0 0 0-.225-5.865Z' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .heading::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M2.75 4a.75.75 0 0 1 .75.75v4.5h5v-4.5a.75.75 0 0 1 1.5 0v10.5a.75.75 0 0 1-1.5 0v-4.5h-5v4.5a.75.75 0 0 1-1.5 0V4.75A.75.75 0 0 1 2.75 4ZM13 8.75a.75.75 0 0 1 .75-.75h1.75a.75.75 0 0 1 .75.75v5.75h1a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1 0-1.5h1v-5h-1a.75.75 0 0 1-.75-.75Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .quote::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M10 2c-2.236 0-4.43.18-6.57.524C1.993 2.755 1 4.014 1 5.426v5.148c0 1.413.993 2.67 2.43 2.902 1.168.188 2.352.327 3.55.414.28.02.521.18.642.413l1.713 3.293a.75.75 0 0 0 1.33 0l1.713-3.293a.783.783 0 0 1 .642-.413 41.102 41.102 0 0 0 3.55-.414c1.437-.231 2.43-1.49 2.43-2.902V5.426c0-1.413-.993-2.67-2.43-2.902A41.289 41.289 0 0 0 10 2ZM6.75 6a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .code::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06ZM11.377 2.011a.75.75 0 0 1 .612.867l-2.5 14.5a.75.75 0 0 1-1.478-.255l2.5-14.5a.75.75 0 0 1 .866-.612Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .unordered-list::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M6 4.75A.75.75 0 0 1 6.75 4h10.5a.75.75 0 0 1 0 1.5H6.75A.75.75 0 0 1 6 4.75ZM6 10a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H6.75A.75.75 0 0 1 6 10Zm0 5.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H6.75a.75.75 0 0 1-.75-.75ZM1.99 4.75a1 1 0 0 1 1-1H3a1 1 0 0 1 1 1v.01a1 1 0 0 1-1 1h-.01a1 1 0 0 1-1-1v-.01ZM1.99 15.25a1 1 0 0 1 1-1H3a1 1 0 0 1 1 1v.01a1 1 0 0 1-1 1h-.01a1 1 0 0 1-1-1v-.01ZM1.99 10a1 1 0 0 1 1-1H3a1 1 0 0 1 1 1v.01a1 1 0 0 1-1 1h-.01a1 1 0 0 1-1-1V10Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .ordered-list::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath d='M3 1.25a.75.75 0 0 0 0 1.5h.25v2.5a.75.75 0 0 0 1.5 0V2A.75.75 0 0 0 4 1.25H3ZM2.97 8.654a3.5 3.5 0 0 1 1.524-.12.034.034 0 0 1-.012.012L2.415 9.579A.75.75 0 0 0 2 10.25v1c0 .414.336.75.75.75h2.5a.75.75 0 0 0 0-1.5H3.927l1.225-.613c.52-.26.848-.79.848-1.371 0-.647-.429-1.327-1.193-1.451a5.03 5.03 0 0 0-2.277.155.75.75 0 0 0 .44 1.434ZM7.75 3a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5ZM7.75 9.25a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5ZM7.75 15.5a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5ZM2.625 13.875a.75.75 0 0 0 0 1.5h1.5a.125.125 0 0 1 0 .25H3.5a.75.75 0 0 0 0 1.5h.625a.125.125 0 0 1 0 .25h-1.5a.75.75 0 0 0 0 1.5h1.5a1.625 1.625 0 0 0 1.37-2.5 1.625 1.625 0 0 0-1.37-2.5h-1.5Z' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .table::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M.99 5.24A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25l.01 9.5A2.25 2.25 0 0 1 16.76 17H3.26A2.267 2.267 0 0 1 1 14.74l-.01-9.5Zm8.26 9.52v-.625a.75.75 0 0 0-.75-.75H3.25a.75.75 0 0 0-.75.75v.615c0 .414.336.75.75.75h5.373a.75.75 0 0 0 .627-.74Zm1.5 0a.75.75 0 0 0 .627.74h5.373a.75.75 0 0 0 .75-.75v-.615a.75.75 0 0 0-.75-.75H11.5a.75.75 0 0 0-.75.75v.625Zm6.75-3.63v-.625a.75.75 0 0 0-.75-.75H11.5a.75.75 0 0 0-.75.75v.625c0 .414.336.75.75.75h5.25a.75.75 0 0 0 .75-.75Zm-8.25 0v-.625a.75.75 0 0 0-.75-.75H3.25a.75.75 0 0 0-.75.75v.625c0 .414.336.75.75.75H8.5a.75.75 0 0 0 .75-.75ZM17.5 7.5v-.625a.75.75 0 0 0-.75-.75H11.5a.75.75 0 0 0-.75.75V7.5c0 .414.336.75.75.75h5.25a.75.75 0 0 0 .75-.75Zm-8.25 0v-.625a.75.75 0 0 0-.75-.75H3.25a.75.75 0 0 0-.75.75V7.5c0 .414.336.75.75.75H8.5a.75.75 0 0 0 .75-.75Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .upload-image::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.219a.75.75 0 0 0-1.06 0l-1.91 1.909.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.091a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .undo::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M7.793 2.232a.75.75 0 0 1-.025 1.06L3.622 7.25h10.003a5.375 5.375 0 0 1 0 10.75H10.75a.75.75 0 0 1 0-1.5h2.875a3.875 3.875 0 0 0 0-7.75H3.622l4.146 3.957a.75.75 0 0 1-1.036 1.085l-5.5-5.25a.75.75 0 0 1 0-1.085l5.5-5.25a.75.75 0 0 1 1.06.025Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-toolbar .redo::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor' class='size-5'%3E%3Cpath fill-rule='evenodd' d='M12.207 2.232a.75.75 0 0 0 .025 1.06l4.146 3.958H6.375a5.375 5.375 0 0 0 0 10.75H9.25a.75.75 0 0 0 0-1.5H6.375a3.875 3.875 0 0 1 0-7.75h10.003l-4.146 3.957a.75.75 0 0 0 1.036 1.085l5.5-5.25a.75.75 0 0 0 0-1.085l-5.5-5.25a.75.75 0 0 0-1.06.025Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
}
& .EasyMDEContainer .editor-statusbar {
@apply hidden;
}
@variant dark {
--color-cm-red: #f87171;
--color-cm-orange: #fb923c;
--color-cm-amber: #fbbf24;
--color-cm-yellow: #facc15;
--color-cm-lime: #a3e635;
--color-cm-green: #4ade80;
--color-cm-emerald: #4ade80;
--color-cm-teal: #2dd4bf;
--color-cm-cyan: #22d3ee;
--color-cm-sky: #38bdf8;
--color-cm-blue: #60a5fa;
--color-cm-indigo: #818cf8;
--color-cm-violet: #a78bfa;
--color-cm-purple: #c084fc;
--color-cm-fuchsia: #e879f9;
--color-cm-pink: #f472b6;
--color-cm-rose: #fb7185;
--color-cm-gray: #fafafa;
--color-cm-gray-muted: #a1a1aa;
--color-cm-gray-background: #52525b;
& .EasyMDEContainer .cm-s-easymde span.CodeMirror-selectedtext {
filter: invert(100%);
}
& .EasyMDEContainer .editor-toolbar button::before {
@apply bg-gray-300;
}
& .EasyMDEContainer .editor-toolbar button.active::before {
@apply bg-primary-400;
}
}
}
@@ -0,0 +1,17 @@
.fi-fo-modal-table-select {
&:not(.fi-fo-modal-table-select-multiple) {
@apply flex items-start gap-x-3 leading-5;
}
&.fi-fo-modal-table-select-multiple {
@apply grid gap-2;
& .fi-fo-modal-table-select-badges-ctn {
@apply flex flex-wrap gap-1.5;
}
}
& .fi-fo-modal-table-select-placeholder {
@apply text-gray-400 dark:text-gray-500;
}
}
@@ -0,0 +1,31 @@
.fi-fo-radio {
@apply gap-4;
&.fi-inline {
@apply flex flex-wrap;
}
&:not(.fi-inline).fi-grid-direction-col {
@apply -mt-4;
& > .fi-fo-radio-label {
@apply break-inside-avoid pt-4;
}
}
& > .fi-fo-radio-label {
@apply flex gap-x-3 self-start;
& > .fi-radio-input {
@apply mt-1 shrink-0;
}
& > .fi-fo-radio-label-text {
@apply grid text-sm leading-6 font-medium text-gray-950 dark:text-white;
}
& .fi-fo-radio-label-description {
@apply font-normal text-gray-500 dark:text-gray-400;
}
}
}
@@ -0,0 +1,377 @@
.fi-fo-repeater {
@apply grid gap-y-4;
& .fi-fo-repeater-actions {
@apply flex gap-x-3;
&.fi-hidden {
@apply hidden;
}
}
& .fi-fo-repeater-items {
@apply items-start gap-4;
}
& .fi-fo-repeater-item {
@apply rounded-xl bg-white shadow-xs ring-1 ring-gray-950/5 dark:bg-white/5 dark:ring-white/10;
&.fi-collapsed {
& .fi-fo-repeater-item-header-collapsible-actions {
@apply -rotate-180;
}
& .fi-fo-repeater-item-header-collapse-action {
@apply pointer-events-none opacity-0;
}
}
&:not(.fi-collapsed) {
& .fi-fo-repeater-item-header-expand-action {
@apply pointer-events-none opacity-0;
}
}
}
& .fi-fo-repeater-item-header {
@apply flex items-center gap-x-3 overflow-hidden px-4 py-3;
}
&.fi-collapsible {
& .fi-fo-repeater-item-header {
@apply cursor-pointer select-none;
}
}
& .fi-fo-repeater-item-header-start-actions {
@apply flex items-center gap-x-3;
}
& .fi-fo-repeater-item-header-icon {
@apply text-gray-400 dark:text-gray-500;
}
& .fi-fo-repeater-item-header-label {
@apply text-sm font-medium text-gray-950 dark:text-white;
&.fi-truncated {
@apply truncate;
}
}
& .fi-fo-repeater-item-header-end-actions {
@apply ms-auto flex items-center gap-x-3;
}
& .fi-fo-repeater-item-header-collapsible-actions {
@apply relative transition;
}
& .fi-fo-repeater-item-header-collapse-action {
@apply transition;
}
& .fi-fo-repeater-item-header-expand-action {
@apply absolute inset-0 rotate-180 transition;
}
& .fi-fo-repeater-item-has-header > .fi-fo-repeater-item-content {
@apply border-t border-gray-100 dark:border-white/10;
}
& .fi-fo-repeater-item-content {
@apply p-4;
}
& .fi-fo-repeater-add-between-items-ctn {
@apply flex w-full justify-center;
}
& .fi-fo-repeater-add-between-items {
@apply rounded-lg bg-white dark:bg-gray-900;
}
& .fi-fo-repeater-label-between-items-ctn {
@apply relative -my-2 flex items-center;
}
& .fi-fo-repeater-label-between-items-divider-before {
@apply w-3 shrink-0 border-t border-gray-200 dark:border-white/10;
}
& .fi-fo-repeater-label-between-items {
@apply shrink-0 px-1 text-sm font-medium text-gray-500 dark:text-gray-400;
}
& .fi-fo-repeater-label-between-items-divider-after {
@apply flex-1 border-t border-gray-200 dark:border-white/10;
}
& .fi-fo-repeater-add {
@apply flex w-full justify-center;
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
}
}
.fi-fo-simple-repeater {
@apply grid gap-y-4;
& .fi-fo-simple-repeater-items {
@apply gap-4;
}
& .fi-fo-simple-repeater-item {
@apply flex justify-start gap-x-3;
}
& .fi-fo-simple-repeater-item-content {
@apply flex-1;
}
& .fi-fo-simple-repeater-item-actions {
@apply flex items-center gap-x-1;
}
& .fi-fo-simple-repeater-add {
@apply flex w-full justify-center;
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
}
}
.fi-fo-table-repeater {
@apply grid gap-3;
& > table {
@apply block w-full divide-y divide-gray-200 rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10;
& > thead {
@apply hidden whitespace-nowrap;
& > tr {
& > th {
@apply border-gray-200 bg-gray-50 px-3 py-2 text-sm font-semibold text-gray-950 first-of-type:rounded-tl-xl last-of-type:rounded-tr-xl dark:border-white/5 dark:bg-white/5 dark:text-white [&:not(:first-of-type)]:border-s [&:not(:last-of-type)]:border-e;
&.fi-align-start,
&.fi-align-left {
@apply text-start;
}
&.fi-align-end,
&.fi-align-right {
@apply text-end;
}
&.fi-wrapped {
@apply whitespace-normal;
}
&:not(.fi-wrapped) {
@apply whitespace-nowrap;
}
&.fi-fo-table-repeater-empty-header-cell {
@apply w-1;
}
}
}
}
& > tbody {
@apply block divide-y divide-gray-200 dark:divide-white/5;
& > tr {
@apply grid gap-6 p-6;
& > td {
@apply block;
&.fi-hidden {
@apply hidden;
}
}
}
}
& .fi-fo-table-repeater-header-required-mark {
@apply text-danger-600 dark:text-danger-400 font-medium;
}
& .fi-fo-table-repeater-actions {
@apply flex h-full items-center gap-x-3;
}
}
@supports (container-type: inline-size) {
@apply @container;
& > table {
@apply @xl:table;
& > thead {
@apply @xl:table-header-group;
}
& > tbody {
@apply @xl:table-row-group;
& > tr {
@apply @xl:table-row @xl:p-0;
& > td {
@apply @xl:table-cell @xl:px-3 @xl:py-2;
&.fi-hidden {
@apply @xl:table-cell;
}
& .fi-fo-field {
@apply @xl:gap-y-0;
}
& .fi-in-entry {
@apply @xl:gap-y-0;
}
& .fi-fo-field-label-content {
@apply @xl:hidden;
}
& .fi-in-entry-label {
@apply @xl:hidden;
}
}
}
}
& .fi-fo-table-repeater-actions {
@apply @xl:px-3 @xl:py-2;
}
}
&.fi-compact {
& > table {
& > tbody {
& > tr {
& > td {
@apply @xl:border-gray-200 @xl:px-0 @xl:py-1 @xl:dark:border-white/5 @xl:[&:not(:first-of-type)]:border-s @xl:[&:not(:last-of-type)]:border-e;
}
}
}
}
& .fi-input-wrp {
@apply @xl:bg-transparent! @xl:shadow-none @xl:ring-0!;
}
& .fi-fo-field-wrp-error-message {
@apply @xl:px-3 @xl:pb-2;
}
& .fi-in-entry-content {
@apply @xl:px-3;
}
}
}
@supports not (container-type: inline-size) {
& > table {
@apply lg:table;
& > thead {
@apply lg:table-header-group;
}
& > tbody {
@apply lg:table-row-group;
& > tr {
@apply lg:table-row lg:p-0;
& > td {
@apply lg:table-cell lg:px-3 lg:py-2;
&.fi-hidden {
@apply lg:table-cell;
}
& .fi-fo-field {
@apply lg:gap-y-0;
}
& .fi-in-entry {
@apply lg:gap-y-0;
}
& .fi-fo-field-label-content {
@apply lg:hidden;
}
& .fi-in-entry-label {
@apply lg:hidden;
}
}
}
}
& .fi-fo-table-repeater-actions {
@apply lg:px-3 lg:py-2;
}
}
&.fi-compact {
& > table {
& > tbody {
& > tr {
& > td {
@apply lg:border-gray-200 lg:px-0 lg:py-1 lg:dark:border-white/5 lg:[&:not(:first-of-type)]:border-s lg:[&:not(:last-of-type)]:border-e;
}
}
}
}
& .fi-input-wrp {
@apply lg:bg-transparent! lg:shadow-none lg:ring-0!;
}
& .fi-fo-field-wrp-error-message {
@apply lg:px-3 lg:pb-2;
}
& .fi-in-entry-content {
@apply lg:px-3;
}
}
}
& .fi-fo-table-repeater-add {
@apply flex w-full justify-center;
&.fi-align-start,
&.fi-align-left {
@apply justify-start;
}
&.fi-align-end,
&.fi-align-right {
@apply justify-end;
}
}
}
@@ -0,0 +1,271 @@
.fi-fo-rich-editor {
& .fi-fo-rich-editor-uploading-file {
@apply pointer-events-none cursor-wait opacity-50;
}
& .fi-fo-rich-editor-toolbar {
@apply relative flex flex-wrap gap-x-3 gap-y-1 border-b border-gray-200 px-2.5 py-2 dark:border-white/10;
}
& .fi-fo-rich-editor-floating-toolbar {
@apply invisible absolute z-[20] -mt-1 flex max-w-full flex-wrap gap-x-3 gap-y-1 rounded-lg border border-gray-300 bg-white p-1 shadow dark:border-gray-600 dark:bg-gray-800;
}
& .fi-fo-rich-editor-toolbar-group {
@apply flex gap-x-1;
}
& .fi-fo-rich-editor-tool {
@apply flex h-8 min-w-8 items-center justify-center rounded-lg text-sm font-semibold text-gray-700 transition duration-75 hover:bg-gray-50 focus-visible:bg-gray-50 dark:text-gray-200 dark:hover:bg-white/5 dark:focus-visible:bg-white/5;
&[disabled] {
@apply pointer-events-none cursor-default opacity-70;
}
&.fi-active {
@apply text-primary-600 dark:text-primary-400 bg-gray-50 dark:bg-white/5;
}
}
& .fi-fo-rich-editor-uploading-file-message {
@apply flex items-center gap-x-3 border-b border-gray-200 bg-gray-50 px-5 py-1.5 text-sm leading-6 font-medium text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200;
& .fi-loading-indicator {
@apply text-gray-400 dark:text-gray-500;
}
}
& .fi-fo-rich-editor-file-validation-message {
@apply border-danger-200 bg-danger-50 text-danger-700 dark:text-danger-200 flex items-center gap-x-3 border-b px-5 py-1.5 text-sm leading-6 font-medium dark:border-white/10 dark:bg-white/5;
}
& .fi-fo-rich-editor-main {
@apply flex flex-col-reverse;
}
& .fi-fo-rich-editor-content {
@apply relative min-h-12 w-full flex-1 px-5 py-3;
}
& span[data-type='mergeTag'] {
&::before {
@apply me-1 font-normal opacity-60;
content: '{{';
}
&::after {
@apply ms-1 font-normal opacity-60;
content: '}}';
}
}
& .fi-fo-rich-editor-panels {
@apply w-full border-b border-gray-200 bg-gray-50 dark:border-white/10 dark:bg-gray-900/30;
}
& .fi-fo-rich-editor-panel-header {
@apply flex items-start gap-3 px-4 py-3;
}
& .fi-fo-rich-editor-panel-heading {
@apply flex-1 text-sm font-semibold text-gray-950 dark:text-white;
}
& .fi-fo-rich-editor-panel-close-btn-ctn {
@apply shrink-0;
}
& .fi-fo-rich-editor-panel {
@apply grid divide-y divide-gray-200 dark:divide-white/10;
}
& .fi-fo-rich-editor-merge-tags-list {
@apply flex flex-wrap gap-2 px-4 py-3;
}
& .fi-fo-rich-editor-merge-tag-btn {
@apply cursor-move rounded-lg bg-white p-1 text-start text-sm text-gray-600 ring-1 ring-gray-600/10 dark:bg-gray-400/10 dark:text-gray-200 dark:ring-gray-400/20;
}
& .fi-fo-rich-editor-custom-blocks-list {
@apply flex flex-wrap gap-2 px-4 py-3;
}
& .fi-fo-rich-editor-custom-block-btn {
@apply flex cursor-move gap-1.5 rounded-lg bg-white px-2 py-1 text-start text-sm text-gray-600 ring-1 ring-gray-600/10 dark:bg-gray-400/10 dark:text-gray-200 dark:ring-gray-400/20;
}
& .tiptap {
@apply h-full;
&:focus {
@apply outline-none;
& .ProseMirror-selectednode {
div&[data-type='customBlock'],
img& {
@apply ring-primary-600 dark:ring-primary-500 ring-2;
}
}
}
& p.is-editor-empty:first-child {
&::before {
@apply pointer-events-none float-start h-0 text-gray-400;
content: attr(data-placeholder);
}
@variant dark {
&::before {
@apply text-gray-500;
}
}
}
& [data-type='details'] {
@apply my-6 flex gap-1 rounded-md border border-gray-950/20 p-4 dark:border-white/20;
& > div:first-of-type {
@apply !mt-0;
}
& summary {
@apply list-none font-medium;
}
& > button {
@apply mt-px mr-2 flex size-5 items-center justify-center rounded-md bg-transparent p-1 text-xs leading-none hover:bg-gray-950/5 dark:hover:bg-white/5;
&::before {
content: '\25B6';
}
}
&.is-open > button::before {
transform: rotate(90deg);
}
& > div {
@apply flex w-full flex-col gap-4;
& > [data-type='detailsContent'] {
@apply !mt-0;
& > :last-child {
@apply mb-4;
}
}
}
}
& table {
@apply m-0 w-full table-fixed border-collapse overflow-hidden first:mt-0;
& td,
& th {
@apply relative min-w-[1em] border border-gray-300 !p-2 align-top dark:border-gray-600;
& > * {
@apply mb-0;
}
}
& th {
@apply bg-gray-100 text-start font-bold dark:bg-gray-800 dark:text-white;
}
& .selectedCell {
&::after {
@apply pointer-events-none absolute start-0 end-0 top-0 bottom-0 z-2 bg-gray-200/80 content-[''];
}
@variant dark {
&::after {
@apply bg-gray-800/80;
}
}
}
& .column-resize-handle {
@apply bg-primary-600 pointer-events-none absolute end-0 top-0 bottom-0 !m-0 w-1;
}
}
& .tableWrapper {
@apply overflow-x-auto;
}
&.resize-cursor {
@apply cursor-col-resize cursor-ew-resize;
}
& .grid-layout > .grid-layout-col {
@apply rounded-md border border-gray-950/20 p-4 dark:border-white/20;
}
}
& .tiptap.ProseMirror {
/* https://defensivecss.dev/tip/input-zoom-safari */
@supports (-webkit-touch-callout: none) {
@apply text-base;
}
}
& img {
@apply inline-block;
}
& div[data-type='customBlock'] {
@apply grid divide-y divide-gray-200 overflow-hidden rounded-lg shadow-sm ring-1 ring-gray-950/10 dark:divide-white/10 dark:ring-white/20;
}
& .fi-fo-rich-editor-custom-block-header {
@apply flex items-start gap-3 bg-gray-50 px-4 py-3 dark:bg-gray-900/30;
}
& .fi-fo-rich-editor-custom-block-heading {
@apply flex-1 text-sm font-semibold text-gray-950 dark:text-white;
}
& .fi-fo-rich-editor-custom-block-edit-btn-ctn {
@apply shrink-0;
}
& .fi-fo-rich-editor-custom-block-delete-btn-ctn {
@apply shrink-0;
}
& .fi-fo-rich-editor-custom-block-preview {
@apply px-4 py-3;
}
@supports (container-type: inline-size) {
@apply @container;
& .fi-fo-rich-editor-main {
@apply @2xl:flex-row;
}
& .fi-fo-rich-editor-panels {
@apply @2xl:max-w-3xs @2xl:rounded-ee-lg @2xl:border-s @2xl:border-b-0;
}
}
@supports not (container-type: inline-size) {
& .fi-fo-rich-editor-main {
@apply md:flex-row;
}
& .fi-fo-rich-editor-panels {
@apply md:max-w-3xs md:rounded-ee-lg md:border-s md:border-b-0;
}
}
}
& .fi-fo-rich-editor-text-color-select-option {
@apply flex items-center gap-2;
& .fi-fo-rich-editor-text-color-select-option-preview {
@apply h-5 w-5 shrink-0 rounded-full bg-(--color) dark:bg-(--dark-color);
}
}
@@ -0,0 +1,13 @@
.fi-fo-select {
& .fi-hidden {
@apply hidden;
}
}
.fi-fo-select-wrp {
&.fi-fo-field-has-inline-label {
& .fi-fo-field-label-col {
@apply sm:pt-1.5;
}
}
}
@@ -0,0 +1,54 @@
.fi-fo-slider {
@apply gap-4 rounded-lg border-0 bg-transparent shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20;
& .noUi-connect {
@apply bg-primary-500 dark:bg-primary-600;
}
& .noUi-connects {
@apply rounded-lg bg-gray-950/5 dark:bg-white/5;
}
& .noUi-handle {
@apply focus:outline-primary-600 dark:focus:outline-primary-500 absolute rounded-lg border-1 border-gray-950/10 bg-white shadow-none backface-hidden focus:outline-2 dark:border-white/20 dark:bg-gray-700;
&::before,
&::after {
@apply border-0 bg-gray-400;
}
}
& .noUi-tooltip {
@apply rounded-md border-0 bg-white text-gray-950 shadow-sm ring-1 ring-gray-950/10 dark:bg-gray-800 dark:text-white dark:ring-white/20;
}
& .noUi-pips {
& .noUi-value {
@apply text-gray-950 dark:text-white;
}
}
&.fi-fo-slider-vertical {
@apply mt-4 h-40;
&.fi-fo-slider-has-tooltips {
@apply ms-10;
}
}
&:not(.fi-fo-slider-vertical) {
&.fi-fo-slider-has-pips {
@apply mb-8;
}
&.fi-fo-slider-has-tooltips {
@apply mt-10;
}
& .noUi-pips {
& .noUi-value {
@apply mt-1;
}
}
}
}
@@ -0,0 +1,33 @@
.fi-fo-tags-input {
&.fi-disabled {
& .fi-badge-delete-btn {
@apply hidden;
}
}
& .fi-fo-tags-input-tags-ctn {
@apply flex w-full flex-wrap gap-1.5 border-t border-t-gray-200 p-2 dark:border-t-white/10;
& > template {
@apply hidden;
}
& > .fi-badge {
&.fi-reorderable {
@apply cursor-move;
}
& .fi-badge-label-ctn {
@apply text-start select-none;
}
}
}
}
.fi-fo-tags-input-wrp {
&.fi-fo-field-has-inline-label {
& .fi-fo-field-label-col {
@apply sm:pt-1.5;
}
}
}
@@ -0,0 +1,9 @@
.fi-fo-text-input {
@apply overflow-hidden;
& input {
&.fi-revealable {
@apply [&::-ms-reveal]:hidden;
}
}
}
@@ -0,0 +1,26 @@
.fi-fo-textarea {
@apply overflow-hidden;
& textarea {
@apply block h-full w-full border-none bg-transparent px-3 py-1.5 text-sm leading-6 text-gray-950 placeholder:text-gray-400 focus:ring-0 focus:outline-none disabled:text-gray-500 disabled:[-webkit-text-fill-color:var(--color-gray-500)] disabled:placeholder:[-webkit-text-fill-color:var(--color-gray-400)] dark:text-white dark:placeholder:text-gray-500 dark:disabled:text-gray-400 dark:disabled:[-webkit-text-fill-color:var(--color-gray-400)] dark:disabled:placeholder:[-webkit-text-fill-color:var(--color-gray-500)];
/* https://defensivecss.dev/tip/input-zoom-safari */
@supports (-webkit-touch-callout: none) {
@apply text-base;
}
}
&.fi-autosizable {
& textarea {
@apply resize-none;
}
}
}
.fi-fo-textarea-wrp {
&.fi-fo-field-has-inline-label {
& .fi-fo-field-label-col {
@apply sm:pt-1.5;
}
}
}
@@ -0,0 +1,33 @@
.fi-fo-toggle-buttons {
&.fi-btn-group {
@apply w-max;
}
&:not(.fi-btn-group) {
@apply gap-3;
&.fi-inline {
@apply flex flex-wrap;
}
&:not(.fi-inline).fi-grid-direction-col {
@apply -mt-3;
& .fi-fo-toggle-buttons-btn-ctn {
@apply break-inside-avoid pt-3;
}
}
}
& .fi-fo-toggle-buttons-input {
@apply pointer-events-none absolute opacity-0;
}
}
.fi-fo-toggle-buttons-wrp {
&.fi-fo-field-has-inline-label {
& .fi-fo-field-label-col {
@apply sm:pt-1.5;
}
}
}
@@ -0,0 +1,20 @@
@import '../../dist/index.css' layer(components);
@import './components/builder.css' layer(components);
@import './components/checkbox-list.css' layer(components);
@import './components/code-editor.css' layer(components);
@import './components/color-picker.css' layer(components);
@import './components/date-time-picker.css' layer(components);
@import './components/field.css' layer(components);
@import './components/file-upload.css' layer(components);
@import './components/key-value.css' layer(components);
@import './components/markdown-editor.css' layer(components);
@import './components/modal-table-select.css' layer(components);
@import './components/radio.css' layer(components);
@import './components/repeater.css' layer(components);
@import './components/rich-editor.css' layer(components);
@import './components/select.css' layer(components);
@import './components/slider.css' layer(components);
@import './components/tags-input.css' layer(components);
@import './components/text-input.css' layer(components);
@import './components/textarea.css' layer(components);
@import './components/toggle-buttons.css' layer(components);
@@ -0,0 +1,112 @@
export default function checkboxListFormComponent({ livewireId }) {
return {
areAllCheckboxesChecked: false,
checkboxListOptions: [],
search: '',
visibleCheckboxListOptions: [],
init() {
this.checkboxListOptions = Array.from(
this.$root.querySelectorAll('.fi-fo-checkbox-list-option'),
)
this.updateVisibleCheckboxListOptions()
this.$nextTick(() => {
this.checkIfAllCheckboxesAreChecked()
})
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
succeed(({ snapshot, effect }) => {
this.$nextTick(() => {
if (component.id !== livewireId) {
return
}
this.checkboxListOptions = Array.from(
this.$root.querySelectorAll(
'.fi-fo-checkbox-list-option',
),
)
this.updateVisibleCheckboxListOptions()
this.checkIfAllCheckboxesAreChecked()
})
})
},
)
this.$watch('search', () => {
this.updateVisibleCheckboxListOptions()
this.checkIfAllCheckboxesAreChecked()
})
},
checkIfAllCheckboxesAreChecked() {
this.areAllCheckboxesChecked =
this.visibleCheckboxListOptions.length ===
this.visibleCheckboxListOptions.filter((checkboxLabel) =>
checkboxLabel.querySelector(
'input[type=checkbox]:checked, input[type=checkbox]:disabled',
),
).length
},
toggleAllCheckboxes() {
this.checkIfAllCheckboxesAreChecked()
const inverseAreAllCheckboxesChecked = !this.areAllCheckboxesChecked
this.visibleCheckboxListOptions.forEach((checkboxLabel) => {
const checkbox = checkboxLabel.querySelector(
'input[type=checkbox]',
)
if (checkbox.disabled) {
return
}
if (checkbox.checked === inverseAreAllCheckboxesChecked) {
return
}
checkbox.checked = inverseAreAllCheckboxesChecked
checkbox.dispatchEvent(new Event('change'))
})
this.areAllCheckboxesChecked = inverseAreAllCheckboxesChecked
},
updateVisibleCheckboxListOptions() {
this.visibleCheckboxListOptions = this.checkboxListOptions.filter(
(checkboxListItem) => {
if (['', null, undefined].includes(this.search)) {
return true
}
if (
checkboxListItem
.querySelector('.fi-fo-checkbox-list-option-label')
?.innerText.toLowerCase()
.includes(this.search.toLowerCase())
) {
return true
}
return checkboxListItem
.querySelector(
'.fi-fo-checkbox-list-option-description',
)
?.innerText.toLowerCase()
.includes(this.search.toLowerCase())
},
)
},
}
}
@@ -0,0 +1,152 @@
import { EditorState, Compartment } from '@codemirror/state'
import { EditorView, basicSetup } from 'codemirror-v6'
import { indentWithTab } from '@codemirror/commands'
import { oneDark } from '@codemirror/theme-one-dark'
import { keymap } from '@codemirror/view'
import { cpp } from '@codemirror/lang-cpp'
import { css } from '@codemirror/lang-css'
import { go } from '@codemirror/lang-go'
import { html } from '@codemirror/lang-html'
import { java } from '@codemirror/lang-java'
import { javascript } from '@codemirror/lang-javascript'
import { json } from '@codemirror/lang-json'
import { markdown } from '@codemirror/lang-markdown'
import { php } from '@codemirror/lang-php'
import { python } from '@codemirror/lang-python'
import { sql } from '@codemirror/lang-sql'
import { xml } from '@codemirror/lang-xml'
import { yaml } from '@codemirror/lang-yaml'
export default function codeEditorFormComponent({
isDisabled,
isLive,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
language,
state,
}) {
return {
editor: null,
themeCompartment: new Compartment(),
isDocChanged: false,
state,
init() {
const languageExtension = this.getLanguageExtension()
const debouncedCommit = Alpine.debounce(
() => this.$wire.commit(),
liveDebounce ?? 300,
)
this.editor = new EditorView({
parent: this.$refs.editor,
state: EditorState.create({
doc: this.state,
extensions: [
basicSetup,
keymap.of([indentWithTab]),
EditorState.readOnly.of(isDisabled),
EditorView.editable.of(!isDisabled),
EditorView.updateListener.of((viewUpdate) => {
if (!viewUpdate.docChanged) {
return
}
this.isDocChanged = true
this.state = viewUpdate.state.doc.toString()
if (!isLiveOnBlur && (isLive || isLiveDebounced)) {
debouncedCommit()
}
}),
EditorView.domEventHandlers({
blur: (event, view) => {
if (isLiveOnBlur && this.isDocChanged) {
this.$wire.$commit()
}
},
}),
...(languageExtension ? [languageExtension] : []),
this.themeCompartment.of(this.getThemeExtensions()),
],
}),
})
this.$watch('state', () => {
if (this.state === undefined) {
return
}
if (this.editor.state.doc.toString() === this.state) {
return
}
this.editor.dispatch({
changes: {
from: 0,
to: this.editor.state.doc.length,
insert: this.state,
},
})
})
this.themeObserver = new MutationObserver(() => {
this.editor.dispatch({
effects: this.themeCompartment.reconfigure(
this.getThemeExtensions(),
),
})
})
this.themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
})
},
isDarkMode() {
return document.documentElement.classList.contains('dark')
},
getThemeExtensions() {
return this.isDarkMode() ? [oneDark] : []
},
getLanguageExtension() {
if (!language) {
return null
}
const extensions = {
cpp,
css,
go,
html,
java,
javascript,
json,
markdown,
php,
python,
sql,
xml,
yaml,
}
return extensions[language]?.() || null
},
destroy() {
if (this.themeObserver) {
this.themeObserver.disconnect()
this.themeObserver = null
}
if (this.editor) {
this.editor.destroy()
this.editor = null
}
},
}
}
@@ -0,0 +1,90 @@
import 'vanilla-colorful/hex-color-picker.js'
import 'vanilla-colorful/hsl-string-color-picker.js'
import 'vanilla-colorful/rgb-string-color-picker.js'
import 'vanilla-colorful/rgba-string-color-picker.js'
export default function colorPickerFormComponent({
isAutofocused,
isDisabled,
isLive,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
state,
}) {
return {
state,
init() {
if (!(this.state === null || this.state === '')) {
this.setState(this.state)
}
if (isAutofocused) {
this.togglePanelVisibility(this.$refs.input)
}
this.$refs.input.addEventListener('change', (event) => {
this.setState(event.target.value)
})
this.$refs.panel.addEventListener('color-changed', (event) => {
this.setState(event.detail.value)
if (isLiveOnBlur || !(isLive || isLiveDebounced)) {
return
}
setTimeout(
() => {
if (this.state !== event.detail.value) {
return
}
this.commitState()
},
isLiveDebounced ? liveDebounce : 250,
)
})
if (isLive || isLiveDebounced || isLiveOnBlur) {
new MutationObserver(() =>
this.isOpen() ? null : this.commitState(),
).observe(this.$refs.panel, {
attributes: true,
childList: true,
})
}
},
togglePanelVisibility() {
if (isDisabled) {
return
}
this.$refs.panel.toggle(this.$refs.input)
},
setState(value) {
this.state = value
this.$refs.input.value = value
this.$refs.panel.color = value
},
isOpen() {
return this.$refs.panel.style.display === 'block'
},
commitState() {
if (
JSON.stringify(this.$wire.__instance.canonical) ===
JSON.stringify(this.$wire.__instance.ephemeral)
) {
return
}
this.$wire.$commit()
},
}
}
@@ -0,0 +1,551 @@
import dayjs from 'dayjs/esm'
import advancedFormat from 'dayjs/plugin/advancedFormat'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import localeData from 'dayjs/plugin/localeData'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(advancedFormat)
dayjs.extend(customParseFormat)
dayjs.extend(localeData)
dayjs.extend(timezone)
dayjs.extend(utc)
window.dayjs = dayjs
export default function dateTimePickerFormComponent({
defaultFocusedDate,
displayFormat,
firstDayOfWeek,
isAutofocused,
locale,
shouldCloseOnDateSelection,
state,
}) {
const timezone = dayjs.tz.guess()
return {
daysInFocusedMonth: [],
displayText: '',
emptyDaysInFocusedMonth: [],
focusedDate: null,
focusedMonth: null,
focusedYear: null,
hour: null,
isClearingState: false,
minute: null,
second: null,
state,
defaultFocusedDate,
dayLabels: [],
months: [],
init() {
dayjs.locale(locales[locale] ?? locales['en'])
this.$nextTick(() => {
this.focusedDate ??= (
this.getDefaultFocusedDate() ?? dayjs()
).tz(timezone)
this.focusedMonth ??= this.focusedDate.month()
this.focusedYear ??= this.focusedDate.year()
})
let date =
this.getSelectedDate() ??
this.getDefaultFocusedDate() ??
dayjs().tz(timezone).hour(0).minute(0).second(0)
if (this.getMaxDate() !== null && date.isAfter(this.getMaxDate())) {
date = null
} else if (
this.getMinDate() !== null &&
date.isBefore(this.getMinDate())
) {
date = null
}
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.setDisplayText()
this.setMonths()
this.setDayLabels()
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button),
)
}
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
if (this.focusedDate.month() === this.focusedMonth) {
return
}
this.focusedDate = this.focusedDate.month(this.focusedMonth)
})
this.$watch('focusedYear', () => {
if (this.focusedYear?.length > 4) {
this.focusedYear = this.focusedYear.substring(0, 4)
}
if (!this.focusedYear || this.focusedYear?.length !== 4) {
return
}
let year = +this.focusedYear
if (!Number.isInteger(year)) {
year = dayjs().tz(timezone).year()
this.focusedYear = year
}
if (this.focusedDate.year() === year) {
return
}
this.focusedDate = this.focusedDate.year(year)
})
this.$watch('focusedDate', () => {
let month = this.focusedDate.month()
let year = this.focusedDate.year()
if (this.focusedMonth !== month) {
this.focusedMonth = month
}
if (this.focusedYear !== year) {
this.focusedYear = year
}
this.setupDaysGrid()
})
this.$watch('hour', () => {
let hour = +this.hour
if (!Number.isInteger(hour)) {
this.hour = 0
} else if (hour > 23) {
this.hour = 0
} else if (hour < 0) {
this.hour = 23
} else {
this.hour = hour
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.hour(this.hour ?? 0))
})
this.$watch('minute', () => {
let minute = +this.minute
if (!Number.isInteger(minute)) {
this.minute = 0
} else if (minute > 59) {
this.minute = 0
} else if (minute < 0) {
this.minute = 59
} else {
this.minute = minute
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.minute(this.minute ?? 0))
})
this.$watch('second', () => {
let second = +this.second
if (!Number.isInteger(second)) {
this.second = 0
} else if (second > 59) {
this.second = 0
} else if (second < 0) {
this.second = 59
} else {
this.second = second
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.second(this.second ?? 0))
})
this.$watch('state', () => {
if (this.state === undefined) {
return
}
let date = this.getSelectedDate()
if (date === null) {
this.clearState()
return
}
if (
this.getMaxDate() !== null &&
date?.isAfter(this.getMaxDate())
) {
date = null
}
if (
this.getMinDate() !== null &&
date?.isBefore(this.getMinDate())
) {
date = null
}
const newHour = date?.hour() ?? 0
if (this.hour !== newHour) {
this.hour = newHour
}
const newMinute = date?.minute() ?? 0
if (this.minute !== newMinute) {
this.minute = newMinute
}
const newSecond = date?.second() ?? 0
if (this.second !== newSecond) {
this.second = newSecond
}
this.setDisplayText()
})
},
clearState() {
this.isClearingState = true
this.setState(null)
this.hour = 0
this.minute = 0
this.second = 0
this.$nextTick(() => (this.isClearingState = false))
},
dateIsDisabled(date) {
if (
this.$refs?.disabledDates &&
JSON.parse(this.$refs.disabledDates.value ?? []).some(
(disabledDate) => {
disabledDate = dayjs(disabledDate)
if (!disabledDate.isValid()) {
return false
}
return disabledDate.isSame(date, 'day')
},
)
) {
return true
}
if (this.getMaxDate() && date.isAfter(this.getMaxDate(), 'day')) {
return true
}
if (this.getMinDate() && date.isBefore(this.getMinDate(), 'day')) {
return true
}
return false
},
dayIsDisabled(day) {
this.focusedDate ??= dayjs().tz(timezone)
return this.dateIsDisabled(this.focusedDate.date(day))
},
dayIsSelected(day) {
let selectedDate = this.getSelectedDate()
if (selectedDate === null) {
return false
}
this.focusedDate ??= dayjs().tz(timezone)
return (
selectedDate.date() === day &&
selectedDate.month() === this.focusedDate.month() &&
selectedDate.year() === this.focusedDate.year()
)
},
dayIsToday(day) {
let date = dayjs().tz(timezone)
this.focusedDate ??= date
return (
date.date() === day &&
date.month() === this.focusedDate.month() &&
date.year() === this.focusedDate.year()
)
},
focusPreviousDay() {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.subtract(1, 'day')
},
focusPreviousWeek() {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.subtract(1, 'week')
},
focusNextDay() {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.add(1, 'day')
},
focusNextWeek() {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.add(1, 'week')
},
getDayLabels() {
const labels = dayjs.weekdaysShort()
if (firstDayOfWeek === 0) {
return labels
}
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
]
},
getMaxDate() {
let date = dayjs(this.$refs.maxDate?.value)
return date.isValid() ? date : null
},
getMinDate() {
let date = dayjs(this.$refs.minDate?.value)
return date.isValid() ? date : null
},
getSelectedDate() {
if (this.state === undefined) {
return null
}
if (this.state === null) {
return null
}
let date = dayjs(this.state)
if (!date.isValid()) {
return null
}
return date
},
getDefaultFocusedDate() {
if (this.defaultFocusedDate === null) {
return null
}
let defaultFocusedDate = dayjs(this.defaultFocusedDate)
if (!defaultFocusedDate.isValid()) {
return null
}
return defaultFocusedDate
},
togglePanelVisibility() {
if (!this.isOpen()) {
this.focusedDate =
this.getSelectedDate() ??
this.focusedDate ??
this.getMinDate() ??
dayjs().tz(timezone)
this.setupDaysGrid()
}
this.$refs.panel.toggle(this.$refs.button)
},
selectDate(day = null) {
if (day) {
this.setFocusedDay(day)
}
this.focusedDate ??= dayjs().tz(timezone)
this.setState(this.focusedDate)
if (shouldCloseOnDateSelection) {
this.togglePanelVisibility()
}
},
setDisplayText() {
this.displayText = this.getSelectedDate()
? this.getSelectedDate().format(displayFormat)
: ''
},
setMonths() {
this.months = dayjs.months()
},
setDayLabels() {
this.dayLabels = this.getDayLabels()
},
setupDaysGrid() {
this.focusedDate ??= dayjs().tz(timezone)
this.emptyDaysInFocusedMonth = Array.from(
{
length: this.focusedDate.date(8 - firstDayOfWeek).day(),
},
(_, i) => i + 1,
)
this.daysInFocusedMonth = Array.from(
{
length: this.focusedDate.daysInMonth(),
},
(_, i) => i + 1,
)
},
setFocusedDay(day) {
this.focusedDate = (this.focusedDate ?? dayjs().tz(timezone)).date(
day,
)
},
setState(date) {
if (date === null) {
this.state = null
this.setDisplayText()
return
}
if (this.dateIsDisabled(date)) {
return
}
this.state = date
.hour(this.hour ?? 0)
.minute(this.minute ?? 0)
.second(this.second ?? 0)
.format('YYYY-MM-DD HH:mm:ss')
this.setDisplayText()
},
isOpen() {
return this.$refs.panel?.style.display === 'block'
},
}
}
const locales = {
am: require('dayjs/locale/am'),
ar: require('dayjs/locale/ar'),
bs: require('dayjs/locale/bs'),
ca: require('dayjs/locale/ca'),
ckb: require('dayjs/locale/ku'),
cs: require('dayjs/locale/cs'),
cy: require('dayjs/locale/cy'),
da: require('dayjs/locale/da'),
de: require('dayjs/locale/de'),
el: require('dayjs/locale/el'),
en: require('dayjs/locale/en'),
es: require('dayjs/locale/es'),
et: require('dayjs/locale/et'),
fa: require('dayjs/locale/fa'),
fi: require('dayjs/locale/fi'),
fr: require('dayjs/locale/fr'),
hi: require('dayjs/locale/hi'),
hu: require('dayjs/locale/hu'),
hy: require('dayjs/locale/hy-am'),
id: require('dayjs/locale/id'),
it: require('dayjs/locale/it'),
ja: require('dayjs/locale/ja'),
ka: require('dayjs/locale/ka'),
km: require('dayjs/locale/km'),
ku: require('dayjs/locale/ku'),
lt: require('dayjs/locale/lt'),
lv: require('dayjs/locale/lv'),
ms: require('dayjs/locale/ms'),
my: require('dayjs/locale/my'),
nb: require('dayjs/locale/nb'),
nl: require('dayjs/locale/nl'),
pl: require('dayjs/locale/pl'),
pt: require('dayjs/locale/pt'),
pt_BR: require('dayjs/locale/pt-br'),
ro: require('dayjs/locale/ro'),
ru: require('dayjs/locale/ru'),
sl: require('dayjs/locale/sl'),
sr_Cyrl: require('dayjs/locale/sr-cyrl'),
sr_Latn: require('dayjs/locale/sr'),
sv: require('dayjs/locale/sv'),
th: require('dayjs/locale/th'),
tr: require('dayjs/locale/tr'),
uk: require('dayjs/locale/uk'),
ur: require('dayjs/locale/ur'),
vi: require('dayjs/locale/vi'),
zh_CN: require('dayjs/locale/zh-cn'),
zh_HK: require('dayjs/locale/zh-hk'),
zh_TW: require('dayjs/locale/zh-tw'),
}
@@ -0,0 +1,849 @@
import * as FilePond from 'filepond'
import Cropper from 'cropperjs'
import mime from 'mime'
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
import FilePondPluginImageCrop from 'filepond-plugin-image-crop'
import FilePondPluginImageEdit from 'filepond-plugin-image-edit'
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import FilePondPluginImageResize from 'filepond-plugin-image-resize'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
import FilePondPluginMediaPreview from 'filepond-plugin-media-preview'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
FilePond.registerPlugin(FilePondPluginFileValidateType)
FilePond.registerPlugin(FilePondPluginImageCrop)
FilePond.registerPlugin(FilePondPluginImageEdit)
FilePond.registerPlugin(FilePondPluginImageExifOrientation)
FilePond.registerPlugin(FilePondPluginImagePreview)
FilePond.registerPlugin(FilePondPluginImageResize)
FilePond.registerPlugin(FilePondPluginImageTransform)
FilePond.registerPlugin(FilePondPluginMediaPreview)
window.FilePond = FilePond
export default function fileUploadFormComponent({
acceptedFileTypes,
imageEditorEmptyFillColor,
imageEditorMode,
imageEditorViewportHeight,
imageEditorViewportWidth,
deleteUploadedFileUsing,
isDeletable,
isDisabled,
getUploadedFilesUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeUpscale,
isAvatar,
hasImageEditor,
hasCircleCropper,
canEditSvgs,
isSvgEditingConfirmed,
confirmSvgEditingMessage,
disabledSvgEditingMessage,
isDownloadable,
isMultiple,
isOpenable,
isPasteable,
isPreviewable,
isReorderable,
itemPanelAspectRatio,
loadingIndicatorPosition,
locale,
maxFiles,
maxFilesValidationMessage,
maxSize,
minSize,
maxParallelUploads,
mimeTypeMap,
panelAspectRatio,
panelLayout,
placeholder,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldOrientImageFromExif,
shouldTransformImage,
state,
uploadButtonPosition,
uploadingMessage,
uploadProgressIndicatorPosition,
uploadUsing,
}) {
return {
fileKeyIndex: {},
pond: null,
shouldUpdateState: true,
state,
lastState: null,
error: null,
uploadedFileIndex: {},
isEditorOpen: false,
editingFile: {},
currentRatio: '',
editor: {},
async init() {
FilePond.setOptions(locales[locale] ?? locales['en'])
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowImageExifOrientation: shouldOrientImageFromExif,
allowPaste: isPasteable,
allowRemove: isDeletable,
allowReorder: isReorderable,
allowImagePreview: isPreviewable,
allowVideoPreview: isPreviewable,
allowAudioPreview: isPreviewable,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
imageResizeUpscale,
imageTransformOutputStripImageHead: false,
itemInsertLocation: shouldAppendFiles ? 'after' : 'before',
...(placeholder && { labelIdle: placeholder }),
maxFiles,
fileAttachmentsMaxFileSize: maxSize,
minFileSize: minSize,
...(maxParallelUploads && { maxParallelUploads }),
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition: removeUploadedFileButtonPosition,
styleItemPanelAspectRatio: itemPanelAspectRatio,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition: uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source, {
cache: 'no-store',
})
let blob = await response.blob()
load(blob)
},
process: (
fieldName,
file,
metadata,
load,
error,
progress,
) => {
this.shouldUpdateState = false
let fileKey = (
[1e7] +
-1e3 +
-4e3 +
-8e3 +
-1e11
).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (c / 4)))
).toString(16),
)
uploadUsing(
fileKey,
file,
(fileKey) => {
this.shouldUpdateState = true
load(fileKey)
},
error,
progress,
)
},
remove: async (source, load) => {
let fileKey = this.uploadedFileIndex[source] ?? null
if (!fileKey) {
return
}
await deleteUploadedFileUsing(fileKey)
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
load()
},
},
allowImageEdit: hasImageEditor,
imageEditEditor: {
open: (file) => this.loadEditor(file),
onconfirm: () => {},
oncancel: () => this.closeEditor(),
onclose: () => this.closeEditor(),
},
fileValidateTypeDetectType: (source, detectedType) => {
return new Promise((resolve, reject) => {
const extension = source.name
.split('.')
.pop()
.toLowerCase()
const mimeType =
mimeTypeMap[extension] ||
detectedType ||
mime.getType(extension)
mimeType ? resolve(mimeType) : reject()
})
},
})
this.$watch('state', async () => {
if (!this.pond) {
return
}
if (!this.shouldUpdateState) {
return
}
if (this.state === undefined) {
return
}
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (
this.state !== null &&
Object.values(this.state).filter((file) =>
file.startsWith('livewire-file:'),
).length
) {
this.lastState = null
return
}
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
this.lastState = JSON.stringify(this.state)
this.pond.files = await this.getFiles()
})
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map((file) =>
file.source instanceof File
? file.serverId
: (this.uploadedFileIndex[file.source] ?? null),
) // file.serverId is null for a file that is not yet uploaded
.filter((fileKey) => fileKey)
await reorderUploadedFilesUsing(
shouldAppendFiles
? orderedFileKeys
: orderedFileKeys.reverse(),
)
})
this.pond.on('initfile', async (fileItem) => {
if (!isDownloadable) {
return
}
if (isAvatar) {
return
}
this.insertDownloadLink(fileItem)
})
this.pond.on('initfile', async (fileItem) => {
if (!isOpenable) {
return
}
if (isAvatar) {
return
}
this.insertOpenLink(fileItem)
})
this.pond.on('addfilestart', async (file) => {
this.error = null
if (file.status !== FilePond.FileStatus.PROCESSING_QUEUED) {
return
}
this.dispatchFormEvent('form-processing-started', {
message: uploadingMessage,
})
})
const handleFileProcessing = async () => {
if (
this.pond
.getFiles()
.filter(
(file) =>
file.status ===
FilePond.FileStatus.PROCESSING ||
file.status ===
FilePond.FileStatus.PROCESSING_QUEUED,
).length
) {
return
}
this.dispatchFormEvent('form-processing-finished')
}
this.pond.on('processfile', handleFileProcessing)
this.pond.on('processfileabort', handleFileProcessing)
this.pond.on('processfilerevert', handleFileProcessing)
this.pond.on('warning', (warning) => {
if (warning.body === 'Max files') {
this.error = maxFilesValidationMessage
}
})
if (panelLayout === 'compact circle') {
// The compact circle layout does not have enough space to render an error message inside the input.
// As such, we need to display the error message outside of the input, using the `error` Alpine.js
// property that is output as a message in the field's view.
this.pond.on('error', (error) => {
// FilePond has a weird English translation for the error message when a file of an unexpected
// type is uploaded, for example: `File of invalid type: Expects or image/*`. This is a
// hacky workaround to fix the message to be `File of invalid type: Expects image/*`.
this.error = `${error.main}: ${error.sub}`.replace(
'Expects or',
'Expects',
)
})
}
this.pond.on('removefile', () => (this.error = null))
},
destroy() {
this.destroyEditor()
FilePond.destroy(this.$refs.input)
this.pond = null
},
dispatchFormEvent(name, detail = {}) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
detail,
}),
)
},
async getUploadedFiles() {
const uploadedFiles = await getUploadedFilesUsing()
this.fileKeyIndex = uploadedFiles ?? {}
this.uploadedFileIndex = Object.entries(this.fileKeyIndex)
.filter(([key, value]) => value?.url)
.reduce((obj, [key, value]) => {
obj[value.url] = key
return obj
}, {})
},
async getFiles() {
await this.getUploadedFiles()
let files = []
for (const uploadedFile of Object.values(this.fileKeyIndex)) {
if (!uploadedFile) {
continue
}
files.push({
source: uploadedFile.url,
options: {
type: 'local',
...(!uploadedFile.type ||
(isPreviewable &&
(/^audio/.test(uploadedFile.type) ||
/^image/.test(uploadedFile.type) ||
/^video/.test(uploadedFile.type)))
? {}
: {
file: {
name: uploadedFile.name,
size: uploadedFile.size,
type: uploadedFile.type,
},
}),
},
})
}
return shouldAppendFiles ? files : files.reverse()
},
insertDownloadLink(file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getDownloadLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
insertOpenLink(file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getOpenLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
getDownloadLink(file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
return anchor
},
getOpenLink(file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
return anchor
},
initEditor() {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
this.editor = new Cropper(this.$refs.editor, {
aspectRatio:
imageEditorViewportWidth / imageEditorViewportHeight,
autoCropArea: 1,
center: true,
crop: (event) => {
this.$refs.xPositionInput.value = Math.round(event.detail.x)
this.$refs.yPositionInput.value = Math.round(event.detail.y)
this.$refs.heightInput.value = Math.round(
event.detail.height,
)
this.$refs.widthInput.value = Math.round(event.detail.width)
this.$refs.rotationInput.value = event.detail.rotate
},
cropBoxResizable: true,
guides: true,
highlight: true,
responsive: true,
toggleDragModeOnDblclick: true,
viewMode: imageEditorMode,
wheelZoomRatio: 0.02,
})
},
closeEditor() {
this.editingFile = {}
this.isEditorOpen = false
this.destroyEditor()
},
fixImageDimensions(file, callback) {
if (file.type !== 'image/svg+xml') {
return callback(file)
}
const svgReader = new FileReader()
svgReader.onload = (event) => {
const svgElement = new DOMParser()
.parseFromString(event.target.result, 'image/svg+xml')
?.querySelector('svg')
if (!svgElement) {
return callback(file)
}
const viewBoxAttribute = ['viewBox', 'ViewBox', 'viewbox'].find(
(attribute) => svgElement.hasAttribute(attribute),
)
if (!viewBoxAttribute) {
return callback(file)
}
const viewBox = svgElement
.getAttribute(viewBoxAttribute)
.split(' ')
if (!viewBox || viewBox.length !== 4) {
return callback(file)
}
svgElement.setAttribute('width', parseFloat(viewBox[2]) + 'pt')
svgElement.setAttribute('height', parseFloat(viewBox[3]) + 'pt')
return callback(
new File(
[
new Blob(
[
new XMLSerializer().serializeToString(
svgElement,
),
],
{ type: 'image/svg+xml' },
),
],
file.name,
{
type: 'image/svg+xml',
_relativePath: '',
},
),
)
}
svgReader.readAsText(file)
},
loadEditor(file) {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
if (!file) {
return
}
const isFileSvg = file.type === 'image/svg+xml'
if (!canEditSvgs && isFileSvg) {
alert(disabledSvgEditingMessage)
return
}
if (
isSvgEditingConfirmed &&
isFileSvg &&
!confirm(confirmSvgEditingMessage)
) {
return
}
this.fixImageDimensions(file, (editingFile) => {
this.editingFile = editingFile
this.initEditor()
const reader = new FileReader()
reader.onload = (event) => {
this.isEditorOpen = true
setTimeout(
() => this.editor.replace(event.target.result),
200,
)
}
reader.readAsDataURL(file)
})
},
getRoundedCanvas(sourceCanvas) {
let width = sourceCanvas.width
let height = sourceCanvas.height
let canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
let context = canvas.getContext('2d')
context.imageSmoothingEnabled = true
context.drawImage(sourceCanvas, 0, 0, width, height)
context.globalCompositeOperation = 'destination-in'
context.beginPath()
context.ellipse(
width / 2,
height / 2,
width / 2,
height / 2,
0,
0,
2 * Math.PI,
)
context.fill()
return canvas
},
saveEditor() {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
let croppedCanvas = this.editor.getCroppedCanvas({
fillColor: imageEditorEmptyFillColor ?? 'transparent',
height: imageResizeTargetHeight,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
width: imageResizeTargetWidth,
})
if (hasCircleCropper) {
croppedCanvas = this.getRoundedCanvas(croppedCanvas)
}
croppedCanvas.toBlob(
(croppedImage) => {
if (isMultiple) {
this.pond.removeFile(
this.pond
.getFiles()
.find(
(uploadedFile) =>
uploadedFile.filename ===
this.editingFile.name,
)?.id,
{ revert: true },
)
}
this.$nextTick(() => {
this.shouldUpdateState = false
let editingFileName = this.editingFile.name.slice(
0,
this.editingFile.name.lastIndexOf('.'),
)
let editingFileExtension = this.editingFile.name
.split('.')
.pop()
if (editingFileExtension === 'svg') {
editingFileExtension = 'png'
}
const fileNameVersionRegex = /-v(\d+)/
if (fileNameVersionRegex.test(editingFileName)) {
editingFileName = editingFileName.replace(
fileNameVersionRegex,
(match, number) => {
const newNumber = Number(number) + 1
return `-v${newNumber}`
},
)
} else {
editingFileName += '-v1'
}
this.pond
.addFile(
new File(
[croppedImage],
`${editingFileName}.${editingFileExtension}`,
{
type:
this.editingFile.type ===
'image/svg+xml' ||
hasCircleCropper
? 'image/png'
: this.editingFile.type,
lastModified: new Date().getTime(),
},
),
)
.then(() => {
this.closeEditor()
})
.catch(() => {
this.closeEditor()
})
})
},
hasCircleCropper ? 'image/png' : this.editingFile.type,
)
},
destroyEditor() {
if (this.editor && typeof this.editor.destroy === 'function') {
this.editor.destroy()
}
this.editor = null
},
}
}
import am from 'filepond/locale/am-et'
import ar from 'filepond/locale/ar-ar'
import az from 'filepond/locale/az-az'
import ca from 'filepond/locale/ca-ca'
import ckb from 'filepond/locale/ku-ckb'
import cs from 'filepond/locale/cs-cz'
import da from 'filepond/locale/da-dk'
import de from 'filepond/locale/de-de'
import el from 'filepond/locale/el-el'
import en from 'filepond/locale/en-en'
import es from 'filepond/locale/es-es'
import fa from 'filepond/locale/fa_ir'
import fi from 'filepond/locale/fi-fi'
import fr from 'filepond/locale/fr-fr'
import he from 'filepond/locale/he-he'
import hr from 'filepond/locale/hr-hr'
import hu from 'filepond/locale/hu-hu'
import id from 'filepond/locale/id-id'
import it from 'filepond/locale/it-it'
import ja from 'filepond/locale/ja-ja'
import km from 'filepond/locale/km-km'
import ko from 'filepond/locale/ko-kr'
import lt from 'filepond/locale/lt-lt'
import lus from 'filepond/locale/lus-lus'
import lv from 'filepond/locale/lv-lv'
import nb from 'filepond/locale/no_nb'
import nl from 'filepond/locale/nl-nl'
import pl from 'filepond/locale/pl-pl'
import pt from 'filepond/locale/pt-pt'
import pt_BR from 'filepond/locale/pt-br'
import ro from 'filepond/locale/ro-ro'
import ru from 'filepond/locale/ru-ru'
import sk from 'filepond/locale/sk-sk'
import sv from 'filepond/locale/sv_se'
import tr from 'filepond/locale/tr-tr'
import uk from 'filepond/locale/uk-ua'
import vi from 'filepond/locale/vi-vi'
import zh_CN from 'filepond/locale/zh-cn'
import zh_HK from 'filepond/locale/zh-hk'
import zh_TW from 'filepond/locale/zh-tw'
const locales = {
am,
ar,
az,
ca,
ckb,
cs,
da,
de,
el,
en,
es,
fa,
fi,
fr,
he,
hr,
hu,
id,
it,
ja,
km,
ko,
lt,
lus,
lv,
nb,
nl,
pl,
pt,
pt_BR,
ro,
ru,
sk,
sv,
tr,
uk,
vi,
zh_CN,
zh_HK,
zh_TW,
}
@@ -0,0 +1,109 @@
export default function keyValueFormComponent({ state }) {
return {
state,
rows: [],
init() {
this.updateRows()
if (this.rows.length <= 0) {
this.rows.push({ key: '', value: '' })
} else {
this.updateState()
}
this.$watch('state', (state, oldState) => {
const getLength = (value) => {
if (value === null) {
return 0
}
if (Array.isArray(value)) {
return value.length
}
if (typeof value !== 'object') {
return 0
}
return Object.keys(value).length
}
if (getLength(state) === 0 && getLength(oldState) === 0) {
return
}
this.updateRows()
})
},
addRow() {
this.rows.push({ key: '', value: '' })
this.updateState()
},
deleteRow(index) {
this.rows.splice(index, 1)
if (this.rows.length <= 0) {
this.addRow()
}
this.updateState()
},
reorderRows(event) {
const rows = Alpine.raw(this.rows)
this.rows = []
const reorderedRow = rows.splice(event.oldIndex, 1)[0]
rows.splice(event.newIndex, 0, reorderedRow)
this.$nextTick(() => {
this.rows = rows
this.updateState()
})
},
// https://github.com/filamentphp/filament/issues/1107
// https://github.com/filamentphp/filament/issues/12824
updateRows() {
const state = Alpine.raw(this.state)
const mergedRows = state.map(({ key, value }) => ({ key, value }))
this.rows.forEach((row) => {
if (row.key === '' || row.key === null) {
mergedRows.push({
key: '',
value: row.value,
})
}
})
this.rows = mergedRows
},
updateState() {
let state = []
this.rows.forEach((row) => {
if (row.key === '' || row.key === null) {
return
}
state.push({
key: row.key,
value: row.value,
})
})
if (JSON.stringify(this.state) !== JSON.stringify(state)) {
this.state = state
}
},
}
}
@@ -0,0 +1,411 @@
window.CodeMirror = require('codemirror/lib/codemirror')
require('codemirror')
require('codemirror/addon/mode/overlay')
require('codemirror/addon/edit/continuelist')
require('codemirror/addon/display/placeholder')
require('codemirror/addon/selection/mark-selection')
require('codemirror/addon/search/searchcursor')
require('codemirror/mode/clike/clike')
require('codemirror/mode/cmake/cmake')
require('codemirror/mode/css/css')
require('codemirror/mode/diff/diff')
require('codemirror/mode/django/django')
require('codemirror/mode/dockerfile/dockerfile')
require('codemirror/mode/gfm/gfm')
require('codemirror/mode/go/go')
require('codemirror/mode/htmlmixed/htmlmixed')
require('codemirror/mode/http/http')
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/jinja2/jinja2')
require('codemirror/mode/jsx/jsx')
require('codemirror/mode/markdown/markdown')
require('codemirror/mode/nginx/nginx')
require('codemirror/mode/pascal/pascal')
require('codemirror/mode/perl/perl')
require('codemirror/mode/php/php')
require('codemirror/mode/protobuf/protobuf')
require('codemirror/mode/python/python')
require('codemirror/mode/ruby/ruby')
require('codemirror/mode/rust/rust')
require('codemirror/mode/sass/sass')
require('codemirror/mode/shell/shell')
require('codemirror/mode/sql/sql')
require('codemirror/mode/stylus/stylus')
require('codemirror/mode/swift/swift')
require('codemirror/mode/vue/vue')
require('codemirror/mode/xml/xml')
require('codemirror/mode/yaml/yaml')
require('./markdown-editor/EasyMDE')
CodeMirror.commands.tabAndIndentMarkdownList = function (codemirror) {
var ranges = codemirror.listSelections()
var pos = ranges[0].head
var eolState = codemirror.getStateAfter(pos.line)
var inList = eolState.list !== false
if (inList) {
codemirror.execCommand('indentMore')
return
}
if (codemirror.options.indentWithTabs) {
codemirror.execCommand('insertTab')
return
}
var spaces = Array(codemirror.options.tabSize + 1).join(' ')
codemirror.replaceSelection(spaces)
}
CodeMirror.commands.shiftTabAndUnindentMarkdownList = function (codemirror) {
var ranges = codemirror.listSelections()
var pos = ranges[0].head
var eolState = codemirror.getStateAfter(pos.line)
var inList = eolState.list !== false
if (inList) {
codemirror.execCommand('indentLess')
return
}
if (codemirror.options.indentWithTabs) {
codemirror.execCommand('insertTab')
return
}
var spaces = Array(codemirror.options.tabSize + 1).join(' ')
codemirror.replaceSelection(spaces)
}
export default function markdownEditorFormComponent({
canAttachFiles,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
maxHeight,
minHeight,
placeholder,
setUpUsing,
state,
translations,
toolbarButtons,
uploadFileAttachmentUsing,
}) {
return {
editor: null,
state,
async init() {
// If the editor is inside a modal, wait for the modal transition to finish before initializing the editor.
// This is necessary to prevent the editor from being initialized before the modal is fully visible,
// which can cause it to render without any content.
if (this.$root.closest('.fi-modal')) {
await new Promise((resolve) => setTimeout(resolve, 300))
}
if (this.$root._editor) {
this.$root._editor.toTextArea()
this.$root._editor = null
}
this.$root._editor = this.editor = new EasyMDE({
autoDownloadFontAwesome: false,
autoRefresh: true,
autoSave: false,
element: this.$refs.editor,
imageAccept:
'image/png, image/jpeg, image/gif, image/avif, image/webp',
imageUploadFunction: uploadFileAttachmentUsing,
initialValue: this.state ?? '',
maxHeight,
minHeight,
placeholder,
previewImagesInEditor: true,
spellChecker: false,
status: [
{
className: 'upload-image',
defaultValue: '',
},
],
toolbar: this.getToolbar(),
uploadImage: canAttachFiles,
})
this.editor.codemirror.setOption(
'direction',
document.documentElement?.dir ?? 'ltr',
)
// When creating a link, highlight the URL instead of the label:
this.editor.codemirror.on('changes', (instance, changes) => {
try {
const lastChange = changes[changes.length - 1]
if (lastChange.origin === '+input') {
const urlPlaceholder = '(https://)'
const urlLineText =
lastChange.text[lastChange.text.length - 1]
if (
urlLineText.endsWith(urlPlaceholder) &&
urlLineText !== '[]' + urlPlaceholder
) {
const from = lastChange.from
const to = lastChange.to
const isSelectionMultiline =
lastChange.text.length > 1
const baseIndex = isSelectionMultiline ? 0 : from.ch
setTimeout(() => {
instance.setSelection(
{
line: to.line,
ch:
baseIndex +
urlLineText.lastIndexOf('(') +
1,
},
{
line: to.line,
ch:
baseIndex +
urlLineText.lastIndexOf(')'),
},
)
}, 25)
}
}
} catch (error) {
// Revert to original behavior.
}
})
this.editor.codemirror.on(
'change',
Alpine.debounce(() => {
if (!this.editor) {
return
}
this.state = this.editor.value()
if (isLiveDebounced) {
this.$wire.commit()
}
}, liveDebounce ?? 300),
)
if (isLiveOnBlur) {
this.editor.codemirror.on('blur', () => this.$wire.commit())
}
this.$watch('state', () => {
if (!this.editor) {
return
}
if (this.editor.codemirror.hasFocus()) {
return
}
Alpine.raw(this.editor).value(this.state ?? '')
})
if (setUpUsing) {
setUpUsing(this)
}
},
destroy() {
this.editor.cleanup()
this.editor = null
},
getToolbar() {
let toolbar = []
toolbarButtons.forEach((buttonGroup) => {
buttonGroup.forEach((button) =>
toolbar.push(this.getToolbarButton(button)),
)
if (buttonGroup.length > 0) {
toolbar.push('|')
}
})
if (toolbar[toolbar.length - 1] === '|') {
toolbar.pop()
}
return toolbar
},
getToolbarButton(name) {
if (name === 'bold') {
return this.getBoldToolbarButton()
}
if (name === 'italic') {
return this.getItalicToolbarButton()
}
if (name === 'strike') {
return this.getStrikeToolbarButton()
}
if (name === 'link') {
return this.getLinkToolbarButton()
}
if (name === 'heading') {
return this.getHeadingToolbarButton()
}
if (name === 'blockquote') {
return this.getBlockquoteToolbarButton()
}
if (name === 'codeBlock') {
return this.getCodeBlockToolbarButton()
}
if (name === 'bulletList') {
return this.getBulletListToolbarButton()
}
if (name === 'orderedList') {
return this.getOrderedListToolbarButton()
}
if (name === 'table') {
return this.getTableToolbarButton()
}
if (name === 'attachFiles') {
return this.getAttachFilesToolbarButton()
}
if (name === 'undo') {
return this.getUndoToolbarButton()
}
if (name === 'redo') {
return this.getRedoToolbarButton()
}
console.error(`Markdown editor toolbar button "${name}" not found.`)
},
getBoldToolbarButton() {
return {
name: 'bold',
action: EasyMDE.toggleBold,
title: translations.tools?.bold,
}
},
getItalicToolbarButton() {
return {
name: 'italic',
action: EasyMDE.toggleItalic,
title: translations.tools?.italic,
}
},
getStrikeToolbarButton() {
return {
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
title: translations.tools?.strike,
}
},
getLinkToolbarButton() {
return {
name: 'link',
action: EasyMDE.drawLink,
title: translations.tools?.link,
}
},
getHeadingToolbarButton() {
return {
name: 'heading',
action: EasyMDE.toggleHeadingSmaller,
title: translations.tools?.heading,
}
},
getBlockquoteToolbarButton() {
return {
name: 'quote',
action: EasyMDE.toggleBlockquote,
title: translations.tools?.blockquote,
}
},
getCodeBlockToolbarButton() {
return {
name: 'code',
action: EasyMDE.toggleCodeBlock,
title: translations.tools?.code_block,
}
},
getBulletListToolbarButton() {
return {
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
title: translations.tools?.bullet_list,
}
},
getOrderedListToolbarButton() {
return {
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
title: translations.tools?.ordered_list,
}
},
getTableToolbarButton() {
return {
name: 'table',
action: EasyMDE.drawTable,
title: translations.tools?.table,
}
},
getAttachFilesToolbarButton() {
return {
name: 'upload-image',
action: EasyMDE.drawUploadedImage,
title: translations.tools?.attach_files,
}
},
getUndoToolbarButton() {
return {
name: 'undo',
action: EasyMDE.undo,
title: translations.tools?.undo,
}
},
getRedoToolbarButton() {
return {
name: 'redo',
action: EasyMDE.redo,
title: translations.tools?.redo,
}
},
}
}
@@ -0,0 +1,361 @@
import { Editor } from '@tiptap/core'
import getExtensions from './rich-editor/extensions'
import { Selection } from '@tiptap/pm/state'
import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'
export default function richEditorFormComponent({
acceptedFileTypes,
acceptedFileTypesValidationMessage,
activePanel,
canAttachFiles,
deleteCustomBlockButtonIconHtml,
editCustomBlockButtonIconHtml,
extensions,
key,
isDisabled,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
livewireId,
maxFileSize,
maxFileSizeValidationMessage,
mergeTags,
noMergeTagSearchResultsMessage,
placeholder,
state,
statePath,
textColors,
uploadingFileMessage,
floatingToolbars,
}) {
let editor
let eventListeners = []
let isDestroyed = false
return {
state,
activePanel,
editorSelection: { type: 'text', anchor: 1, head: 1 },
isUploadingFile: false,
fileValidationMessage: null,
shouldUpdateState: true,
editorUpdatedAt: Date.now(),
async init() {
editor = new Editor({
editable: !isDisabled,
element: this.$refs.editor,
extensions: await getExtensions({
acceptedFileTypes,
acceptedFileTypesValidationMessage,
canAttachFiles,
customExtensionUrls: extensions,
deleteCustomBlockButtonIconHtml,
editCustomBlockButtonIconHtml,
editCustomBlockUsing: (id, config) =>
this.$wire.mountAction(
'customBlock',
{
editorSelection: this.editorSelection,
id,
config,
mode: 'edit',
},
{ schemaComponent: key },
),
insertCustomBlockUsing: (id, dragPosition = null) =>
this.$wire.mountAction(
'customBlock',
{ id, dragPosition, mode: 'insert' },
{ schemaComponent: key },
),
key,
maxFileSize,
maxFileSizeValidationMessage,
mergeTags,
noMergeTagSearchResultsMessage,
placeholder,
statePath,
textColors,
uploadingFileMessage,
$wire: this.$wire,
floatingToolbars,
}),
content: this.state,
})
Object.keys(floatingToolbars).forEach((key) => {
const element = this.$refs[`floatingToolbar::${key}`]
if (!element) {
console.warn(`Floating toolbar [${key}] not found.`)
return
}
editor.registerPlugin(
BubbleMenuPlugin({
editor,
element,
pluginKey: `floatingToolbar::${key}`,
shouldShow: ({ editor }) =>
editor.isFocused && editor.isActive(key),
options: {
placement: 'bottom',
offset: 15,
},
}),
)
})
editor.on('create', () => {
this.editorUpdatedAt = Date.now()
})
const debouncedCommit = Alpine.debounce(() => {
if (!isDestroyed) {
this.$wire.commit()
}
}, liveDebounce ?? 300)
editor.on('update', ({ editor }) =>
this.$nextTick(() => {
if (isDestroyed) return
this.editorUpdatedAt = Date.now()
this.state = editor.getJSON()
this.shouldUpdateState = false
this.fileValidationMessage = null
if (isLiveDebounced) {
debouncedCommit()
}
}),
)
editor.on('selectionUpdate', ({ transaction }) => {
if (isDestroyed) return
this.editorUpdatedAt = Date.now()
this.editorSelection = transaction.selection.toJSON()
})
if (isLiveOnBlur) {
editor.on('blur', () => {
if (!isDestroyed) {
this.$wire.commit()
}
})
}
this.$watch('state', () => {
if (isDestroyed) return
if (!this.shouldUpdateState) {
this.shouldUpdateState = true
return
}
editor.commands.setContent(this.state)
})
const runCommandsHandler = (event) => {
if (event.detail.livewireId !== livewireId) {
return
}
if (event.detail.key !== key) {
return
}
this.runEditorCommands(event.detail)
}
window.addEventListener(
'run-rich-editor-commands',
runCommandsHandler,
)
eventListeners.push([
'run-rich-editor-commands',
runCommandsHandler,
])
const uploadingFileHandler = (event) => {
if (event.detail.livewireId !== livewireId) {
return
}
if (event.detail.key !== key) {
return
}
this.isUploadingFile = true
this.fileValidationMessage = null
event.stopPropagation()
}
window.addEventListener(
'rich-editor-uploading-file',
uploadingFileHandler,
)
eventListeners.push([
'rich-editor-uploading-file',
uploadingFileHandler,
])
const uploadedFileHandler = (event) => {
if (event.detail.livewireId !== livewireId) {
return
}
if (event.detail.key !== key) {
return
}
this.isUploadingFile = false
event.stopPropagation()
}
window.addEventListener(
'rich-editor-uploaded-file',
uploadedFileHandler,
)
eventListeners.push([
'rich-editor-uploaded-file',
uploadedFileHandler,
])
const validationMessageHandler = (event) => {
if (event.detail.livewireId !== livewireId) {
return
}
if (event.detail.key !== key) {
return
}
this.isUploadingFile = false
this.fileValidationMessage = event.detail.validationMessage
event.stopPropagation()
}
window.addEventListener(
'rich-editor-file-validation-message',
validationMessageHandler,
)
eventListeners.push([
'rich-editor-file-validation-message',
validationMessageHandler,
])
window.dispatchEvent(
new CustomEvent(`schema-component-${livewireId}-${key}-loaded`),
)
},
getEditor() {
return editor
},
$getEditor() {
return this.getEditor()
},
setEditorSelection(selection) {
if (!selection) {
return
}
this.editorSelection = selection
editor
.chain()
.command(({ tr }) => {
tr.setSelection(
Selection.fromJSON(
editor.state.doc,
this.editorSelection,
),
)
return true
})
.run()
},
runEditorCommands({ commands, editorSelection }) {
this.setEditorSelection(editorSelection)
let commandChain = editor.chain()
commands.forEach(
(command) =>
(commandChain = commandChain[command.name](
...(command.arguments ?? []),
)),
)
commandChain.run()
},
togglePanel(id = null) {
if (this.isPanelActive(id)) {
this.activePanel = null
return
}
this.activePanel = id
},
isPanelActive(id = null) {
if (id === null) {
return this.activePanel !== null
}
return this.activePanel === id
},
insertMergeTag(id) {
editor
.chain()
.focus()
.insertContent([
{
type: 'mergeTag',
attrs: { id },
},
{
type: 'text',
text: ' ',
},
])
.run()
},
destroy() {
isDestroyed = true
eventListeners.forEach(([eventName, handler]) => {
window.removeEventListener(eventName, handler)
})
eventListeners = []
if (editor) {
editor.destroy()
editor = null
}
this.shouldUpdateState = true
},
}
}
@@ -0,0 +1,233 @@
import { mergeAttributes, Node, NodePos } from '@tiptap/core'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
export default Node.create({
name: 'customBlock',
group: 'block',
atom: true,
defining: true,
draggable: true,
selectable: true,
isolating: true,
allowGapCursor: true,
inline: false,
addNodeView() {
return ({
editor,
node,
getPos,
HTMLAttributes,
decorations,
extension,
}) => {
const dom = document.createElement('div')
dom.setAttribute('data-config', node.attrs.config)
dom.setAttribute('data-id', node.attrs.id)
dom.setAttribute('data-type', 'customBlock')
const header = document.createElement('div')
header.className =
'fi-fo-rich-editor-custom-block-header fi-not-prose'
dom.appendChild(header)
if (
editor.isEditable &&
typeof node.attrs.config === 'object' &&
node.attrs.config !== null &&
Object.keys(node.attrs.config).length > 0
) {
const editButtonContainer = document.createElement('div')
editButtonContainer.className =
'fi-fo-rich-editor-custom-block-edit-btn-ctn'
header.appendChild(editButtonContainer)
const editButton = document.createElement('button')
editButton.className = 'fi-icon-btn'
editButton.type = 'button'
editButton.innerHTML =
extension.options.editCustomBlockButtonIconHtml
editButton.addEventListener('click', () =>
extension.options.editCustomBlockUsing(
node.attrs.id,
node.attrs.config,
),
)
editButtonContainer.appendChild(editButton)
}
const heading = document.createElement('p')
heading.className = 'fi-fo-rich-editor-custom-block-heading'
heading.textContent = node.attrs.label
header.appendChild(heading)
if (editor.isEditable) {
const deleteButtonContainer = document.createElement('div')
deleteButtonContainer.className =
'fi-fo-rich-editor-custom-block-delete-btn-ctn'
header.appendChild(deleteButtonContainer)
const deleteButton = document.createElement('button')
deleteButton.className = 'fi-icon-btn'
deleteButton.type = 'button'
deleteButton.innerHTML =
extension.options.deleteCustomBlockButtonIconHtml
deleteButton.addEventListener('click', () =>
editor
.chain()
.setNodeSelection(getPos())
.deleteSelection()
.run(),
)
deleteButtonContainer.appendChild(deleteButton)
}
if (node.attrs.preview) {
const preview = document.createElement('div')
preview.className =
'fi-fo-rich-editor-custom-block-preview fi-not-prose'
preview.innerHTML = new TextDecoder().decode(
Uint8Array.from(atob(node.attrs.preview), (char) =>
char.charCodeAt(0),
),
)
dom.appendChild(preview)
}
return {
dom,
}
}
},
addOptions() {
return {
deleteCustomBlockButtonIconHtml: null,
editCustomBlockButtonIconHtml: null,
editCustomBlockUsing: () => {},
insertCustomBlockUsing: () => {},
}
},
addAttributes() {
return {
config: {
default: null,
parseHTML: (element) =>
JSON.parse(element.getAttribute('data-config')),
},
id: {
default: null,
parseHTML: (element) => element.getAttribute('data-id'),
renderHTML: (attributes) => {
if (!attributes.id) {
return {}
}
return {
'data-id': attributes.id,
}
},
},
label: {
default: null,
parseHTML: (element) => element.getAttribute('data-label'),
rendered: false,
},
preview: {
default: null,
parseHTML: (element) => element.getAttribute('data-preview'),
rendered: false,
},
}
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
]
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes)]
},
addKeyboardShortcuts() {
return {
Backspace: () =>
this.editor.commands.command(({ tr, state }) => {
let isCustomBlock = false
const { selection } = state
const { empty, anchor } = selection
if (!empty) {
return false
}
// Store node and position for later use
let customBlockNode = new ProseMirrorNode()
let customBlockPos = 0
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
if (node.type.name === this.name) {
isCustomBlock = true
customBlockNode = node
customBlockPos = pos
return false
}
})
return isCustomBlock
}),
}
},
addProseMirrorPlugins() {
const { insertCustomBlockUsing } = this.options
return [
new Plugin({
props: {
handleDrop(view, event) {
if (!event) {
return false
}
event.preventDefault()
if (!event.dataTransfer.getData('customBlock')) {
return false
}
const customBlockId =
event.dataTransfer.getData('customBlock')
insertCustomBlockUsing(
customBlockId,
view.posAtCoords({
left: event.clientX,
top: event.clientY,
}).pos,
)
return false
},
},
}),
]
},
})
@@ -0,0 +1,58 @@
import { Node, mergeAttributes } from '@tiptap/core'
export default Node.create({
name: 'gridColumn',
content: 'block+',
isolating: true,
addOptions() {
return {
HTMLAttributes: {
class: 'grid-layout-col',
},
}
},
addAttributes() {
return {
'data-col-span': {
default: 1,
parseHTML: (element) => element.getAttribute('data-col-span'),
renderHTML: (attributes) => {
return {
'data-col-span': attributes['data-col-span'] ?? 1,
}
},
},
style: {
default: null,
parseHTML: (element) => element.getAttribute('style'),
renderHTML: (attributes) => {
return {
style: `grid-column: span ${attributes['data-col-span'] ?? 1};`,
}
},
},
}
},
parseHTML() {
return [
{
tag: 'div',
getAttrs: (node) =>
node.classList.contains('grid-layout-col') && null,
},
]
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
]
},
})
@@ -0,0 +1,132 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { TextSelection } from '@tiptap/pm/state'
export default Node.create({
name: 'grid',
group: 'block',
defining: true,
isolating: true,
allowGapCursor: false,
content: 'gridColumn+',
addOptions() {
return {
HTMLAttributes: {
class: 'grid-layout',
},
}
},
addAttributes() {
return {
'data-cols': {
default: 2,
parseHTML: (element) => element.getAttribute('data-cols'),
},
'data-from-breakpoint': {
default: 'md',
parseHTML: (element) =>
element.getAttribute('data-from-breakpoint'),
},
style: {
default: null,
parseHTML: (element) => element.getAttribute('style'),
renderHTML: (attributes) => {
return {
style: `grid-template-columns: repeat(${attributes['data-cols']}, 1fr)`,
}
},
},
}
},
parseHTML() {
return [
{
tag: 'div',
getAttrs: (node) =>
node.classList.contains('grid-layout') && null,
},
]
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
]
},
addCommands() {
return {
insertGrid:
({
columns = [1, 1],
fromBreakpoint,
coordinates = null,
} = {}) =>
({ tr, dispatch, editor }) => {
const columnNodeType = editor.schema.nodes.gridColumn
const spans =
Array.isArray(columns) && columns.length
? columns
: [1, 1]
const columnNodes = []
for (let index = 0; index < spans.length; index += 1) {
columnNodes.push(
columnNodeType.createAndFill({
'data-col-span': Number(spans[index] ?? 1) || 1,
}),
)
}
const totalColumnsCount = spans
.map((v) => Number(v) || 1)
.reduce((a, b) => a + b, 0)
const node = editor.schema.nodes.grid.createChecked(
{
'data-cols': totalColumnsCount,
'data-from-breakpoint': fromBreakpoint,
},
columnNodes,
)
if (dispatch) {
const offset = tr.selection.anchor + 1
if (![null, undefined].includes(coordinates?.from)) {
tr.replaceRangeWith(
coordinates.from,
coordinates.to,
node,
)
.scrollIntoView()
.setSelection(
TextSelection.near(
tr.doc.resolve(coordinates.from),
),
)
} else {
tr.replaceSelectionWith(node)
.scrollIntoView()
.setSelection(
TextSelection.near(tr.doc.resolve(offset)),
)
}
}
return true
},
}
},
})
@@ -0,0 +1,23 @@
import Image from '@tiptap/extension-image'
export default Image.extend({
addAttributes() {
return {
...this.parent?.(),
id: {
default: null,
parseHTML: (element) => element.getAttribute('data-id'),
renderHTML: (attributes) => {
if (!attributes.id) {
return {}
}
return {
'data-id': attributes.id,
}
},
},
}
},
})
@@ -0,0 +1,44 @@
import { Node, mergeAttributes } from '@tiptap/core'
export default Node.create({
name: 'lead',
group: 'block',
content: 'block+',
addOptions() {
return {
HTMLAttributes: {
class: 'lead',
},
}
},
parseHTML() {
return [
{
tag: 'div',
getAttrs: (element) => element.classList.contains('lead'),
},
]
},
renderHTML({ HTMLAttributes }) {
return [
'div',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
]
},
addCommands() {
return {
toggleLead:
() =>
({ commands }) => {
return commands.toggleWrap(this.name)
},
}
},
})
@@ -0,0 +1,317 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
const dispatchFormEvent = (editorView, name, detail = {}) => {
editorView.dom.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
detail,
}),
)
}
const validateFiles = ({
files,
acceptedTypes,
acceptedTypesValidationMessage,
maxSize,
maxSizeValidationMessage,
}) => {
for (const file of files) {
if (acceptedTypes && !acceptedTypes.includes(file.type)) {
return acceptedTypesValidationMessage
}
if (maxSize && file.size > +maxSize * 1024) {
return maxSizeValidationMessage
}
}
return null
}
const LocalFilesPlugin = ({
editor,
acceptedTypes,
acceptedTypesValidationMessage,
get$WireUsing,
key,
maxSize,
maxSizeValidationMessage,
statePath,
uploadingMessage,
}) => {
const getFileAttachmentUrl = (fileKey) =>
get$WireUsing().callSchemaComponentMethod(
key,
'getUploadedFileAttachmentTemporaryUrl',
{
attachment: fileKey,
},
)
return new Plugin({
key: new PluginKey('localFiles'),
props: {
handleDrop(editorView, event) {
if (!event.dataTransfer?.files.length) {
return false
}
const files = Array.from(event.dataTransfer.files)
const validationMessage = validateFiles({
files,
acceptedTypes,
acceptedTypesValidationMessage,
maxSize,
maxSizeValidationMessage,
})
if (validationMessage) {
editorView.dom.dispatchEvent(
new CustomEvent('rich-editor-file-validation-message', {
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
validationMessage,
},
}),
)
return false
}
if (!files.length) {
return false
}
dispatchFormEvent(editorView, 'form-processing-started', {
message: uploadingMessage,
})
event.preventDefault()
event.stopPropagation()
const position = editorView.posAtCoords({
left: event.clientX,
top: event.clientY,
})
files.forEach((file, fileIndex) => {
editor.setEditable(false)
editorView.dom.dispatchEvent(
new CustomEvent('rich-editor-uploading-file', {
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
},
}),
)
let fileKey = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
(c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (c / 4)))
).toString(16),
)
get$WireUsing().upload(
`componentFileAttachments.${statePath}.${fileKey}`,
file,
() => {
getFileAttachmentUrl(fileKey).then((url) => {
if (!url) {
return
}
editor
.chain()
.insertContentAt(position?.pos ?? 0, {
type: 'image',
attrs: {
id: fileKey,
src: url,
},
})
.run()
editor.setEditable(true)
editorView.dom.dispatchEvent(
new CustomEvent(
'rich-editor-uploaded-file',
{
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
},
},
),
)
if (fileIndex === files.length - 1) {
dispatchFormEvent(
editorView,
'form-processing-finished',
)
}
})
},
)
})
return true
},
handlePaste(editorView, event) {
if (!event.clipboardData?.files.length) {
return false
}
if (event.clipboardData?.getData('text').length) {
return false
}
const files = Array.from(event.clipboardData.files)
const validationMessage = validateFiles({
files,
acceptedTypes,
acceptedTypesValidationMessage,
maxSize,
maxSizeValidationMessage,
})
if (validationMessage) {
editorView.dom.dispatchEvent(
new CustomEvent('rich-editor-file-validation-message', {
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
validationMessage,
},
}),
)
return false
}
if (!files.length) {
return false
}
event.preventDefault()
event.stopPropagation()
dispatchFormEvent(editorView, 'form-processing-started', {
message: uploadingMessage,
})
files.forEach((file, fileIndex) => {
editor.setEditable(false)
editorView.dom.dispatchEvent(
new CustomEvent('rich-editor-uploading-file', {
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
},
}),
)
let fileKey = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
(c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (c / 4)))
).toString(16),
)
get$WireUsing().upload(
`componentFileAttachments.${statePath}.${fileKey}`,
file,
() => {
getFileAttachmentUrl(fileKey).then((url) => {
if (!url) {
return
}
editor
.chain()
.insertContentAt(
editor.state.selection.anchor,
{
type: 'image',
attrs: {
id: fileKey,
src: url,
},
},
)
.run()
editor.setEditable(true)
editorView.dom.dispatchEvent(
new CustomEvent(
'rich-editor-uploaded-file',
{
bubbles: true,
detail: {
key,
livewireId: get$WireUsing().id,
},
},
),
)
if (fileIndex === files.length - 1) {
dispatchFormEvent(
editorView,
'form-processing-finished',
)
}
})
},
)
})
return true
},
},
})
}
export default Extension.create({
name: 'localFiles',
addOptions() {
return {
acceptedTypes: [],
acceptedTypesValidationMessage: null,
key: null,
maxSize: null,
maxSizeValidationMessage: null,
statePath: null,
uploadingMessage: null,
get$WireUsing: null,
}
},
addProseMirrorPlugins() {
return [
LocalFilesPlugin({
editor: this.editor,
...this.options,
}),
]
},
})
@@ -0,0 +1,279 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import Suggestion from '@tiptap/suggestion'
const getSuggestionOptions = function ({
editor: tiptapEditor,
overrideSuggestionOptions,
extensionName,
}) {
const pluginKey = new PluginKey()
return {
editor: tiptapEditor,
char: '{{',
pluginKey,
command: ({ editor, range, props }) => {
// increase range.to by one when the next node is of type "text"
// and starts with a space character
const nodeAfter = editor.view.state.selection.$to.nodeAfter
const overrideSpace = nodeAfter?.text?.startsWith(' ')
if (overrideSpace) {
range.to += 1
}
editor
.chain()
.focus()
.insertContentAt(range, [
{
type: extensionName,
attrs: { ...props },
},
{
type: 'text',
text: ' ',
},
])
.run()
// get reference to `window` object from editor element, to support cross-frame JS usage
editor.view.dom.ownerDocument.defaultView
?.getSelection()
?.collapseToEnd()
},
allow: ({ state, range }) => {
const $from = state.doc.resolve(range.from)
const type = state.schema.nodes[extensionName]
const allow = !!$from.parent.type.contentMatch.matchType(type)
return allow
},
...overrideSuggestionOptions,
}
}
export default Node.create({
name: 'mergeTag',
priority: 101,
addStorage() {
return {
mergeTags: [],
suggestions: [],
getSuggestionFromChar: () => null,
}
},
addOptions() {
return {
HTMLAttributes: {},
renderText({ node }) {
return `{{ ${this.mergeTags[node.attrs.id]} }}`
},
deleteTriggerWithBackspace: false,
renderHTML({ options, node }) {
return [
'span',
mergeAttributes(
this.HTMLAttributes,
options.HTMLAttributes,
),
`${this.mergeTags[node.attrs.id]}`,
]
},
suggestions: [],
suggestion: {},
}
},
group: 'inline',
inline: true,
selectable: false,
atom: true,
addAttributes() {
return {
id: {
default: null,
parseHTML: (element) => element.getAttribute('data-id'),
renderHTML: (attributes) => {
if (!attributes.id) {
return {}
}
return {
'data-id': attributes.id,
}
},
},
}
},
parseHTML() {
return [
{
tag: `span[data-type="${this.name}"]`,
},
]
},
renderHTML({ node, HTMLAttributes }) {
// We cannot use the `this.storage` property here because, when accessed this method,
// it returns the initial value of the extension storage
const suggestion =
this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar(
'{{',
)
const mergedOptions = { ...this.options }
mergedOptions.HTMLAttributes = mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
)
const html = this.options.renderHTML({
options: mergedOptions,
node,
suggestion,
})
if (typeof html === 'string') {
return [
'span',
mergeAttributes(
{ 'data-type': this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
html,
]
}
return html
},
renderText({ node }) {
const args = {
options: this.options,
node,
suggestion:
this.editor?.extensionStorage?.[
this.name
]?.getSuggestionFromChar('{{'),
}
return this.options.renderText(args)
},
addKeyboardShortcuts() {
return {
Backspace: () =>
this.editor.commands.command(({ tr, state }) => {
let isMergeTag = false
const { selection } = state
const { empty, anchor } = selection
if (!empty) {
return false
}
// Store node and position for later use
let mergeTagNode = new ProseMirrorNode()
let mergeTagPos = 0
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
if (node.type.name === this.name) {
isMergeTag = true
mergeTagNode = node
mergeTagPos = pos
return false
}
})
if (isMergeTag) {
tr.insertText(
this.options.deleteTriggerWithBackspace ? '' : '{{',
mergeTagPos,
mergeTagPos + mergeTagNode.nodeSize,
)
}
return isMergeTag
}),
}
},
addProseMirrorPlugins() {
return [
...this.storage.suggestions.map(Suggestion), // Create a plugin for each suggestion configuration
new Plugin({
props: {
handleDrop(view, event) {
if (!event) {
return false
}
event.preventDefault()
if (!event.dataTransfer.getData('mergeTag')) {
return false
}
const mergeTagId =
event.dataTransfer.getData('mergeTag')
view.dispatch(
view.state.tr.insert(
view.posAtCoords({
left: event.clientX,
top: event.clientY,
}).pos,
view.state.schema.nodes.mergeTag.create({
id: mergeTagId,
}),
),
)
return false
},
},
}),
]
},
onBeforeCreate() {
this.storage.suggestions = (
this.options.suggestions.length
? this.options.suggestions
: [this.options.suggestion]
).map((suggestion) =>
getSuggestionOptions({
editor: this.editor,
overrideSuggestionOptions: suggestion,
extensionName: this.name,
}),
)
this.storage.getSuggestionFromChar = (char) => {
const suggestion = this.storage.suggestions.find(
(s) => s.char === char,
)
if (suggestion) {
return suggestion
}
if (this.storage.suggestions.length) {
return this.storage.suggestions[0]
}
return null
}
},
})
@@ -0,0 +1,37 @@
import { Mark } from '@tiptap/core'
export default Mark.create({
name: 'small',
parseHTML() {
return [
{
tag: 'small',
},
]
},
renderHTML({ HTMLAttributes }) {
return ['small', HTMLAttributes, 0]
},
addCommands() {
return {
setSmall:
() =>
({ commands }) => {
return commands.setMark(this.name)
},
toggleSmall:
() =>
({ commands }) => {
return commands.toggleMark(this.name)
},
unsetSmall:
() =>
({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
})
@@ -0,0 +1,78 @@
import { Mark } from '@tiptap/core'
export default Mark.create({
name: 'textColor',
addOptions() {
return {
textColors: {},
}
},
parseHTML() {
return [
{
tag: 'span',
getAttrs: (element) => element.classList?.contains('color'),
},
]
},
renderHTML({ HTMLAttributes }) {
const attrs = { ...HTMLAttributes }
const existingClass = HTMLAttributes.class
attrs.class = ['color', existingClass].filter(Boolean).join(' ')
const colorName = HTMLAttributes['data-color']
const colors = this.options.textColors || {}
const config = colors[colorName]
const hasColorName =
typeof colorName === 'string' && colorName.length > 0
const cssVars = config
? `--color: ${config.color}; --dark-color: ${config.darkColor}`
: hasColorName
? `--color: ${colorName}; --dark-color: ${colorName}`
: null
if (cssVars) {
const existingStyle =
typeof HTMLAttributes.style === 'string'
? HTMLAttributes.style
: ''
attrs.style = existingStyle
? `${cssVars}; ${existingStyle}`
: cssVars
}
return ['span', attrs, 0]
},
addAttributes() {
return {
'data-color': {
default: null,
parseHTML: (element) => element.getAttribute('data-color'),
renderHTML: (attributes) => {
if (!attributes['data-color']) return {}
return { 'data-color': attributes['data-color'] }
},
},
}
},
addCommands() {
return {
setTextColor:
({ color }) =>
({ commands }) => {
return commands.setMark(this.name, { 'data-color': color })
},
unsetTextColor:
() =>
({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
})
@@ -0,0 +1,197 @@
import { Dropcursor, Gapcursor, UndoRedo } from '@tiptap/extensions'
import Blockquote from '@tiptap/extension-blockquote'
import Bold from '@tiptap/extension-bold'
import Code from '@tiptap/extension-code'
import CodeBlock from '@tiptap/extension-code-block'
import CustomBlock from './extension-custom-block.js'
import {
Details,
DetailsSummary,
DetailsContent,
} from '@tiptap/extension-details'
import Document from '@tiptap/extension-document'
import Grid from './extension-grid.js'
import GridColumn from './extension-grid-column.js'
import HardBreak from '@tiptap/extension-hard-break'
import Heading from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import HorizontalRule from '@tiptap/extension-horizontal-rule'
import Italic from '@tiptap/extension-italic'
import Image from './extension-image.js'
import Lead from './extension-lead.js'
import Link from '@tiptap/extension-link'
import { BulletList, ListItem, OrderedList } from '@tiptap/extension-list'
import LocalFiles from './extension-local-files.js'
import MergeTag from './extension-merge-tag.js'
import Paragraph from '@tiptap/extension-paragraph'
import Placeholder from '@tiptap/extension-placeholder'
import Small from './extension-small.js'
import TextColor from './extension-text-color.js'
import Strike from '@tiptap/extension-strike'
import Subscript from '@tiptap/extension-subscript'
import Superscript from '@tiptap/extension-superscript'
import { TableKit } from '@tiptap/extension-table'
import Text from '@tiptap/extension-text'
import TextAlign from '@tiptap/extension-text-align'
import Underline from '@tiptap/extension-underline'
import getMergeTagSuggestion from './merge-tag-suggestion.js'
export default async ({
acceptedFileTypes,
acceptedFileTypesValidationMessage,
canAttachFiles,
customExtensionUrls,
deleteCustomBlockButtonIconHtml,
editCustomBlockButtonIconHtml,
editCustomBlockUsing,
insertCustomBlockUsing,
key,
maxFileSize,
maxFileSizeValidationMessage,
mergeTags,
noMergeTagSearchResultsMessage,
placeholder,
statePath,
textColors,
uploadingFileMessage,
$wire,
}) => {
const extensions = [
Blockquote,
Bold,
BulletList,
Code,
CodeBlock,
CustomBlock.configure({
deleteCustomBlockButtonIconHtml,
editCustomBlockButtonIconHtml,
editCustomBlockUsing,
insertCustomBlockUsing,
}),
Details,
DetailsSummary,
DetailsContent,
Document,
Dropcursor,
Gapcursor,
Grid,
GridColumn,
HardBreak,
Heading,
Highlight,
HorizontalRule,
Italic,
Image.configure({
inline: true,
}),
Lead,
Link.configure({
autolink: true,
openOnClick: false,
}),
ListItem,
...(canAttachFiles
? [
LocalFiles.configure({
acceptedTypes: acceptedFileTypes,
acceptedTypesValidationMessage:
acceptedFileTypesValidationMessage,
get$WireUsing: () => $wire,
key,
maxSize: maxFileSize,
maxSizeValidationMessage: maxFileSizeValidationMessage,
statePath,
uploadingMessage: uploadingFileMessage,
}),
]
: []),
...(Object.keys(mergeTags).length
? [
MergeTag.configure({
deleteTriggerWithBackspace: true,
suggestion: getMergeTagSuggestion({
mergeTags,
noMergeTagSearchResultsMessage,
}),
mergeTags,
}),
]
: []),
OrderedList,
Paragraph,
Placeholder.configure({
placeholder,
}),
TextColor.configure({
textColors,
}),
Small,
Strike,
Subscript,
Superscript,
TableKit.configure({
table: {
resizable: true,
},
}),
Text,
TextAlign.configure({
types: ['heading', 'paragraph'],
alignments: ['start', 'center', 'end', 'justify'],
defaultAlignment: 'start',
}),
Underline,
UndoRedo,
]
const loadedCustomExtensions = await Promise.all(
customExtensionUrls.map(async (url) => {
const absoluteUrlRegExp = new RegExp('^(?:[a-z+]+:)?//', 'i')
if (!absoluteUrlRegExp.test(url)) {
url = new URL(url, document.baseURI).href
}
try {
const factoryOrInstance = (await import(url)).default
return typeof factoryOrInstance === 'function'
? factoryOrInstance()
: factoryOrInstance
} catch (error) {
console.error(
`Failed to load rich editor custom extension from [${url}]:`,
error,
)
return null
}
}),
)
for (let customExtension of loadedCustomExtensions) {
if (!customExtension || !customExtension.name) {
continue
}
const existingIndex = extensions.findIndex(
(extension) => extension.name === customExtension.name,
)
if (
customExtension.name === 'placeholder' &&
customExtension.parent === null
) {
customExtension = Placeholder.configure(customExtension.options)
}
if (existingIndex !== -1) {
extensions[existingIndex] = customExtension
} else {
extensions.push(customExtension)
}
}
return extensions
}
@@ -0,0 +1,222 @@
import { computePosition, flip, shift } from '@floating-ui/dom'
const updatePosition = (editor, element) => {
const referenceElement = {
getBoundingClientRect: () => {
const { from, to } = editor.state.selection
const start = editor.view.coordsAtPos(from)
const end = editor.view.coordsAtPos(to)
return {
top: Math.min(start.top, end.top),
bottom: Math.max(start.bottom, end.bottom),
left: Math.min(start.left, end.left),
right: Math.max(start.right, end.right),
width: Math.abs(end.right - start.left),
height: Math.abs(end.bottom - start.top),
x: Math.min(start.left, end.left),
y: Math.min(start.top, end.top),
}
},
}
computePosition(referenceElement, element, {
placement: 'bottom-start',
strategy: 'absolute',
middleware: [shift(), flip()],
}).then(({ x, y, strategy }) => {
element.style.width = 'max-content'
element.style.position = strategy
element.style.left = `${x}px`
element.style.top = `${y}px`
})
}
export default ({ mergeTags, noMergeTagSearchResultsMessage }) => ({
items: ({ query }) => {
return Object.entries(mergeTags)
.filter(
([id, label]) =>
id
.toLowerCase()
.replace(/\s/g, '')
.includes(query.toLowerCase()) ||
label
.toLowerCase()
.replace(/\s/g, '')
.includes(query.toLowerCase()),
)
.map(([id, label]) => ({ id, label }))
},
render: () => {
let element
let selectedIndex = 0
let currentProps = null
const createDropdown = () => {
const dropdown = document.createElement('div')
dropdown.className = 'fi-dropdown-panel fi-dropdown-list'
return dropdown
}
const renderItems = () => {
if (!element || !currentProps) return
const items = currentProps.items || []
// Clear existing items
element.innerHTML = ''
if (items.length) {
items.forEach((item, index) => {
const button = document.createElement('button')
button.className = `fi-dropdown-list-item fi-dropdown-list-item-label ${index === selectedIndex ? 'fi-selected' : ''}`
button.textContent = item.label
button.type = 'button'
button.addEventListener('click', () => selectItem(index))
element.appendChild(button)
})
} else {
const noSearchResultsMessage = document.createElement('div')
noSearchResultsMessage.className = 'fi-dropdown-header'
noSearchResultsMessage.textContent =
noMergeTagSearchResultsMessage
element.appendChild(noSearchResultsMessage)
}
}
const selectItem = (index) => {
if (!currentProps) return
const items = currentProps.items || []
const item = items[index]
if (item) {
currentProps.command({ id: item.id })
}
}
const scrollToSelected = () => {
if (!element || !currentProps || currentProps.items.length === 0)
return
const selectedButton = element.children[selectedIndex]
if (selectedButton) {
const rect = selectedButton.getBoundingClientRect()
const containerRect = element.getBoundingClientRect()
if (
rect.top < containerRect.top ||
rect.bottom > containerRect.bottom
) {
selectedButton.scrollIntoView({ block: 'nearest' })
}
}
}
const upHandler = () => {
if (!currentProps) return
const items = currentProps.items || []
if (items.length === 0) return
selectedIndex = (selectedIndex + items.length - 1) % items.length
renderItems()
scrollToSelected()
}
const downHandler = () => {
if (!currentProps) return
const items = currentProps.items || []
if (items.length === 0) return
selectedIndex = (selectedIndex + 1) % items.length
renderItems()
scrollToSelected()
}
const enterHandler = () => {
selectItem(selectedIndex)
}
return {
onStart: (props) => {
// Store current props
currentProps = props
// Reset selected index when items change
selectedIndex = 0
// Create dropdown element
element = createDropdown()
element.style.position = 'absolute'
// Render initial items
renderItems()
// Append to DOM
document.body.appendChild(element)
if (!props.clientRect) {
return
}
updatePosition(props.editor, element)
},
onUpdate: (props) => {
// Store current props
currentProps = props
// Reset selected index when items change
selectedIndex = 0
// Update dropdown items
renderItems()
scrollToSelected()
if (!props.clientRect) {
return
}
updatePosition(props.editor, element)
},
onKeyDown: (props) => {
if (props.event.key === 'Escape') {
if (element && element.parentNode) {
element.parentNode.removeChild(element)
}
return true
}
if (props.event.key === 'ArrowUp') {
upHandler()
return true
}
if (props.event.key === 'ArrowDown') {
downHandler()
return true
}
if (props.event.key === 'Enter') {
enterHandler()
return true
}
return false
},
onExit: () => {
if (element && element.parentNode) {
element.parentNode.removeChild(element)
}
},
}
},
})
@@ -0,0 +1,96 @@
import { Select } from '../../../../support/resources/js/utilities/select.js'
export default function selectFormComponent({
canOptionLabelsWrap,
canSelectPlaceholder,
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
initialOptionLabel,
initialOptionLabels,
initialState,
isAutofocused,
isDisabled,
isMultiple,
isSearchable,
hasDynamicOptions,
hasDynamicSearchResults,
livewireId,
loadingMessage,
maxItems,
maxItemsMessage,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
position,
searchDebounce,
searchingMessage,
searchPrompt,
searchableOptionFields,
state,
statePath,
}) {
return {
select: null,
state,
init() {
this.select = new Select({
element: this.$refs.select,
options,
placeholder,
state: this.state,
canOptionLabelsWrap,
canSelectPlaceholder,
initialOptionLabel,
initialOptionLabels,
initialState,
isHtmlAllowed,
isAutofocused,
isDisabled,
isMultiple,
isSearchable,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
hasDynamicOptions,
hasDynamicSearchResults,
searchPrompt,
searchDebounce,
loadingMessage,
searchingMessage,
noSearchResultsMessage,
maxItems,
maxItemsMessage,
optionsLimit,
position,
searchableOptionFields,
livewireId,
statePath,
onStateChange: (newState) => {
this.state = newState
},
})
this.$watch('state', (newState) => {
if (this.select && this.select.state !== newState) {
this.select.state = newState
this.select.updateSelectedDisplay()
this.select.renderOptions()
}
})
},
destroy() {
if (this.select) {
this.select.destroy()
this.select = null
}
},
}
}
@@ -0,0 +1,85 @@
import noUiSlider from 'nouislider'
export default function sliderFormComponent({
arePipsStepped,
behavior,
decimalPlaces,
fillTrack,
isDisabled,
isRtl,
isVertical,
maxDifference,
minDifference,
maxValue,
minValue,
nonLinearPoints,
pipsDensity,
pipsFilter,
pipsFormatter,
pipsMode,
pipsValues,
rangePadding,
state,
step,
tooltips,
}) {
return {
state,
slider: null,
init() {
this.slider = noUiSlider.create(this.$el, {
behaviour: behavior,
direction: isRtl ? 'rtl' : 'ltr',
connect: fillTrack,
format: {
from: (value) => +value,
to: (value) =>
decimalPlaces !== null
? +value.toFixed(decimalPlaces)
: value,
},
limit: maxDifference,
margin: minDifference,
orientation: isVertical ? 'vertical' : 'horizontal',
padding: rangePadding,
pips: pipsMode
? {
density: pipsDensity ?? 10,
filter: pipsFilter,
format: pipsFormatter,
mode: pipsMode,
stepped: arePipsStepped,
values: pipsValues,
}
: null,
range: {
min: minValue,
...(nonLinearPoints ?? {}),
max: maxValue,
},
start: Alpine.raw(this.state),
step,
tooltips,
})
if (isDisabled) {
this.slider.disable()
}
this.slider.on('change', (values) => {
this.state = values.length > 1 ? values : values[0]
})
this.$watch('state', () => {
this.slider.set(Alpine.raw(this.state))
})
},
destroy() {
this.slider.destroy()
this.slider = null
},
}
}
@@ -0,0 +1,72 @@
export default function tagsInputFormComponent({ state, splitKeys }) {
return {
newTag: '',
state,
createTag() {
this.newTag = this.newTag.trim()
if (this.newTag === '') {
return
}
if (this.state.includes(this.newTag)) {
this.newTag = ''
return
}
this.state.push(this.newTag)
this.newTag = ''
},
deleteTag(tagToDelete) {
this.state = this.state.filter((tag) => tag !== tagToDelete)
},
reorderTags(event) {
const reordered = this.state.splice(event.oldIndex, 1)[0]
this.state.splice(event.newIndex, 0, reordered)
this.state = [...this.state]
},
input: {
['x-on:blur']: 'createTag()',
['x-model']: 'newTag',
['x-on:keydown'](event) {
if (['Enter', ...splitKeys].includes(event.key)) {
event.preventDefault()
event.stopPropagation()
this.createTag()
}
},
['x-on:paste']() {
this.$nextTick(() => {
if (splitKeys.length === 0) {
this.createTag()
return
}
const pattern = splitKeys
.map((key) =>
key.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'),
)
.join('|')
this.newTag
.split(new RegExp(pattern, 'g'))
.forEach((tag) => {
this.newTag = tag
this.createTag()
})
})
},
},
}
}
@@ -0,0 +1,57 @@
export default function textareaFormComponent({
initialHeight,
shouldAutosize,
state,
}) {
return {
state,
wrapperEl: null,
init() {
this.wrapperEl = this.$el.parentNode
this.setInitialHeight()
if (shouldAutosize) {
this.$watch('state', () => {
this.resize()
})
} else {
this.setUpResizeObserver()
}
},
setInitialHeight() {
if (this.$el.scrollHeight <= 0) {
return
}
this.wrapperEl.style.height = initialHeight + 'rem'
},
resize() {
this.setInitialHeight()
if (this.$el.scrollHeight <= 0) {
return
}
const newHeight = this.$el.scrollHeight + 'px'
if (this.wrapperEl.style.height === newHeight) {
return
}
this.wrapperEl.style.height = newHeight
},
setUpResizeObserver() {
const observer = new ResizeObserver(() => {
this.wrapperEl.style.height = this.$el.style.height
})
observer.observe(this.$el)
},
}
}
@@ -0,0 +1,7 @@
import 'easymde/dist/easymde.min.css'
import 'cropperjs/dist/cropper.min.css'
import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css'
import 'nouislider/dist/nouislider.css'
@@ -0,0 +1,442 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'ድገም',
],
'add' => [
'label' => 'ወደ :label ጨምር',
'modal' => [
'heading' => 'ወደ :label ጨምር',
'actions' => [
'add' => [
'label' => 'ጨምር',
],
],
],
],
'add_between' => [
'label' => 'በብሎኮች መካከል አስገባ',
'modal' => [
'heading' => 'ወደ :label ጨምር',
'actions' => [
'add' => [
'label' => 'ጨምር',
],
],
],
],
'delete' => [
'label' => 'አጥፋ',
],
'edit' => [
'label' => 'አድስ',
'modal' => [
'heading' => 'የማደሻ ክልል',
'actions' => [
'save' => [
'label' => 'ለውጦችን አኑር',
],
],
],
],
'reorder' => [
'label' => 'አንቀሳቅስ',
],
'move_down' => [
'label' => 'ወደ ታች አንቀሳቅስ',
],
'move_up' => [
'label' => 'ወደ ላይ አንቀሳቅስ',
],
'collapse' => [
'label' => 'ሰብስብ',
],
'expand' => [
'label' => 'ዘርጋ',
],
'collapse_all' => [
'label' => 'ሁሉንም ሰብስብ',
],
'expand_all' => [
'label' => 'ሁሉንም ዘርጋ',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'ሁሉንም አለመምረጥ',
],
'select_all' => [
'label' => 'ሁሉንም መምረጥ',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'ይቅር',
],
'drag_crop' => [
'label' => 'Drag mode "crop"',
],
'drag_move' => [
'label' => 'Drag mode "move"',
],
'flip_horizontal' => [
'label' => 'ምስሉን በአግድም ገልብጥ',
],
'flip_vertical' => [
'label' => '',
],
'move_down' => [
'label' => 'ምስሉን በአቀባዊ ገልብጥ',
],
'move_left' => [
'label' => 'ምስል ወደ ግራ ውሰድ',
],
'move_right' => [
'label' => 'ምስል ወደ ቀኝ ውሰድ',
],
'move_up' => [
'label' => 'ምስል ወደ ላይ ውሰድ',
],
'reset' => [
'label' => 'ዳግም አስጀምር',
],
'rotate_left' => [
'label' => 'ምስሉን ወደ ግራ አሽከርክር',
],
'rotate_right' => [
'label' => 'ምስሉን ወደ ቀኝ አሽከርክር',
],
'set_aspect_ratio' => [
'label' => 'ምስሉን ወደ :ratio መጥን',
],
'save' => [
'label' => 'አስቀምጥ',
],
'zoom_100' => [
'label' => 'ምስል ወደ 100% አሳድግ',
],
'zoom_in' => [
'label' => 'አሳንስ',
],
'zoom_out' => [
'label' => 'አሳድግ',
],
],
'fields' => [
'height' => [
'label' => 'ቁመት',
'unit' => 'px',
],
'rotation' => [
'label' => 'አዙር',
'unit' => 'ድግሪ',
],
'width' => [
'label' => 'ስፋት',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'ምጥጥነ ገጽታ',
'no_fixed' => [
'label' => 'ነጻ',
],
],
'svg' => [
'messages' => [
'confirmation' => 'SVG ፋይሎችን ማስተካከል አይመከርም ምክንያቱም የጥራት መጥፋት ሊያስከትል ስለሚችል ነው።\\n እርግጠኛ ነዎት መቀጠል ይፈልጋሉ?',
'disabled' => 'የ SVG ፋይሎችን ማስተካከል አክቲቭ አይደለም። ምክንያቱም የጥራት መጥፋትን ሊያስከትል ስለሚችል ነው።',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'ረድፍ ለመጨመር',
],
'delete' => [
'label' => 'ረድፍ አጥፋ',
],
'reorder' => [
'label' => 'ረድፍን እንደገና ደርድር',
],
],
'fields' => [
'key' => [
'label' => 'ስም',
],
'value' => [
'label' => 'ዋጋ',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'ፋይል ለማጣመር',
'blockquote' => 'ጥቅስ',
'bold' => 'ለማጉላት',
'bullet_list' => 'በነጥብ ዝርዝር',
'code_block' => 'Code block',
'heading' => 'ርዕስ',
'italic' => 'Italic',
'link' => 'ማስፈንጠርያ',
'ordered_list' => 'በቁጥር ዝርዝር',
'redo' => 'እንደገና',
'strike' => 'በላዩላይ ለማስመር',
'table' => 'ሰንጠረዝ',
'undo' => 'ይቅር(ወደኋላ)',
],
],
'radio' => [
'boolean' => [
'true' => 'አዎ',
'false' => 'ኣይ',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'ወደ :label ጨምር',
],
'add_between' => [
'label' => 'መካከል አስገባ',
],
'delete' => [
'label' => 'አጥፋ',
],
'clone' => [
'label' => 'ቅዳ',
],
'reorder' => [
'label' => 'አንቀሳቅስ',
],
'move_down' => [
'label' => 'ወደ ታች አንቀሳቅስ',
],
'move_up' => [
'label' => 'ወደ ላይ አንቀሳቅስ',
],
'collapse' => [
'label' => 'ሰብስብ',
],
'expand' => [
'label' => 'ዘርጋ',
],
'collapse_all' => [
'label' => 'ሁሉንም ሰብስብ',
],
'expand_all' => [
'label' => 'ሁሉንም ዘርጋ',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'አገናኝ',
'unlink' => 'አለያይ',
],
'label' => 'URL',
'placeholder' => 'URL አስገባ',
],
],
'tools' => [
'attach_files' => 'ፋይሎችን ያያይዙ',
'blockquote' => 'Blockquote',
'bold' => 'ለማጉላት',
'bullet_list' => 'በነጥብ ዝርዝር',
'code_block' => 'Code block',
'h1' => 'ዋና ርዕስ',
'h2' => 'ርዕስ',
'h3' => 'ንኡስ ርዕስ',
'italic' => 'Italic',
'link' => 'ማስፈንጠርያ',
'ordered_list' => 'በቁጥር ዝርዝር',
'redo' => 'እንደገና',
'strike' => 'በላዩላይ ለማስመር',
'underline' => 'ለማስመር',
'undo' => 'ይቅር(ወደኋላ)',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'መዝግብ',
'actions' => [
'create' => [
'label' => 'መዝግብ',
],
'create_another' => [
'label' => 'መዝግብና ሌላ ምዝገባ ጀምር',
],
],
],
],
'edit_option' => [
'modal' => [
'heading' => 'አድስ',
'actions' => [
'save' => [
'label' => 'አስቀምጥ',
],
],
],
],
],
'boolean' => [
'true' => 'አዎ',
'false' => 'ኣይ',
],
'loading_message' => 'በመጫን ላይ...',
'max_items_message' => 'መምረጥ የምቻለው :count ብቻ ነው።',
'no_search_results_message' => 'ከፍለጋዎ ጋር የሚዛመዱ አማራጮች የሉም።',
'placeholder' => 'ከአማራጮች ይምረጡ',
'searching_message' => 'በመፈለግ ላይ...',
'search_prompt' => 'ለመፈለግ መተየብ ይጀምሩ...',
],
'tags_input' => [
'placeholder' => 'አዲስ መለያ',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'የይለፍ ቃል ደብቅ',
],
'show_password' => [
'label' => 'የይለፍ ቃል አሳይ',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'አዎ',
'false' => 'ኣይ',
],
],
];
@@ -0,0 +1,9 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'በ:attribute መስክ ቢያንስ አንድ መመረጥ አለበት።',
'only_one_must_be_selected' => 'በ:attribute መስክ አንድ ብቻ ነው መመረጥ አለበት።',
],
];
@@ -0,0 +1,658 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'نسخ',
],
'add' => [
'label' => 'إضافة إلى :label',
'modal' => [
'heading' => 'إضافة إلى :label',
'actions' => [
'add' => [
'label' => 'إضافة',
],
],
],
],
'add_between' => [
'label' => 'إدراج بين الوحدات',
'modal' => [
'heading' => 'إضافة إلى :label',
'actions' => [
'add' => [
'label' => 'إضافة',
],
],
],
],
'delete' => [
'label' => 'حذف',
],
'edit' => [
'label' => 'تعديل',
'modal' => [
'heading' => 'تعديل القسم',
'actions' => [
'save' => [
'label' => 'حفظ التغييرات',
],
],
],
],
'reorder' => [
'label' => 'نقل',
],
'move_down' => [
'label' => 'تحريك لأسفل',
],
'move_up' => [
'label' => 'تحريك لأعلى',
],
'collapse' => [
'label' => 'طيّ',
],
'expand' => [
'label' => 'توسيع',
],
'collapse_all' => [
'label' => 'طيّ الكل',
],
'expand_all' => [
'label' => 'توسيع الكل',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'إلغاء تحديد الكل',
],
'select_all' => [
'label' => 'تحديد الكل',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'إلغاء',
],
'drag_crop' => [
'label' => 'وضع السحب "قص"',
],
'drag_move' => [
'label' => 'وضع السحب "تحريك"',
],
'flip_horizontal' => [
'label' => 'قلب الصورة أفقياً',
],
'flip_vertical' => [
'label' => 'قلب الصورة عمودياً',
],
'move_down' => [
'label' => 'تحريك الصورة لأسفل',
],
'move_left' => [
'label' => 'تحريك الصورة لليسار',
],
'move_right' => [
'label' => 'تحريك الصورة لليمين',
],
'move_up' => [
'label' => 'تحريك الصورة لأعلى',
],
'reset' => [
'label' => 'استعادة',
],
'rotate_left' => [
'label' => 'تدوير الصورة لليسار',
],
'rotate_right' => [
'label' => 'تدوير الصورة لليمين',
],
'set_aspect_ratio' => [
'label' => 'تعيين نسبة العرض للإرتفاع إلى :ratio',
],
'save' => [
'label' => 'حفظ',
],
'zoom_100' => [
'label' => 'تحجيم الصورة إلى 100%',
],
'zoom_in' => [
'label' => 'تكبير',
],
'zoom_out' => [
'label' => 'تصغير',
],
],
'fields' => [
'height' => [
'label' => 'الارتفاع',
'unit' => 'px',
],
'rotation' => [
'label' => 'الدوران',
'unit' => 'deg',
],
'width' => [
'label' => 'العرض',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'نسبة الأبعاد',
'no_fixed' => [
'label' => 'حر',
],
],
'svg' => [
'messages' => [
'confirmation' => 'لا يوصى بتحرير ملفات SVG لأنه قد يؤدي إلى فقدان الجودة عند تغيير الحجم.\n هل أنت متأكد من رغبتك في المتابعة؟',
'disabled' => 'تم تعطيل تحرير ملفات SVG لأنه قد يؤدي إلى فقدان الجودة عند تغيير الحجم.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'إضافة صف',
],
'delete' => [
'label' => 'حذف صف',
],
'reorder' => [
'label' => 'إعادة ترتيب الصف',
],
],
'fields' => [
'key' => [
'label' => 'المفتاح',
],
'value' => [
'label' => 'القيمة',
],
],
],
'markdown_editor' => [
'file_attachments_accepted_file_types_message' => 'يجب أن تكون الملفات المرفوعة من نوع: :values.',
'file_attachments_max_size_message' => 'يجب ألا يتجاوز حجم الملفات المرفوعة :max كيلوبايت.',
'tools' => [
'attach_files' => 'إرفاق ملفات',
'blockquote' => 'اقتباس',
'bold' => 'عريض',
'bullet_list' => 'قائمة نقطية',
'code_block' => 'نص برمجي',
'heading' => 'العناوين',
'italic' => 'مائل',
'link' => 'رابط تشعبي',
'ordered_list' => 'قائمة رقمية',
'redo' => 'إعادة',
'strike' => 'يتوسطه خط',
'table' => 'جدول',
'undo' => 'تراجع',
],
],
'modal_table_select' => [
'actions' => [
'select' => [
'label' => 'تحديد',
'actions' => [
'select' => [
'label' => 'تحديد',
],
],
],
],
],
'radio' => [
'boolean' => [
'true' => 'نعم',
'false' => 'لا',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'إضافة إلى :label',
],
'add_between' => [
'label' => 'إدراج بين',
],
'delete' => [
'label' => 'حذف',
],
'clone' => [
'label' => 'نسخ',
],
'reorder' => [
'label' => 'نقل',
],
'move_down' => [
'label' => 'تحريك لأسفل',
],
'move_up' => [
'label' => 'تحريك لأعلى',
],
'collapse' => [
'label' => 'طيّ',
],
'expand' => [
'label' => 'توسيع',
],
'collapse_all' => [
'label' => 'طيّ الكل',
],
'expand_all' => [
'label' => 'توسيع الكل',
],
],
],
'rich_editor' => [
'actions' => [
'attach_files' => [
'label' => 'رفع ملف',
'modal' => [
'heading' => 'رفع ملف',
'form' => [
'file' => [
'label' => [
'new' => 'الملف',
'existing' => 'استبدال الملف',
],
],
'alt' => [
'label' => [
'new' => 'النص البديل',
'existing' => 'تعديل النص البديل',
],
],
],
],
],
'custom_block' => [
'modal' => [
'actions' => [
'insert' => [
'label' => 'إدراج',
],
'save' => [
'label' => 'حفظ',
],
],
],
],
'link' => [
'label' => 'تعديل',
'modal' => [
'heading' => 'رابط',
'form' => [
'url' => [
'label' => 'الرابط',
],
'should_open_in_new_tab' => [
'label' => 'فتح في تبويب جديد',
],
],
],
],
'text_color' => [
'label' => 'لون النص',
'modal' => [
'heading' => 'لون النص',
'form' => [
'color' => [
'label' => 'اللون',
],
'custom_color' => [
'label' => 'لون مخصص',
],
],
],
],
],
'file_attachments_accepted_file_types_message' => 'يجب أن تكون الملفات المرفوعة من نوع: :values.',
'file_attachments_max_size_message' => 'يجب ألا يتجاوز حجم الملفات المرفوعة :max كيلوبايت.',
'no_merge_tag_search_results_message' => 'لا توجد نتائج لوسوم الدمج.',
'tools' => [
'align_center' => 'محاذاة للوسط',
'align_end' => 'محاذاة للنهاية',
'align_justify' => 'محاذاة للضبط',
'align_start' => 'محاذاة للبداية',
'attach_files' => 'إرفاق ملفات',
'blockquote' => 'إقتباس',
'bold' => 'عريض',
'bullet_list' => 'قائمة نقطية',
'clear_formatting' => 'مسح التنسيق',
'code' => 'كود',
'code_block' => 'نص برمجي',
'custom_blocks' => 'الكتل المخصصة',
'details' => 'التفاصيل',
'h1' => 'عنوان',
'h2' => 'عنوان رئيسي',
'h3' => 'عنوان فرعي',
'highlight' => 'تظليل',
'horizontal_rule' => 'خط أفقي',
'italic' => 'مائل',
'lead' => 'نص بارز',
'link' => 'رابط تشعبي',
'merge_tags' => 'حقول الدمج',
'ordered_list' => 'قائمة رقمية',
'redo' => 'إعادة',
'small' => 'نص صغير',
'strike' => 'خط في المنتصف',
'subscript' => 'نص سفلي',
'superscript' => 'نص علوي',
'table' => 'جدول',
'table_delete' => 'حذف الجدول',
'table_add_column_before' => 'إضافة عمود قبل',
'table_add_column_after' => 'إضافة عمود بعد',
'table_delete_column' => 'حذف العمود',
'table_add_row_before' => 'إضافة صف قبل',
'table_add_row_after' => 'إضافة صف بعد',
'table_delete_row' => 'حذف الصف',
'table_merge_cells' => 'دمج الخلايا',
'table_split_cell' => 'فصل الخلايا',
'table_toggle_header_row' => 'إظهار/إخفاء الترويسة',
'text_color' => 'لون النص',
'underline' => 'خط اسفل النص',
'undo' => 'تراجع',
],
'uploading_file_message' => 'جاري رفع الملف...',
],
'select' => [
'actions' => [
'create_option' => [
'label' => 'إضافة',
'modal' => [
'heading' => 'إضافة',
'actions' => [
'create' => [
'label' => 'إضافة',
],
'create_another' => [
'label' => 'إضافة وبدء إضافة المزيد',
],
],
],
],
'edit_option' => [
'label' => 'تعديل',
'modal' => [
'heading' => 'تحرير',
'actions' => [
'save' => [
'label' => 'حفظ',
],
],
],
],
],
'boolean' => [
'true' => 'نعم',
'false' => 'لا',
],
'loading_message' => 'تحميل...',
'max_items_message' => 'يمكنك اختيار :count فقط.',
'no_search_results_message' => 'لا توجد خيارات تطابق بحثك.',
'placeholder' => 'اختر',
'searching_message' => 'جاري البحث...',
'search_prompt' => 'ابدأ بالكتابة للبحث...',
],
'tags_input' => [
'placeholder' => 'كلمة مفتاحية جديدة',
],
'text_input' => [
'actions' => [
'copy' => [
'label' => 'نسخ',
'message' => 'تم النسخ',
],
'hide_password' => [
'label' => 'إخفاء كلمة المرور',
],
'show_password' => [
'label' => 'عرض كلمة المرور',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'نعم',
'false' => 'لا',
],
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'يجب تحديد حقل :attribute واحد على الأقل.',
'only_one_must_be_selected' => 'يجب تحديد حقل :attribute واحد فقط.',
],
];
@@ -0,0 +1,495 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Klonla',
],
'add' => [
'label' => ':label əlavə et',
'modal' => [
'heading' => ':label əlavə et',
'actions' => [
'add' => [
'label' => 'əlavə et',
],
],
],
],
'add_between' => [
'label' => 'Bloklar arasına əlavə et',
'modal' => [
'heading' => ':label əlavə et',
'actions' => [
'add' => [
'label' => 'əlavə et',
],
],
],
],
'delete' => [
'label' => 'Sil',
],
'edit' => [
'label' => 'Dəyişdir',
'modal' => [
'heading' => 'Bloku redaktə et',
'actions' => [
'save' => [
'label' => 'Dəyişiklikləri yadda saxla',
],
],
],
],
'reorder' => [
'label' => 'Sırala',
],
'move_down' => [
'label' => 'Aşağı hərəkət etdir',
],
'move_up' => [
'label' => 'Yuxarı hərəkət etdir',
],
'collapse' => [
'label' => 'Kiçilt',
],
'expand' => [
'label' => 'Genişlət',
],
'collapse_all' => [
'label' => 'Hamısını kiçilt',
],
'expand_all' => [
'label' => 'Hamısını genişlət',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Heçbirini seçmə',
],
'select_all' => [
'label' => 'Hamısını seç',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'İmtina',
],
'drag_crop' => [
'label' => 'Kəsmə moduna sürüşdürün',
],
'drag_move' => [
'label' => 'Daşıma moduna sürüşdürün',
],
'flip_horizontal' => [
'label' => 'Şəkili üfüqi çevir',
],
'flip_vertical' => [
'label' => 'Şəkili şaquli çevir',
],
'move_down' => [
'label' => 'Şəkili aşağı hərəkət etdir',
],
'move_left' => [
'label' => 'Şəkili sola hərəkət etdir',
],
'move_right' => [
'label' => 'Şəkili sağa hərəkət etdir',
],
'move_up' => [
'label' => 'Şəkili yuxarı hərəkət etdir',
],
'reset' => [
'label' => 'Sıfırla',
],
'rotate_left' => [
'label' => 'Şəkili sola çevir',
],
'rotate_right' => [
'label' => 'Şəkili sağa çevir',
],
'set_aspect_ratio' => [
'label' => 'En uzun nisbətini :ratio et',
],
'save' => [
'label' => 'Yadda saxla',
],
'zoom_100' => [
'label' => 'Şəkili %100 yaxınlaşdır',
],
'zoom_in' => [
'label' => 'Yaxınlaşdır',
],
'zoom_out' => [
'label' => 'Uzaqlaşdır',
],
],
'fields' => [
'height' => [
'label' => 'Yüksəklik',
'unit' => 'px',
],
'rotation' => [
'label' => 'Çevirmə',
'unit' => '°',
],
'width' => [
'label' => 'Genişlik',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'En uzun nisbətləri',
'no_fixed' => [
'label' => 'Sərbəst',
],
],
'svg' => [
'messages' => [
'confirmation' => 'SVG fayllarını redaktə etmək tövsiyə edilmir, çünki miqyaslandırılarkən keyfiyyət itkisinə səbəb ola bilər.\n Davam etmək istədiyinizə əminsiniz.',
'disabled' => 'SVG fayllarını redaktə etmək deaktiv edilib, çünki miqyaslandırılarkən keyfiyyət itkisinə səbəb ola bilər.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Sətir əlavə et',
],
'delete' => [
'label' => 'Sətir sil',
],
'reorder' => [
'label' => 'Sətir sırala',
],
],
'fields' => [
'key' => [
'label' => 'Açar',
],
'value' => [
'label' => 'Dəyər',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'Fayl əlavə et',
'blockquote' => 'Sitat',
'bold' => 'Qalın',
'bullet_list' => 'List',
'code_block' => 'Kod bloku',
'heading' => 'Başlıq',
'italic' => 'Əyik',
'link' => 'Keçid',
'ordered_list' => 'Nömrəli list',
'redo' => 'Təkrarla',
'strike' => 'Üstü xətli',
'table' => 'Cədvəl',
'undo' => 'Geri al',
],
],
'radio' => [
'boolean' => [
'true' => 'Bəli',
'false' => 'Xeyr',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => ':label\'e əlavə et',
],
'add_between' => [
'label' => 'Arasına daxil et',
],
'delete' => [
'label' => 'Sil',
],
'clone' => [
'label' => 'Klonla',
],
'reorder' => [
'label' => 'Sırala',
],
'move_down' => [
'label' => 'Aşağı hərəkət etdir',
],
'move_up' => [
'label' => 'Yuxarı hərəkət etdir',
],
'collapse' => [
'label' => 'Kiçilt',
],
'expand' => [
'label' => 'Genişlət',
],
'collapse_all' => [
'label' => 'Hamısını kiçilt',
],
'expand_all' => [
'label' => 'Hamısını genişlət',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'Keçid',
'unlink' => 'Keçidi yığışdır',
],
'label' => 'URL',
'placeholder' => 'Bir URL daxil edin',
],
],
'tools' => [
'attach_files' => 'Fayl əlavə et',
'blockquote' => 'Sitat',
'bold' => 'Qalın',
'bullet_list' => 'Sırasız list',
'code_block' => 'Kod bloku',
'h1' => 'Başlıq',
'h2' => 'Başlıq 2',
'h3' => 'Alt başlıq',
'italic' => 'Əyik',
'link' => 'Keçid',
'ordered_list' => 'Sıralı list',
'redo' => 'Təkrarla',
'strike' => 'Üstü xətli',
'underline' => 'Altı xətli',
'undo' => 'Geri al',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'Yarat',
'actions' => [
'create' => [
'label' => 'Yarat',
],
'create_another' => [
'label' => 'Yarat & başqasını yarat',
],
],
],
],
'edit_option' => [
'modal' => [
'heading' => 'Redaktə et',
'actions' => [
'save' => [
'label' => 'Yadda saxla',
],
],
],
],
],
'boolean' => [
'true' => 'Hə',
'false' => 'Yox',
],
'loading_message' => 'Yüklənir...',
'max_items_message' => 'Sadəcə :count seçiləbilər.',
'no_search_results_message' => 'Axtarışa uyğun seçim yoxdur.',
'placeholder' => 'Bir seçim seçin',
'searching_message' => 'Axtarılır...',
'search_prompt' => 'Axtarmaq üçün yazmağa başlayın...',
],
'tags_input' => [
'placeholder' => 'Yeni etiket',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'Şifrəni gizlət',
],
'show_password' => [
'label' => 'Şifrəni göstər',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Bəli',
'false' => 'Xeyr',
],
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'Ən azı bir :attribute sahəsi seçilməlidir.',
'only_one_must_be_selected' => 'Yalnız bir :attribute sahəsi seçilməlidir',
],
];
@@ -0,0 +1,400 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Клонирай',
],
'add' => [
'label' => 'Добави към :label',
],
'add_between' => [
'label' => 'Вмъкни между блоковете',
],
'delete' => [
'label' => 'Изтриване',
],
'reorder' => [
'label' => 'Преместване',
],
'move_down' => [
'label' => 'Преместване надолу',
],
'move_up' => [
'label' => 'Преместване нагоре',
],
'collapse' => [
'label' => 'Свиване',
],
'expand' => [
'label' => 'Разширяване',
],
'collapse_all' => [
'label' => 'Свиване на всички',
],
'expand_all' => [
'label' => 'Разширяване на всички',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Отмаркирай всички',
],
'select_all' => [
'label' => 'Избери всички',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'Отказ',
],
'drag_crop' => [
'label' => 'Влачене режим "изрязване"',
],
'drag_move' => [
'label' => 'Влачене режим "преместване"',
],
'flip_horizontal' => [
'label' => 'Обърни изображението хоризонтално',
],
'flip_vertical' => [
'label' => 'Обърни изображението вертикално',
],
'move_down' => [
'label' => 'Преместване надолу',
],
'move_left' => [
'label' => 'Преместване наляво',
],
'move_right' => [
'label' => 'Преместване надясно',
],
'move_up' => [
'label' => 'Преместване нагоре',
],
'reset' => [
'label' => 'Нулиране',
],
'rotate_left' => [
'label' => 'Завъртане на изображението наляво',
],
'rotate_right' => [
'label' => 'Завъртане на изображението надясно',
],
'set_aspect_ratio' => [
'label' => 'Задай съотношение на страните на :ratio',
],
'save' => [
'label' => 'Запазване',
],
'zoom_100' => [
'label' => 'Увеличение 100%',
],
'zoom_in' => [
'label' => 'Увеличаване',
],
'zoom_out' => [
'label' => 'Намаляне',
],
],
'fields' => [
'height' => [
'label' => 'Височина',
'unit' => 'px',
],
'rotation' => [
'label' => 'Ротация',
'unit' => 'deg',
],
'width' => [
'label' => 'Ширина',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'Съотношение на страните',
'no_fixed' => [
'label' => 'Нефиксирано',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Добави ред',
],
'delete' => [
'label' => 'Изтрий ред',
],
'reorder' => [
'label' => 'Пренареди редове',
],
],
'fields' => [
'key' => [
'label' => 'Ключ',
],
'value' => [
'label' => 'Стойност',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'Прикачи файлове',
'blockquote' => 'Цитат',
'bold' => 'Удебелен текст',
'bullet_list' => 'Списък с точки',
'code_block' => 'Код',
'heading' => 'Заглавие',
'italic' => 'Курсив',
'link' => 'Връзка',
'ordered_list' => 'Номериран списък',
'redo' => 'Повтори',
'strike' => 'Зачертан текст',
'table' => 'Таблица',
'undo' => 'Отмени',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'Добави към :label',
],
'delete' => [
'label' => 'Изтриване',
],
'clone' => [
'label' => 'Клониране',
],
'reorder' => [
'label' => 'Преместване',
],
'move_down' => [
'label' => 'Преместване надолу',
],
'move_up' => [
'label' => 'Преместване нагоре',
],
'collapse' => [
'label' => 'Свиване',
],
'expand' => [
'label' => 'Разширяване',
],
'collapse_all' => [
'label' => 'Свиване на всички',
],
'expand_all' => [
'label' => 'Разширяване на всички',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'Добави връзка',
'unlink' => 'Премахни връзка',
],
'label' => 'URL',
'placeholder' => 'Въведи URL',
],
],
'tools' => [
'attach_files' => 'Прикачи файлове',
'blockquote' => 'Цитат',
'bold' => 'Удебелен текст',
'bullet_list' => 'Списък с точки',
'code_block' => 'Код',
'h1' => 'Заглавие',
'h2' => 'Подзаглавие',
'h3' => 'Под-подзаглавие',
'italic' => 'Курсив',
'link' => 'Връзка',
'ordered_list' => 'Номериран списък',
'redo' => 'Повтори',
'strike' => 'Зачертан текст',
'underline' => 'Подчертан текст',
'undo' => 'Отмени',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'Създаване на опция',
'actions' => [
'create' => [
'label' => 'Създаване',
],
'create_another' => [
'label' => 'Създаване и добавяне на друга',
],
],
],
],
'edit_option' => [
'modal' => [
'heading' => 'Редакция',
'actions' => [
'save' => [
'label' => 'Запазване',
],
],
],
],
],
'boolean' => [
'true' => 'Да',
'false' => 'Не',
],
'loading_message' => 'Зареждане...',
'max_items_message' => 'Само :count могат да бъдат избрани.',
'no_search_results_message' => 'Няма намерени резултати.',
'placeholder' => 'Избери опция',
'searching_message' => 'Търсене...',
'search_prompt' => 'Търсене...',
],
'tags_input' => [
'placeholder' => 'Нов таг',
],
];
@@ -0,0 +1,255 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'অনুলিপি করুন',
],
'add' => [
'label' => ':label এ যোগ করুন',
],
'add_between' => [
'label' => 'প্রবেশ করান',
],
'delete' => [
'label' => 'মুছে ফেলুন',
],
'reorder' => [
'label' => 'সরান',
],
'move_down' => [
'label' => 'নিচে সরান',
],
'move_up' => [
'label' => 'উপরে সরান',
],
'collapse' => [
'label' => 'ছোট করুন',
],
'expand' => [
'label' => 'বড় করুন',
],
'collapse_all' => [
'label' => 'সব ছোট করুন',
],
'expand_all' => [
'label' => 'সব বড় করুন',
],
],
],
'checkbox_list' => [
'actions' => [
'select_all' => [
'label' => 'সব নির্বাচিত করুন',
],
'deselect_all' => [
'label' => 'সব অনির্বাচিত করুন',
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'সারি যোগ করুন',
],
'delete' => [
'label' => 'সারি মুছে ফেলুন',
],
'reorder' => [
'label' => 'সারি পুনর্বিন্যাস করুন',
],
],
'fields' => [
'key' => [
'label' => 'চাবি',
],
'value' => [
'label' => 'মান',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'নথি যোগ করুন',
'bold' => 'বোল্ড',
'bullet_list' => 'বুলেট তালিকা',
'code_block' => 'কোড ব্লক',
'edit' => 'সম্পাদন',
'italic' => 'তির্যক',
'link' => 'লিংক',
'ordered_list' => 'সংখ্যাযুক্ত তালিকা',
'preview' => 'পূর্বরূপ',
'strike' => 'স্ট্রাইকথ্রু',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => ':label এ যোগ করুন',
],
'delete' => [
'label' => 'মুছে ফেলুন',
],
'clone' => [
'label' => 'অনুলিপি করুন',
],
'reorder' => [
'label' => 'সরান',
],
'move_down' => [
'label' => 'নিচে সরান',
],
'move_up' => [
'label' => 'উপরে সরান',
],
'collapse' => [
'label' => 'ছোট করুন',
],
'expand' => [
'label' => 'বড় করুন',
],
'collapse_all' => [
'label' => 'সব ছোট করুন',
],
'expand_all' => [
'label' => 'সব বড় করুন',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'লিংক',
'unlink' => 'আনলিংক',
],
'label' => 'ইউআরএল',
'placeholder' => 'ইউআরএল দিন',
],
],
'tools' => [
'attach_files' => 'নথি যোগ করুন',
'blockquote' => 'ব্লককোট',
'bold' => 'বোল্ড',
'bullet_list' => 'বুলেট তালিকা',
'code_block' => 'কোড ব্লক',
'h1' => 'শিরোনাম',
'h2' => 'শিরোনাম',
'h3' => 'উপশিরোনাম',
'italic' => 'তির্যক',
'link' => 'লিংক',
'ordered_list' => 'সংখ্যাযুক্ত তালিকা',
'redo' => 'পরবর্তী',
'strike' => 'স্ট্রাইকথ্রু',
'undo' => 'পূর্বাবস্থা',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'তৈরি করুন',
'actions' => [
'create' => [
'label' => 'তৈরি করুন',
],
],
],
],
],
'boolean' => [
'true' => 'হ্যাঁ',
'false' => 'না',
],
'loading_message' => 'লোড হচ্ছে...',
'max_items_message' => 'মাত্র :count টা নির্বাচন করা যাবে।',
'no_search_results_message' => 'খুঁজে পাওয়া যায় নি।',
'placeholder' => 'নির্বাচন করুন',
'searching_message' => 'খুঁজুন...',
'search_prompt' => 'লিখুন...',
],
'tags_input' => [
'placeholder' => 'নতুন ট্যাগ',
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'অন্তত একটি :attribute ক্ষেত্র নির্বাচিত হতে হবে।',
'only_one_must_be_selected' => 'শুধুমাত্র একটি :attribute ক্ষেত্র নির্বাচিত হতে হবে।',
],
];
@@ -0,0 +1,255 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Kloniraj',
],
'add' => [
'label' => 'Dodaj :label',
],
'add_between' => [
'label' => 'Ubaci',
],
'delete' => [
'label' => 'Izbriši',
],
'reorder' => [
'label' => 'Pomjeri',
],
'move_down' => [
'label' => 'Dolje',
],
'move_up' => [
'label' => 'Gore',
],
'collapse' => [
'label' => 'Sažimanje',
],
'expand' => [
'label' => 'Proširivanje',
],
'collapse_all' => [
'label' => 'Sažmi sve',
],
'expand_all' => [
'label' => 'Proširi sve',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Odznači sve',
],
'select_all' => [
'label' => 'Označi sve',
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Dodaj red',
],
'delete' => [
'label' => 'Izbriši red',
],
'reorder' => [
'label' => 'Preuredi red',
],
],
'fields' => [
'key' => [
'label' => 'Ključ',
],
'value' => [
'label' => 'Vrijednost',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'Priloži fajlove',
'bold' => 'Bold',
'bullet_list' => 'Bullet list',
'code_block' => 'Blok koda',
'edit' => 'Uredi',
'italic' => 'Kurziv',
'link' => 'Link',
'ordered_list' => 'Numerisana lista',
'preview' => 'Prethodan pregled',
'strike' => 'Precrtano',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'Dodaj :label',
],
'delete' => [
'label' => 'Izbriši',
],
'clone' => [
'label' => 'Kloniraj',
],
'reorder' => [
'label' => 'Pomjeriti',
],
'move_down' => [
'label' => 'Dolje',
],
'move_up' => [
'label' => 'Gore',
],
'collapse' => [
'label' => 'Sažimanje',
],
'expand' => [
'label' => 'Proširivanje',
],
'collapse_all' => [
'label' => 'Sažmi sve',
],
'expand_all' => [
'label' => 'Proširi sve',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'Link',
'unlink' => 'Otkačite link',
],
'label' => 'URL',
'placeholder' => 'Unesite URL',
],
],
'tools' => [
'attach_files' => 'Priloži fajlove',
'blockquote' => 'Blok citat',
'bold' => 'Bold',
'bullet_list' => 'Bullet list',
'code_block' => 'Blok koda',
'h1' => 'Titula',
'h2' => 'Naslov',
'h3' => 'Podnaslov',
'italic' => 'Kurziv',
'link' => 'Link',
'ordered_list' => 'Numerisana lista',
'redo' => 'Ponovo uradite',
'strike' => 'Precrtano',
'undo' => 'Poništi',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'Napravi',
'actions' => [
'create' => [
'label' => 'Napravi',
],
],
],
],
],
'boolean' => [
'true' => 'Da',
'false' => 'Ne',
],
'loading_message' => 'Učitavanje ...',
'max_items_message' => 'Mogu se odabrati samo :count.',
'no_search_results_message' => 'Nijedna opcija ne odgovara vašoj pretrazi.',
'placeholder' => 'Izaberi opciju',
'searching_message' => 'Traženje ...',
'search_prompt' => 'Počni da kucate da biste pretraživali ...',
],
'tags_input' => [
'placeholder' => 'Nova oznaka',
],
];
@@ -0,0 +1,497 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Clonar',
],
'add' => [
'label' => 'Afegir a :label',
'modal' => [
'heading' => 'Afegeix a :label',
'actions' => [
'add' => [
'label' => 'Afegeix',
],
],
],
],
'add_between' => [
'label' => 'Inserir entre blocs',
'modal' => [
'heading' => 'Afegir a :label',
'actions' => [
'add' => [
'label' => 'Afegir',
],
],
],
],
'edit' => [
'label' => 'Edita',
'modal' => [
'heading' => 'Edita bloc',
'actions' => [
'save' => [
'label' => 'Desa els canvis',
],
],
],
],
'delete' => [
'label' => 'Esborrar',
],
'reorder' => [
'label' => 'Moure',
],
'move_down' => [
'label' => 'Moure cap avall',
],
'move_up' => [
'label' => 'Moure cap amunt',
],
'collapse' => [
'label' => 'Replegar',
],
'expand' => [
'label' => 'Ampliar',
],
'collapse_all' => [
'label' => 'Replegar tots',
],
'expand_all' => [
'label' => 'Ampliar tots',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Desseleccionar tots',
],
'select_all' => [
'label' => 'Seleccionar tots',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'Cancel·lar',
],
'drag_crop' => [
'label' => 'Mode d\'arrossegament "retallar"',
],
'drag_move' => [
'label' => 'Mode d\'arrossegament "moure"',
],
'flip_horizontal' => [
'label' => 'Girar imatge horitzontalment',
],
'flip_vertical' => [
'label' => 'Girar imatge verticalment',
],
'move_down' => [
'label' => 'Mou la imatge cap avall',
],
'move_left' => [
'label' => 'Mou la imatge cap a l\'esquerra',
],
'move_right' => [
'label' => 'Mou la imatge cap a la dreta',
],
'move_up' => [
'label' => 'Mou la imatge cap amunt',
],
'reset' => [
'label' => 'Restablir',
],
'rotate_left' => [
'label' => 'Rota la imatge cap a l\'esquerra',
],
'rotate_right' => [
'label' => 'Rota la imatge cap a la dreta',
],
'set_aspect_ratio' => [
'label' => 'Estableix la relació d\'aspecte a :ratio',
],
'save' => [
'label' => 'Desar',
],
'zoom_100' => [
'label' => 'Amplia la imatge a 100%',
],
'zoom_in' => [
'label' => 'Ampliar el zoom',
],
'zoom_out' => [
'label' => 'Reduir el zoom',
],
],
'fields' => [
'height' => [
'label' => 'Altura',
'unit' => 'px',
],
'rotation' => [
'label' => 'Rotació',
'unit' => 'graus',
],
'width' => [
'label' => 'Amplada',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'Relacions d\'aspecte',
'no_fixed' => [
'label' => 'Lliure',
],
],
'svg' => [
'messages' => [
'confirmation' => 'No es recomana editar fitxers SVG ja que pot provocar una pèrdua de qualitat en escalar-los.\n Esteu segur que voleu continuar?',
'disabled' => 'L\'edició de fitxers SVG està desactivada ja que pot provocar una pèrdua de qualitat en escalar-los.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Afegir fila',
],
'delete' => [
'label' => 'Esborrar fila',
],
'reorder' => [
'label' => 'Reordenar fila',
],
],
'fields' => [
'key' => [
'label' => 'Clau',
],
'value' => [
'label' => 'Valor',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'Adjuntar fitxers',
'blockquote' => 'Cita de bloc',
'bold' => 'Negreta',
'bullet_list' => 'Llista de vinyetes',
'code_block' => 'Bloc de codi',
'heading' => 'Encapçalament',
'italic' => 'Cursiva',
'link' => 'Enllaç',
'ordered_list' => 'Llista numerada',
'redo' => 'Refer',
'strike' => 'Ratllat',
'table' => 'Taula',
'undo' => 'Desfer',
],
],
'radio' => [
'boolean' => [
'true' => 'Sí',
'false' => 'No',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'Afegir a :label',
],
'add_between' => [
'label' => 'Inserir entre',
],
'delete' => [
'label' => 'Esborrar',
],
'clone' => [
'label' => 'Clonar',
],
'reorder' => [
'label' => 'Moure',
],
'move_down' => [
'label' => 'Moure cap avall',
],
'move_up' => [
'label' => 'Moure cap amunt',
],
'collapse' => [
'label' => 'Replegar',
],
'expand' => [
'label' => 'Ampliar',
],
'collapse_all' => [
'label' => 'Replegar tots',
],
'expand_all' => [
'label' => 'Ampliar tots',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'actions' => [
'link' => 'Enllaç',
'unlink' => 'Elimina l\'enllaç',
],
'label' => 'URL',
'placeholder' => 'Escriu una adreça URL',
],
],
'tools' => [
'attach_files' => 'Adjuntar fitxers',
'blockquote' => 'Bloc de cita',
'bold' => 'Negreta',
'bullet_list' => 'Llista de vinyetes',
'code_block' => 'Bloc de codi',
'h1' => 'Títol',
'h2' => 'Capçalera',
'h3' => 'Subtítol',
'italic' => 'Cursiva',
'link' => 'Enllaç',
'ordered_list' => 'Llista numerada',
'redo' => 'Refer',
'strike' => 'Ratllat',
'underline' => 'Subratllat',
'undo' => 'Desfer',
],
],
'select' => [
'actions' => [
'create_option' => [
'modal' => [
'heading' => 'Nou',
'actions' => [
'create' => [
'label' => 'Crear',
],
'create_another' => [
'label' => 'Crear i crear-ne un altre',
],
],
],
],
'edit_option' => [
'modal' => [
'heading' => 'Editar',
'actions' => [
'save' => [
'label' => 'Desar',
],
],
],
],
],
'boolean' => [
'true' => 'Sí',
'false' => 'No',
],
'loading_message' => 'Carregant...',
'max_items_message' => 'Només :count poden ser seleccionats.',
'no_search_results_message' => 'No s\'ha trobat cap opció que coincideixi amb la vostra cerca.',
'placeholder' => 'Trieu una opció',
'searching_message' => 'Cercant...',
'search_prompt' => 'Comenceu a escriure per cercar...',
],
'tags_input' => [
'placeholder' => 'Nova etiqueta',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'Ocultar contrasenya',
],
'show_password' => [
'label' => 'Mostrar contrasenya',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Sí',
'false' => 'No',
],
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'S\'ha de seleccionar almenys un camp :attribute.',
'only_one_must_be_selected' => 'S\'ha de seleccionar només un camp :attribute.',
],
];
@@ -0,0 +1,617 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'لەبەرگرتنەە',
],
'add' => [
'label' => 'زیادکردن بۆ :label',
'modal' => [
'heading' => 'زیادکردن بۆ :label',
'actions' => [
'add' => [
'label' => 'زیادکردن',
],
],
],
],
'add_between' => [
'label' => 'زیادکردن لەنێوان بلۆکەکان',
'modal' => [
'heading' => 'زیادکردن بۆ :label',
'actions' => [
'add' => [
'label' => 'زیادکردن',
],
],
],
],
'delete' => [
'label' => 'سڕینەوە',
],
'edit' => [
'label' => 'دەستکاریکردن',
'modal' => [
'heading' => 'دەستکاریکردنی بلۆک',
'actions' => [
'save' => [
'label' => 'پاشەکەوتکردنی بلۆکەکان',
],
],
],
],
'reorder' => [
'label' => 'جوڵاندن',
],
'move_down' => [
'label' => 'جوڵان بۆ خوارەوە',
],
'move_up' => [
'label' => 'جوڵان بۆ سەرەوە',
],
'collapse' => [
'label' => 'داخستن',
],
'expand' => [
'label' => 'کردنەوە',
],
'collapse_all' => [
'label' => 'داخستنی هەموو',
],
'expand_all' => [
'label' => 'کردنەوەی هەموو',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'هەڵنەبژاردنی هەموو',
],
'select_all' => [
'label' => 'هەڵبژاردنی هەموو',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'پاشگەزبوونەوە',
],
'drag_crop' => [
'label' => 'دۆخی ڕاکێشان "بڕین"',
],
'drag_move' => [
'label' => 'دۆخی ڕاکێشان "جوڵاندن"',
],
'flip_horizontal' => [
'label' => 'هەڵگەڕاندنەوەی وێنە بە ئاسۆیی',
],
'flip_vertical' => [
'label' => 'هەڵگەڕاندنەوەی وێنە بە ستوونی',
],
'move_down' => [
'label' => 'جوڵاندنی وێنە بۆ خوارەوە',
],
'move_left' => [
'label' => 'جوڵاندنی وێنە بۆلای چەپ',
],
'move_right' => [
'label' => 'جوڵاندنی وێنە بۆلای ڕاست',
],
'move_up' => [
'label' => 'جوڵاندنی وێنە بۆ سەرەوە',
],
'reset' => [
'label' => 'هەڵوەشاندنەوە',
],
'rotate_left' => [
'label' => 'وێنەکە بسووڕێنە بۆ لای چەپ',
],
'rotate_right' => [
'label' => 'وێنەکە بسووڕێنە بۆ لای ڕاست',
],
'set_aspect_ratio' => [
'label' => 'ڕێژەی ڕووبەر بۆ :ratio',
],
'save' => [
'label' => 'پاشەکەوتکردن',
],
'zoom_100' => [
'label' => 'نزیککردنەوەی وێنە بۆ 100%',
],
'zoom_in' => [
'label' => 'نزیککردنەوە',
],
'zoom_out' => [
'label' => 'دوورخستنەوە',
],
],
'fields' => [
'height' => [
'label' => 'بەرزی',
'unit' => 'px',
],
'rotation' => [
'label' => 'خولانەوە',
'unit' => 'deg',
],
'width' => [
'label' => 'پانی',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'ڕێژەی ڕووبەرەکان',
'no_fixed' => [
'label' => 'خۆڕایی',
],
],
'svg' => [
'messages' => [
'confirmation' => 'دەستکاریکردنی پەڕگەی SVG ڕێگەپێدراو نییە، چونکە لەوانەیە ببێتە هۆی لەدەستدانی کوالیتی کاتێک بەقەبارە دەگۆڕدرێت.\n ئایا دڵنیایت کە دەتەوێت بەردەوام بیت؟',
'disabled' => 'دەستکاریکردنی پەڕگەی SVG ناکرێت، چونکە لەوانەیە ببێتە هۆی لەدەستدانی کوالیتی کاتێک بەقەبارە دەگۆڕدرێت.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'زیادکردنی ڕیز',
],
'delete' => [
'label' => 'سڕینەوەی ڕیز',
],
'reorder' => [
'label' => 'ڕیزکردنەوەی ڕیز',
],
],
'fields' => [
'key' => [
'label' => 'کلیل',
],
'value' => [
'label' => 'بەها',
],
],
],
'markdown_editor' => [
'tools' => [
'attach_files' => 'هاوپێچکردنی پەڕگەکان',
'blockquote' => 'بلۆکی وتەی وەرگیراو',
'bold' => 'تۆخ',
'bullet_list' => 'لیستی خاڵدار',
'code_block' => 'بلۆکی کۆد',
'heading' => 'سەردێڕ',
'italic' => 'لار',
'link' => 'بەستەر',
'ordered_list' => 'لیستی ژمارەکراو',
'redo' => 'دووبارەکردنەوە',
'strike' => 'هێڵ بەسەرداهێنان',
'table' => 'خشتە',
'undo' => 'پاشەکشە',
],
],
'modal_table_select' => [
'actions' => [
'select' => [
'label' => 'هەڵبژاردن',
'actions' => [
'select' => [
'label' => 'هەڵبژاردن',
],
],
],
],
],
'radio' => [
'boolean' => [
'true' => 'بەڵێ',
'false' => 'نەخێر',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'زیادکردن بۆ :label',
],
'add_between' => [
'label' => 'دانان لەنێوان',
],
'delete' => [
'label' => 'سڕینەوە',
],
'clone' => [
'label' => 'لەبەرگرتنەوە',
],
'reorder' => [
'label' => 'جوڵاندن',
],
'move_down' => [
'label' => 'جوڵان بۆ خوارەوە',
],
'move_up' => [
'label' => 'جوڵان بۆ سەرەوە',
],
'collapse' => [
'label' => 'داخستن',
],
'expand' => [
'label' => 'کردنەوە',
],
'collapse_all' => [
'label' => 'داخستنی هەمووی',
],
'expand_all' => [
'label' => 'کردنەوەی هەمووی',
],
],
],
'rich_editor' => [
'actions' => [
'attach_files' => [
'label' => 'بارکردنی پەڕگە',
'modal' => [
'heading' => 'بارکردنی پەڕگە',
'form' => [
'file' => [
'label' => [
'new' => 'پەڕگە',
'existing' => 'شوێنگرتنەوەی پەڕگە',
],
],
'alt' => [
'label' => [
'new' => 'دەقی جێگرەوە',
'existing' => 'گۆڕینی دەقی جێگرەوە',
],
],
],
],
],
'custom_block' => [
'modal' => [
'actions' => [
'insert' => [
'label' => 'دانان',
],
'save' => [
'label' => 'پاشەکەوتکردن',
],
],
],
],
'link' => [
'label' => 'دەستکاریکردن',
'modal' => [
'heading' => 'بەستەر',
'form' => [
'url' => [
'label' => 'URL',
],
'should_open_in_new_tab' => [
'label' => 'کردنەوە لە تابێکی تردا',
],
],
],
],
],
'no_merge_tag_search_results_message' => 'هیچ ئەنجامێکی تاگی تێکەڵکردن نییە.',
'tools' => [
'align_center' => 'لاگرتنی ناوەڕاست',
'align_end' => 'لاگرتنی کۆتایی',
'align_justify' => 'لاگرتنی هاوڕێک',
'align_start' => 'لاگرتنی سەرەتا',
'attach_files' => 'هاوپێچکردنی پەڕگەکان',
'blockquote' => 'بلۆکی وتەی وەرگیراو',
'bold' => 'تۆخ',
'bullet_list' => 'لیستی خاڵدار',
'clear_formatting' => 'پاککردنەوەی شێواز',
'code_block' => 'بلۆکی کۆد',
'custom_blocks' => 'بلۆکەکان',
'details' => 'وەردەکارییەکان',
'h1' => 'ناونشان',
'h2' => 'سەردێڕ',
'h3' => 'ژێرسەردێڕ',
'highlight' => 'بەرچاوکردن',
'horizontal_rule' => 'یاسای ئاسۆیی',
'italic' => 'لار',
'lead' => 'دەقی سەرەکی',
'link' => 'بەستەر',
'merge_tags' => 'تاگەکان تێکەڵ بکە',
'ordered_list' => 'لیستی ژمارەکراو',
'redo' => 'دووبارەکردنەوە',
'small' => 'دەقی بچووک',
'strike' => 'هێڵ بەسەرداهێنان',
'subscript' => 'ژێرنووس',
'superscript' => 'سەرنووس',
'table' => 'خشتە',
'table_delete' => 'سڕینەوەی خشتە',
'table_add_column_before' => 'زیادکردنی ستوون لەپێشەوە',
'table_add_column_after' => 'زیادکردنی ستوون لەدواوە',
'table_delete_column' => 'سڕینەوەی ستوون',
'table_add_row_before' => 'زیادکردنی ڕیز لەسەرەوە',
'table_add_row_after' => 'زیادکردنی ڕیز لەخوارەوە',
'table_delete_row' => 'سڕینەوەی ڕیز',
'table_merge_cells' => 'تێکەڵکردنی خانەکان',
'table_split_cell' => 'جیاکردنەوەی خانە',
'table_toggle_header_row' => 'گۆڕینی سەرەوەی ڕیز',
'underline' => 'هێلبەژێرداهاتوو',
'undo' => 'پاشەکشە',
],
],
'select' => [
'actions' => [
'create_option' => [
'label' => 'زیادکردن',
'modal' => [
'heading' => 'زیادکردن',
'actions' => [
'create' => [
'label' => 'زیادکردن',
],
'create_another' => [
'label' => 'زیادکردن & زیادکردنی دانەیەکی تر',
],
],
],
],
'edit_option' => [
'label' => 'دەستکاریکردن',
'modal' => [
'heading' => 'دەستکاریکردن',
'actions' => [
'save' => [
'label' => 'پاشەکەوتکردن',
],
],
],
],
],
'boolean' => [
'true' => 'بەڵێ',
'false' => 'نەخێر',
],
'loading_message' => 'بارکردن...',
'max_items_message' => 'دەتوانرێت تەنها :count هەڵبژێردرێت.',
'no_search_results_message' => 'هیچ شتێک هاوشێوەی گەڕانەکەت نیە.',
'placeholder' => 'هەڵبژێرە',
'searching_message' => 'گەڕان...',
'search_prompt' => 'دەستبکە بە گەڕان...',
],
'tags_input' => [
'placeholder' => 'تاگی نوێ',
],
'text_input' => [
'actions' => [
'hide_password' => [
'label' => 'شاردنەوەی تێپەڕەوشە',
],
'show_password' => [
'label' => 'پیشاندانی تێپەڕەوشە',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'بەڵێ',
'false' => 'نەخێر',
],
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'دەبێت لانیکەم یەک :attribute هەڵبژێردرێت.',
'only_one_must_be_selected' => 'دەبێت تەنها یەک :attribute هەڵبژێردرێت.',
],
];
@@ -0,0 +1,725 @@
<?php
return [
'builder' => [
'actions' => [
'clone' => [
'label' => 'Klonovat',
],
'add' => [
'label' => 'Přidat k :label',
'modal' => [
'heading' => 'Přidat k :label',
'actions' => [
'add' => [
'label' => 'Přidat',
],
],
],
],
'add_between' => [
'label' => 'Vložit',
'modal' => [
'heading' => 'Přidat k :label',
'actions' => [
'add' => [
'label' => 'Přidat',
],
],
],
],
'edit' => [
'label' => 'Upravit',
'modal' => [
'heading' => 'Upravit blok',
'actions' => [
'save' => [
'label' => 'Uložit',
],
],
],
],
'delete' => [
'label' => 'Smazat',
],
'reorder' => [
'label' => 'Přesunout',
],
'move_down' => [
'label' => 'Posunout dolů',
],
'move_up' => [
'label' => 'Posunout nahoru',
],
'collapse' => [
'label' => 'Skrýt',
],
'expand' => [
'label' => 'Zobrazit',
],
'collapse_all' => [
'label' => 'Skrýt vše',
],
'expand_all' => [
'label' => 'Zobrazit vše',
],
],
],
'checkbox_list' => [
'actions' => [
'deselect_all' => [
'label' => 'Odznačit vše',
],
'select_all' => [
'label' => 'Vybrat vše',
],
],
],
'file_upload' => [
'editor' => [
'actions' => [
'cancel' => [
'label' => 'Zrušit',
],
'drag_crop' => [
'label' => 'Táhněte pro oříznutí',
],
'drag_move' => [
'label' => 'Táhněte pro přesun',
],
'flip_horizontal' => [
'label' => 'Překlopit obrázek horizontálně',
],
'flip_vertical' => [
'label' => 'Překlopit obrázek vertikálně',
],
'move_down' => [
'label' => 'Posunout obrázek dolů',
],
'move_left' => [
'label' => 'Posunout obrázek doleva',
],
'move_right' => [
'label' => 'Posunout obrázek doprava',
],
'move_up' => [
'label' => 'Posunout obrázek nahoru',
],
'reset' => [
'label' => 'Reset',
],
'rotate_left' => [
'label' => 'Otočit obrázek doleva',
],
'rotate_right' => [
'label' => 'Otočit obrázek doprava',
],
'set_aspect_ratio' => [
'label' => 'Nastavit poměr stran na :ratio',
],
'save' => [
'label' => 'Uložit',
],
'zoom_100' => [
'label' => 'Zvětšit obrázek na 100 %',
],
'zoom_in' => [
'label' => 'Přiblížit',
],
'zoom_out' => [
'label' => 'Oddálit',
],
],
'fields' => [
'height' => [
'label' => 'Výška',
'unit' => 'px',
],
'rotation' => [
'label' => 'Rotace',
'unit' => 'deg',
],
'width' => [
'label' => 'Šířka',
'unit' => 'px',
],
'x_position' => [
'label' => 'X',
'unit' => 'px',
],
'y_position' => [
'label' => 'Y',
'unit' => 'px',
],
],
'aspect_ratios' => [
'label' => 'Poměr stran',
'no_fixed' => [
'label' => 'Vlastní',
],
],
'svg' => [
'messages' => [
'confirmation' => 'Editace SVG souborů není doporučena, protože může dojít ke ztrátě kvality při škálování.\n Opravdu chcete pokračovat?',
'disabled' => 'Úprava souborů SVG je zakázána, protože může vést ke ztrátě kvality při škálování.',
],
],
],
],
'key_value' => [
'actions' => [
'add' => [
'label' => 'Přidat řádek',
],
'delete' => [
'label' => 'Smazat řádek',
],
'reorder' => [
'label' => 'Přesunout řádek',
],
],
'fields' => [
'key' => [
'label' => 'Klíč',
],
'value' => [
'label' => 'Hodnota',
],
],
],
'markdown_editor' => [
'file_attachments_accepted_file_types_message' => 'Nahrané soubory musí být typu: :values.',
'file_attachments_max_size_message' => 'Nahrané soubory nesmí být větší než :max kilobajtů.',
'tools' => [
'attach_files' => 'Přidat soubory',
'blockquote' => 'Bloková citace',
'bold' => 'Tučně',
'bullet_list' => 'Seznam s odrážkami',
'code_block' => 'Blok kódu',
'heading' => 'Nadpis',
'italic' => 'Kurzíva',
'link' => 'Odkaz',
'ordered_list' => 'Číslovaný seznam',
'redo' => 'Vpřed',
'strike' => 'Přeškrtnutí',
'table' => 'Tabulka',
'undo' => 'Zpět',
],
],
'modal_table_select' => [
'actions' => [
'select' => [
'label' => 'Vybrat',
'actions' => [
'select' => [
'label' => 'Vybrat',
],
],
],
],
],
'radio' => [
'boolean' => [
'true' => 'Ano',
'false' => 'Ne',
],
],
'repeater' => [
'actions' => [
'add' => [
'label' => 'Přidat k :label',
],
'add_between' => [
'label' => 'Vložit mezi',
],
'delete' => [
'label' => 'Smazat',
],
'clone' => [
'label' => 'Klonovat',
],
'reorder' => [
'label' => 'Přesunout',
],
'move_down' => [
'label' => 'Posunout dolů',
],
'move_up' => [
'label' => 'Posunout nahoru',
],
'collapse' => [
'label' => 'Skrýt',
],
'expand' => [
'label' => 'Zobrazit',
],
'collapse_all' => [
'label' => 'Skrýt vše',
],
'expand_all' => [
'label' => 'Zobrazit vše',
],
],
],
'rich_editor' => [
'actions' => [
'attach_files' => [
'label' => 'Nahrát soubor',
'modal' => [
'heading' => 'Nahrát soubor',
'form' => [
'file' => [
'label' => [
'new' => 'Soubor',
'existing' => 'Nahradit soubor',
],
],
'alt' => [
'label' => [
'new' => 'Alternativní text',
'existing' => 'Změnit alternativní text',
],
],
],
],
],
'custom_block' => [
'modal' => [
'actions' => [
'insert' => [
'label' => 'Vložit',
],
'save' => [
'label' => 'Uložit',
],
],
],
],
'grid' => [
'label' => 'Mřížka',
'modal' => [
'heading' => 'Mřížka',
'form' => [
'preset' => [
'label' => 'Přednastavení',
'placeholder' => 'Žádné',
'options' => [
'two' => 'Dvě',
'three' => 'Tři',
'four' => 'Čtyři',
'five' => 'Pět',
'two_start_third' => 'Dvě (Začátek třetí)',
'two_end_third' => 'Dvě (Konec třetí)',
'two_start_fourth' => 'Dvě (Začátek čtvrté)',
'two_end_fourth' => 'Dvě (Konec čtvrté)',
],
],
'columns' => [
'label' => 'Sloupce',
],
'from_breakpoint' => [
'label' => 'Od breakpointu',
'options' => [
'default' => 'Vše',
'sm' => 'Malý',
'md' => 'Střední',
'lg' => 'Velký',
'xl' => 'Extra velký',
'2xl' => 'Dvakrát extra velký',
],
],
'is_asymmetric' => [
'label' => 'Dva asymetrické sloupce',
],
'start_span' => [
'label' => 'Začátek rozsahu',
],
'end_span' => [
'label' => 'Konec rozsahu',
],
],
],
],
'link' => [
'label' => 'Upravit',
'modal' => [
'heading' => 'Odkaz',
'form' => [
'url' => [
'label' => 'URL',
],
'should_open_in_new_tab' => [
'label' => 'Otevřít v nové záložce',
],
],
],
],
'text_color' => [
'label' => 'Barva textu',
'modal' => [
'heading' => 'Barva textu',
'form' => [
'color' => [
'label' => 'Barva',
],
'custom_color' => [
'label' => 'Vlastní barva',
],
],
],
],
],
'file_attachments_accepted_file_types_message' => 'Nahrané soubory musí být typu: :values.',
'file_attachments_max_size_message' => 'Nahrané soubory nesmí být větší než :max kilobajtů.',
'no_merge_tag_search_results_message' => 'Žádné výsledky pro značky slučování.',
'tools' => [
'align_center' => 'Zarovnat na střed',
'align_end' => 'Zarovnat vpravo',
'align_justify' => 'Zarovnat do bloku',
'align_start' => 'Zarovnat vlevo',
'attach_files' => 'Přidat soubory',
'blockquote' => 'Bloková citace',
'bold' => 'Tučně',
'bullet_list' => 'Seznam s odrážkami',
'clear_formatting' => 'Vymazat formátování',
'code' => 'Kód',
'code_block' => 'Blok kódu',
'custom_blocks' => 'Bloky',
'details' => 'Detaily',
'h1' => 'Nadpis 1',
'h2' => 'Nadpis 2',
'h3' => 'Nadpis 3',
'grid' => 'Mřížka',
'grid_delete' => 'Smazat mřížku',
'highlight' => 'Zvýraznit',
'horizontal_rule' => 'Vodorovná čára',
'italic' => 'Kurzíva',
'lead' => 'Úvodní text',
'link' => 'Odkaz',
'merge_tags' => 'Sloučit značky',
'ordered_list' => 'Číslovaný seznam',
'redo' => 'Vpřed',
'small' => 'Malý text',
'strike' => 'Přeškrtnutí',
'subscript' => 'Dolní index',
'superscript' => 'Horní index',
'table' => 'Tabulka',
'table_delete' => 'Smazat tabulku',
'table_add_column_before' => 'Přidat sloupec před',
'table_add_column_after' => 'Přidat sloupec za',
'table_delete_column' => 'Smazat sloupec',
'table_add_row_before' => 'Přidat řádek nad',
'table_add_row_after' => 'Přidat řádek pod',
'table_delete_row' => 'Smazat řádek',
'table_merge_cells' => 'Sloučit buňky',
'table_split_cell' => 'Rozdělit buňku',
'table_toggle_header_row' => 'Přepnout řádek záhlaví',
'text_color' => 'Barva textu',
'underline' => 'Podtržení',
'undo' => 'Zpět',
],
'uploading_file_message' => 'Nahrávání souboru...',
],
'select' => [
'actions' => [
'create_option' => [
'label' => 'Vytvořit',
'modal' => [
'heading' => 'Vytvořit',
'actions' => [
'create' => [
'label' => 'Vytvořit',
],
'create_another' => [
'label' => 'Vytvořit a přidat další',
],
],
],
],
'edit_option' => [
'label' => 'Upravit',
'modal' => [
'heading' => 'Upravit',
'actions' => [
'save' => [
'label' => 'Uložit',
],
],
],
],
],
'boolean' => [
'true' => 'Ano',
'false' => 'Ne',
],
'loading_message' => 'Načítání...',
'max_items_message' => 'Lze vybrat pouze 1 položka.|Lze vybrat pouze :count položky.|Lze vybrat pouze :count položek.',
'no_search_results_message' => 'Vašemu hledání neodpovídají žádné výsledky.',
'placeholder' => 'Zvolte některou z možností',
'searching_message' => 'Hledání...',
'search_prompt' => 'Zadejte hledaný výraz...',
],
'tags_input' => [
'placeholder' => 'Nový štítek',
],
'text_input' => [
'actions' => [
'copy' => [
'label' => 'Kopírovat',
'message' => 'Zkopírováno',
],
'hide_password' => [
'label' => 'Skrýt heslo',
],
'show_password' => [
'label' => 'Zobrazit heslo',
],
],
],
'toggle_buttons' => [
'boolean' => [
'true' => 'Ano',
'false' => 'Ne',
],
],
];
@@ -0,0 +1,10 @@
<?php
return [
'distinct' => [
'must_be_selected' => 'Musí být vybráno alespoň jedno pole :attribute.',
'only_one_must_be_selected' => 'Musí být vybráno pouze jedno pole :attribute.',
],
];

Some files were not shown because too many files have changed in this diff Show More