Revert "Merge branch '2.x' of https://github.com/laravel-filament/filament into 2.x"

This reverts commit 6ed9a57e33, reversing
changes made to 2728e60ca4.
This commit is contained in:
Dan Harrin
2021-11-17 19:53:29 +00:00
parent 6ed9a57e33
commit ffed3a59d0
346 changed files with 68476 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml, yaml}]
indent_size = 2
+1
View File
@@ -0,0 +1 @@
github: [danharrin]
+74
View File
@@ -0,0 +1,74 @@
name: Bug report
description: Create a report to help us improve
labels: bug,unconfirmed
body:
- type: markdown
attributes:
value: |
Before opening a bug report, please search for the behaviour in the existing issues.
---
Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information.
- type: dropdown
id: package
attributes:
label: Package
description: Which package is this bug report for?
options:
- filament/filament
- filament/forms
- filament/tables
- filament/spatie-laravel-media-library-plugin
- filament/spatie-tags-plugin
- Other
validations:
required: true
- type: input
id: package-version
attributes:
label: Package Version
description: "Please provide the full version of your package you have installed."
placeholder: "v2.0.0"
validations:
required: true
- type: input
id: laravel-version
attributes:
label: Laravel Version
description: "Please provide the full Laravel version of your project."
placeholder: "v8.0.0"
validations:
required: true
- type: input
id: livewire-version
attributes:
label: Livewire Version
description: "Please provide the full Livewire version of your project, if applicable."
placeholder: "v2.0.0"
- type: input
id: php-version
attributes:
label: PHP Version
description: "Please provide the full PHP version of your server."
placeholder: "PHP 8.0.0"
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug description
description: What happened?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Which steps do we need to take to reproduce this error?
- type: textarea
id: logs
attributes:
label: Relevant log output
description: If applicable, provide relevant log output. No need for backticks here.
render: shell
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://github.com/laravel-filament/filament/discussions/new
about: For ideas or feature requests
- name: Support questions & other
url: https://github.com/laravel-filament/filament/discussions/new
about: If you have a question or need help installing using the package
- name: Discord
url: https://discord.gg/livewire
about: We are partnered with the Laravel Livewire Discord server. For quick help, ask questions in the Filament channel.
+43
View File
@@ -0,0 +1,43 @@
name: monorepo-split
on:
push:
tags: '*'
jobs:
provide-packages-list:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- uses: "ramsey/composer-install@v1"
- id: packages-list
run: echo "::set-output name=matrix::$(vendor/bin/monorepo-builder packages-json)"
outputs:
matrix: ${{ steps.packages-list.outputs.matrix }}
split-monorepo:
needs: provide-packages-list
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: ${{fromJson(needs.provide-packages-list.outputs.matrix)}}
steps:
- uses: actions/checkout@v2
- id: previous-tag
uses: "WyriHaximus/github-action-get-previous-tag@master"
- name: Monorepo Split of ${{ matrix.package }}
uses: symplify/github-action-monorepo-split@main
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
with:
package_directory: 'packages/${{ matrix.package }}'
repository_organization: 'laravel-filament'
repository_name: '${{ matrix.package }}'
branch: 2.x
tag: ${{ steps.previous-tag.outputs.tag }}
user_name: "Dan Harrin"
user_email: "dan@danharrin.com"
+21
View File
@@ -0,0 +1,21 @@
name: php-cs-fixer
on: push
jobs:
php-cs-fixer:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Run PHP CS Fixer
uses: docker://oskarstark/php-cs-fixer-ga
with:
args: --config=.php-cs-fixer.dist.php --allow-risky=yes
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: >
chore: styling
+38
View File
@@ -0,0 +1,38 @@
name: tests
on:
push:
pull_request:
jobs:
run-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.1, 8.0 ]
laravel: [ 8.* ]
dependency-version: [ prefer-stable ]
include:
- laravel: 8.*
testbench: 6.*
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@v2
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache/files
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, pdo, pdo_sqlite
coverage: none
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
- name: Execute tests
run: ./vendor/bin/pest --verbose
+12
View File
@@ -0,0 +1,12 @@
/.idea
/build
/coverage
/node_modules
/vendor
/.vscode
.DS_Store
.php-cs-fixer.cache
.phpunit.result.cache
composer.phar
Thumbs.db
phpunit.xml
+40
View File
@@ -0,0 +1,40 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->in([
__DIR__ . '/packages',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Ryan Scherler at ryan@eastslope.studio. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+21
View File
@@ -0,0 +1,21 @@
# MIT License
Copyright (c) Filament
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+59
View File
@@ -0,0 +1,59 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/41773797/131910226-676cb28a-332d-4162-a6a8-136a93d5a70f.png" alt="Banner" style="width: 100%; max-width: 800px;" />
</p>
<p align="center">
<a href="https://github.com/laravel-filament/filament/actions"><img alt="Tests passing" src="https://img.shields.io/badge/Tests-passing-green?style=for-the-badge&logo=github"></a>
<a href="https://laravel.com"><img alt="Laravel v8.x" src="https://img.shields.io/badge/Laravel-v8.x-FF2D20?style=for-the-badge&logo=laravel"></a>
<a href="https://laravel-livewire.com"><img alt="Livewire v2.x" src="https://img.shields.io/badge/Livewire-v2.x-FB70A9?style=for-the-badge"></a>
<a href="https://php.net"><img alt="PHP 8.0" src="https://img.shields.io/badge/PHP-8.0-777BB4?style=for-the-badge&logo=php"></a>
</p>
Filament is a collection of tools for rapidly building beautiful TALL stack interfaces, designed for humans.
## Packages
> **Packages in the same version series are compatible.** For more information, please see this [GitHub Discussion](https://github.com/laravel-filament/filament/discussions/495).
### Admin Panel • [Documentation](https://filamentadmin.com/docs/admin)
**Current version series:** v1.x. **Next version series:** v2.x, expected Winter 2021.
```bash
composer require filament/filament
```
### Form Builder • [Documentation](https://filamentadmin.com/docs/forms)
**Current version series:** v2.x.
```bash
composer require filament/forms
```
### Table Builder • [Documentation](https://filamentadmin.com/docs/tables)
**Current version series:** v2.x.
```bash
composer require filament/tables
```
### spatie/laravel-medialibrary Plugin • [Documentation](https://filamentadmin.com/docs/spatie-laravel-media-library-plugin)
**Current version series:** v2.x.
```bash
composer require filament/spatie-laravel-media-library-plugin
```
### spatie/laravel-tags Plugin • [Documentation](https://filamentadmin.com/docs/spatie-laravel-tags-plugin)
**Current version series:** v2.x.
```bash
composer require filament/spatie-laravel-tags-plugin
```
## Need Help?
🐞 If you spot a bug, please [submit a detailed issue](https://github.com/laravel-filament/filament/issues/new), and wait for assistance.
🤔 If you have a question or feature request, please [start a new discussion](https://github.com/laravel-filament/filament/discussions/new). We also have a [Discord community](https://discord.gg/cpqnMTHZja). For quick help, ask questions in the appropriate package help channel.
🔐 If you discover a vulnerability, please review our [security policy](https://github.com/laravel-filament/filament/blob/1.x/SECURITY.md).
+12
View File
@@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
| 2.x | :white_check_mark: |
## Reporting a Vulnerability
If you discover a security vulnerability within Filament, please email Dan Harrin via [dan@danharrin.com](mailto:dan@danharrin.com). All security vulnerabilities will be promptly addressed.
+13
View File
@@ -0,0 +1,13 @@
const shouldWatch = process.argv.includes('--watch')
require('esbuild').build({
define: {
'process.env.NODE_ENV': shouldWatch ? `'production'` : `'development'`,
},
entryPoints: ['packages/forms/resources/js/index.js'],
outfile: 'packages/forms/dist/module.esm.js',
bundle: true,
platform: 'neutral',
mainFields: ['module'],
watch: shouldWatch,
}).catch(() => process.exit(1))
+32
View File
@@ -0,0 +1,32 @@
{
"name": "filament/filament",
"description": "The elegant TALL stack admin panel for Laravel artisans.",
"license": "MIT",
"require": {
"php": "^8.0"
},
"require-dev": {
"laravel/ui": "^3.3",
"livewire/livewire": "^2.5",
"orchestra/testbench": "^6.0",
"pestphp/pest": "^1.17",
"pestphp/pest-plugin-parallel": "^0.3",
"phpstan/phpstan": "^0.12",
"spatie/laravel-package-tools": "^1.9",
"symplify/monorepo-builder": "^9.0"
},
"autoload-dev": {
"psr-4": {
"Filament\\Forms\\": "packages/forms/src",
"Filament\\Tests\\": "tests"
}
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "./vendor/bin/pest"
},
"minimum-stability": "dev",
"prefer-stable": true
}
Generated
+8550
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushNextDevReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\PushTagReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetCurrentMutualDependenciesReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\SetNextMutualDependenciesReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\TagVersionReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateBranchAliasReleaseWorker;
use Symplify\MonorepoBuilder\Release\ReleaseWorker\UpdateReplaceReleaseWorker;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
# Release workers - in order to execute
$services->set(UpdateReplaceReleaseWorker::class);
$services->set(SetCurrentMutualDependenciesReleaseWorker::class);
$services->set(TagVersionReleaseWorker::class);
$services->set(PushTagReleaseWorker::class);
$services->set(SetNextMutualDependenciesReleaseWorker::class);
$services->set(UpdateBranchAliasReleaseWorker::class);
$services->set(PushNextDevReleaseWorker::class);
};
+299
View File
@@ -0,0 +1,299 @@
{
"name": "filament",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@babel/runtime": "^7.15.3",
"@github/file-attachment-element": "^2.0.1",
"@github/markdown-toolbar-element": "^1.5.1",
"dayjs": "^1.10.6",
"esbuild": "^0.12.16",
"filepond": "^4.28.2",
"filepond-plugin-file-validate-size": "^2.2.4",
"filepond-plugin-file-validate-type": "^1.2.6",
"filepond-plugin-image-crop": "^2.0.6",
"filepond-plugin-image-exif-orientation": "^1.0.11",
"filepond-plugin-image-preview": "^4.6.7",
"filepond-plugin-image-resize": "^2.0.10",
"filepond-plugin-image-transform": "^3.8.6",
"imask": "^6.1.0",
"marked": "^3.0.0",
"mdhl": "^0.0.6",
"trix": "^1.3.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@github/file-attachment-element": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@github/file-attachment-element/-/file-attachment-element-2.0.1.tgz",
"integrity": "sha512-GQUgDPZeuE9xco4GbReAKFvtOFe9js+yctSrSYUSTGeD3N4xO2kX2MvaEiQRuVkERFgGPD9r57CLbbUoyJBn2Q==",
"dev": true
},
"node_modules/@github/markdown-toolbar-element": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-1.5.3.tgz",
"integrity": "sha512-fNgAuHBWWzqxMvkTMM7ijqPryLmnPVOi4OWp8YyUC74YgtJCRvQyL7gl9SzNLMXmGQxiRVIYbUJxa61X1oRObw==",
"dev": true
},
"node_modules/dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
"integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.12.29",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
}
},
"node_modules/filepond": {
"version": "4.30.3",
"resolved": "https://registry.npmjs.org/filepond/-/filepond-4.30.3.tgz",
"integrity": "sha512-G2b1LEe90Sq2vH0SYDASTB+vVU735NBctzIaFPlZtb14QAgi/AL89WyQ6LhTfqgyrMyuZur2O9yHAmzS2E9ZnA==",
"dev": true
},
"node_modules/filepond-plugin-file-validate-size": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/filepond-plugin-file-validate-size/-/filepond-plugin-file-validate-size-2.2.5.tgz",
"integrity": "sha512-x+fYZl317R17w3dExKab1oYZa10Wl2HcXKD6dzIgRKyngEHtjBF99geGaiDPk9IfozV8Zjzx69D7urcthF2CYQ==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.1.2 <5.x"
}
},
"node_modules/filepond-plugin-file-validate-type": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/filepond-plugin-file-validate-type/-/filepond-plugin-file-validate-type-1.2.6.tgz",
"integrity": "sha512-3JFxpb4o5D4BssP/Drjn2GPpejg/QXzU4e3/Pq8IvIJuokHz95b6+RPYstQ+6ITKREYup0N8F/CO/OQfhVkUgg==",
"dev": true,
"peerDependencies": {
"filepond": ">=1.x <5.x"
}
},
"node_modules/filepond-plugin-image-crop": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-crop/-/filepond-plugin-image-crop-2.0.6.tgz",
"integrity": "sha512-bSy/KND4EP/QtW8MxIj23qgSEPUVRrT8XdktJ+OU9y8xRjFjTG+qcuw1LsycARQYRtzi8cZrD3P4POOITfkywQ==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.x <5.x"
}
},
"node_modules/filepond-plugin-image-exif-orientation": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-exif-orientation/-/filepond-plugin-image-exif-orientation-1.0.11.tgz",
"integrity": "sha512-hLBc12Fk6Zkj3L8mSAn+elugHOqT5rLUbgVXQQIQjMe0FsGjtpoxqeVR6jt4IWHGat2L9sFAgU2TGmd1mqosCg==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.x <5.x"
}
},
"node_modules/filepond-plugin-image-preview": {
"version": "4.6.10",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-preview/-/filepond-plugin-image-preview-4.6.10.tgz",
"integrity": "sha512-C7Q5Sr/d4RQb2nMNQR93OiMt91OHxpvmIBGl+CO1dZE46lM49NUfI08dyElAOxTslDcA8pAfjFFCKo0sNYbrBg==",
"dev": true,
"peerDependencies": {
"filepond": ">=4.x <5.x"
}
},
"node_modules/filepond-plugin-image-resize": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-resize/-/filepond-plugin-image-resize-2.0.10.tgz",
"integrity": "sha512-irX+J275u8Ph1KcciCeSQmxeFjbu0+co5XVCkiwdSNnz6KiqrCKN7RXTvEbdgSdDzyi5omr2oP1rKWps5L1RsQ==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.x <5.x"
}
},
"node_modules/filepond-plugin-image-transform": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-transform/-/filepond-plugin-image-transform-3.8.7.tgz",
"integrity": "sha512-vgKwyIDG2y5twanf7YpqZvxkaLudTjwd9vRcoq5sQDB8egUlX5/NA0bQ0823pocrm0fjbFeetICu44mkqeDkIA==",
"dev": true,
"peerDependencies": {
"filepond": ">=3.6.0 <5.x"
}
},
"node_modules/imask": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/imask/-/imask-6.2.2.tgz",
"integrity": "sha512-oREITqngsjD0YxO62UpVQSxLNjIcb/mNO9hX2F8xQ282Kj3X9KXEAobipbUXz5R5NgtiL6nOPrWwo+AnLuz5AQ==",
"dev": true,
"engines": {
"npm": ">=4.0.0"
}
},
"node_modules/marked": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.7.tgz",
"integrity": "sha512-ctKqbnLuNbsHbI26cfMyOlKgXGfl1orOv1AvWWDX7AkgfMOwCWvmuYc+mVLeWhQ9W6hdWVBynOs96VkcscKo0Q==",
"dev": true,
"bin": {
"marked": "bin/marked"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/mdhl": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/mdhl/-/mdhl-0.0.6.tgz",
"integrity": "sha512-dY0ld/laPE9+9aq7LwGhg+zME0autnOR0juhf6AA/Njs0/awITgbxmUD/cHq8y07bV9j1JQbYM1RnLqhFym5KA==",
"dev": true,
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
},
"node_modules/trix": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/trix/-/trix-1.3.1.tgz",
"integrity": "sha512-BbH6mb6gk+AV4f2as38mP6Ucc1LE3OD6XxkZnAgPIduWXYtvg2mI3cZhIZSLqmMh9OITEpOBCCk88IVmyjU7bA==",
"dev": true
}
},
"dependencies": {
"@babel/runtime": {
"version": "7.15.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz",
"integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@github/file-attachment-element": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@github/file-attachment-element/-/file-attachment-element-2.0.1.tgz",
"integrity": "sha512-GQUgDPZeuE9xco4GbReAKFvtOFe9js+yctSrSYUSTGeD3N4xO2kX2MvaEiQRuVkERFgGPD9r57CLbbUoyJBn2Q==",
"dev": true
},
"@github/markdown-toolbar-element": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-1.5.3.tgz",
"integrity": "sha512-fNgAuHBWWzqxMvkTMM7ijqPryLmnPVOi4OWp8YyUC74YgtJCRvQyL7gl9SzNLMXmGQxiRVIYbUJxa61X1oRObw==",
"dev": true
},
"dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
"integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==",
"dev": true
},
"esbuild": {
"version": "0.12.29",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
"dev": true
},
"filepond": {
"version": "4.30.3",
"resolved": "https://registry.npmjs.org/filepond/-/filepond-4.30.3.tgz",
"integrity": "sha512-G2b1LEe90Sq2vH0SYDASTB+vVU735NBctzIaFPlZtb14QAgi/AL89WyQ6LhTfqgyrMyuZur2O9yHAmzS2E9ZnA==",
"dev": true
},
"filepond-plugin-file-validate-size": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/filepond-plugin-file-validate-size/-/filepond-plugin-file-validate-size-2.2.5.tgz",
"integrity": "sha512-x+fYZl317R17w3dExKab1oYZa10Wl2HcXKD6dzIgRKyngEHtjBF99geGaiDPk9IfozV8Zjzx69D7urcthF2CYQ==",
"dev": true,
"requires": {}
},
"filepond-plugin-file-validate-type": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/filepond-plugin-file-validate-type/-/filepond-plugin-file-validate-type-1.2.6.tgz",
"integrity": "sha512-3JFxpb4o5D4BssP/Drjn2GPpejg/QXzU4e3/Pq8IvIJuokHz95b6+RPYstQ+6ITKREYup0N8F/CO/OQfhVkUgg==",
"dev": true,
"requires": {}
},
"filepond-plugin-image-crop": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-crop/-/filepond-plugin-image-crop-2.0.6.tgz",
"integrity": "sha512-bSy/KND4EP/QtW8MxIj23qgSEPUVRrT8XdktJ+OU9y8xRjFjTG+qcuw1LsycARQYRtzi8cZrD3P4POOITfkywQ==",
"dev": true,
"requires": {}
},
"filepond-plugin-image-exif-orientation": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-exif-orientation/-/filepond-plugin-image-exif-orientation-1.0.11.tgz",
"integrity": "sha512-hLBc12Fk6Zkj3L8mSAn+elugHOqT5rLUbgVXQQIQjMe0FsGjtpoxqeVR6jt4IWHGat2L9sFAgU2TGmd1mqosCg==",
"dev": true,
"requires": {}
},
"filepond-plugin-image-preview": {
"version": "4.6.10",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-preview/-/filepond-plugin-image-preview-4.6.10.tgz",
"integrity": "sha512-C7Q5Sr/d4RQb2nMNQR93OiMt91OHxpvmIBGl+CO1dZE46lM49NUfI08dyElAOxTslDcA8pAfjFFCKo0sNYbrBg==",
"dev": true,
"requires": {}
},
"filepond-plugin-image-resize": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-resize/-/filepond-plugin-image-resize-2.0.10.tgz",
"integrity": "sha512-irX+J275u8Ph1KcciCeSQmxeFjbu0+co5XVCkiwdSNnz6KiqrCKN7RXTvEbdgSdDzyi5omr2oP1rKWps5L1RsQ==",
"dev": true,
"requires": {}
},
"filepond-plugin-image-transform": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/filepond-plugin-image-transform/-/filepond-plugin-image-transform-3.8.7.tgz",
"integrity": "sha512-vgKwyIDG2y5twanf7YpqZvxkaLudTjwd9vRcoq5sQDB8egUlX5/NA0bQ0823pocrm0fjbFeetICu44mkqeDkIA==",
"dev": true,
"requires": {}
},
"imask": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/imask/-/imask-6.2.2.tgz",
"integrity": "sha512-oREITqngsjD0YxO62UpVQSxLNjIcb/mNO9hX2F8xQ282Kj3X9KXEAobipbUXz5R5NgtiL6nOPrWwo+AnLuz5AQ==",
"dev": true
},
"marked": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.7.tgz",
"integrity": "sha512-ctKqbnLuNbsHbI26cfMyOlKgXGfl1orOv1AvWWDX7AkgfMOwCWvmuYc+mVLeWhQ9W6hdWVBynOs96VkcscKo0Q==",
"dev": true
},
"mdhl": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/mdhl/-/mdhl-0.0.6.tgz",
"integrity": "sha512-dY0ld/laPE9+9aq7LwGhg+zME0autnOR0juhf6AA/Njs0/awITgbxmUD/cHq8y07bV9j1JQbYM1RnLqhFym5KA==",
"dev": true
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
},
"trix": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/trix/-/trix-1.3.1.tgz",
"integrity": "sha512-BbH6mb6gk+AV4f2as38mP6Ucc1LE3OD6XxkZnAgPIduWXYtvg2mI3cZhIZSLqmMh9OITEpOBCCk88IVmyjU7bA==",
"dev": true
}
}
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "filament",
"scripts": {
"build": "node bin/build",
"watch": "node bin/build --watch"
},
"devDependencies": {
"@babel/runtime": "^7.15.3",
"@github/file-attachment-element": "^2.0.1",
"@github/markdown-toolbar-element": "^1.5.1",
"dayjs": "^1.10.6",
"esbuild": "^0.12.16",
"filepond": "^4.28.2",
"filepond-plugin-file-validate-size": "^2.2.4",
"filepond-plugin-file-validate-type": "^1.2.6",
"filepond-plugin-image-crop": "^2.0.6",
"filepond-plugin-image-exif-orientation": "^1.0.11",
"filepond-plugin-image-preview": "^4.6.7",
"filepond-plugin-image-resize": "^2.0.10",
"filepond-plugin-image-transform": "^3.8.6",
"imask": "^6.1.0",
"marked": "^3.0.0",
"mdhl": "^0.0.6",
"trix": "^1.3.1"
}
}
+1
View File
@@ -0,0 +1 @@
/vendor
+41
View File
@@ -0,0 +1,41 @@
{
"name": "filament/forms",
"description": "Effortlessly build TALL-powered forms.",
"license": "MIT",
"homepage": "https://github.com/laravel-filament/filament",
"support": {
"issues": "https://github.com/laravel-filament/filament/issues",
"source": "https://github.com/laravel-filament/filament"
},
"require": {
"php": "^8.0",
"blade-ui-kit/blade-heroicons": "^1.2",
"danharrin/date-format-converter": "^0.2",
"illuminate/console": "^8.6",
"illuminate/contracts": "^8.6",
"illuminate/database": "^8.6",
"illuminate/filesystem": "^8.6",
"illuminate/support": "^8.6",
"illuminate/validation": "^8.6",
"illuminate/view": "^8.6",
"livewire/livewire": "^2.6",
"spatie/laravel-package-tools": "^1.9"
},
"autoload": {
"psr-4": {
"Filament\\Forms\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Filament\\Forms\\FormsServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
+3680
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
<?php
return [
'components' => [
'date_time_picker' => [
'first_day_of_week' => 1, // 0 to 7 are accepted values, with Monday as 1 and Sunday as 7 or 0.
],
],
'default_filesystem_disk' => env('FORMS_FILESYSTEM_DRIVER', 'public'),
];
+1449
View File
File diff suppressed because it is too large Load Diff
+22212
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
---
title: Installation
---
## New Laravel projects
<iframe width="560" height="315" src="https://www.youtube.com/embed/iy1DO8JXRDQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
To get started with the form builder quickly, you can set up [Alpine.js](https://alpinejs.dev), [TailwindCSS](https://tailwindcss.com) and [Livewire](https://laravel-livewire.com) with just one command:
```bash
composer require filament/forms && php artisan forms:install && npm install && npm run dev
```
> This command will ruthlessly overwrite existing files in your application, hence why we only recommend using it for new projects.
You're now ready to start [building forms](building-forms)!
## Existing Laravel projects
> Please note that this package is incompatible with `filament/filament` v1, until v2 is released in late 2021. This is due to namespacing collisions.
<iframe width="560" height="315" src="https://www.youtube.com/embed/XslPKxtMR70" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
You may download the form builder using Composer:
```bash
composer require filament/forms
```
The package uses [Alpine.js](https://alpinejs.dev), [Tailwind CSS](https://tailwindcss.com), the Tailwind Forms plugin, and the Tailwind Typography plugin. You may install these through NPM:
```bash
npm install alpinejs tailwindcss @tailwindcss/forms @tailwindcss/typography --save-dev
```
To finish installing Tailwind, you must create a new `tailwind.config.js` file in the root of your project. The easiest way to do this is by running `npm tailwindcss init`.
In `tailwind.config.js`, enable JIT mode, register the plugins you installed, and add custom colors used by the form builder:
```js
const colors = require('tailwindcss/colors')
module.exports = {
mode: 'jit',
purge: [
'./resources/**/*.blade.php', // [tl! focus:start]
'./vendor/filament/forms/resources/views/**/*.blade.php', // [tl! focus:end]
],
theme: {
extend: {
colors: { // [tl! focus:start]
danger: colors.rose,
primary: colors.blue,
}, // [tl! focus:end]
},
},
plugins: [
require('@tailwindcss/forms'), // [tl! focus:start]
require('@tailwindcss/typography'), // [tl! focus:end]
],
}
```
Of course, you may specify your own custom `primary` and `danger` colors, which will be used instead.
In your `webpack.mix.js` file, Register Tailwind CSS as a PostCSS plugin :
```js
const mix = require('laravel-mix')
mix.js('resources/js/app.js', 'public/js')
.postCss('resources/css/app.css', 'public/css', [
require('tailwindcss'), // [tl! focus]
])
```
In `/resources/css/app.css`, import `filament/forms` vendor CSS and [TailwindCSS](https://tailwindcss.com):
```css
@import '../../vendor/filament/forms/dist/module.esm.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
```
In `/resources/js/app.js`, import [Alpine.js](https://alpinejs.dev), the `filament/forms` plugin, and register it:
```js
import Alpine from 'alpinejs'
import FormsAlpinePlugin from '../../vendor/filament/forms/dist/module.esm'
Alpine.plugin(FormsAlpinePlugin)
window.Alpine = Alpine
Alpine.start()
```
Compile your new CSS and JS assets using `npm run dev`.
Finally, create a new `resources/views/layouts/app.blade.php` layout file for Livewire components:
```blade
<!DOCTYPE html>
<html lang="{{ \Illuminate\Support\Str::of(app()->getLocale())->lower()->kebab() }}">
<head>
<meta charset="utf-8">
<meta name="application-name" content="{{ config('app.name') }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name') }}</title>
<!-- Styles -->
<style>[x-cloak] { display: none !important; }</style>
@livewireStyles
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<!-- Scripts -->
@livewireScripts
<script src="{{ mix('js/app.js') }}" defer></script>
@stack('scripts')
</head>
<body class="antialiased">
{{ $slot }}
</body>
</html>
```
You're now ready to start [building forms](building-forms)!
## Upgrade Guide
To upgrade the package to the latest version, you must run:
```bash
composer update
php artisan config:clear
php artisan view:clear
```
+691
View File
@@ -0,0 +1,691 @@
---
title: Building Forms
---
<iframe width="560" height="315" src="https://www.youtube.com/embed/Kab21689E5A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Preparing your Livewire component
Implement the `HasForms` interface and use the `InteractsWithForms` trait:
```php
<?php
namespace App\Http\Livewire;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms // [tl! focus]
{
use Forms\Concerns\InteractsWithForms; // [tl! focus]
public function render(): View
{
return view('edit-post');
}
}
```
In your Livewire component's view, render the form:
```blade
<div>
{{ $this->form }}
</div>
```
Finally, add any [fields](fields), [layout components](layout), and [custom components](building-custom-components) to the Livewire component's `getFormSchema()` method:
```php
<?php
namespace App\Http\Livewire;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Post $post;
public $title;
public $content;
public function mount(): void
{
$this->form->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
}
protected function getFormSchema(): array // [tl! focus:start]
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
// ...
];
} // [tl! focus:end]
public function render(): View
{
return view('edit-post');
}
}
```
Visit your Livewire component in the browser, and you should see the form components from `getFormSchema()`.
## Filling forms with data
Often, you will need to prefill your form fields with data. In normal Livewire components, this is often done in the `mount()` method, as this is only run once, immediately after the component is instantiated.
For your fields to hold data, they should have a corresponding property on your Livewire component, just as in Livewire normally.
To fill a form with data, call the `fill()` method on your form, and pass an array of data to fill it with:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Post $post;
public $title;
public $content;
public function mount(): void // [tl! focus:start]
{
$this->form->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
} // [tl! focus:end]
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
];
}
public function render(): View
{
return view('edit-post');
}
}
```
### Default data
Fields can use a `default()` configuration method, which allows you to specify a default values to fill your form with.
To fill a form with default values, call the `fill()` method on your form without any parameters:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreatePost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public $title = '';
public $content = '';
public function mount(): void // [tl! focus:start]
{
$this->form->fill();
} // [tl! focus:end]
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')
->default('Status Update') // [tl! focus]
->required(),
Forms\Components\MarkdownEditor::make('content'),
];
}
public function render(): View
{
return view('create-post');
}
}
```
> You may customize what happens after fields are hydrated [using the `afterStateHydrated()` method](advanced-forms#hydration).
## Getting data from forms
To get all form data in an array, call the `getState()` method on your form.
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreatePost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public $title = '';
public $content = '';
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
];
}
public function create(): void // [tl! focus:start]
{
Post::create($this->form->getState());
} // [tl! focus:end]
public function render(): View
{
return view('create-post');
}
}
```
When `getState()` is run:
1) [Validation](#validation) rules are checked, and if errors are present, the form is not submitted.
2) Any pending file uploads are stored permanently in the filesystem.
3) [Field relationships](#field-relationships), if they are defined, are saved.
> You may transform the value that is dehydrated from a field [using the `dehydrateStateUsing()` method](advanced-forms#dehydration).
## Validation
You may add 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).
### Dedicated methods
There are also dedicated methods for some validation rules, some of which are able to add frontend validation as well as backend validation.
We recommend that you use dedicated validation methods wherever possible.
#### 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](#registering-a-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')
```
#### 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()
```
#### Required
The field value must not be empty. [See the Laravel documentation](https://laravel.com/docs/validation#rule-required)
```php
Field::make('name')->required()
```
#### 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')
```
#### 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()
```
By default, the form's model will be searched, [if it is registered](#registering-a-model). You may 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')
```
Sometimes, you may 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.
```php
Field::make('email')->unique(ignorable: $ignoredUser)
```
## Registering a model
You may register a model to a form. The form builder is able to use this model to unlock DX features, such as:
- Automatically retrieving the database table name when using database validation rules like `exists` and `unique`.
- Automatically attaching relationships to the model when the form is saved, when using fields such as the `BelongsToManyMultiSelect`, `SpatieMediaLibraryFileUpload`, or `SpatieTagsInput`.
Pass a model instance to a form using the `getFormModel()` method:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Post $post;
public $title;
public $content;
public $tags;
public function mount(): void
{
$this->form->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
Forms\Components\SpatieTagsInput::make('tags'),
];
}
protected function getFormModel(): Post // [tl! focus:start]
{
return $this->post;
} // [tl! focus:end]
public function render(): View
{
return view('edit-post');
}
}
```
Alternatively, you may pass the model instance to the field that requires it directly, using the `model()` method:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Post $post;
public $title;
public $content;
public $tags;
public function mount(): void
{
$this->form->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
Forms\Components\SpatieTagsInput::make('tags')->model($this->post), // [tl! focus]
];
}
public function render(): View
{
return view('edit-post');
}
}
```
You may now use [field relationships](#field-relationships);
### Registering a model class
In some cases, the model instance is not available until the form has been submitted. For example, in a form that creates a post, the post model instance cannot be passed to the form before it has been submitted.
You may receive some of the same benefits of registering a model by registering its class instead:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreatePost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public $title = '';
public $content = '';
public $categories = [];
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
Forms\Components\BelongsToManyMultiSelect::make('categories')->relationship('categories', 'name'),
];
}
protected function getFormModel(): string // [tl! focus:start]
{
return Post::class;
} // [tl! focus:end]
public function render(): View
{
return view('create-post');
}
}
```
You may now use [field relationships](#field-relationships).
## Field relationships
Some fields, such as the `BelongsToSelect`, `BelongsToManyMultiSelect`, `SpatieMediaLibraryFileUpload`, or `SpatieTagsInput` are able to interact with model relationships.
For example, `BelongsToManyMultiSelect` is a multi-select field that can be used to attach records to a `BelongstoMany` relationship. When [registering a model](#registering-a-model) to the form or component, these relationships will be automatically saved to the pivot table [when `getState()` is called](#getting-data-from-forms):
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Post $post;
public $title;
public $content;
public $categories;
public function mount(): void
{
$this->form->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
Forms\Components\BelongsToManyMultiSelect::make('categories')->relationship('categories', 'name'),
];
}
protected function getFormModel(): Post // [tl! focus:start]
{
return $this->post;
}
public function save(): void
{
$this->post->update(
$this->form->getState(),
);
} // [tl! focus:end]
public function render(): View
{
return view('edit-post');
}
}
```
### Saving field relationships manually
In some cases, the model instance is not available until the form has been submitted. For example, in a form that creates a post, the post model instance cannot be passed to the form before it has been submitted. In this case, you will [pass the model class](#registering-a-model-class) instead, but any field relationships will need to be saved manually after.
In this situation, you may call the `model()` and `saveRelationships()` methods on the form after the instance has been created:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Model;
use Livewire\Component;
class CreatePost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public $title = '';
public $content = '';
public $tags = [];
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
Forms\Components\SpatieTagsInput::make('tags'),
];
}
protected function getFormModel(): string
{
return Post::class;
}
public function create(): void
{
$post = Post::create($this->form->getState());
$this->form->model($post)->saveRelationships(); // [tl! focus]
}
public function render(): View
{
return view('create-post');
}
}
```
## Using multiple forms
By default, the `InteractsWithForms` trait only handles one form per Livewire component. To change this, you can override the `getForms()` method to return more than one form, each with a unique name:
```php
<?php
namespace App\Http\Livewire;
use App\Models\Author;
use App\Models\Post;
use Filament\Forms;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditPost extends Component implements Forms\Contracts\HasForms
{
use Forms\Concerns\InteractsWithForms;
public Author $author;
public Post $post;
public $title;
public $content;
public $name;
public $email;
public function mount(): void
{
$this->postForm->fill([
'title' => $this->post->title,
'content' => $this->post->content,
]);
$this->authorForm->fill([
'name' => $this->author->name,
'email' => $this->author->email,
]);
}
protected function getPostFormSchema(): array
{
return [
Forms\Components\TextInput::make('title')->required(),
Forms\Components\MarkdownEditor::make('content'),
];
}
protected function getAuthorFormSchema(): array
{
return [
Forms\Components\TextInput::make('name')->required(),
Forms\Components\TextInput::make('email')->email()->required(),
];
}
public function savePost(): void
{
$this->post->update(
$this->postForm->getState(),
);
}
public function saveAuthor(): void
{
$this->author->update(
$this->authorForm->getState(),
);
}
protected function getForms(): array // [tl! focus:start]
{
return [
'postForm' => $this->makeForm()
->schema($this->getPostFormSchema())
->model($this->post),
'authorForm' => $this->makeForm()
->schema($this->getAuthorFormSchema())
->model($this->author),
];
} // [tl! focus:end]
public function render(): View
{
return view('edit-post');
}
}
```
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
---
title: Layout
---
## Getting started
Layout component classes can be found in the `Filament\Form\Components` namespace.
They reside within the schema of your form, alongside any [fields](fields):
```php
protected function getFormSchema(): array
{
return [
// ...
];
}
```
Components may be created using the static `make()` method. Usually, you will then define the child component `schema()` to display inside:
```php
use Filament\Forms\Components\Grid;
Grid::make()
->schema([
// ...
])
```
### Columns
You may create multiple columns within each layout component using the `columns()` method:
```php
use Filament\Forms\Components\Card;
Card::make()->columns(2)
```
> For more information about creating advanced, responsive column layouts, please see the [grid section](#grid). All column options in that section are also available in other layout components.
### Setting an ID
You may define an ID for the component using the `id()` method:
```php
use Filament\Forms\Components\Card;
Card::make()->id('main-card')
```
### Custom attributes
The HTML of components can be customized even further, by passing an array of `extraAttributes()`:
```php
use Filament\Forms\Components\Card;
Card::make()->extraAttributes(['class' => 'bg-gray-50'])
```
## Grid
Generally, form fields are stacked on top of each other in one column. To change this, you may use a grid component:
```php
use Filament\Forms\Components\Grid;
Grid::make()
->schema([
// ...
])
```
By default, grid components will create a two column grid for [the Tailwind `md` breakpoint](https://tailwindcss.com/docs/responsive-design#overview) and higher.
To customize the number of columns in any grid at different [breakpoints](https://tailwindcss.com/docs/responsive-design#overview), you may pass an array of breakpoints and columns:
```php
use Filament\Forms\Components\Grid;
Grid::make([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 4,
'xl' => 6,
'2xl' => 8,
])
->schema([
// ...
])
```
Since Tailwind is mobile-first, if you leave out a breakpoint, it will fall back to the one set below it:
```php
use Filament\Forms\Components\Grid;
Grid::make([
'sm' => 2,
'xl' => 6,
])
->schema([
// ...
])
```
You may specify the number of columns that any component may span, at any breakpoint in an identical way:
```php
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\TextInput;
Grid::make([
'default' => 1,
'sm' => 3,
'xl' => 6,
'2xl' => 8,
])
->schema([
TextInput::make('name')
->columnSpan([
'sm' => 2,
'xl' => 3,
'2xl' => 4,
]),
// ...
])
```
## Fieldset
You may want to group fields into a Fieldset. Each fieldset has a label, a border, and a two-column grid by default:
```php
use Filament\Forms\Components\Fieldset;
Fieldset::make('Label')
->schema([
// ...
])
```
You may use the `columns()` method to customize the [grid](#grid) within the fieldset:
```php
use Filament\Forms\Components\Fieldset;
Fieldset::make('Label')
->schema([
// ...
])
->columns(3)
```
## Tabs
Some forms can be long and complex. You may want to use tabs to reduce the number of components that are visible at once:
```php
use Filament\Forms\Components\Tabs;
Tabs::make('Heading')
->tabs([
Tabs\Tab::make('Label 1')
->schema([
// ...
]),
Tabs\Tab::make('Label 2')
->schema([
// ...
]),
Tabs\Tab::make('Label 3')
->schema([
// ...
]),
])
```
## Section
You may want to separate your fields into sections, each with a heading and description. To do this, you can use a section component:
```php
use Filament\Forms\Components\Section;
Section::make('Heading')
->description('Description')
->schema([
// ...
])
```
You may use the `columns()` method to easily create a [grid](#grid) within the section:
```php
use Filament\Forms\Components\Section;
Section::make('Heading')
->schema([
// ...
])
->columns(2)
```
Sections may be `collapsible()` to optionally hide content in long forms:
```php
use Filament\Forms\Components\Section;
Section::make('Heading')
->schema([
// ...
])
->collapsible()
```
Your sections may be `collapsed()` by default:
```php
use Filament\Forms\Components\Section;
Section::make('Heading')
->schema([
// ...
])
->collapsed()
```
## Placeholder
Placeholders can be used to render text-only "fields" within your forms. Each placeholder has a state, which is cannot be changed by the user.
```php
use Filament\Forms\Components\Placeholder;
Placeholder::make('Label')->state('Value, displayed underneath the label')
```
## Card
The card component may be used to render the form components inside a card:
```php
use Filament\Forms\Components\Card;
Card::make()
->schema([
// ...
])
```
You may use the `columns()` method to easily create a [grid](#grid) within the card:
```php
use Filament\Forms\Components\Card;
Card::make()
->schema([
// ...
])
->columns(2)
```
## View
Aside from [building custom layout components](#building-custom-layout-components), you may create "view" components which allow you to create custom layouts without extra PHP classes.
```php
use Filament\Forms\Components\View;
View::make('filament.forms.components.wizard')
```
Inside your view, you may render the component's `schema()` using the `$getChildComponentContainer()` callable:
```blade
<div>
{{ $getChildComponentContainer() }}
</div>
```
## Building custom layout components
You may create your own custom component classes and views, which you can reuse across your project, and even release as a plugin to the community.
> If you're just creating a simple custom component to use once, you could instead use a [view component](#view) to render any custom Blade file.
Extend the `Filament\Forms\Components\Component` class, and define the `$view` path of the custom component:
```php
use Filament\Forms\Components\Component;
class Wizard extends Component
{
protected string $view = 'filament.forms.components.wizard';
public static function make(): static
{
return new static();
}
}
```
Inside your view, you may render the component's `schema()` using the `$getChildComponentContainer()` callable:
```blade
<div>
{{ $getChildComponentContainer() }}
</div>
```
+189
View File
@@ -0,0 +1,189 @@
---
title: Advanced Forms
---
## Using callback customisation
All configuration methods for [fields](fields) and [layout components](layout) accept callback functions as parameters instead of hardcoded values:
```php
use App\Models\User;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
DatePicker::make('date_of_birth')
->displayFormat(function () {
if (auth()->user()->country_id === 'us') {
return 'm/d/Y'
} else {
return 'd/m/Y'
}
})
Select::make('userId')
->options(function () {
return User::all()->pluck('name', 'id');
})
TextInput::make('middle_name')
->required(function () {
return auth()->user()->hasMiddleName();
})
```
This alone unlocks many customization possibilities.
The package is also able to inject many utilities to use inside these callback functions, as parameters.
If you wish to access the current state (value) of the component, define a `$state` parameter:
```php
function ($state) {
// ...
}
```
If you wish to access the current component instance, define a `$component` parameter:
```php
use Filament\Forms\Components\Component;
function (Component $component) {
// ...
}
```
If you wish to access the current Livewire component instance, define a `$livewire` parameter:
```php
use Livewire\Component as Livewire;
function (Livewire $livewire) {
// ...
}
```
If you have defined a form or component Eloquent model, define a `$model` parameter:
```php
use Illuminate\Database\Eloquent\Model;
function (Model $model) {
// ...
}
```
You may also retrieve the value of another field from within a callback, using a callable `$get` parameter:
```php
function (callable $get) {
$email = $get('email'); // Store the value of the `email` field in the `$email` variable.
//...
}
```
In a similar way to `$get`, you may also set the value of another field from within a callback, using a callable `$set` parameter:
```php
function (callable $set) {
$set('title', 'Blog Post'); // Set the `title` field to `Blog Post`.
//...
}
```
Callbacks are evaluated using Laravel's `app()->call()` under the hood, so you are able to combine multiple parameters in any order:
```php
use Livewire\Component as Livewire;
function (Livewire $livewire, callable $get, callable $set) {
// ...
}
```
### Reloading the form when a field is updated
By default, forms are only reloaded when they are validated or submitted. You may allow a form to be reloaded when a field is changed, by using the `reactive()` method on that field:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('title')->reactive()
```
A great example to give a use case for this is when you wish to generate a slug from a title field automatically:
<iframe width="560" height="315" src="https://www.youtube.com/embed/GNsk5z7-PEs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
## Conditionally hiding components
Sometimes, you may wish to conditionally hide any form component. You may do this with a `hidden()` method:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('newPassword')->password()
TextInput::make('newPasswordConfirmation')
->password()
->hidden(fn (callable $get) => $get('newPassword') !== null)
```
## Field lifecycle
### Hydration
Hydration is the process which fill fields with data. It runs when you call the [form's `fill()` method](building-forms#filling-forms-with-data). You may customize what happens after a field is hydrated using the `afterStateHydrated()`.
In this example, the `name` field will always be hydrated with the correctly capitalized name:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')
->afterStateHydrated(function (TextInput $component, callable $set, $state) {
$set($component, ucwords($state));
})
```
### Updates
You may use the `afterStateHydrated()` method to customize what happens after a field is updated.
In this example, the `slug` field is updated with the slug version of the `title` field automatically:
```php
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;
TextInput::make('title')
->reactive()
->afterStateUpdated(function (callable $set, $state) {
$set('slug', Str::slug($state));
})
TextInput::make('slug')
```
### Dehydration
Hydration is the process which gets data from fields, and transforms it. It runs when you call the [form's `getState()` method](building-forms#getting-data-from-forms). You may customize how the state is dehydrated from the form by returning the transformed state from the `afterStateHydrated()` callback.
In this example, the `name` field will always be dehydrated with the correctly capitalized name:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('name')->dehydrateStateUsing(fn ($state) => ucwords($state))
```
You may also prevent the field from being dehydrated altogether by passing `false` to `dehydrated()`.
In this example, the `passwordConfirmation` field will not be present in the array returned from `getData()`:
```php
use Filament\Forms\Components\TextInput;
TextInput::make('passwordConfirmation')
->password()
->dehydrated(false)
```
@@ -0,0 +1,3 @@
.filepond--root {
margin-bottom: 0;
}
@@ -0,0 +1,367 @@
import dayjs from 'dayjs/esm'
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(customParseFormat)
dayjs.extend(localeData)
dayjs.extend(timezone)
dayjs.extend(utc)
window.dayjs = dayjs
export default (Alpine) => {
Alpine.data('dateTimePickerFormComponent', ({
displayFormat,
firstDayOfWeek,
format,
isAutofocused,
maxDate,
minDate,
state,
}) => {
const timezone = dayjs.tz.guess()
dayjs.locale(window.dayjs_locale)
return {
daysInFocusedMonth: [],
displayText: '',
emptyDaysInFocusedMonth: [],
focusedDate: null,
focusedMonth: null,
focusedYear: null,
hour: null,
maxDate,
minDate,
minute: null,
open: false,
second: null,
state,
init: function () {
this.maxDate = dayjs(this.maxDate)
if (! this.maxDate.isValid()) {
this.maxDate = null
}
this.minDate = dayjs(this.minDate)
if (! this.minDate.isValid()) {
this.minDate = null
}
let date = this.getSelectedDate() ?? dayjs().tz(timezone)
.hour(0)
.minute(0)
.second(0)
if (this.maxDate !== null && date.isAfter(this.maxDate)) {
date = null
}
if (this.minDate !== null && date.isBefore(this.minDate)) {
date = null
}
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.setDisplayText()
if (isAutofocused) {
this.openPicker()
}
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
if (this.focusedDate.month() === this.focusedMonth) {
return
}
this.focusedDate = this.focusedDate.month(this.focusedMonth)
})
this.$watch('focusedYear', () => {
this.focusedYear = Number.isInteger(+this.focusedYear) ? +this.focusedYear : dayjs().year()
if (this.focusedDate.year() === this.focusedYear) {
return
}
this.focusedDate = this.focusedDate.year(this.focusedYear)
})
this.$watch('focusedDate', () => {
this.focusedMonth = this.focusedDate.month()
this.focusedYear = this.focusedDate.year()
this.setupDaysGrid()
this.$nextTick(() => {
this.evaluatePosition()
})
})
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
}
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
}
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
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.second(this.second ?? 0))
})
this.$watch('state', () => {
let date = this.getSelectedDate()
if (this.maxDate !== null && date.isAfter(this.maxDate)) {
date = null
}
if (this.minDate !== null && date.isBefore(this.minDate)) {
date = null
}
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.setDisplayText()
})
},
clearState: function () {
this.setState(null)
this.closePicker()
},
closePicker: function () {
this.open = false
},
dateIsDisabled: function (date) {
if (this.maxDate && date.isAfter(this.maxDate)) {
return true
}
if (this.minDate && date.isBefore(this.minDate)) {
return true
}
return false
},
dayIsDisabled: function (day) {
return this.dateIsDisabled(this.focusedDate.date(day))
},
dayIsSelected: function (day) {
let selectedDate = this.getSelectedDate()
if (selectedDate === null) {
return false
}
return selectedDate.date() === day &&
selectedDate.month() === this.focusedDate.month() &&
selectedDate.year() === this.focusedDate.year()
},
dayIsToday: function (day) {
let date = dayjs().tz(timezone)
return date.date() === day &&
date.month() === this.focusedDate.month() &&
date.year() === this.focusedDate.year()
},
evaluatePosition: function () {
let availableHeight = window.innerHeight - this.$refs.button.offsetHeight
let element = this.$refs.button
while (element) {
availableHeight -= element.offsetTop
element = element.offsetParent
}
if (this.$refs.picker.offsetHeight <= availableHeight) {
this.$refs.picker.style.bottom = 'auto'
return
}
this.$refs.picker.style.bottom = `${this.$refs.button.offsetHeight}px`
},
focusPreviousDay: function () {
this.focusedDate = this.focusedDate.subtract(1, 'day')
},
focusPreviousWeek: function () {
this.focusedDate = this.focusedDate.subtract(1, 'week')
},
focusNextDay: function () {
this.focusedDate = this.focusedDate.add(1, 'day')
},
focusNextWeek: function () {
this.focusedDate = this.focusedDate.add(1, 'week')
},
getDayLabels: function () {
const labels = dayjs.weekdaysShort()
if (firstDayOfWeek === 0) {
return labels
}
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
]
},
getSelectedDate: function () {
let date = dayjs(this.state, format)
if (! date.isValid()) {
return null
}
return date
},
openPicker: function () {
this.focusedDate = this.getSelectedDate() ?? dayjs().tz(timezone)
this.setupDaysGrid()
this.open = true
this.$nextTick(() => {
this.evaluatePosition()
})
},
selectDate: function (day = null) {
if (day) {
this.setFocusedDay(day)
}
this.setState(this.focusedDate)
},
setDisplayText: function () {
this.displayText = this.getSelectedDate() ? this.getSelectedDate().format(displayFormat) : ''
},
setupDaysGrid: function () {
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: function (day) {
this.focusedDate = this.focusedDate.date(day)
},
setState: function (date) {
if (date === null) {
this.state = null
this.setDisplayText()
return
} else {
if (this.dateIsDisabled(date)) {
return
}
}
this.state = date
.hour(this.hour ?? 0)
.minute(this.minute ?? 0)
.second(this.second ?? 0)
.format(format)
this.setDisplayText()
},
togglePickerVisibility: function () {
if (this.open) {
this.closePicker()
return
}
this.openPicker()
},
}
})
}
@@ -0,0 +1,129 @@
import * as FilePond from 'filepond'
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
import FilePondPluginImageCrop from 'filepond-plugin-image-crop'
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 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import '../../css/components/file-upload.css'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
FilePond.registerPlugin(FilePondPluginFileValidateType)
FilePond.registerPlugin(FilePondPluginImageCrop)
FilePond.registerPlugin(FilePondPluginImageExifOrientation)
FilePond.registerPlugin(FilePondPluginImagePreview)
FilePond.registerPlugin(FilePondPluginImageResize)
FilePond.registerPlugin(FilePondPluginImageTransform)
export default (Alpine) => {
Alpine.data('fileUploadFormComponent', ({
acceptedFileTypes,
getUploadedFileUrlUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
loadingIndicatorPosition,
panelAspectRatio,
panelLayout,
placeholder,
maxSize,
minSize,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
state,
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
}) => {
return {
files: [],
pond: null,
state,
init: async function () {
let uploadedFileUrl = await getUploadedFileUrlUsing()
if (uploadedFileUrl) {
this.files = [{
source: uploadedFileUrl,
options: {
type: 'local',
},
}]
}
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
credits: false,
files: this.files,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
...(placeholder && {labelIdle: placeholder}),
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition: removeUploadedFileButtonPosition,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition: uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source)
let blob = await response.blob()
load(blob)
},
process: (fieldName, file, metadata, load, error, progress) => {
uploadUsing(file, load, error, progress)
},
remove: async (source, load) => {
await removeUploadedFileUsing()
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
load()
},
},
})
this.$watch('state', async () => {
if (! this.state) {
this.pond.removeFiles()
return
}
if (this.state.startsWith('livewire-file:')) {
return
}
let uploadedFileUrl = await getUploadedFileUrlUsing()
if (uploadedFileUrl) {
this.pond.files = [{
source: uploadedFileUrl,
options: {
type: 'local',
},
}]
} else {
this.pond.files = []
}
})
}
}
})
}
@@ -0,0 +1,83 @@
import '@github/file-attachment-element'
import '@github/markdown-toolbar-element'
import marked from 'marked'
import { highlight } from 'mdhl'
export default (Alpine) => {
Alpine.data('markdownEditorFormComponent', ({
state,
tab,
}) => {
return {
attachment: null,
overlay: null,
preview: '',
state,
tab,
init: function () {
if (this.state !== null) {
this.render()
}
this.$watch('state', () => {
this.render()
})
},
render: function () {
if (this.$refs.textarea.scrollHeight > 0) {
this.$refs.overlay.style.height = '150px'
this.$refs.overlay.style.height = this.$refs.textarea.scrollHeight + 'px'
}
this.overlay = highlight(this.state)
this.preview = marked(this.state)
},
checkForAutoInsertion($event) {
const lines = this.$refs.textarea.value.split("\n")
const currentLine = this.$refs.textarea.value.substring(
0, this.$refs.textarea.value.selectionStart
).split("\n").length
const previousLine = lines[currentLine - 2]
if (! previousLine.match(/^(\*\s|-\s)|^(\d)+\./)) {
return;
}
if (previousLine.match(/^(\*\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '* '
} else {
delete lines[currentLine - 2]
}
} else if (previousLine.match(/^(-\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '- '
} else {
delete lines[currentLine - 2]
}
} else {
const matches = previousLine.match(/^(\d)+/)
const number = matches[0]
if (previousLine.trim().length > (number.length + 2)) {
lines[currentLine - 1] = `${parseInt(number) + 1}. `
} else {
delete lines[currentLine - 2]
}
}
this.state = lines.join("\n")
this.render()
},
}
})
}
@@ -0,0 +1,188 @@
export default (Alpine) => {
Alpine.data('multiSelectFormComponent', ({
getOptionLabelsUsing,
getSearchResultsUsing,
isAutofocused,
options,
state,
}) => {
return {
focusedOptionIndex: null,
isLoading: false,
isOpen: false,
labels: [],
options,
search: '',
state,
init: async function () {
if (isAutofocused) {
this.openListbox()
}
if (! this.state) {
this.state = []
}
this.labels = await getOptionLabelsUsing()
this.$watch('search', async () => {
if (! this.isOpen || this.search === '' || this.search === null) {
this.options = options
this.focusedOptionIndex = 0
return
}
if (Object.keys(options).length) {
this.options = {}
let search = this.search.trim().toLowerCase()
for (let key in options) {
if (options[key].trim().toLowerCase().includes(search)) {
this.options[key] = options[key]
}
}
this.focusedOptionIndex = 0
} else {
this.isLoading = true
this.options = await getSearchResultsUsing(this.search)
this.focusedOptionIndex = 0
this.isLoading = false
}
})
this.$watch('state', async () => {
this.labels = await getOptionLabelsUsing()
})
},
clearState: function () {
this.state = []
this.labels = []
this.closeListbox()
},
closeListbox: function () {
this.isOpen = false
this.focusedOptionIndex = null
this.search = ''
},
evaluatePosition: function () {
let availableHeight = window.innerHeight - this.$refs.button.offsetHeight
let element = this.$refs.button
while (element) {
availableHeight -= element.offsetTop
element = element.offsetParent
}
if (this.$refs.listbox.offsetHeight <= availableHeight) {
this.$refs.listbox.style.bottom = 'auto'
return
}
this.$refs.listbox.style.bottom = `${this.$refs.button.offsetHeight}px`
},
focusNextOption: function () {
if (this.focusedOptionIndex === null) {
this.focusedOptionIndex = Object.keys(this.options).length - 1
return
}
if (this.focusedOptionIndex + 1 >= Object.keys(this.options).length) {
return
}
this.focusedOptionIndex++
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center',
})
},
focusPreviousOption: function () {
if (this.focusedOptionIndex === null) {
this.focusedOptionIndex = 0
return
}
if (this.focusedOptionIndex <= 0) {
return
}
this.focusedOptionIndex--
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center',
})
},
openListbox: function () {
this.focusedOptionIndex = 0
this.isOpen = true
this.$nextTick(() => {
this.$refs.search.focus()
this.evaluatePosition()
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center'
})
})
},
selectOption: function (index = null) {
if (! this.isOpen) {
this.closeListbox()
return
}
let value = Object.keys(this.options)[index ?? this.focusedOptionIndex]
if (this.state.indexOf(value) < 0) {
this.state.push(value)
} else {
this.deselectOption(value)
}
this.closeListbox()
},
deselectOption: function (optionToDeselect) {
this.state = this.state.filter((option) => option !== optionToDeselect)
},
toggleListboxVisibility: function () {
if (this.isOpen) {
this.closeListbox()
return
}
this.openListbox()
},
}
})
}
@@ -0,0 +1,39 @@
import Trix from 'trix/dist/trix'
import 'trix/dist/trix.css'
Trix.config.blockAttributes.heading = {
tagName: "h2",
terminal: true,
breakOnReturn: true,
group: false
}
Trix.config.blockAttributes.subHeading = {
tagName: "h3",
terminal: true,
breakOnReturn: true,
group: false
}
export default (Alpine) => {
Alpine.data('richEditorFormComponent', ({
state,
}) => {
return {
state,
init: function () {
this.$refs.trix?.editor?.loadHTML(this.state)
this.$watch('state', () => {
if (document.activeElement === this.$refs.trix) {
return
}
this.$refs.trix?.editor?.loadHTML(this.state)
})
}
}
})
}
@@ -0,0 +1,179 @@
export default (Alpine) => {
Alpine.data('selectFormComponent', ({
getOptionLabelUsing,
getSearchResultsUsing,
isAutofocused,
options,
state,
}) => {
return {
focusedOptionIndex: null,
isLoading: false,
isOpen: false,
label: null,
options,
search: '',
state,
init: async function () {
if (isAutofocused) {
this.openListbox()
}
this.label = await getOptionLabelUsing()
this.$watch('search', async () => {
if (! this.isOpen || this.search === '' || this.search === null) {
this.options = options
this.focusedOptionIndex = 0
return
}
if (Object.keys(options).length) {
this.options = {}
let search = this.search.trim().toLowerCase()
for (let key in options) {
if (options[key].trim().toLowerCase().includes(search)) {
this.options[key] = options[key]
}
}
this.focusedOptionIndex = 0
} else {
this.isLoading = true
this.options = await getSearchResultsUsing(this.search)
this.focusedOptionIndex = 0
this.isLoading = false
}
})
this.$watch('state', async () => {
this.label = await getOptionLabelUsing()
})
},
clearState: function () {
this.state = null
this.label = null
this.closeListbox()
},
closeListbox: function () {
this.isOpen = false
this.focusedOptionIndex = null
this.search = ''
},
evaluatePosition: function () {
let availableHeight = window.innerHeight - this.$refs.button.offsetHeight
let element = this.$refs.button
while (element) {
availableHeight -= element.offsetTop
element = element.offsetParent
}
if (this.$refs.listbox.offsetHeight <= availableHeight) {
this.$refs.listbox.style.bottom = 'auto'
return
}
this.$refs.listbox.style.bottom = `${this.$refs.button.offsetHeight}px`
},
focusNextOption: function () {
if (this.focusedOptionIndex === null) {
this.focusedOptionIndex = Object.keys(this.options).length - 1
return
}
if (this.focusedOptionIndex + 1 >= Object.keys(this.options).length) {
return
}
this.focusedOptionIndex++
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center',
})
},
focusPreviousOption: function () {
if (this.focusedOptionIndex === null) {
this.focusedOptionIndex = 0
return
}
if (this.focusedOptionIndex <= 0) {
return
}
this.focusedOptionIndex--
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center',
})
},
openListbox: function () {
this.focusedOptionIndex = Object.keys(this.options).indexOf(this.state)
if (this.focusedOptionIndex < 0) {
this.focusedOptionIndex = 0
}
this.isOpen = true
this.$nextTick(() => {
this.$refs.search.focus()
this.evaluatePosition()
this.$refs.listboxOptionsList.children[this.focusedOptionIndex].scrollIntoView({
block: 'center'
})
})
},
selectOption: function (index = null) {
if (! this.isOpen) {
this.closeListbox()
return
}
this.state = Object.keys(this.options)[index ?? this.focusedOptionIndex]
this.label = this.options[this.state]
this.closeListbox()
},
toggleListboxVisibility: function () {
if (this.isOpen) {
this.closeListbox()
return
}
this.openListbox()
},
}
})
}
@@ -0,0 +1,33 @@
export default (Alpine) => {
Alpine.data('tagsInputFormComponent', ({
state,
}) => {
return {
newTag: '',
state,
createTag: function () {
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: function (tagToDelete) {
this.state = this.state.filter((tag) => tag !== tagToDelete)
},
}
})
}
@@ -0,0 +1,32 @@
import IMask from 'imask'
export default (Alpine) => {
Alpine.data('textInputFormComponent', ({
getMaskOptionsUsing,
state,
}) => {
return {
mask: null,
state,
init: function () {
if (! getMaskOptionsUsing) {
return
}
if (this.state) {
this.$el.value = this.state?.valueOf()
}
this.mask = IMask(this.$el, getMaskOptionsUsing(IMask)).on('accept', () => {
this.state = this.mask.unmaskedValue
})
this.$watch('state', () => {
this.mask.unmaskedValue = this.state?.valueOf()
})
}
}
})
}
+19
View File
@@ -0,0 +1,19 @@
import DateTimePickerFormComponentAlpinePlugin from './components/date-time-picker'
import FileUploadFormComponentAlpinePlugin from './components/file-upload'
import MarkdownEditorFormComponentAlpinePlugin from './components/markdown-editor'
import MultiSelectFormComponentAlpinePlugin from './components/multi-select'
import RichEditorFormComponentAlpinePlugin from './components/rich-editor'
import SelectFormComponentAlpinePlugin from './components/select'
import TagsInputFormComponentAlpinePlugin from './components/tags-input'
import TextInputFormComponentAlpinePlugin from './components/text-input'
export default (Alpine) => {
Alpine.plugin(DateTimePickerFormComponentAlpinePlugin)
Alpine.plugin(FileUploadFormComponentAlpinePlugin)
Alpine.plugin(MarkdownEditorFormComponentAlpinePlugin)
Alpine.plugin(MultiSelectFormComponentAlpinePlugin)
Alpine.plugin(RichEditorFormComponentAlpinePlugin)
Alpine.plugin(SelectFormComponentAlpinePlugin)
Alpine.plugin(TagsInputFormComponentAlpinePlugin)
Alpine.plugin(TextInputFormComponentAlpinePlugin)
}
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'لا توجد خيارات تطابق بحثك.',
'placeholder' => 'إختر خيارًا',
'search_prompt' => 'لا توجد خيارات للاختيار من بينها.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'ربط',
'unlink' => 'فصل',
],
'label' => 'عنوان url',
'placeholder' => 'إدخل عنوان url',
],
],
'toolbar_buttons' => [
'attach_files' => 'إرفاق ملفات',
'blockquote' => 'اقتباس',
'bold' => 'عريض',
'bullet_list' => 'نقاط',
'code_block' => 'كود',
'h1' => 'عنوان',
'h2' => 'عنوان',
'h3' => 'عنوان فرعي',
'italic' => 'مائل',
'link' => 'رابط',
'ordered_list' => 'أرقام',
'redo' => 'إعادة',
'strike' => 'يتوسطه خط',
'undo' => 'تراجع',
],
],
'select' => [
'no_search_results_message' => 'لا توجد خيارات تطابق بحثك.',
'placeholder' => 'إختر خيارًا',
'search_prompt' => 'ابدأ الكتابة للبحث ...',
],
'tags_input' => [
'placeholder' => 'كلمة مفتاحية جديدة',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'No s\'ha trobat cap opció que coincideixi amb la vostra cerca.',
'placeholder' => 'Trieu una opció',
'search_prompt' => 'No hi ha opcions disponibles',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Enllaç',
'unlink' => 'Elimina l\'enllaç',
],
'label' => 'URL',
'placeholder' => 'Escriu una adreça URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Adjuntar fitxers',
'blockquote' => 'Bloc de cita',
'bold' => 'Negreta',
'bullet_list' => 'Bullets',
'code_block' => 'Codi',
'h1' => 'Títol',
'h2' => 'Capçaleres',
'h3' => 'Subtítol',
'italic' => 'Cursiva',
'link' => 'Enllaç',
'ordered_list' => 'Números',
'redo' => 'Refés',
'strike' => 'Barrat',
'undo' => 'Desfer',
],
],
'select' => [
'no_search_results_message' => 'No s\'ha trobat cap opció que coincideixi amb la vostra cerca',
'placeholder' => 'Trieu una opció',
'search_prompt' => 'Comenceu a escriure per cercar...',
],
'tags_input' => [
'placeholder' => 'Nova Etiqueta',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Ingen resultater der matcher din søgning.',
'placeholder' => 'Vælg en indstilling',
'search_prompt' => 'Ingen muligheder at vælge imellem.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Link',
'unlink' => 'Fjern link',
],
'label' => 'URL',
'placeholder' => 'Indtast en URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Vedhæft filer',
'blockquote' => 'Citere',
'bold' => 'Fed',
'bullet_list' => 'Punkter',
'code_block' => 'Kode',
'h1' => 'Titel',
'h2' => 'Overskrift',
'h3' => 'Underoverskrift',
'italic' => 'Kursiv',
'link' => 'Link',
'ordered_list' => 'Tal',
'redo' => 'Gentag',
'strike' => 'Gennemstreget',
'undo' => 'Fortryd',
],
],
'select' => [
'no_search_results_message' => 'Ingen muligheder der matcher din søgning.',
'placeholder' => 'Vælg en indstilling',
'search_prompt' => 'Begynd at skrive for at søge ...',
],
'tags_input' => [
'placeholder' => 'Ny mærkat',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Keine Optionen passen zu Ihrer Suche.',
'placeholder' => 'Wählen Sie eine Option',
'search_prompt' => 'Es stehen keine Optionen zur Auswahl.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Verlinken',
'unlink' => 'Verlinkung aufheben',
],
'label' => 'URL',
'placeholder' => 'URL eingeben',
],
],
'toolbar_buttons' => [
'attach_files' => 'Dateien anhängen',
'blockquote' => 'Zitat',
'bold' => 'Fett',
'bullet_list' => 'Aufzählung',
'code_block' => 'codeBlock',
'h1' => 'Titel',
'h2' => 'Überschrift',
'h3' => 'Unterüberschrift',
'italic' => 'kursiv',
'link' => 'Link',
'ordered_list' => 'Nummerierte Aufzählung',
'redo' => 'Wiederholen',
'strike' => 'Durchgestrichen',
'undo' => 'Rückgängig',
],
],
'select' => [
'no_search_results_message' => 'Keine Optionen passen zu Ihrer Suche.',
'placeholder' => 'Wählen Sie eine Option',
'search_prompt' => 'Beginnen Sie mit der Eingabe, um zu suchen...',
],
'tags_input' => [
'placeholder' => 'Neues Etikett',
],
];
@@ -0,0 +1,162 @@
<?php
return [
'builder' => [
'buttons' => [
'create_item' => [
'label' => 'Add to :label',
],
'create_item_between' => [
'label' => 'Add between',
],
'delete_item' => [
'label' => 'Delete',
],
'move_item_down' => [
'label' => 'Move down',
],
'move_item_up' => [
'label' => 'Move up',
],
],
],
'key_value' => [
'buttons' => [
'add' => [
'label' => 'Add Row',
],
'delete' => [
'label' => 'Delete Row',
],
'sort' => [
'label' => 'Hold to Sort',
],
],
'fields' => [
'key' => [
'label' => 'Key',
'placeholder' => 'Enter a key...',
],
'value' => [
'label' => 'Value',
'placeholder' => 'Enter a value...',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => 'Attach files',
'bold' => 'Bold',
'bullet_list' => 'Bullet list',
'code_block' => 'Code block',
'edit' => 'Edit',
'italic' => 'Italic',
'link' => 'Link',
'ordered_list' => 'Numbered list',
'preview' => 'Preview',
'strike' => 'Strikethrough',
],
],
'multi_select' => [
'no_search_results_message' => 'No options match your search.',
'placeholder' => 'Select an option',
'search_prompt' => 'Start typing to search...',
],
'repeater' => [
'buttons' => [
'create_item' => [
'label' => 'Add to :label',
],
'delete_item' => [
'label' => 'Delete',
],
'move_item_down' => [
'label' => 'Move down',
],
'move_item_up' => [
'label' => 'Move up',
],
],
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Link',
'unlink' => 'Unlink',
],
'label' => 'URL',
'placeholder' => 'Enter a URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Attach files',
'blockquote' => 'Blockquote',
'bold' => 'Bold',
'bullet_list' => 'Bullet list',
'code_block' => 'Code block',
'h1' => 'Title',
'h2' => 'Heading',
'h3' => 'Subheading',
'italic' => 'Italic',
'link' => 'Link',
'ordered_list' => 'Numbered list',
'redo' => 'Redo',
'strike' => 'Strikethrough',
'undo' => 'Undo',
],
],
'select' => [
'no_search_results_message' => 'No options match your search.',
'placeholder' => 'Select an option',
'search_prompt' => 'Start typing to search...',
],
'tags_input' => [
'placeholder' => 'New tag',
],
];
@@ -0,0 +1,110 @@
<?php
return [
'key_value' => [
'buttons' => [
'add' => [
'label' => 'Añadir Fila',
],
'delete' => [
'label' => 'Eliminar Fila',
],
'sort' => [
'label' => 'Mantener para Reordenar',
],
],
'fields' => [
'key' => [
'label' => 'Clave',
'placeholder' => 'Introduce una clave...',
],
'value' => [
'label' => 'Valor',
'placeholder' => 'Introduce un valor...',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => 'Adjuntar archivos',
'bold' => 'Negrita',
'bullet_list' => 'Viñetas',
'code_block' => 'Código',
'edit' => 'Escribir',
'italic' => 'Cursiva',
'link' => 'Link',
'ordered_list' => 'Números',
'preview' => 'Vista previa',
'strike' => 'Tachado',
],
],
'multi_select' => [
'no_search_results_message' => 'No se encontranron coincidencias con su búsqueda.',
'placeholder' => 'Seleccionar una opción',
'search_prompt' => 'No hay opciones para elegir.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Link',
'unlink' => 'Unlink',
],
'label' => 'URL',
'placeholder' => 'Insertar una URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Adjuntar archivos',
'blockquote' => 'Cita',
'bold' => 'Negrita',
'bullet_list' => 'Viñetas',
'code_block' => 'Código',
'h1' => 'Título',
'h2' => 'Encabezado',
'h3' => 'Subtítulo',
'italic' => 'Cursiva',
'link' => 'Link',
'ordered_list' => 'Números',
'redo' => 'Rehacer',
'strike' => 'Tachado',
'undo' => 'Deshacer',
],
],
'select' => [
'no_search_results_message' => 'No se encontraron coincidencias con su búsqueda.',
'placeholder' => 'Selecciona una opción',
'search_prompt' => 'Escribe para buscar...',
],
'tags_input' => [
'placeholder' => 'Nueva etiqueta',
],
];
@@ -0,0 +1,110 @@
<?php
return [
'key_value' => [
'buttons' => [
'add' => [
'label' => 'اضافه ردیف',
],
'delete' => [
'label' => 'حذف ریدیف',
],
'sort' => [
'label' => 'نگهدارید تا مرتب شود',
],
],
'fields' => [
'key' => [
'label' => 'کلید',
'placeholder' => 'یک کلید وارید کنید...',
],
'value' => [
'label' => 'مقدار',
'placeholder' => 'یک مقدار وارید کنید...',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => 'پوستن فایلها',
'bold' => 'پر رنگ',
'bullet_list' => 'گولوله',
'code_block' => 'کود',
'edit' => 'نوشتن',
'italic' => 'خوابیده',
'link' => 'ارتباط',
'ordered_list' => 'اعداد',
'preview' => 'پیش نمایش',
'strike' => 'خط زده',
],
],
'multi_select' => [
'no_search_results_message' => 'هیچ گزینه ای با جستجوی شما مطابقت ندارد',
'placeholder' => 'یک گزینه را انتخاب کنید',
'search_prompt' => 'برای جستجو تایپ کنید...',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'ارتباط',
'unlink' => 'لغو ارتباط',
],
'label' => 'آدرس انترنیتی',
'placeholder' => 'یک آدرس انترنیتی وارید کنید',
],
],
'toolbar_buttons' => [
'attach_files' => 'پیوستن فایلها',
'blockquote' => 'نقل قول',
'bold' => 'پررنگ',
'bullet_list' => 'گولوله',
'code_block' => 'کود',
'h1' => 'عنوان',
'h2' => 'عنوان برجسته',
'h3' => 'زیرنویس',
'italic' => 'خوابیده',
'link' => 'ارتباط',
'ordered_list' => 'اعداد',
'redo' => 'انجام مجدد',
'strike' => 'خط زده',
'undo' => 'واگرد',
],
],
'select' => [
'no_search_results_message' => 'هیچ گزینه ای با جستجوی شما مطابقت ندارد',
'placeholder' => 'یک گزینه را انتخاب کنید',
'search_prompt' => 'برای جستجو تایپ کنید...',
],
'tags_input' => [
'placeholder' => 'تگ جدید',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Aucune option ne correspond à votre recherche.',
'placeholder' => 'Sélectionnez une option',
'search_prompt' => 'Aucune option à choisir.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Lien',
'unlink' => 'Dissocier',
],
'label' => 'URL',
'placeholder' => 'Entrez une URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Joindre fichiers',
'blockquote' => 'Citation',
'bold' => 'Gras',
'bullet_list' => 'Points',
'code_block' => 'codeBlock',
'h1' => 'Titre',
'h2' => 'Titre',
'h3' => 'Sous-titre',
'italic' => 'Italique',
'link' => 'Lien',
'ordered_list' => 'Nombres',
'redo' => 'Refaire',
'strike' => 'Barré',
'undo' => 'Annuler',
],
],
'select' => [
'no_search_results_message' => 'Aucune option ne correspond à votre recherche.',
'placeholder' => 'Sélectionnez une option',
'search_prompt' => 'Commencez à taper pour rechercher...',
],
'tags_input' => [
'placeholder' => 'Nouveau tag',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Tidak ada opsi yang sesuai pencarian anda.',
'placeholder' => 'Pilih Opsi',
'search_prompt' => 'Tidak ada opsi yang bisa dipilih.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Tautan',
'unlink' => 'Batalkan tautan',
],
'label' => 'Tautan',
'placeholder' => 'Masukkan tautan',
],
],
'toolbar_buttons' => [
'attach_files' => 'Lampirkan File',
'blockquote' => 'Quote',
'bold' => 'Bold',
'bullet_list' => 'bullet_list',
'code_block' => 'Kode',
'h1' => 'Judul',
'h2' => 'Judul',
'h3' => 'Anak judul',
'italic' => 'Italic',
'link' => 'Tautan',
'ordered_list' => 'Nomor',
'redo' => 'Redo',
'strike' => 'Coret',
'undo' => 'Undo',
],
],
'select' => [
'no_search_results_message' => 'Tidak ada opsi yang sesuai pencarian anda.',
'placeholder' => 'Pilih salah satu opsi',
'search_prompt' => 'Mulai mengetik untuk mencari...',
],
'tags_input' => [
'placeholder' => 'Tag baru',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Nessuna opzione trovata per la ricerca.',
'placeholder' => 'Seleziona un\'opzione',
'search_prompt' => 'Nessuna opzione disponibile.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Collega',
'unlink' => 'Rimuovi',
],
'label' => 'URL',
'placeholder' => 'Inserisci un URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Allega file',
'blockquote' => 'Citazione',
'bold' => 'Grassetto',
'bullet_list' => 'Elenco puntato',
'code_block' => 'Codice',
'h1' => 'Titolo',
'h2' => 'Testata',
'h3' => 'Sottotestata',
'italic' => 'Corsivo',
'link' => 'Link',
'ordered_list' => 'Numeri',
'redo' => 'Rifai',
'strike' => 'Barrato',
'undo' => 'Annulla',
],
],
'select' => [
'no_search_results_message' => 'Nessuna opzione trovata per la ricerca.',
'placeholder' => 'Seleziona un\'opzione',
'search_prompt' => 'Digita per cercare...',
],
'tags_input' => [
'placeholder' => 'Nuovo tag',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'ამ პარამეტრებით არცერთი ჩანაწერი არ მოიძებნა.',
'placeholder' => 'აირჩიეთ ჩანაწერი',
'search_prompt' => 'ჩანაწერების სია ცარიელია.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'დამატება',
'unlink' => 'გაუქმება',
],
'label' => 'ლინკი',
'placeholder' => 'ჩაწერეთ URL მისამართი',
],
],
'toolbar_buttons' => [
'attach_files' => 'ფაილების დამატება',
'blockquote' => 'ციტატა',
'bold' => 'მსხვილი',
'bullet_list' => 'სია',
'code_block' => 'კოდი',
'h1' => 'სათაური',
'h2' => 'სათაური',
'h3' => 'ქვესათაური',
'italic' => 'დახრილი',
'link' => 'ლინკი',
'ordered_list' => 'რიცხვები',
'redo' => 'Redo',
'strike' => 'ხაზგადასმული',
'undo' => 'Undo',
],
],
'select' => [
'no_search_results_message' => 'ამ პარამეტრებით არცერთი ჩანაწერი არ მოიძებნა.',
'placeholder' => 'აირჩიეთ ჩანაწერი',
'search_prompt' => 'ძიება...',
],
'tags_input' => [
'placeholder' => 'ახალი ტეგი',
],
];
@@ -0,0 +1,110 @@
<?php
return [
'key_value' => [
'buttons' => [
'add' => [
'label' => 'زیادکردنی ڕیز',
],
'delete' => [
'label' => 'سڕینەوەی ڕیز',
],
'sort' => [
'label' => 'ڕایگرە بۆ ڕێکخستن',
],
],
'fields' => [
'key' => [
'label' => 'کلیل',
'placeholder' => 'کلیلێک بنوسە ...',
],
'value' => [
'label' => 'Value',
'placeholder' => 'نرخێک بنوسە',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => 'لکاندنی فایل',
'bold' => 'ڕەشکردن',
'bullet_list' => 'خاڵ',
'code_block' => 'جۆد',
'edit' => 'نوسین',
'italic' => 'لارکردنەوە',
'link' => 'لینک',
'ordered_list' => 'ژمارە',
'preview' => 'بینین',
'strike' => 'هێڵ بەسەردا',
],
],
'multi_select' => [
'no_search_results_message' => 'هیچ هەڵبژاردەیەک نییە وەک گەڕانەکەت.',
'placeholder' => 'هەڵبژاردەیەک دیاریبکە',
'search_prompt' => 'هیچ هەڵبژاردەیەک نییە تا دیاریبکەیت',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'لینک',
'unlink' => 'لیکن لابردن',
],
'label' => 'لینک',
'placeholder' => 'لینکێک بنوسە',
],
],
'toolbar_buttons' => [
'attach_files' => 'لکاندنی فایل',
'blockquote' => 'کۆت',
'bold' => 'ڕەشکردن',
'bullet_list' => 'خاڵ',
'code_block' => 'کۆد',
'h1' => 'ناونیشان',
'h2' => 'هێدەر',
'h3' => 'ژێر هێدەر',
'italic' => 'لارکردنەوە',
'link' => 'لینک',
'ordered_list' => 'ژمارە',
'redo' => 'بیکەرەوە',
'strike' => 'هێڵ بەسەردا',
'undo' => 'گەڕانەوە',
],
],
'select' => [
'no_search_results_message' => 'هیچ هەڵبژاردەیەک نەدۆزرایەوە بۆ گەڕانەکەت.',
'placeholder' => 'هەڵبژاردەیەک دیاری بکە',
'search_prompt' => 'دەستبکە بە نوسین بۆ گەڕان ...',
],
'tags_input' => [
'placeholder' => 'تاگی نوێ',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Er zijn geen resultaten voor je zoekopdracht.',
'placeholder' => 'Selecteer een optie',
'search_prompt' => 'Geen opties om te selecteren.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Link',
'unlink' => 'Unlink',
],
'label' => 'URL',
'placeholder' => 'Voer een URL in',
],
],
'toolbar_buttons' => [
'attach_files' => 'Voeg bestanden toe',
'blockquote' => 'Quote',
'bold' => 'Dikgedrukt',
'bullet_list' => 'Bullets',
'code_block' => 'codeBlock',
'h1' => 'Titel',
'h2' => 'Hoofdtekst',
'h3' => 'Sub hoofdtekst',
'italic' => 'Schuingedrukt',
'link' => 'Link',
'ordered_list' => 'Nummers',
'redo' => 'Opnieuw doen',
'strike' => 'Doorgehaald',
'undo' => 'Ongedaan maken',
],
],
'select' => [
'no_search_results_message' => 'Er zijn geen resultaten voor je zoekopdracht.',
'placeholder' => 'Selecteer een optie',
'search_prompt' => 'Start met typen om te zoeken...',
],
'tags_input' => [
'placeholder' => 'Nieuwe tag',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'Nenhuma opção corresponde à sua pesquisa.',
'placeholder' => 'Selecione uma opção',
'search_prompt' => 'Sem opções disponívels.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Link',
'unlink' => 'Unlink',
],
'label' => 'URL',
'placeholder' => 'Digite uma URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Anexar arquivos',
'blockquote' => 'Citar',
'bold' => 'Negrito',
'bullet_list' => 'Marcadores',
'code_block' => 'codeBlock',
'h1' => 'Título',
'h2' => 'Cabeçalho',
'h3' => 'Subtítulo',
'italic' => 'Itálico',
'link' => 'Link',
'ordered_list' => 'Números',
'redo' => 'Refazer',
'strike' => 'Tachado',
'undo' => 'Desfazer',
],
],
'select' => [
'no_search_results_message' => 'Nenhuma opção corresponde à sua pesquisa.',
'placeholder' => 'Selecione uma opção',
'search_prompt' => 'Comece a digitar para pesquisar...',
],
'tags_input' => [
'placeholder' => 'Novo rótulo',
],
];
@@ -0,0 +1,59 @@
<?php
return [
'multi_select' => [
'no_search_results_message' => 'не найдено результатов по вашему поиску.',
'placeholder' => 'выберите опцию',
'search_prompt' => 'не найдено вариантов для выбора.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'ссылка',
'unlink' => 'Убрать ссылку',
],
'label' => 'URL',
'placeholder' => 'Введите URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Прикрепить файлы',
'blockquote' => 'Цитата',
'bold' => 'Жирный',
'bullet_list' => 'маркировочный список',
'code_block' => 'код',
'h1' => 'заглавие',
'h2' => 'заголовок',
'h3' => 'подзаголовок',
'italic' => 'Курсив',
'link' => 'ссылка',
'ordered_list' => 'нумерованный список',
'redo' => 'Повторить',
'strike' => 'Зачеркнутый',
'undo' => 'Отменить',
],
],
'select' => [
'no_search_results_message' => 'не найдено результатов по вашему поиску',
'placeholder' => 'выберите опцию',
'search_prompt' => 'введите текст для поиска',
],
'tags_input' => [
'placeholder' => 'новый тег',
],
];
@@ -0,0 +1,110 @@
<?php
return [
'key_value' => [
'buttons' => [
'add' => [
'label' => 'Ny Rad',
],
'delete' => [
'label' => 'Radera Rad',
],
'sort' => [
'label' => 'Tryck, ordna',
],
],
'fields' => [
'key' => [
'label' => 'Namn',
'placeholder' => 'Ange namn...',
],
'value' => [
'label' => 'Värde',
'placeholder' => 'Ange ett värde...',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => 'Lägg till filer',
'bold' => 'Fet',
'bullet_list' => 'Lista',
'code_block' => 'Kod',
'edit' => 'Skriv',
'italic' => 'Kursiv',
'link' => 'Länk',
'ordered_list' => 'Nummer',
'preview' => 'Förhandsvisa',
'strike' => 'Överstruken',
],
],
'multi_select' => [
'no_search_results_message' => 'Hittade inget som matchade din sökning.',
'placeholder' => 'Välj ett alternativ',
'search_prompt' => 'Finns inga alternativ.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => 'Länka',
'unlink' => 'Radera länk',
],
'label' => 'URL',
'placeholder' => 'Ange en URL',
],
],
'toolbar_buttons' => [
'attach_files' => 'Lägg till filer',
'blockquote' => 'Citat',
'bold' => 'Fet',
'bullet_list' => 'Lista',
'code_block' => 'Kod',
'h1' => 'Titel',
'h2' => 'Rubrik',
'h3' => 'Underrubrik',
'italic' => 'Kursiv',
'link' => 'Länk',
'ordered_list' => 'Nummer',
'redo' => 'Igen',
'strike' => 'Överstruken',
'undo' => 'Ångra',
],
],
'select' => [
'no_search_results_message' => 'Hittar inget som matchar din sökning.',
'placeholder' => 'Välj ett alternativ',
'search_prompt' => 'Börja skriva för att söka...',
],
'tags_input' => [
'placeholder' => 'Ny tagg',
],
];
@@ -0,0 +1,110 @@
<?php
return [
'key_value' => [
'buttons' => [
'add' => [
'label' => '添加行',
],
'delete' => [
'label' => '删除行',
],
'sort' => [
'label' => '排序',
],
],
'fields' => [
'key' => [
'label' => '键名',
'placeholder' => '输入键名...',
],
'value' => [
'label' => '值',
'placeholder' => '输入值...',
],
],
],
'markdown_editor' => [
'toolbar_buttons' => [
'attach_files' => '附件',
'bold' => '加粗',
'bullet_list' => '普通列表',
'code_block' => '代码',
'edit' => 'Edit',
'italic' => '斜体',
'link' => '链接',
'ordered_list' => '数字列表',
'preview' => '预览',
'strike' => '文字中线',
],
],
'multi_select' => [
'no_search_results_message' => '没有选项匹配您的搜索.',
'placeholder' => '请选择',
'search_prompt' => '没有可供选择的选项.',
],
'rich_editor' => [
'dialogs' => [
'link' => [
'buttons' => [
'link' => '链接',
'unlink' => '取消链接',
],
'label' => 'URL',
'placeholder' => '输入URL',
],
],
'toolbar_buttons' => [
'attach_files' => '附件',
'blockquote' => '引用',
'bold' => '加粗',
'bullet_list' => '普通列表',
'code_block' => '代码',
'h1' => '标题',
'h2' => '标题',
'h3' => '副标题',
'italic' => '斜体',
'link' => '链接',
'ordered_list' => '数字列表',
'redo' => '下一步',
'strike' => '文字中线',
'undo' => '上一步',
],
],
'select' => [
'no_search_results_message' => '没有选项匹配您的搜索.',
'placeholder' => '请选择',
'search_prompt' => '搜索...',
],
'tags_input' => [
'placeholder' => '新标签',
],
];
@@ -0,0 +1,177 @@
<div @class([
'grid gap-6',
'grid-cols-1' => $getColumns('default') === 1,
'grid-cols-2' => $getColumns('default') === 2,
'grid-cols-3' => $getColumns('default') === 3,
'grid-cols-4' => $getColumns('default') === 4,
'grid-cols-5' => $getColumns('default') === 5,
'grid-cols-6' => $getColumns('default') === 6,
'grid-cols-7' => $getColumns('default') === 7,
'grid-cols-8' => $getColumns('default') === 8,
'grid-cols-9' => $getColumns('default') === 9,
'grid-cols-10' => $getColumns('default') === 10,
'grid-cols-11' => $getColumns('default') === 11,
'grid-cols-12' => $getColumns('default') === 12,
'sm:grid-cols-1' => $getColumns('sm') === 1,
'sm:grid-cols-2' => $getColumns('sm') === 2,
'sm:grid-cols-3' => $getColumns('sm') === 3,
'sm:grid-cols-4' => $getColumns('sm') === 4,
'sm:grid-cols-5' => $getColumns('sm') === 5,
'sm:grid-cols-6' => $getColumns('sm') === 6,
'sm:grid-cols-7' => $getColumns('sm') === 7,
'sm:grid-cols-8' => $getColumns('sm') === 8,
'sm:grid-cols-9' => $getColumns('sm') === 9,
'sm:grid-cols-10' => $getColumns('sm') === 10,
'sm:grid-cols-11' => $getColumns('sm') === 11,
'sm:grid-cols-12' => $getColumns('sm') === 12,
'md:grid-cols-1' => $getColumns('md') === 1,
'md:grid-cols-2' => $getColumns('md') === 2,
'md:grid-cols-3' => $getColumns('md') === 3,
'md:grid-cols-4' => $getColumns('md') === 4,
'md:grid-cols-5' => $getColumns('md') === 5,
'md:grid-cols-6' => $getColumns('md') === 6,
'md:grid-cols-7' => $getColumns('md') === 7,
'md:grid-cols-8' => $getColumns('md') === 8,
'md:grid-cols-9' => $getColumns('md') === 9,
'md:grid-cols-10' => $getColumns('md') === 10,
'md:grid-cols-11' => $getColumns('md') === 11,
'md:grid-cols-12' => $getColumns('md') === 12,
'lg:grid-cols-1' => $getColumns('lg') === 1,
'lg:grid-cols-2' => $getColumns('lg') === 2,
'lg:grid-cols-3' => $getColumns('lg') === 3,
'lg:grid-cols-4' => $getColumns('lg') === 4,
'lg:grid-cols-5' => $getColumns('lg') === 5,
'lg:grid-cols-6' => $getColumns('lg') === 6,
'lg:grid-cols-7' => $getColumns('lg') === 7,
'lg:grid-cols-8' => $getColumns('lg') === 8,
'lg:grid-cols-9' => $getColumns('lg') === 9,
'lg:grid-cols-10' => $getColumns('lg') === 10,
'lg:grid-cols-11' => $getColumns('lg') === 11,
'lg:grid-cols-12' => $getColumns('lg') === 12,
'xl:grid-cols-1' => $getColumns('xl') === 1,
'xl:grid-cols-2' => $getColumns('xl') === 2,
'xl:grid-cols-3' => $getColumns('xl') === 3,
'xl:grid-cols-4' => $getColumns('xl') === 4,
'xl:grid-cols-5' => $getColumns('xl') === 5,
'xl:grid-cols-6' => $getColumns('xl') === 6,
'xl:grid-cols-7' => $getColumns('xl') === 7,
'xl:grid-cols-8' => $getColumns('xl') === 8,
'xl:grid-cols-9' => $getColumns('xl') === 9,
'xl:grid-cols-10' => $getColumns('xl') === 10,
'xl:grid-cols-11' => $getColumns('xl') === 11,
'xl:grid-cols-12' => $getColumns('xl') === 12,
'2xl:grid-cols-1' => $getColumns('2xl') === 1,
'2xl:grid-cols-2' => $getColumns('2xl') === 2,
'2xl:grid-cols-3' => $getColumns('2xl') === 3,
'2xl:grid-cols-4' => $getColumns('2xl') === 4,
'2xl:grid-cols-5' => $getColumns('2xl') === 5,
'2xl:grid-cols-6' => $getColumns('2xl') === 6,
'2xl:grid-cols-7' => $getColumns('2xl') === 7,
'2xl:grid-cols-8' => $getColumns('2xl') === 8,
'2xl:grid-cols-9' => $getColumns('2xl') === 9,
'2xl:grid-cols-10' => $getColumns('2xl') === 10,
'2xl:grid-cols-11' => $getColumns('2xl') === 11,
'2xl:grid-cols-12' => $getColumns('2xl') === 12,
])>
@foreach ($getComponents() as $formComponent)
<div @class([
'max-w-xs' => $formComponent->getMaxWidth() === 'xs',
'max-w-sm' => $formComponent->getMaxWidth() === 'sm',
'max-w-md' => $formComponent->getMaxWidth() === 'md',
'max-w-lg' => $formComponent->getMaxWidth() === 'lg',
'max-w-xl' => $formComponent->getMaxWidth() === 'xl',
'max-w-2xl' => $formComponent->getMaxWidth() === '2xl',
'max-w-3xl' => $formComponent->getMaxWidth() === '3xl',
'max-w-4xl' => $formComponent->getMaxWidth() === '4xl',
'max-w-5xl' => $formComponent->getMaxWidth() === '5xl',
'max-w-6xl' => $formComponent->getMaxWidth() === '6xl',
'max-w-7xl' => $formComponent->getMaxWidth() === '7xl',
'col-span-1' => $formComponent->getColumnSpan('default') === 1,
'col-span-2' => $formComponent->getColumnSpan('default') === 2,
'col-span-3' => $formComponent->getColumnSpan('default') === 3,
'col-span-4' => $formComponent->getColumnSpan('default') === 4,
'col-span-5' => $formComponent->getColumnSpan('default') === 5,
'col-span-6' => $formComponent->getColumnSpan('default') === 6,
'col-span-7' => $formComponent->getColumnSpan('default') === 7,
'col-span-8' => $formComponent->getColumnSpan('default') === 8,
'col-span-9' => $formComponent->getColumnSpan('default') === 9,
'col-span-10' => $formComponent->getColumnSpan('default') === 10,
'col-span-11' => $formComponent->getColumnSpan('default') === 11,
'col-span-12' => $formComponent->getColumnSpan('default') === 12,
'col-span-full' => $formComponent->getColumnSpan('default') === 'full',
$classes = $formComponent->getColumnSpan('default') => is_string($classes) && $classes !== 'full',
'sm:col-span-1' => $formComponent->getColumnSpan('sm') === 1,
'sm:col-span-2' => $formComponent->getColumnSpan('sm') === 2,
'sm:col-span-3' => $formComponent->getColumnSpan('sm') === 3,
'sm:col-span-4' => $formComponent->getColumnSpan('sm') === 4,
'sm:col-span-5' => $formComponent->getColumnSpan('sm') === 5,
'sm:col-span-6' => $formComponent->getColumnSpan('sm') === 6,
'sm:col-span-7' => $formComponent->getColumnSpan('sm') === 7,
'sm:col-span-8' => $formComponent->getColumnSpan('sm') === 8,
'sm:col-span-9' => $formComponent->getColumnSpan('sm') === 9,
'sm:col-span-10' => $formComponent->getColumnSpan('sm') === 10,
'sm:col-span-11' => $formComponent->getColumnSpan('sm') === 11,
'sm:col-span-12' => $formComponent->getColumnSpan('sm') === 12,
'sm:col-span-full' => $formComponent->getColumnSpan('sm') === 'full',
$classes = $formComponent->getColumnSpan('sm') => is_string($classes) && $classes !== 'full',
'md:col-span-1' => $formComponent->getColumnSpan('md') === 1,
'md:col-span-2' => $formComponent->getColumnSpan('md') === 2,
'md:col-span-3' => $formComponent->getColumnSpan('md') === 3,
'md:col-span-4' => $formComponent->getColumnSpan('md') === 4,
'md:col-span-5' => $formComponent->getColumnSpan('md') === 5,
'md:col-span-6' => $formComponent->getColumnSpan('md') === 6,
'md:col-span-7' => $formComponent->getColumnSpan('md') === 7,
'md:col-span-8' => $formComponent->getColumnSpan('md') === 8,
'md:col-span-9' => $formComponent->getColumnSpan('md') === 9,
'md:col-span-10' => $formComponent->getColumnSpan('md') === 10,
'md:col-span-11' => $formComponent->getColumnSpan('md') === 11,
'md:col-span-12' => $formComponent->getColumnSpan('md') === 12,
'md:col-span-full' => $formComponent->getColumnSpan('md') === 'full',
$classes = $formComponent->getColumnSpan('md') => is_string($classes) && $classes !== 'full',
'lg:col-span-1' => $formComponent->getColumnSpan('lg') === 1,
'lg:col-span-2' => $formComponent->getColumnSpan('lg') === 2,
'lg:col-span-3' => $formComponent->getColumnSpan('lg') === 3,
'lg:col-span-4' => $formComponent->getColumnSpan('lg') === 4,
'lg:col-span-5' => $formComponent->getColumnSpan('lg') === 5,
'lg:col-span-6' => $formComponent->getColumnSpan('lg') === 6,
'lg:col-span-7' => $formComponent->getColumnSpan('lg') === 7,
'lg:col-span-8' => $formComponent->getColumnSpan('lg') === 8,
'lg:col-span-9' => $formComponent->getColumnSpan('lg') === 9,
'lg:col-span-10' => $formComponent->getColumnSpan('lg') === 10,
'lg:col-span-11' => $formComponent->getColumnSpan('lg') === 11,
'lg:col-span-12' => $formComponent->getColumnSpan('lg') === 12,
'lg:col-span-full' => $formComponent->getColumnSpan('lg') === 'full',
$classes = $formComponent->getColumnSpan('lg') => is_string($classes) && $classes !== 'full',
'xl:col-span-1' => $formComponent->getColumnSpan('xl') === 1,
'xl:col-span-2' => $formComponent->getColumnSpan('xl') === 2,
'xl:col-span-3' => $formComponent->getColumnSpan('xl') === 3,
'xl:col-span-4' => $formComponent->getColumnSpan('xl') === 4,
'xl:col-span-5' => $formComponent->getColumnSpan('xl') === 5,
'xl:col-span-6' => $formComponent->getColumnSpan('xl') === 6,
'xl:col-span-7' => $formComponent->getColumnSpan('xl') === 7,
'xl:col-span-8' => $formComponent->getColumnSpan('xl') === 8,
'xl:col-span-9' => $formComponent->getColumnSpan('xl') === 9,
'xl:col-span-10' => $formComponent->getColumnSpan('xl') === 10,
'xl:col-span-11' => $formComponent->getColumnSpan('xl') === 11,
'xl:col-span-12' => $formComponent->getColumnSpan('xl') === 12,
'xl:col-span-full' => $formComponent->getColumnSpan('xl') === 'full',
$classes = $formComponent->getColumnSpan('xl') => is_string($classes) && $classes !== 'full',
'2xl:col-span-1' => $formComponent->getColumnSpan('2xl') === 1,
'2xl:col-span-2' => $formComponent->getColumnSpan('2xl') === 2,
'2xl:col-span-3' => $formComponent->getColumnSpan('2xl') === 3,
'2xl:col-span-4' => $formComponent->getColumnSpan('2xl') === 4,
'2xl:col-span-5' => $formComponent->getColumnSpan('2xl') === 5,
'2xl:col-span-6' => $formComponent->getColumnSpan('2xl') === 6,
'2xl:col-span-7' => $formComponent->getColumnSpan('2xl') === 7,
'2xl:col-span-8' => $formComponent->getColumnSpan('2xl') === 8,
'2xl:col-span-9' => $formComponent->getColumnSpan('2xl') === 9,
'2xl:col-span-10' => $formComponent->getColumnSpan('2xl') === 10,
'2xl:col-span-11' => $formComponent->getColumnSpan('2xl') === 11,
'2xl:col-span-12' => $formComponent->getColumnSpan('2xl') === 12,
'2xl:col-span-full' => $formComponent->getColumnSpan('2xl') === 'full',
$classes = $formComponent->getColumnSpan('2xl') => is_string($classes) && $classes !== 'full',
])>
{{ $formComponent }}
</div>
@endforeach
</div>
@@ -0,0 +1,163 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div class="space-y-4">
@if (count($containers = $getChildComponentContainers()))
<ul>
@foreach ($containers as $uuid => $item)
<li
x-data="{ isCreateButtonDropdownOpen: false, isCreateButtonVisible: false }"
x-on:click="isCreateButtonVisible = true"
x-on:click.away="isCreateButtonVisible = false"
wire:key="{{ $item->getStatePath() }}"
>
<div class="flex">
<div class="w-8">
<div class="bg-white divide-y shadow-sm rounded-l-lg border-b border-l border-t border-gray-300 overflow-hidden">
@unless ($loop->first || $isItemMovementDisabled())
<button
wire:click="dispatchFormEvent('builder.moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-gray-800 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.builder.buttons.move_item_up.label') }}
</span>
<x-heroicon-s-chevron-up class="w-5 h-5" />
</button>
@endunless
@unless ($loop->last || $isItemMovementDisabled())
<button
wire:click="dispatchFormEvent('builder.moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-gray-800 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.builder.buttons.move_item_down.label') }}
</span>
<x-heroicon-s-chevron-down class="w-5 h-5" />
</button>
@endunless
<button
wire:click="dispatchFormEvent('builder.deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-danger-600 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-danger-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.builder.buttons.delete_item.label') }}
</span>
<x-heroicon-s-trash class="w-5 h-5" />
</button>
</div>
</div>
<div class="flex-1 p-6 bg-white shadow-sm rounded-r-lg rounded-b-lg border border-gray-300">
{{ $item }}
</div>
</div>
@unless ($loop->last)
<div class="h-12 flex items-center justify-center">
<div
x-show="isCreateButtonVisible || isCreateButtonDropdownOpen"
x-transition
class="relative flex justify-center"
>
<button
x-on:click="isCreateButtonDropdownOpen = true"
type="button"
class="flex items-center justify-center h-8 w-8 rounded-full text-gray-800 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50"
x-bind:class="{
'bg-gray-50': isCreateButtonDropdownOpen,
}"
>
<span class="sr-only">
{{ $getCreateItemBetweenButtonLabel() }}
</span>
<x-heroicon-o-plus class="w-5 h-5" />
</button>
<div
x-show="isCreateButtonDropdownOpen"
x-on:click.away="isCreateButtonDropdownOpen = false"
x-transition
class="absolute z-10 mt-9 shadow-xl overflow-hidden rounded-xl w-52"
>
<ul class="py-1 space-y-1 bg-white shadow rounded-xl">
@foreach ($getBlocks() as $block)
<li>
<button
wire:click="dispatchFormEvent('builder.createItem', '{{ $getStatePath() }}', '{{ $block->getName() }}', '{{ $uuid }}')"
x-on:click="isCreateButtonDropdownOpen = false"
type="button"
class="flex items-center w-full h-8 px-3 text-sm font-medium focus:outline-none hover:text-white hover:bg-primary-600 focus:bg-primary-700 focus:text-white group"
>
@if ($icon = $block->getIcon())
<x-dynamic-component :component="$icon" class="mr-2 -ml-1 text-primary-500 w-5 h-5 group-hover:text-white group-focus:text-white" />
@endif
{{ $block->getLabel() }}
</button>
</li>
@endforeach
</ul>
</div>
</div>
</div>
@endunless
</li>
@endforeach
</ul>
@endif
<div x-data="{ isCreateButtonDropdownOpen: false }" class="relative flex justify-center">
<button
x-on:click="isCreateButtonDropdownOpen = true"
type="button"
class="w-full h-9 px-4 inline-flex items-center justify-center font-medium tracking-tight transition rounded-lg text-gray-800 bg-white border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<x-heroicon-s-plus class="w-6 h-6 mr-1 -ml-2" />
{{ $getCreateItemButtonLabel() }}
</button>
<div
x-show="isCreateButtonDropdownOpen"
x-on:click.away="isCreateButtonDropdownOpen = false"
x-transition
class="absolute z-10 mt-9 shadow-xl overflow-hidden rounded-xl w-52"
>
<ul class="py-1 space-y-1 bg-white shadow rounded-xl">
@foreach ($getBlocks() as $block)
<li>
<button
wire:click="dispatchFormEvent('builder.createItem', '{{ $getStatePath() }}', '{{ $block->getName() }}')"
x-on:click="isCreateButtonDropdownOpen = false"
type="button"
class="flex items-center w-full h-8 px-3 text-sm font-medium focus:outline-none hover:text-white hover:bg-primary-600 focus:bg-primary-700 focus:text-white group"
>
@if ($icon = $block->getIcon())
<x-dynamic-component :component="$icon" class="mr-2 -ml-1 text-primary-500 w-5 h-5 group-hover:text-white group-focus:text-white" />
@endif
{{ $block->getLabel() }}
</button>
</li>
@endforeach
</ul>
</div>
</div>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,3 @@
<div class="py-8">
{{ $getChildComponentContainer() }}
</div>
@@ -0,0 +1,6 @@
<div
{!! $getId() ? "id=\"{$getId()}\"" : null !!}
{{ $attributes->merge($getExtraAttributes())->class(['p-6 bg-white shadow rounded-xl']) }}
>
{{ $getChildComponentContainer() }}
</div>
@@ -0,0 +1,29 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
@if ($isInline())
<x-slot name="labelPrefix">
@endif
<input
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
id="{{ $getId() }}"
type="checkbox"
{!! $isRequired() ? 'required' : null !!}
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
{{ $attributes->merge($getExtraAttributes())->class([
'transition duration-75 rounded shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-300 ring-danger-500' => $errors->has($getStatePath()),
]) }}
/>
@if ($isInline())
</x-slot>
@endif
</x-forms::field-wrapper>
@@ -0,0 +1,204 @@
@once
@push('scripts')
<script src="//unpkg.com/dayjs@1.10.4/dayjs.min.js"></script>
<script src="//unpkg.com/dayjs@1.10.4/plugin/localeData.js"></script>
<script>
dayjs.extend(window.dayjs_plugin_localeData)
window.dayjs_locale = dayjs.locale()
</script>
<script src="//unpkg.com/dayjs@1.10.4/locale/{{ \Illuminate\Support\Str::of(app()->getLocale())->lower()->kebab() }}.js"></script>
@endpush
@endonce
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
x-data="dateTimePickerFormComponent({
displayFormat: '{{ convert_date_format($getDisplayFormat())->to('day.js') }}',
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
format: '{{ convert_date_format($getFormat())->to('day.js') }}',
isAutofocused: {{ $isAutofocused() ? 'true' : 'false' }},
maxDate: '{{ $getMaxDate() }}',
minDate: '{{ $getMinDate() }}',
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
x-on:click.away="closePicker()"
x-on:keydown.escape.stop="closePicker()"
x-on:blur="closePicker()"
class="relative"
{{ $attributes->merge($getExtraAttributes()) }}
>
<button
@unless($isDisabled())
x-ref="button"
x-on:click="togglePickerVisibility()"
x-on:keydown.enter.stop.prevent="open ? selectDate() : openPicker()"
x-on:keydown.arrow-left.stop.prevent="focusPreviousDay()"
x-on:keydown.arrow-right.stop.prevent="focusNextDay()"
x-on:keydown.arrow-up.stop.prevent="focusPreviousWeek()"
x-on:keydown.arrow-down.stop.prevent="focusNextWeek()"
x-on:keydown.backspace.stop.prevent="clearState()"
x-on:keydown.clear.stop.prevent="clearState()"
x-on:keydown.delete.stop.prevent="clearState()"
x-bind:aria-expanded="open"
aria-label="{{ $getPlaceholder() }}"
@endunless
type="button"
class="bg-white relative w-full border pl-3 pr-10 py-2 text-left cursor-default rounded-lg shadow-sm focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-inset focus-within:ring-primary-600 {{ $isDisabled() ? 'text-gray-500' : '' }} {{ $errors->has($getStatePath()) ? 'border-danger-600 motion-safe:animate-shake' : 'border-gray-300' }}"
>
<input
readonly
placeholder="{{ $getPlaceholder() }}"
x-model="displayText"
{!! ($id = $getId()) ? "id=\"{$id}\"" : null !!}
class="w-full h-full p-0 placeholder-gray-400 border-0 focus:placeholder-gray-500 focus:ring-0 focus:outline-none"
/>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</span>
</button>
@unless ($isDisabled())
<div
x-ref="picker"
x-on:click.away="closePicker()"
x-show.transition="open"
aria-modal="true"
role="dialog"
x-cloak
@class([
'absolute z-10 my-1 bg-white border border-gray-300 rounded-lg shadow-sm',
'p-4 w-64' => $hasDate(),
])
>
<div class="space-y-3">
@if ($hasDate())
<div class="flex items-center justify-between space-x-1 rtl:space-x-reverse">
<select
x-model="focusedMonth"
class="flex-grow p-0 text-lg font-medium text-gray-800 border-0 cursor-pointer focus:ring-0 focus:outline-none"
>
<template x-for="(month, index) in dayjs.months()">
<option x-bind:value="index" x-text="month"></option>
</template>
</select>
<input
type="number"
x-model.debounce="focusedYear"
class="w-20 p-0 text-lg text-right border-0 focus:ring-0 focus:outline-none"
/>
</div>
<div class="grid grid-cols-7 gap-1">
<template x-for="(day, index) in getDayLabels()" :key="index">
<div
x-text="day"
class="text-xs font-medium text-center text-gray-800"
></div>
</template>
</div>
<div role="grid" class="grid grid-cols-7 gap-1">
<template x-for="day in emptyDaysInFocusedMonth" x-bind:key="day">
<div class="text-sm text-center border border-transparent"></div>
</template>
<template x-for="day in daysInFocusedMonth" x-bind:key="day">
<div
x-text="day"
x-on:click="dayIsDisabled(day) || selectDate(day)"
x-on:mouseenter="setFocusedDay(day)"
role="option"
x-bind:aria-selected="focusedDate.date() === day"
x-bind:class="{
'text-gray-700': ! dayIsSelected(day),
'cursor-pointer': ! dayIsDisabled(day),
'bg-primary-50': dayIsToday(day) && ! dayIsSelected(day) && focusedDate.date() !== day && ! dayIsDisabled(day),
'bg-primary-200': focusedDate.date() === day && ! dayIsSelected(day),
'bg-primary-500 text-white': dayIsSelected(day),
'cursor-not-allowed': dayIsDisabled(day),
'opacity-50': focusedDate.date() !== day && dayIsDisabled(day),
}"
class="text-sm leading-none leading-loose text-center transition duration-100 ease-in-out rounded-full"
></div>
</template>
</div>
@endif
@if ($hasTime())
<div
@class([
'flex items-center justify-center py-2 rounded-lg',
'bg-gray-50' => $hasDate(),
])
>
<input
max="23"
min="0"
type="number"
inputmode="numeric"
x-model.debounce="hour"
@class([
'w-16 p-0 pr-1 text-xl text-center text-gray-700 border-0 focus:ring-0 focus:outline-none',
'bg-gray-50' => $hasDate(),
])
/>
<span
@class([
'text-xl font-medium text-gray-700',
'bg-gray-50' => $hasDate(),
])
>:</span>
<input
max="59"
min="0"
type="number"
inputmode="numeric"
x-model.debounce="minute"
@class([
'w-16 p-0 pr-1 text-xl text-center text-gray-700 border-0 focus:ring-0 focus:outline-none',
'bg-gray-50' => $hasDate(),
])
/>
@if ($hasSeconds())
<span
@class([
'text-xl font-medium text-gray-700',
'bg-gray-50' => $hasDate(),
])
>:</span>
<input
max="59"
min="0"
type="number"
inputmode="numeric"
x-model.debounce="second"
@class([
'w-16 p-0 pr-1 text-xl text-center text-gray-700 border-0 focus:ring-0 focus:outline-none',
'bg-gray-50' => $hasDate(),
])
/>
@endif
</div>
@endif
</div>
</div>
@endunless
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,3 @@
<p {{ $attributes->class(['text-sm text-danger-600']) }}>
{{ $slot }}
</p>
@@ -0,0 +1,3 @@
<div {{ $attributes->class(['text-sm text-gray-600']) }}>
{{ $slot }}
</div>
@@ -0,0 +1,3 @@
<div {{ $attributes->class(['text-xs leading-tight text-gray-500']) }}>
{{ $slot }}
</div>
@@ -0,0 +1,55 @@
@props([
'id',
'label' => null,
'labelPrefix' => null,
'labelSrOnly' => false,
'helperText' => null,
'hint' => null,
'required' => false,
'statePath',
])
<div {{ $attributes }}>
@if ($label && $labelSrOnly)
<label for="{{ $id }}" class="sr-only">
{{ $label }}
</label>
@endif
<div class="space-y-2">
@if (($label && ! $labelSrOnly) || $hint)
<div class="flex items-center justify-between space-x-2 rtl:space-x-reverse">
@if ($label && ! $labelSrOnly)
<x-forms::field-wrapper.label
:for="$id"
:error="$errors->has($statePath)"
:prefix="$labelPrefix"
:required="$required"
>
{{ $label }}
</x-forms::field-wrapper.label>
@endif
@if ($hint)
<x-forms::field-wrapper.hint>
{!! Str::of($hint)->markdown() !!}
</x-forms::field-wrapper.hint>
@endif
</div>
@endif
{{ $slot }}
@if ($errors->has($statePath))
<x-forms::field-wrapper.error-message>
{{ $errors->first($statePath) }}
</x-forms::field-wrapper.error-message>
@endif
@if ($helperText)
<x-forms::field-wrapper.helper-text>
{!! Str::of($helperText)->markdown() !!}
</x-forms::field-wrapper.helper-text>
@endif
</div>
</div>
@@ -0,0 +1,21 @@
@props([
'error' => false,
'prefix' => null,
'required' => false,
])
<label {{ $attributes->class(['inline-flex items-center space-x-3']) }}>
{{ $prefix }}
<span @class([
'text-sm font-medium leading-4',
'text-gray-700' => ! $error,
'text-danger-700' => $error,
])>
{{ $slot }}
@if ($required)
<sup class="font-medium text-danger-700">*</sup>
@endif
</span>
</label>
@@ -0,0 +1,10 @@
<fieldset
{!! $getId() ? "id=\"{$getId()}\"" : null !!}
{{ $attributes->merge($getExtraAttributes())->class(['rounded-xl shadow-sm border border-gray-300 p-6']) }}
>
<legend class="text-sm leading-tight font-medium px-2 -ml-2">
{{ $getLabel() }}
</legend>
{{ $getChildComponentContainer() }}
</fieldset>
@@ -0,0 +1,50 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isAvatar() || $isMultiple() || $isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
@class([
'w-32 mx-auto' => $isAvatar(),
])
x-data="fileUploadFormComponent({
acceptedFileTypes: {{ json_encode($getAcceptedFileTypes()) }},
getUploadedFileUrlUsing: async () => {
return await $wire.getUploadedFileUrl('{{ $getStatePath() }}')
},
imageCropAspectRatio: {{ ($aspectRatio = $getImageCropAspectRatio()) ? "'{$aspectRatio}'" : 'null' }},
imagePreviewHeight: {{ ($height = $getImagePreviewHeight()) ? "'{$height}'" : 'null' }},
imageResizeTargetHeight: {{ ($height = $getImageResizeTargetHeight()) ? "'{$height}'" : 'null' }},
imageResizeTargetWidth: {{ ($width = $getImageResizeTargetWidth()) ? "'{$width}'" : 'null' }},
loadingIndicatorPosition: '{{ $getLoadingIndicatorPosition() }}',
panelAspectRatio: {{ ($aspectRatio = $getPanelAspectRatio()) ? "'{$aspectRatio}'" : 'null' }},
panelLayout: {{ ($layout = $getPanelLayout()) ? "'{$layout}'" : 'null' }},
placeholder: {{ ($placeholder = $getPlaceholder()) ? "'{$placeholder}'" : 'null' }},
maxSize: {{ ($size = $getMaxSize()) ? "'{$size} KB'" : 'null' }},
minSize: {{ ($size = $getMinSize()) ? "'{$size} KB'" : 'null' }},
removeUploadedFileUsing: async (file = null) => {
return await $wire.removeUploadedFile('{{ $getStatePath() }}', file)
},
removeUploadedFileButtonPosition: '{{ $getRemoveUploadedFileButtonPosition() }}',
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
uploadButtonPosition: '{{ $getUploadButtonPosition() }}',
uploadProgressIndicatorPosition: '{{ $getUploadProgressIndicatorPosition() }}',
uploadUsing: async (file, load, error, progress) => {
return await $wire.upload('{{ $getStatePath() }}', file, load, error, progress)
},
})"
wire:ignore
{{ $attributes->merge($getExtraAttributes()) }}
>
<input
x-ref="input"
{{ $isDisabled() ? 'disabled' : '' }}
{!! ($id = $getId()) ? "id=\"{$id}\"" : null !!}
type="file"
/>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,36 @@
<div
x-data="FileUploadFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
statePath: '{{ $getStatePath() }}',
disk: '{{ $getDiskName() }}',
uploadedFileUrl: {{ ($url = $getUploadedFileUrl()) ? "'{$url}'" : 'null' }},
getUploadedFileUrlUsing: $wire.getUploadedFileUrl,
uploadUsing: $wire.upload,
removeTemporaryUploadedFileUsing: $wire.removeUpload,
removeUploadedFileUsing: async function (statePath) {
await $wire.set(statePath, null)
},
acceptedFileTypes: {{ json_encode($getAcceptedFileTypes()) }},
imageCropAspectRatio: {{ ($aspectRatio = $getImageCropAspectRatio()) ? "'{$aspectRatio}'" : 'null' }},
imagePreviewHeight: {{ ($height = $getImagePreviewHeight()) ? "'{$height}'" : 'null' }},
imageResizeTargetHeight: {{ ($height = $getImageResizeTargetHeight()) ? "'{$height}'" : 'null' }},
imageResizeTargetWidth: {{ ($width = $getImageResizeTargetWidth()) ? "'{$width}'" : 'null' }},
loadingIndicatorPosition: '{{ $getLoadingIndicatorPosition() }}',
panelAspectRatio: {{ ($aspectRatio = $getPanelAspectRatio()) ? "'{$aspectRatio}'" : 'null' }},
panelLayout: {{ ($layout = $getPanelLayout()) ? "'{$layout}'" : 'null' }},
placeholder: {{ ($placeholder = $getPlaceholder()) ? "'{$placeholder}'" : 'null' }},
maxSize: {{ ($size = $getMaxSize()) ? "'{$size} KB'" : 'null' }},
minSize: {{ ($size = $getMinSize()) ? "'{$size} KB'" : 'null' }},
removeUploadedFilePosition: '{{ $getRemoveUploadedFileButtonPosition() }}',
uploadButtonPosition: '{{ $getUploadButtonPosition() }}',
uploadProgressIndicatorPosition: '{{ $getUploadProgressIndicatorPosition() }}',
})"
wire:ignore
>
<input
x-ref="input"
{{ $isDisabled() ? 'disabled' : '' }}
id="{{ $getId() }}"
type="file"
/>
</div>
@@ -0,0 +1 @@
{{ $getChildComponentContainer() }}
@@ -0,0 +1 @@
{{ $getChildComponentContainer() }}
@@ -0,0 +1,7 @@
<input
id="{{ $getId() }}"
{!! $isRequired() ? 'required' : null !!}
type="hidden"
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
{{ $attributes->merge($getExtraAttributes()) }}
/>
@@ -0,0 +1,204 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
x-data="markdownEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
tab: '{{ $isDisabled() ? 'preview' : 'edit' }}',
})"
x-cloak
wire:ignore
>
<div class="space-y-2">
@unless ($isDisabled())
<div class="flex justify-between space-x-4 items-stretch h-8">
<markdown-toolbar
for="{{ $getId() }}"
x-bind:class="{ 'pointer-events-none opacity-75': tab === 'preview' }"
class="flex items-stretch space-x-4 rtl:space-x-reverse focus:outline-none"
>
@if ($hasToolbarButton(['bold', 'italic', 'strike']))
<div class="flex items-stretch space-x-1 rtl:space-x-reverse">
@if ($hasToolbarButton('bold'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.bold') }}"
>
<md-bold>
<svg class="w-4" viewBox="0 0 16 16" version="1.1" aria-hidden="true"><path fill-rule="evenodd" d="M4 2a1 1 0 00-1 1v10a1 1 0 001 1h5.5a3.5 3.5 0 001.852-6.47A3.5 3.5 0 008.5 2H4zm4.5 5a1.5 1.5 0 100-3H5v3h3.5zM5 9v3h4.5a1.5 1.5 0 000-3H5z"></path></svg>
</md-bold>
</x-forms::markdown-editor.toolbar-button>
@endif
@if ($hasToolbarButton('italic'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.italic') }}"
>
<md-italic>
<svg class="w-4" height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M6 2.75A.75.75 0 016.75 2h6.5a.75.75 0 010 1.5h-2.505l-3.858 9H9.25a.75.75 0 010 1.5h-6.5a.75.75 0 010-1.5h2.505l3.858-9H6.75A.75.75 0 016 2.75z"></path></svg>
</md-italic>
</x-forms::markdown-editor.toolbar-button>
@endif
@if ($hasToolbarButton('strike'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.strike') }}"
>
<md-strikethrough>
<svg class="w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M7.581 3.25c-2.036 0-2.778 1.082-2.778 1.786 0 .055.002.107.006.157a.75.75 0 01-1.496.114 3.56 3.56 0 01-.01-.271c0-1.832 1.75-3.286 4.278-3.286 1.418 0 2.721.58 3.514 1.093a.75.75 0 11-.814 1.26c-.64-.414-1.662-.853-2.7-.853zm3.474 5.25h3.195a.75.75 0 000-1.5H1.75a.75.75 0 000 1.5h6.018c.835.187 1.503.464 1.951.81.439.34.647.725.647 1.197 0 .428-.159.895-.594 1.267-.444.38-1.254.726-2.676.726-1.373 0-2.38-.493-2.86-.956a.75.75 0 00-1.042 1.079C3.992 13.393 5.39 14 7.096 14c1.652 0 2.852-.403 3.65-1.085a3.134 3.134 0 001.12-2.408 2.85 2.85 0 00-.811-2.007z"></path></svg>
</md-strikethrough>
</x-forms::markdown-editor.toolbar-button>
@endif
</div>
@endif
@if ($hasToolbarButton(['link', 'attachFiles', 'codeBlock']))
<div class="flex items-stretch space-x-1 rtl:space-x-reverse">
@if ($hasToolbarButton('link'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.link') }}"
>
<md-link class="w-full h-full">
<x-heroicon-o-link class="w-4" />
</md-link>
</x-forms::markdown-editor.toolbar-button>
@endif
@if ($hasToolbarButton('attachFiles'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.attach_files') }}"
>
<md-image class="w-full h-full" x-ref="imageTrigger">
<x-heroicon-o-photograph class="w-4" />
</md-image>
</x-forms::markdown-editor.toolbar-button>
@endif
@if ($hasToolbarButton('codeBlock'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.code_block') }}"
>
<md-code class="w-full h-full">
<x-heroicon-o-code class="w-4" />
</md-code>
</x-forms::markdown-editor.toolbar-button>
@endif
</div>
@endif
@if ($hasToolbarButton(['bulletList', 'orderedList']))
<div class="flex items-stretch space-x-1 rtl:space-x-reverse">
@if ($hasToolbarButton('bulletList'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.bullet_list') }}"
>
<md-unordered-list class="w-full h-full">
<svg height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M2 4a1 1 0 100-2 1 1 0 000 2zm3.75-1.5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zM3 8a1 1 0 11-2 0 1 1 0 012 0zm-1 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>
</md-unordered-list>
</x-forms::markdown-editor.toolbar-button>
@endif
@if ($hasToolbarButton('orderedList'))
<x-forms::markdown-editor.toolbar-button
title="{{ __('forms::components.markdown_editor.toolbar_buttons.ordered_list') }}"
>
<md-ordered-list class="w-full h-full">
<svg height="16" viewBox="0 0 16 16" version="1.1" width="16" aria-hidden="true"><path fill-rule="evenodd" d="M2.003 2.5a.5.5 0 00-.723-.447l-1.003.5a.5.5 0 00.446.895l.28-.14V6H.5a.5.5 0 000 1h2.006a.5.5 0 100-1h-.503V2.5zM5 3.25a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 3.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 8.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5a.75.75 0 01-.75-.75zM.924 10.32l.003-.004a.851.851 0 01.144-.153A.66.66 0 011.5 10c.195 0 .306.068.374.146a.57.57 0 01.128.376c0 .453-.269.682-.8 1.078l-.035.025C.692 11.98 0 12.495 0 13.5a.5.5 0 00.5.5h2.003a.5.5 0 000-1H1.146c.132-.197.351-.372.654-.597l.047-.035c.47-.35 1.156-.858 1.156-1.845 0-.365-.118-.744-.377-1.038-.268-.303-.658-.484-1.126-.484-.48 0-.84.202-1.068.392a1.858 1.858 0 00-.348.384l-.007.011-.002.004-.001.002-.001.001a.5.5 0 00.851.525zM.5 10.055l-.427-.26.427.26z"></path></svg>
</md-ordered-list>
</x-forms::markdown-editor.toolbar-button>
@endif
</div>
@endif
</markdown-toolbar>
@if ($hasToolbarButton(['edit', 'preview']) && ! $isDisabled())
<div class="flex items-center space-x-4 rtl:space-x-reverse">
@if ($hasToolbarButton('edit'))
<button
x-on:click.prevent="tab = 'edit'"
x-bind:class="{ 'text-gray-400': tab !== 'edit' }"
class="text-sm hover:underline"
>
{{ __('forms::components.markdown_editor.toolbar_buttons.edit') }}
</button>
@endif
@if ($hasToolbarButton('preview'))
<button
x-on:click.prevent="tab = 'preview'"
x-bind:class="{ 'text-gray-400': tab !== 'preview' }"
class="text-sm hover:underline"
>
{{ __('forms::components.markdown_editor.toolbar_buttons.preview') }}
</button>
@endif
</div>
@endif
</div>
@endunless
<div x-show="tab === 'edit'" class="relative w-full h-full" style="min-height: 150px;">
<file-attachment directory>
<textarea
{!! $isAutofocused() ? 'autofocus' : null !!}
id="{{ $getId() }}"
{!! ($placeholder = $getPlaceholder()) ? "placeholder=\"{$placeholder}\"" : null !!}
{!! $isRequired() ? 'required' : null !!}
x-model="state"
x-on:keyup.enter="checkForAutoInsertion"
x-on:file-attachment-accepted.window="
attachment = $event.detail?.attachments?.[0]
if (! attachment || ! attachment.file) return
$wire.upload(`componentFileAttachments.{{ $getStatePath() }}`, attachment.file, () => {
$wire.getComponentFileAttachmentUrl('{{ $getStatePath() }}').then((url) => {
if (! url) return
$refs.imageTrigger.click()
const urlStart = $refs.textarea.selectionStart + 2
const urlEnd = urlStart + 3
$refs.textarea.value = [
$refs.textarea.value.substring(0, urlStart),
url,
$refs.textarea.value.substring(urlEnd)
].join('')
$refs.textarea.selectionStart = urlStart - 2
$refs.textarea.selectionEnd = urlStart - 2
resize()
})
})
"
x-ref="textarea"
style="caret-color: black; color: transparent"
{{ $attributes->merge($getExtraAttributes())->class([
'tracking-normal whitespace-pre-wrap overflow-y-hidden font-mono block absolute bg-transparent top-0 text-sm left-0 block z-1 w-full h-full min-h-full resize-none transition duration-75 rounded-lg shadow-sm focus:border-primary-500 focus:ring-1 focus:ring-inset focus:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
></textarea>
</file-attachment>
<div
x-ref="overlay"
x-html="overlay"
style="min-height: 150px;"
class="w-full h-full rounded-lg px-3 py-2 border border-transparent font-mono tracking-normal bg-white text-sm text-gray-900 break-words whitespace-pre-wrap"
></div>
</div>
<div class="block w-full h-full min-h-full px-3 py-2 bg-white border border-gray-300 rounded-lg shadow-sm focus:border-primary-300" x-show="tab === 'preview'" style="min-height: 150px;">
<div class="prose" x-html="preview"></div>
</div>
</div>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,7 @@
<button
x-bind:disabled="tab === 'preview'"
type="button"
{{ $attributes->class(['border-gray-300 bg-white text-base text-gray-800 text-xs py-1 px-3 cursor-pointer font-medium border rounded transition duration-200 shadow-sm hover:bg-gray-100 focus:ring-primary-200 focus:ring focus:ring-opacity-50']) }}
>
{{ $slot }}
</button>
@@ -0,0 +1,166 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
x-data="multiSelectFormComponent({
getOptionLabelsUsing: async (values) => {
return await $wire.getMultiSelectOptionLabels('{{ $getStatePath() }}')
},
getSearchResultsUsing: async (query) => {
return await $wire.getMultiSelectSearchResults('{{ $getStatePath() }}', query)
},
isAutofocused: {{ $isAutofocused() ? 'true' : 'false' }},
options: {{ json_encode($getOptions()) }},
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
{!! ($id = $getId()) ? "id=\"{$id}\"" : null !!}
{{ $attributes->merge($getExtraAttributes())->class([
'block w-full transition duration-75 divide-y rounded-lg shadow-sm border focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
>
@unless ($isDisabled())
<div
x-on:click.away="closeListbox()"
x-on:blur="closeListbox()"
x-on:keydown.escape.stop="closeListbox()"
class="relative"
>
<div
x-ref="button"
x-on:click="toggleListboxVisibility()"
x-on:keydown.enter.stop.prevent="isOpen ? selectOption() : openListbox()"
x-on:keydown.space="if (! isOpen) openListbox()"
x-on:keydown.backspace="if (! search) clearState()"
x-on:keydown.clear="if (! search) clearState()"
x-on:keydown.delete="if (! search) clearState()"
x-bind:aria-expanded="isOpen"
aria-haspopup="listbox"
tabindex="1"
class="relative rounded-lg overflow-hidden"
>
<input
x-ref="search"
x-model.debounce.500="search"
x-on:keydown.enter.stop.prevent="selectOption()"
x-on:keydown.arrow-up.stop.prevent="focusPreviousOption()"
x-on:keydown.arrow-down.stop.prevent="focusNextOption()"
placeholder="{{ $getPlaceholder() }}"
type="text"
autocomplete="off"
class="block w-full border-0"
/>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg x-show="! isLoading" x-cloak class="w-5 h-5"xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4" />
</svg>
<svg x-show="isLoading" class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="currentColor">
<path d="M6.306 28.014c1.72 10.174 11.362 17.027 21.536 15.307C38.016 41.6 44.87 31.958 43.15 21.784l-4.011.678c1.345 7.958-4.015 15.502-11.974 16.847-7.959 1.346-15.501-4.014-16.847-11.973l-4.011.678z" />
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur=".7s" repeatCount="indefinite" /></path>
</svg>
</span>
</div>
<div
x-ref="listbox"
x-show="isOpen"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
role="listbox"
x-bind:aria-activedescendant="focusedOptionIndex ? '{{ $getStatePath() }}' + 'Option' + focusedOptionIndex : null"
tabindex="-1"
x-cloak
class="absolute z-10 w-full my-1 bg-white border border-gray-300 rounded-lg shadow-sm focus:outline-none"
>
<ul
x-ref="listboxOptionsList"
class="py-1 overflow-auto text-base leading-6 max-h-60 focus:outline-none"
>
<template x-for="(key, index) in Object.keys(options)" :key="key">
<li
x-bind:id="'{{ $getName() }}' + 'Option' + focusedOptionIndex"
x-on:click="selectOption(index)"
x-on:mouseenter="focusedOptionIndex = index"
x-on:mouseleave="focusedOptionIndex = null"
role="option"
x-bind:aria-selected="focusedOptionIndex === index"
x-bind:class="{
'text-white bg-primary-500': index === focusedOptionIndex,
'text-gray-900': index !== focusedOptionIndex,
}"
class="relative py-2 pl-3 h-10 flex items-center text-gray-900 cursor-default select-none pr-9"
>
<span
x-text="Object.values(options)[index]"
x-bind:class="{
'font-medium': index === focusedOptionIndex,
'font-normal': index !== focusedOptionIndex,
}"
class="block font-normal truncate"
></span>
<span
x-show="state.indexOf(key) >= 0"
x-bind:class="{
'text-white': index === focusedOptionIndex,
'text-primary-500': index !== focusedOptionIndex,
}"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-primary-500"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</span>
</li>
</template>
<div
x-show="! Object.keys(options).length"
x-text="! search || isLoading ? '{{ $getSearchPrompt() }}' : '{{ $getNoSearchResultsMessage() }}'"
class="px-3 py-2 text-sm text-gray-700 cursor-default select-none"
></div>
</ul>
</div>
</div>
@endunless
<div
x-show="state.length"
class="overflow-hidden rtl:space-x-reverse relative w-full px-1 py-1"
>
<div class="flex gap-1">
<template class="inline" x-for="option in state" x-bind:key="option">
<button
@unless ($isDisabled())
x-on:click="deselectOption(option)"
@endunless
type="button"
@class([
'inline-flex items-center justify-center h-6 px-2 my-1 text-sm font-medium tracking-tight text-primary-700 rounded-full bg-primary-500/10 space-x-1',
'cursor-default' => $isDisabled(),
])
>
<span x-text="labels[option]"></span>
@unless ($isDisabled())
<x-heroicon-s-x class="w-3 h-3" />
@endunless
</button>
</template>
</div>
</div>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,17 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<ul class="space-y-2">
@foreach ($getChildComponentContainers() as $uuid => $item)
<li wire:key="{{ $item->getStatePath() }}">
{{ $item }}
</li>
@endforeach
</ul>
</x-forms::field-wrapper>
@@ -0,0 +1,10 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:state-path="$getStatePath()"
>
{{ $getState() }}
</x-forms::field-wrapper>
@@ -0,0 +1,80 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div class="space-y-4">
@if (count($containers = $getChildComponentContainers()))
<ul class="space-y-4">
@foreach ($containers as $uuid => $item)
<li
wire:key="{{ $item->getStatePath() }}"
class="flex"
>
<div class="w-8">
<div class="bg-white divide-y shadow-sm rounded-l-lg border-b border-l border-t border-gray-300 overflow-hidden">
@unless ($loop->first || $isItemMovementDisabled())
<button
wire:click="dispatchFormEvent('repeater.moveItemUp', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-gray-800 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.repeater.buttons.move_item_up.label') }}
</span>
<x-heroicon-s-chevron-up class="w-5 h-5" />
</button>
@endunless
@unless ($loop->last || $isItemMovementDisabled())
<button
wire:click="dispatchFormEvent('repeater.moveItemDown', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-gray-800 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.repeater.buttons.move_item_down.label') }}
</span>
<x-heroicon-s-chevron-down class="w-5 h-5" />
</button>
@endunless
<button
wire:click="dispatchFormEvent('repeater.deleteItem', '{{ $getStatePath() }}', '{{ $uuid }}')"
type="button"
class="w-full flex items-center justify-center h-8 text-danger-600 transition hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-white focus:ring-primary-600 focus:text-danger-600 focus:bg-primary-50 focus:border-primary-600"
>
<span class="sr-only">
{{ __('forms::components.repeater.buttons.delete_item.label') }}
</span>
<x-heroicon-s-trash class="w-5 h-5" />
</button>
</div>
</div>
<div class="flex-1 p-6 bg-white shadow-sm rounded-r-lg rounded-b-lg border border-gray-300">
{{ $item }}
</div>
</li>
@endforeach
</ul>
@endif
<button
wire:click="dispatchFormEvent('repeater.createItem', '{{ $getStatePath() }}')"
type="button"
class="w-full h-9 px-4 inline-flex items-center justify-center font-medium tracking-tight transition rounded-lg text-gray-800 bg-white border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-offset-2 focus:ring-2 focus:ring-inset focus:ring-primary-600 focus:text-primary-600 focus:bg-primary-50 focus:border-primary-600"
>
<x-heroicon-s-plus class="w-6 h-6 mr-1 -ml-2" />
{{ $getCreateItemButtonLabel() }}
</button>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,287 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
x-data="richEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
x-on:trix-change="state = $event.target.value"
x-on:trix-attachment-add="
if (! $event.attachment.file) return
let attachment = $event.attachment
$wire.upload(`componentFileAttachments.{{ $getStatePath() }}`, attachment.file, () => {
$wire.getComponentFileAttachmentUrl('{{ $getStatePath() }}').then((url) => {
attachment.setAttributes({
url: url,
href: url,
})
})
})
"
x-cloak
wire:ignore
>
@unless ($isDisabled())
<input id="trix-value-{{ $getId() }}" type="hidden" />
<trix-toolbar id="trix-toolbar-{{ $getId() }}">
<div class="trix-button-row">
@if ($hasToolbarButton(['bold', 'italic', 'strike', 'link']))
<span data-trix-button-group="text-tools" class="trix-button-group trix-button-group--text-tools">
@if ($hasToolbarButton('bold'))
<button
data-trix-attribute="bold"
data-trix-key="b"
title="{{ __('forms::components.rich_editor.toolbar_buttons.bold') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-bold"
>
{{ __('forms::components.rich_editor.toolbar_buttons.bold') }}
</button>
@endif
@if ($hasToolbarButton('italic'))
<button
data-trix-attribute="italic"
data-trix-key="i"
title="{{ __('forms::components.rich_editor.toolbar_buttons.italic') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-italic"
>
{{ __('forms::components.rich_editor.toolbar_buttons.italic') }}
</button>
@endif
@if ($hasToolbarButton('strike'))
<button
data-trix-attribute="strike"
title="{{ __('forms::components.rich_editor.toolbar_buttons.strike') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-strike"
>
{{ __('forms::components.rich_editor.toolbar_buttons.strike') }}
</button>
@endif
@if ($hasToolbarButton('link'))
<button
data-trix-attribute="href"
data-trix-action="link"
data-trix-key="k"
title="{{ __('forms::components.rich_editor.toolbar_buttons.link') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-link"
>
{{ __('forms::components.rich_editor.toolbar_buttons.link') }}
</button>
@endif
</span>
@endif
@if ($hasToolbarButton(['h1', 'h2', 'h3']))
<span data-trix-button-group="heading-tools" class="trix-button-group trix-button-group--heading-tools">
@if ($hasToolbarButton('h1'))
<button
data-trix-attribute="heading1"
title="{{ __('forms::components.rich_editor.toolbar_buttons.h1') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-heading-1"
>
{{ __('forms::components.rich_editor.toolbar_buttons.h1') }}
</button>
@endif
@if ($hasToolbarButton('h2'))
<button
data-trix-attribute="heading"
title="{{ __('forms::components.rich_editor.toolbar_buttons.h2') }}"
tabindex="-1"
type="button"
class="trix-button"
>
{{ __('forms::components.rich_editor.toolbar_buttons.h2') }}
</button>
@endif
@if ($hasToolbarButton('h3'))
<button
data-trix-attribute="subHeading"
title="{{ __('forms::components.rich_editor.toolbar_buttons.h3') }}"
tabindex="-1"
type="button"
class="trix-button"
>
{{ __('forms::components.rich_editor.toolbar_buttons.h3') }}
</button>
@endif
</span>
@endif
@if ($hasToolbarButton(['blockquote', 'codeBlock', 'bulletList', 'orderedList']))
<span data-trix-button-group="block-tools" class="trix-button-group trix-button-group--block-tools">
@if ($hasToolbarButton('blockquote'))
<button
data-trix-attribute="quote"
title="{{ __('forms::components.rich_editor.toolbar_buttons.blockquote') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-quote"
>
{{ __('forms::components.rich_editor.toolbar_buttons.blockquote') }}
</button>
@endif
@if ($hasToolbarButton('codeBlock'))
<button
data-trix-attribute="code"
title="{{ __('forms::components.rich_editor.toolbar_buttons.code_block') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-code"
>
{{ __('forms::components.rich_editor.toolbar_buttons.code_block') }}
</button>
@endif
@if ($hasToolbarButton('bulletList'))
<button
data-trix-attribute="bullet"
title="{{ __('forms::components.rich_editor.toolbar_buttons.bullet_list') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-bullet-list"
>
{{ __('forms::components.rich_editor.toolbar_buttons.bullet_list') }}
</button>
@endif
@if ($hasToolbarButton('orderedList'))
<button
data-trix-attribute="number"
title="{{ __('forms::components.rich_editor.toolbar_buttons.ordered_list') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-number-list"
>
{{ __('forms::components.rich_editor.toolbar_buttons.ordered_list') }}
</button>
@endif
</span>
@endif
@if ($hasToolbarButton('attachFiles'))
<span data-trix-button-group="file-tools" class="trix-button-group trix-button-group--file-tools">
<button
data-trix-action="attachFiles"
title="{{ __('forms::components.rich_editor.toolbar_buttons.attach_files') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-attach"
>
{{ __('forms::components.rich_editor.toolbar_buttons.attach_files') }}
</button>
</span>
@endif
@if ($hasToolbarButton(['undo', 'redo']))
<span class="trix-button-group-spacer"></span>
<span
data-trix-button-group="history-tools"
class="trix-button-group trix-button-group--history-tools"
>
@if ($hasToolbarButton('undo'))
<button
data-trix-action="undo"
data-trix-key="z"
title="{{ __('forms::components.rich_editor.toolbar_buttons.undo') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-undo"
>
{{ __('forms::components.rich_editor.toolbar_buttons.undo') }}
</button>
@endif
@if ($hasToolbarButton('redo'))
<button
data-trix-action="redo"
data-trix-key="shift+z"
title="{{ __('forms::components.rich_editor.toolbar_buttons.redo') }}"
tabindex="-1"
type="button"
class="trix-button trix-button--icon trix-button--icon-redo"
>
{{ __('forms::components.rich_editor.toolbar_buttons.redo') }}
</button>
@endif
</span>
@endif
</div>
<div data-trix-dialogs class="trix-dialogs">
<div
data-trix-dialog="href"
data-trix-dialog-attribute="href"
class="trix-dialog trix-dialog--link"
>
<div class="trix-dialog__link-fields">
<input
name="href"
placeholder="{{ __('forms::components.rich_editor.dialogs.link.placeholder') }}" aria-label="{{ __('forms::components.rich_editor.dialogs.link.label') }}" required data-trix-input
disabled
type="url"
class="trix-input trix-input--dialog"
>
<div class="trix-button-group">
<input
value="{{ __('forms::components.rich_editor.dialogs.link.buttons.link') }}"
data-trix-method="setAttribute"
type="button"
class="trix-button trix-button--dialog"
>
<input
value="{{ __('forms::components.rich_editor.dialogs.link.buttons.unlink') }}"
data-trix-method="removeAttribute"
type="button"
class="trix-button trix-button--dialog"
>
</div>
</div>
</div>
</div>
</trix-toolbar>
<trix-editor
{!! $isAutofocused() ? 'autofocus' : null !!}
id="{{ $getId() }}"
input="trix-value-{{ $getId() }}"
placeholder="{{ $getPlaceholder() }}"
toolbar="trix-toolbar-{{ $getId() }}"
x-ref="trix"
{{ $attributes->merge($getExtraAttributes())->class([
'block w-full transition duration-75 rounded-lg shadow-sm focus:border-primary-500 focus:ring-1 focus:ring-inset focus:ring-primary-600 prose max-w-none',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
/>
@else
<div x-html="state" class="p-3 prose border border-gray-300 rounded shadow-sm"></div>
@endunless
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,46 @@
<div
@if ($isCollapsible())
x-data="{ isCollapsed: {{ $isCollapsed() ? 'true' : 'false' }} }"
x-on:expand-concealing-component.window="if ($event.detail.id === $el.id) isCollapsed = false"
@endif
id="{{ $getId() }}"
{{ $attributes->merge($getExtraAttributes())->class(['p-6 space-y-6 rounded-xl shadow-sm border border-gray-300']) }}
>
<div class="flex space-x-3">
<div class="flex-1 space-y-1">
<h3 class="text-xl font-bold tracking-tight">
{{ $getHeading() }}
</h3>
@if ($description = $getDescription())
<p class="text-gray-500">
{{ $description }}
</p>
@endif
</div>
@if ($isCollapsible())
<button
x-on:click="isCollapsed = ! isCollapsed"
x-bind:class="{
'-rotate-180': ! isCollapsed,
}"
type="button"
class="flex items-center justify-center w-10 h-10 text-primary-500 transform transition rounded-full hover:bg-gray-500/5 focus:bg-primary-500/10 focus:outline-none"
>
<svg class="h-7 w-7" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
@endif
</div>
<div
@if ($isCollapsible())
x-show="! isCollapsed"
x-bind:aria-expanded="(! isCollapsed).toString()"
@endif
>
{{ $getChildComponentContainer() }}
</div>
</div>
@@ -0,0 +1,175 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
@unless ($isSearchable())
<select
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
id="{{ $getId() }}"
{!! $isRequired() ? 'required' : null !!}
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
{{ $attributes->merge($getExtraAttributes())->class([
'text-gray-900 invalid:text-gray-400 block w-full h-10 transition duration-75 rounded-lg shadow-sm focus:border-primary-600 focus:ring-1 focus:ring-inset focus:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
>
<option value="">{{ $getPlaceholder() }}</option>
@foreach ($getOptions() as $value => $label)
<option
value="{{ $value }}"
{!! $isOptionDisabled($value, $label) ? 'disabled' : null !!}
>
{{ $label }}
</option>
@endforeach
</select>
@else
<div
x-data="selectFormComponent({
getOptionLabelUsing: async (value) => {
return await $wire.getSelectOptionLabel('{{ $getStatePath() }}')
},
getSearchResultsUsing: async (query) => {
return await $wire.getSelectSearchResults('{{ $getStatePath() }}', query)
},
isAutofocused: {{ $isAutofocused() ? 'true' : 'false' }},
options: {{ json_encode($getOptions()) }},
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
x-on:click.away="closeListbox()"
x-on:blur="closeListbox()"
x-on:keydown.escape.stop="closeListbox()"
{!! ($id = $getId()) ? "id=\"{$id}\"" : null !!}
class="relative"
{{ $attributes->merge($getExtraAttributes()) }}
>
<div
@unless($isDisabled())
x-ref="button"
x-on:click="toggleListboxVisibility()"
x-on:keydown.enter.stop.prevent="isOpen ? selectOption() : openListbox()"
x-on:keydown.space="if (! isOpen) openListbox()"
x-on:keydown.backspace="if (! search) clearState()"
x-on:keydown.clear="if (! search) clearState()"
x-on:keydown.delete="if (! search) clearState()"
x-bind:aria-expanded="isOpen"
aria-haspopup="listbox"
tabindex="1"
@endunless
@class([
'relative flex items-center h-10 pl-3 pr-10 border bg-white overflow-hidden transition duration-75 rounded-lg shadow-sm focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-inset focus-within:ring-primary-600 focus:outline-none',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
])
>
<span
x-show="! isOpen"
x-text="label ?? '{{ $getPlaceholder() }}'"
class="absolute w-full bg-white"
x-bind:class="{
'text-gray-500': label === null,
}"
></span>
@unless ($isDisabled())
<input
x-ref="search"
x-model.debounce.500="search"
x-on:keydown.enter.stop.prevent="selectOption()"
x-on:keydown.arrow-up.stop.prevent="focusPreviousOption()"
x-on:keydown.arrow-down.stop.prevent="focusNextOption()"
type="text"
autocomplete="off"
class="w-full my-1 p-0 border-0 focus:ring-0 focus:outline-none"
/>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg x-show="! isLoading" x-cloak class="w-5 h-5"xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4" />
</svg>
<svg x-show="isLoading" class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" fill="currentColor">
<path d="M6.306 28.014c1.72 10.174 11.362 17.027 21.536 15.307C38.016 41.6 44.87 31.958 43.15 21.784l-4.011.678c1.345 7.958-4.015 15.502-11.974 16.847-7.959 1.346-15.501-4.014-16.847-11.973l-4.011.678z" />
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur=".7s" repeatCount="indefinite" /></path>
</svg>
</span>
@endunless
</div>
@unless($isDisabled())
<div
x-ref="listbox"
x-show="isOpen"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
role="listbox"
x-bind:aria-activedescendant="focusedOptionIndex ? '{{ $getStatePath() }}' + 'Option' + focusedOptionIndex : null"
tabindex="-1"
x-cloak
class="absolute z-10 w-full my-1 bg-white border border-gray-300 rounded-lg shadow-sm focus:outline-none"
>
<ul
x-ref="listboxOptionsList"
class="py-1 overflow-auto text-base leading-6 max-h-60 focus:outline-none"
>
<template x-for="(key, index) in Object.keys(options)" :key="key">
<li
x-bind:id="'{{ $getName() }}' + 'Option' + focusedOptionIndex"
x-on:click="selectOption(index)"
x-on:mouseenter="focusedOptionIndex = index"
x-on:mouseleave="focusedOptionIndex = null"
role="option"
x-bind:aria-selected="focusedOptionIndex === index"
x-bind:class="{
'text-white bg-primary-500': index === focusedOptionIndex,
'text-gray-900': index !== focusedOptionIndex,
}"
class="relative py-2 pl-3 h-10 flex items-center text-gray-900 cursor-default select-none pr-9"
>
<span
x-text="Object.values(options)[index]"
x-bind:class="{
'font-medium': index === focusedOptionIndex,
'font-normal': index !== focusedOptionIndex,
}"
class="block font-normal truncate"
></span>
<span
x-show="key === state"
x-bind:class="{
'text-white': index === focusedOptionIndex,
'text-primary-500': index !== focusedOptionIndex,
}"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-primary-500"
>
<svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</span>
</li>
</template>
<div
x-show="! Object.keys(options).length"
x-text="! search || isLoading ? '{{ $getSearchPrompt() }}' : '{{ $getNoSearchResultsMessage() }}'"
class="px-3 py-2 text-sm text-gray-700 cursor-default select-none"
></div>
</ul>
</div>
@endunless
</div>
@endif
</x-forms::field-wrapper>
@@ -0,0 +1,32 @@
<div
x-data="{ tab: '{{ count($getTabsConfig()) ? array_key_first($getTabsConfig()) : null }}', tabs: {{ json_encode($getTabsConfig()) }} }"
x-on:expand-concealing-component.window="if ($event.detail.id in tabs) tab = $event.detail.id"
x-cloak
{!! $getId() ? "id=\"{$getId()}\"" : null !!}
{{ $attributes->merge($getExtraAttributes())->class(['rounded-xl shadow-sm border border-gray-300']) }}
>
<div
{!! $getLabel() ? 'aria-label="' . $getLabel() . '"' : null !!}
role="tablist"
class="rounded-t-xl flex overflow-y-auto bg-gray-100"
>
@foreach ($getTabsConfig() as $tabId => $tabLabel)
<button
type="button"
aria-controls="{{ $tabId }}"
x-bind:aria-selected="tab === '{{ $tabId }}'"
x-on:click="tab = '{{ $tabId }}'"
role="tab"
x-bind:tabindex="tab === '{{ $tabId }}' ? 0 : -1"
class="flex-shrink-0 p-3 text-sm font-medium"
x-bind:class="{ 'bg-white': tab === '{{ $tabId }}' }"
>
{{ $tabLabel }}
</button>
@endforeach
</div>
@foreach ($getChildComponentContainer()->getComponents() as $tab)
{{ $tab }}
@endforeach
</div>
@@ -0,0 +1,10 @@
<div
aria-labelledby="{{ $getId() }}"
id="{{ $getId() }}"
role="tabpanel"
tabindex="0"
x-show="tab === '{{ $getId() }}'"
class="p-6 focus:outline-none"
>
{{ $getChildComponentContainer() }}
</div>
@@ -0,0 +1,64 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div
x-data="tagsInputFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
{!! ($id = $getId()) ? "id=\"{$id}\"" : null !!}
{{ $attributes->merge($getExtraAttributes()) }}
>
<div
x-show="state.length || {{ $isDisabled() ? 'false' : 'true' }}"
@class([
'block w-full transition duration-75 divide-y rounded-lg shadow-sm border overflow-hidden focus-within:border-primary-600 focus-within:ring-1 focus-within:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
])
>
@unless ($isDisabled())
<input
autocomplete="off"
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $getPlaceholder() ? 'placeholder="' . $getPlaceholder() . '"' : null !!}
type="text"
x-on:keydown.enter.stop.prevent="createTag()"
x-model="newTag"
class="block w-full border-0"
/>
@endunless
<div
x-show="state.length"
class="overflow-hidden rtl:space-x-reverse relative w-full px-1 py-1"
>
<div class="flex gap-1">
<template class="inline" x-for="tag in state" x-bind:key="tag">
<button
@unless ($isDisabled())
x-on:click="deleteTag(tag)"
@endunless
type="button"
@class([
'inline-flex items-center justify-center h-6 px-2 my-1 text-sm font-medium tracking-tight text-primary-700 rounded-full bg-primary-500/10 space-x-1',
'cursor-default' => $isDisabled(),
])
>
<span x-text="tag"></span>
@unless ($isDisabled())
<x-heroicon-s-x class="w-3 h-3" />
@endunless
</button>
</template>
</div>
</div>
</div>
</div>
</x-forms::field-wrapper>
@@ -0,0 +1,76 @@
@php
$datalistOptions = $getDatalistOptions();
$sideLabelClasses = [
'whitespace-nowrap transition group-focus-within:text-primary-500',
'text-gray-400' => ! $errors->has($getStatePath()),
'text-danger-400' => $errors->has($getStatePath()),
];
@endphp
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div class="flex items-center space-x-1 group">
@if ($label = $getPrefixLabel())
<span @class($sideLabelClasses)>
{{ $label }}
</span>
@endif
<div class="flex-1">
<input
@unless ($hasMask())
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
type="{{ $getType() }}"
@else
x-data="textInputFormComponent({
{{ $hasMask() ? "getMaskOptionsUsing: (IMask) => ({$getJsonMaskConfiguration()})," : null }}
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
type="text"
wire:ignore
@endunless
{!! ($autocapitalize = $getAutocapitalize()) ? "autocapitalize=\"{$autocapitalize}\"" : null !!}
{!! ($autocomplete = $getAutocomplete()) ? "autocomplete=\"{$autocomplete}\"" : null !!}
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
id="{{ $getId() }}"
{!! ($inputMode = $getInputMode()) ? "inputmode=\"{$inputMode}\"" : null !!}
{!! $datalistOptions ? "list=\"{$getId()}-list\"" : null !!}
{!! ($length = $getMaxLength()) ? "maxlength=\"{$length}\"" : null !!}
{!! ($value = $getMaxValue()) ? "max=\"{$value}\"" : null !!}
{!! ($length = $getMinLength()) ? "minlength=\"{$length}\"" : null !!}
{!! ($value = $getMinValue()) ? "min=\"{$value}\"" : null !!}
{!! ($placeholder = $getPlaceholder()) ? "placeholder=\"{$placeholder}\"" : null !!}
{!! ($interval = $getStep()) ? "step=\"{$interval}\"" : null !!}
{!! $isRequired() ? 'required' : null !!}
{{ $attributes->merge($getExtraAttributes())->class([
'block w-full h-10 transition duration-75 rounded-lg shadow-sm focus:border-primary-600 focus:ring-1 focus:ring-inset focus:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
/>
</div>
@if ($label = $getPostfixLabel())
<span @class($sideLabelClasses)>
{{ $label }}
</span>
@endif
</div>
@if ($datalistOptions)
<datalist id="{{ $getId() }}-list">
@foreach ($datalistOptions as $option)
<option value="{{ $option }}" />
@endforeach
</datalist>
@endif
</x-forms::field-wrapper>
@@ -0,0 +1,29 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<textarea
{!! ($autocapitalize = $getAutocapitalize()) ? "autocapitalize=\"{$autocapitalize}\"" : null !!}
{!! ($autocomplete = $getAutocomplete()) ? "autocomplete=\"{$autocomplete}\"" : null !!}
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! ($cols = $getCols()) ? "cols=\"{$cols}\"" : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
id="{{ $getId() }}"
{!! ($length = $getMaxLength()) ? "maxlength=\"{$length}\"" : null !!}
{!! ($length = $getMinLength()) ? "minlength=\"{$length}\"" : null !!}
{!! ($placeholder = $getPlaceholder()) ? "placeholder=\"{$placeholder}\"" : null !!}
{!! $isRequired() ? 'required' : null !!}
{!! ($rows = $getRows()) ? "rows=\"{$rows}\"" : null !!}
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
{{ $attributes->merge($getExtraAttributes())->class([
'block w-full transition duration-75 rounded-lg shadow-sm focus:border-primary-500 focus:ring-1 focus:ring-inset focus:ring-primary-600',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-600 ring-danger-600' => $errors->has($getStatePath()),
]) }}
></textarea>
</x-forms::field-wrapper>
@@ -0,0 +1,71 @@
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
@if ($isInline())
<x-slot name="labelPrefix">
@endif
<button
x-data="{ state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }} }"
role="switch"
aria-checked="false"
x-bind:aria-checked="state.toString()"
x-on:click="state = ! state"
x-bind:class="{
'bg-primary-600': state,
'bg-gray-200': ! state,
}"
x-cloak
{!! $isAutofocused() ? 'autofocus' : null !!}
{!! $isDisabled() ? 'disabled' : null !!}
id="{{ $getId() }}"
type="button"
{{ $attributes->merge($getExtraAttributes())->class([
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500 bg-gray-200',
'border-gray-300' => ! $errors->has($getStatePath()),
'border-danger-300 ring-danger-500' => $errors->has($getStatePath()),
]) }}
>
<span
class="pointer-events-none relative inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"
:class="{
'translate-x-5': state,
'translate-x-0': ! state,
}"
>
<span
class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity opacity-100 ease-in duration-200"
aria-hidden="true"
:class="{
'opacity-0 ease-out duration-100': state,
'opacity-100 ease-in duration-200': ! state,
}"
>
@if ($hasOffIcon())
<x-dynamic-component :component="$getOffIcon()" class="bg-white h-3 w-3 text-gray-400" />
@endif
</span>
<span
class="absolute inset-0 h-full w-full flex items-center justify-center transition-opacity opacity-0 ease-out duration-100"
aria-hidden="true"
:class="{
'opacity-100 ease-in duration-200': state,
'opacity-0 ease-out duration-100': ! state,
}"
>
@if ($hasOnIcon())
<x-dynamic-component :component="$getOnIcon()" class="bg-white h-3 w-3 text-primary-600" />
@endif
</span>
</span>
</button>
@if ($isInline())
</x-slot>
@endif
</x-forms::field-wrapper>
@@ -0,0 +1,12 @@
<?php
namespace Filament\Forms\Commands\Aliases;
use Filament\Forms\Commands;
class MakeFieldCommand extends Commands\MakeFieldCommand
{
protected $hidden = true;
protected $signature = 'forms:field {name}';
}
@@ -0,0 +1,12 @@
<?php
namespace Filament\Forms\Commands\Aliases;
use Filament\Forms\Commands;
class MakeLayoutComponentCommand extends Commands\MakeLayoutComponentCommand
{
protected $hidden = true;
protected $signature = 'forms:layout {name}';
}
@@ -0,0 +1,60 @@
<?php
namespace Filament\Forms\Commands\Concerns;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
trait CanManipulateFiles
{
protected function checkForCollision(array $paths): bool
{
foreach ($paths as $path) {
if ($this->fileExists($path)) {
$this->error("$path already exists, aborting.");
return true;
}
}
return false;
}
protected function copyStubToApp(string $stub, string $targetPath, array $replacements = []): void
{
$filesystem = new Filesystem();
if (! $this->fileExists($stubPath = base_path("stubs/filament/{$stub}.stub"))) {
$stubPath = __DIR__ . "/../../../stubs/{$stub}.stub";
}
$stub = Str::of($filesystem->get($stubPath));
foreach ($replacements as $key => $replacement) {
$stub = $stub->replace("{{ {$key} }}", $replacement);
}
$stub = (string) $stub;
$this->writeFile($targetPath, $stub);
}
protected function fileExists(string $path): bool
{
$filesystem = new Filesystem();
return $filesystem->exists($path);
}
protected function writeFile(string $path, string $contents): void
{
$filesystem = new Filesystem();
$filesystem->ensureDirectoryExists(
(string) Str::of($path)
->beforeLast('/'),
);
$filesystem->put($path, $contents);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Filament\Forms\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
class InstallCommand extends Command
{
protected $signature = 'forms:install';
protected $description = 'Set up form builder CSS and JS in a fresh Laravel installation.';
public function __invoke(): int
{
static::updateNpmPackages();
$filesystem = new Filesystem();
$filesystem->delete(resource_path('js/bootstrap.js'));
$filesystem->copyDirectory(__DIR__ . '/../../stubs/scaffolding', base_path());
$this->info('Scaffolding installed successfully.');
$this->comment('Please run `npm install && npm run dev` to compile your new assets.');
return static::SUCCESS;
}
protected static function updateNpmPackages(bool $dev = true): void
{
if (! file_exists(base_path('package.json'))) {
return;
}
$configurationKey = $dev ? 'devDependencies' : 'dependencies';
$packages = json_decode(file_get_contents(base_path('package.json')), true);
$packages[$configurationKey] = static::updateNpmPackageArray(
array_key_exists($configurationKey, $packages) ? $packages[$configurationKey] : []
);
ksort($packages[$configurationKey]);
file_put_contents(
base_path('package.json'),
json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL
);
}
protected static function updateNpmPackageArray(array $packages): array
{
return array_merge(
[
'@tailwindcss/forms' => '^0.3',
'@tailwindcss/typography' => '^0.4',
'alpinejs' => '^3.4',
'tailwindcss' => '^2.2',
],
Arr::except($packages, [
'axios',
'lodash',
]),
);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Filament\Forms\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class MakeFieldCommand extends Command
{
use Concerns\CanManipulateFiles;
protected $description = 'Creates a form field class and view.';
protected $signature = 'make:form-field {name}';
public function handle(): int
{
$field = (string) Str::of($this->argument('name'))
->trim('/')
->trim('\\')
->trim(' ')
->replace('/', '\\');
$fieldClass = (string) Str::of($field)->afterLast('\\');
$fieldNamespace = Str::of($field)->contains('\\') ?
(string) Str::of($field)->beforeLast('\\') :
'';
$view = Str::of($field)
->prepend('forms\\components\\')
->explode('\\')
->map(fn ($segment) => Str::kebab($segment))
->implode('.');
$path = app_path(
(string) Str::of($field)
->prepend('Forms\\Components\\')
->replace('\\', '/')
->append('.php'),
);
$viewPath = resource_path(
(string) Str::of($view)
->replace('.', '/')
->prepend('views/')
->append('.blade.php'),
);
if ($this->checkForCollision([
$path,
])) {
return static::INVALID;
}
$this->copyStubToApp('Field', $path, [
'class' => $fieldClass,
'namespace' => 'App\\Forms\\Components' . ($fieldNamespace !== '' ? "\\{$fieldNamespace}" : ''),
'view' => $view,
]);
if (! $this->fileExists($viewPath)) {
$this->copyStubToApp('FieldView', $viewPath);
}
$this->info("Successfully created {$field}!");
return static::SUCCESS;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Filament\Forms\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class MakeLayoutComponentCommand extends Command
{
use Concerns\CanManipulateFiles;
protected $description = 'Creates a form layout component class and view.';
protected $signature = 'make:form-layout {name}';
public function handle(): int
{
$component = (string) Str::of($this->argument('name'))
->trim('/')
->trim('\\')
->trim(' ')
->replace('/', '\\');
$componentClass = (string) Str::of($component)->afterLast('\\');
$componentNamespace = Str::of($component)->contains('\\') ?
(string) Str::of($component)->beforeLast('\\') :
'';
$view = Str::of($component)
->prepend('forms\\components\\')
->explode('\\')
->map(fn ($segment) => Str::kebab($segment))
->implode('.');
$path = app_path(
(string) Str::of($component)
->prepend('Forms\\Components\\')
->replace('\\', '/')
->append('.php'),
);
$viewPath = resource_path(
(string) Str::of($view)
->replace('.', '/')
->prepend('views/')
->append('.blade.php'),
);
if ($this->checkForCollision([
$path,
])) {
return static::INVALID;
}
$this->copyStubToApp('LayoutComponent', $path, [
'class' => $componentClass,
'namespace' => 'App\\Forms\\Components' . ($componentNamespace !== '' ? "\\{$componentNamespace}" : ''),
'view' => $view,
]);
if (! $this->fileExists($viewPath)) {
$this->copyStubToApp('LayoutComponentView', $viewPath);
}
$this->info("Successfully created {$component}!");
return static::SUCCESS;
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Filament\Forms;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\Tappable;
use Illuminate\View\Component as ViewComponent;
class ComponentContainer extends ViewComponent implements Htmlable
{
use Concerns\BelongsToLivewire;
use Concerns\BelongsToModel;
use Concerns\BelongsToParentComponent;
use Concerns\CanBeDisabled;
use Concerns\CanBeHidden;
use Concerns\CanBeValidated;
use Concerns\Cloneable;
use Concerns\HasColumns;
use Concerns\HasComponents;
use Concerns\HasState;
use Concerns\HasStateBindingModifiers;
use Concerns\ListensToEvents;
use Concerns\SupportsComponentFileAttachments;
use Concerns\SupportsFileUploadFields;
use Concerns\SupportsMultiSelectFields;
use Concerns\SupportsSelectFields;
use Macroable;
use Tappable;
protected array $meta = [];
final public function __construct(HasForms $livewire)
{
$this->livewire($livewire);
}
public static function make(HasForms $livewire): static
{
return new static($livewire);
}
public function toHtml(): string
{
return $this->render()->render();
}
public function render(): View
{
return view('forms::component-container', array_merge($this->data(), [
'container' => $this,
]));
}
}
@@ -0,0 +1,145 @@
<?php
namespace Filament\Forms\Components;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Str;
class BelongsToManyMultiSelect extends MultiSelect
{
protected $displayColumnName = null;
protected $isPreloaded = false;
protected $relationship = null;
protected function setUp(): void
{
parent::setUp();
$this->afterStateHydrated(function (BelongsToManyMultiSelect $component): void {
$relationship = $component->getRelationship();
$relatedModels = $relationship->getResults();
if (! $relatedModels) {
return;
}
$component->state(
$relatedModels->pluck(
$relationship->getRelatedKeyName(),
)->toArray(),
);
});
$this->dehydrated(false);
}
public function preload(bool | callable $condition = true): static
{
$this->isPreloaded = $condition;
return $this;
}
public function relationship(string | callable $relationshipName, string | callable $displayColumnName, ?callable $callback = null): static
{
$this->displayColumnName = $displayColumnName;
$this->relationship = $relationshipName;
$this->getOptionLabelsUsing(function (BelongsToManyMultiSelect $component, array $values): array {
$relationship = $component->getRelationship();
$relatedKeyName = $relationship->getRelatedKeyName();
return $relationship->getRelated()
->whereIn($relatedKeyName, $values)
->pluck($component->getDisplayColumnName(), $relatedKeyName)
->toArray();
});
$this->getSearchResultsUsing(function (BelongsToManyMultiSelect $component, ?string $query) use ($callback): array {
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->orderBy($component->getDisplayColumnName());
if ($callback) {
$relationshipQuery = $callback($relationshipQuery);
}
$query = strtolower($query);
$searchOperator = match ($relationshipQuery->getConnection()->getDriverName()) {
'pgsql' => 'ilike',
default => 'like',
};
return $relationshipQuery
->where($component->getDisplayColumnName(), $searchOperator, "%{$query}%")
->pluck($component->getDisplayColumnName(), $relationship->getRelatedKeyName())
->toArray();
});
$this->options(function (BelongsToManyMultiSelect $component) use ($callback): array {
if (! $component->isPreloaded()) {
return [];
}
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->orderBy($component->getDisplayColumnName());
if ($callback) {
$relationshipQuery = $callback($relationshipQuery);
}
return $relationshipQuery
->pluck($component->getDisplayColumnName(), $relationship->getRelatedKeyName())
->toArray();
});
return $this;
}
public function saveRelationships(): void
{
$this->getRelationship()->sync($this->getState());
}
public function getDisplayColumnName(): string
{
return $this->evaluate($this->displayColumnName);
}
public function getLabel(): string
{
if ($this->label === null) {
return (string) Str::of($this->getRelationshipName())
->before('.')
->kebab()
->replace(['-', '_'], ' ')
->ucfirst();
}
return parent::getLabel();
}
public function getRelationship(): BelongsToMany
{
$model = $this->getModel();
if (is_string($model)) {
$model = new $model();
}
return $model->{$this->getRelationshipName()}();
}
public function getRelationshipName(): string
{
return $this->evaluate($this->relationship);
}
public function isPreloaded(): bool
{
return $this->evaluate($this->isPreloaded);
}
}
@@ -0,0 +1,147 @@
<?php
namespace Filament\Forms\Components;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class BelongsToSelect extends Select
{
protected $displayColumnName = null;
protected $isPreloaded = false;
protected $relationship = null;
protected function setUp(): void
{
parent::setUp();
$this->afterStateHydrated(function (BelongsToSelect $component): void {
$relationship = $component->getRelationship();
$relatedModel = $relationship->getResults();
if (! $relatedModel) {
return;
}
$component->state(
$relatedModel->getAttribute(
$relationship->getOwnerKeyName(),
),
);
});
}
public function preload(bool | callable $condition = true): static
{
$this->isPreloaded = $condition;
return $this;
}
public function relationship(string | callable $relationshipName, string | callable $displayColumnName, ?callable $callback = null): static
{
$this->displayColumnName = $displayColumnName;
$this->relationship = $relationshipName;
$this->getOptionLabelUsing(function (BelongsToSelect $component, $value) {
$relationship = $component->getRelationship();
$record = $relationship->getRelated()->where($relationship->getOwnerKeyName(), $value)->first();
return $record ? $record->getAttributeValue($component->getDisplayColumnName()) : null;
});
$this->getSearchResultsUsing(function (BelongsToSelect $component, ?string $query) use ($callback): array {
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->orderBy($component->getDisplayColumnName());
if ($callback) {
$relationshipQuery = $callback($relationshipQuery);
}
$query = strtolower($query);
$searchOperator = match ($relationshipQuery->getConnection()->getDriverName()) {
'pgsql' => 'ilike',
default => 'like',
};
return $relationshipQuery
->where($component->getDisplayColumnName(), $searchOperator, "%{$query}%")
->pluck($component->getDisplayColumnName(), $relationship->getOwnerKeyName())
->toArray();
});
$this->options(function (BelongsToSelect $component) use ($callback): array {
if ($component->isSearchable() && ! $component->isPreloaded()) {
return [];
}
$relationship = $component->getRelationship();
$relationshipQuery = $relationship->getRelated()->orderBy($component->getDisplayColumnName());
if ($callback) {
$relationshipQuery = $callback($relationshipQuery);
}
return $relationshipQuery
->pluck($component->getDisplayColumnName(), $relationship->getOwnerKeyName())
->toArray();
});
$this->exists(
fn (BelongsToSelect $component): string => get_class($component->getRelationship()->getModel()),
fn (BelongsToSelect $component): string => $component->getRelationship()->getOwnerKeyName(),
);
return $this;
}
public function saveRelationships(): void
{
$this->getRelationship()->associate($this->getState());
$this->getModel()->save();
}
public function getDisplayColumnName(): string
{
return $this->evaluate($this->displayColumnName);
}
public function getLabel(): string
{
if ($this->label === null) {
return (string) Str::of($this->getRelationshipName())
->before('.')
->kebab()
->replace(['-', '_'], ' ')
->ucfirst();
}
return parent::getLabel();
}
public function getRelationship(): BelongsTo
{
$model = $this->getModel();
if (is_string($model)) {
$model = new $model();
}
return $model->{$this->getRelationshipName()}();
}
public function getRelationshipName(): string
{
return $this->evaluate($this->relationship);
}
public function isPreloaded(): bool
{
return $this->evaluate($this->isPreloaded);
}
}
+219
View File
@@ -0,0 +1,219 @@
<?php
namespace Filament\Forms\Components;
use Filament\Forms\ComponentContainer;
use Filament\Forms\Components\Builder\Block;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class Builder extends Field
{
protected string $view = 'forms::components.builder';
protected $createItemBetweenButtonLabel = null;
protected $createItemButtonLabel = null;
protected $isItemMovementDisabled = false;
protected function setUp(): void
{
parent::setUp();
$this->registerListeners([
'builder.createItem' => [
function (Builder $component, string $statePath, string $block, ?string $afterUuid = null): void {
if ($component->isDisabled()) {
return;
}
if ($statePath !== $component->getStatePath()) {
return;
}
$livewire = $component->getLivewire();
$newUuid = (string) Str::uuid();
$newItem = [
'type' => $block,
'data' => [],
];
if ($afterUuid) {
$newItems = [];
foreach ($component->getNormalisedState() as $uuid => $item) {
$newItems[$uuid] = $item;
if ($uuid === $afterUuid) {
$newItems[$newUuid] = $newItem;
}
}
data_set($livewire, $statePath, $newItems);
} else {
data_set($livewire, "{$statePath}.{$newUuid}", $newItem);
}
$component->hydrateDefaultItemState($newUuid);
},
],
'builder.deleteItem' => [
function (Builder $component, string $statePath, string $uuidToDelete): void {
if ($component->isDisabled()) {
return;
}
if ($statePath !== $component->getStatePath()) {
return;
}
$items = $component->getNormalisedState();
unset($items[$uuidToDelete]);
$livewire = $component->getLivewire();
data_set($livewire, $statePath, $items);
},
],
'builder.moveItemDown' => [
function (Builder $component, string $statePath, string $uuidToMoveDown): void {
if ($component->isDisabled()) {
return;
}
if ($component->isItemMovementDisabled()) {
return;
}
if ($statePath !== $component->getStatePath()) {
return;
}
$items = Arr::moveElementAfter($component->getNormalisedState(), $uuidToMoveDown);
$livewire = $component->getLivewire();
data_set($livewire, $statePath, $items);
},
],
'builder.moveItemUp' => [
function (Builder $component, string $statePath, string $uuidToMoveUp): void {
if ($component->isDisabled()) {
return;
}
if ($component->isItemMovementDisabled()) {
return;
}
if ($statePath !== $component->getStatePath()) {
return;
}
$items = Arr::moveElementBefore($component->getNormalisedState(), $uuidToMoveUp);
$livewire = $component->getLivewire();
data_set($livewire, $statePath, $items);
},
],
]);
$this->createItemBetweenButtonLabel(__('forms::components.builder.buttons.create_item_between.label'));
$this->createItemButtonLabel(function (Builder $component) {
return __('forms::components.builder.buttons.create_item.label', [
'label' => lcfirst($component->getLabel()),
]);
});
}
public function blocks(array $blocks): static
{
$this->childComponents($blocks);
return $this;
}
public function createItemBetweenButtonLabel(string | callable $label): static
{
$this->createItemBetweenButtonLabel = $label;
return $this;
}
public function createItemButtonLabel(string | callable $label): static
{
$this->createItemButtonLabel = $label;
return $this;
}
public function disableItemMovement(bool | callable $condition = true): static
{
$this->isItemMovementDisabled = $condition;
return $this;
}
public function hydrateDefaultItemState(string $uuid): void
{
$this->getChildComponentContainers()[$uuid]->hydrateDefaultState();
}
public function getBlock($name): ?Block
{
return Arr::first(
$this->getBlocks(),
fn (Block $block) => $block->getName() === $name,
);
}
public function getBlocks(): array
{
return $this->getChildComponentContainer()->getComponents();
}
public function getChildComponentContainers(): array
{
return collect($this->getNormalisedState())
->map(function ($item, $index): ComponentContainer {
return $this->getBlock($item['type'])
->getChildComponentContainer()
->getClone()
->statePath("{$index}.data");
})->toArray();
}
public function getCreateItemBetweenButtonLabel(): string
{
return $this->evaluate($this->createItemBetweenButtonLabel);
}
public function getCreateItemButtonLabel(): string
{
return $this->evaluate($this->createItemButtonLabel);
}
public function getNormalisedState(): array
{
if (! is_array($state = $this->getState())) {
return [];
}
return array_filter(
$state,
fn ($item) => is_array($item) && $this->hasBlock($item['type'] ?? null),
);
}
public function hasBlock($name): bool
{
return (bool) $this->getBlock($name);
}
public function isItemMovementDisabled(): bool
{
return $this->evaluate($this->isItemMovementDisabled);
}
}

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