mirror of
https://github.com/filamentphp/filament.git
synced 2026-08-29 03:09:12 +08:00
Revert "Merge branch '2.x' of https://github.com/laravel-filament/filament into 2.x"
This reverts commit6ed9a57e33, reversing changes made to2728e60ca4.
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
github: [danharrin]
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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))
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
};
|
||||
Generated
+299
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/vendor
|
||||
@@ -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
|
||||
}
|
||||
Generated
+3680
File diff suppressed because it is too large
Load Diff
@@ -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'),
|
||||
|
||||
];
|
||||
Vendored
+1449
File diff suppressed because it is too large
Load Diff
Vendored
+22212
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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>
|
||||
```
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user