Skip to content

Commit

Permalink
Add FilePond integration (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
aerni authored Apr 19, 2024
1 parent 4215214 commit d55c555
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 83 deletions.
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
},
"dependencies": {
"alpinejs": "^3.13.3",
"filepond": "^4.30.6",
"filepond-plugin-file-validate-size": "^2.2.8",
"filepond-plugin-file-validate-type": "^1.2.9",
"filepond-plugin-image-preview": "^4.6.12",
"filepond-plugin-image-validate-size": "^1.2.7",
"marked": "^4.0.10",
"underscore": "~1.13.2",
"uniqid": "^5.2.0"
Expand Down
125 changes: 82 additions & 43 deletions resources/dist/js/livewire-forms.js

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions resources/js/alpine/filepond.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as FilePond from 'filepond';
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginImageValidateSize from 'filepond-plugin-image-validate-size';
import ar_AR from 'filepond/locale/ar-ar';
import az_AZ from 'filepond/locale/az-az';
import cs_CZ from 'filepond/locale/cs-cz';
import da_DK from 'filepond/locale/da-dk';
import de_DE from 'filepond/locale/de-de';
import el_EL from 'filepond/locale/el-el';
import en_EN from 'filepond/locale/en-en';
import es_ES from 'filepond/locale/es-es';
import fa_IR from 'filepond/locale/fa_ir';
import fi_FI from 'filepond/locale/fi-fi';
import fr_FR from 'filepond/locale/fr-fr';
import he_HE from 'filepond/locale/he-he';
import hr_HR from 'filepond/locale/hr-hr';
import hu_HU from 'filepond/locale/hu-hu';
import id_ID from 'filepond/locale/id-id';
import it_IT from 'filepond/locale/it-it';
import ja_JA from 'filepond/locale/ja-ja';
import km_KM from 'filepond/locale/km-km';
import lt_LT from 'filepond/locale/lt-lt';
import nl_NL from 'filepond/locale/nl-nl';
import no_NB from 'filepond/locale/no_nb';
import pl_PL from 'filepond/locale/pl-pl';
import pt_BR from 'filepond/locale/pt-br';
import pt_PT from 'filepond/locale/pt-pt';
import ro_RO from 'filepond/locale/ro-ro';
import ru_RU from 'filepond/locale/ru-ru';
import sk_SK from 'filepond/locale/sk-sk';
import sv_SE from 'filepond/locale/sv_se';
import tr_TR from 'filepond/locale/tr-tr';
import uk_UA from 'filepond/locale/uk-ua';
import vi_VI from 'filepond/locale/vi-vi';
import zh_CN from 'filepond/locale/zh-cn';
import zh_TW from 'filepond/locale/zh-tw';

const locales = {
'ar-ar': ar_AR,
'az-az': az_AZ,
'cs-cz': cs_CZ,
'da-dk': da_DK,
'de-de': de_DE,
'el-el': el_EL,
'en-en': en_EN,
'es-es': es_ES,
'fa-ir': fa_IR,
'fi-fi': fi_FI,
'fr-fr': fr_FR,
'he-he': he_HE,
'hr-hr': hr_HR,
'hu-hu': hu_HU,
'id-id': id_ID,
'it-it': it_IT,
'ja-ja': ja_JA,
'km-km': km_KM,
'lt-lt': lt_LT,
'nl-nl': nl_NL,
'no-nb': no_NB,
'pl-pl': pl_PL,
'pt-br': pt_BR,
'pt-pt': pt_PT,
'ro-ro': ro_RO,
'ru-ru': ru_RU,
'sk-sk': sk_SK,
'sv-se': sv_SE,
'tr-tr': tr_TR,
'uk-ua': uk_UA,
'vi-vi': vi_VI,
'zh-cn': zh_CN,
'zh-tw': zh_TW,
};

export default (config) => ({
init() {
FilePond.registerPlugin(FilePondPluginFileValidateSize);
FilePond.registerPlugin(FilePondPluginFileValidateType);
FilePond.registerPlugin(FilePondPluginImagePreview);
FilePond.registerPlugin(FilePondPluginImageValidateSize);

const field = this.$wire.fields[config.field].properties;

FilePond.create(this.$refs.input, {
allowMultiple: field.multiple,
minFileSize: field.file_size.min ? `${field.file_size.min}KB` : null,
maxFileSize: field.file_size.max ? `${field.file_size.max}KB` : null,
acceptedFileTypes: field.file_types,
imageValidateSizeMinWidth: field.dimensions.min_width ?? 1,
imageValidateSizeMinHeight: field.dimensions.min_height ?? 1,
imageValidateSizeMaxWidth: field.dimensions.max_width ?? 65535,
imageValidateSizeMaxHeight: field.dimensions.max_height ?? 65535,
credits: false,
server: {
process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
this.$wire.upload(field.key, file, load, error, progress)
},
revert: (filename, load) => {
this.$wire.removeUpload(field.key, filename, load)
},
},
...locales[config.locale],
});
},

reset(livewireId) {
// Only reset the FilePond instance if the form-success event was fired by the same Livewire component
if (livewireId !== this.$wire.id) return;

FilePond.find(this.$el.querySelector('.filepond--root')).removeFiles();
}
})
2 changes: 1 addition & 1 deletion resources/js/form.js → resources/js/alpine/form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FieldConditions from '../../vendor/statamic/cms/resources/js/frontend/components/FieldConditions.js';
import FieldConditions from '../../../vendor/statamic/cms/resources/js/frontend/components/FieldConditions.js';

export default () => ({
fields: {},
Expand Down
4 changes: 3 additions & 1 deletion resources/js/livewire-forms.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import form from './form.js'
import filepond from './alpine/filepond.js'
import form from './alpine/form.js'

Alpine.data('filepond', filepond)
Alpine.data('form', form)
35 changes: 18 additions & 17 deletions resources/views/default/fields/assets.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,36 @@
@endif
</div>

<input
<div
id="{{ $field->id }}"
name="{{ $field->id }}"
type="file"

@if($field->multiple)
multiple
@endif

@if($field->wire_model)
wire:model.{{ $field->wire_model }}="{{ $field->key }}"
@else
wire:model="{{ $field->key }}"
@endif

@if(! $errors->has($field->key))
class="block w-full"
@if($field->instructions)
aria-describedby="{{ $field->id }}-instructions"
@endif
@else
class="block w-full text-red-900"
aria-invalid="true"
aria-describedby="{{ $field->id }}-error"
@endif
/>
>
<div
x-data="filepond({
field: '{{ $field->handle }}',
locale: @antlers'{{ site:attributes:filepond_locale ?? 'en-en' }}'@endantlers,
})"
x-on:form-success.window="reset($event.detail.id)"
wire:ignore
>
<input type="file" x-ref="input" />
</div>
</div>

@if($errors->has($field->key))
@formView('messages.error')
@elseif($field->instructions && $field->instructions_position === 'below')
@formView('messages.instructions')
@endif

@assets
<link href="https://unpkg.com/filepond@^4/dist/filepond.css" rel="stylesheet" />
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet" />
@endassets
63 changes: 45 additions & 18 deletions src/Fields/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

namespace Aerni\LivewireForms\Fields;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Statamic\Fieldtypes\Assets\DimensionsRule;
use Statamic\Fieldtypes\Assets\ImageRule;
use Statamic\Fieldtypes\Assets\MaxRule;
use Statamic\Fieldtypes\Assets\MimesRule;
use Statamic\Fieldtypes\Assets\MimetypesRule;
use Statamic\Fieldtypes\Assets\MinRule;
use Statamic\Forms\Uploaders\AssetsUploader;
use Symfony\Component\Mime\MimeTypes;

class Assets extends Field
{
Expand All @@ -14,32 +21,52 @@ protected function multipleProperty(?bool $multiple = null): bool
return $multiple ?? $this->field->get('max_files') !== 1;
}

protected function defaultProperty(mixed $default = null): ?array
protected function fileSizeProperty(): array
{
return $this->multiple ? [] : null;
}
$rules = collect($this->rules)->flatten();

protected function rulesProperty(string|array|null $rules = null): array
{
$rules = parent::rulesProperty($rules);
$minFileSize = $rules->whereInstanceOf(MinRule::class)
->flatMap(fn ($rule) => invade($rule)->parameters)
->first();

if ($this->multiple) {
return $rules;
}
$maxFileSize = $rules->whereInstanceOf(MaxRule::class)
->flatMap(fn ($rule) => invade($rule)->parameters)
->first();

/* Remove validation rules that would only work if multiple is enabled */
$rules = collect(array_first($rules))
->filter(fn ($rule) => $rule !== 'array')
->filter(fn ($rule) => $rule !== "min:{$this->min_files}")
->filter(fn ($rule) => $rule !== "max:{$this->max_files}")
->values()->all();
return [
'min' => $minFileSize,
'max' => $maxFileSize,
];
}

return [$this->key => $rules];
protected function fileTypesProperty(): array
{
return collect($this->rules)
->flatten()
->map(fn ($rule) => match (true) {
$rule instanceof ImageRule => ['image/*'],
$rule instanceof MimetypesRule => invade($rule)->parameters,
$rule instanceof MimesRule => collect(invade($rule)->parameters)
->flatMap(fn ($mime) => (new MimeTypes)->getMimeTypes($mime)),
default => null,
})
->filter()
->first() ?? [];
}

protected function dimensionsProperty(): array
{
return collect($this->rules)
->flatten()
->whereInstanceOf(DimensionsRule::class)
->flatMap(fn ($rule) => invade($rule)->parameters)
->mapWithKeys(fn ($dimension) => [Str::before($dimension, '=') => Str::after($dimension, '=')])
->toArray();
}

public function process(): mixed
{
$this->value = collect(Arr::wrap($this->value))
$this->value = collect($this->value)
->flatMap(fn ($file) => AssetsUploader::field($this->handle)->upload($file));

return parent::process();
Expand Down
2 changes: 1 addition & 1 deletion src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ abstract class Field implements Arrayable
use HandlesProperties;
use WithDefaultProperties;

protected mixed $value = null;
public mixed $value = null;

public function __construct(protected FormField $field)
{
Expand Down
7 changes: 5 additions & 2 deletions src/Livewire/Concerns/HandlesSuccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ protected function handleSuccess(): self
{
session()->flash('success', $this->successMessage());

// Get the captcha value before we are resetting the field values.
/* Get the captcha value before we are resetting the field values. */
$captcha = $this->captcha()?->value();

$this->resetValues();

// Preserve the captcha state by setting the value to its previous state.
/* Preserve the captcha state by setting the value to its previous state. */
$this->captcha()?->value($captcha);

/* Dispatch event so we can reset the FilePond field. */
$this->dispatch('form-success', id: $this->getId());

return $this;
}
}
29 changes: 29 additions & 0 deletions src/Livewire/Synthesizers/RuleSynth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Aerni\LivewireForms\Livewire\Synthesizers;

use Illuminate\Contracts\Validation\Rule;
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;

class RuleSynth extends Synth
{
public static $key = 'rule';

public static function match($target)
{
return $target instanceof Rule;
}

public function dehydrate($target)
{
return [
invade($target)->parameters,
['class' => get_class($target)],
];
}

public function hydrate($value, $meta)
{
return new $meta['class']($value);
}
}
Loading

0 comments on commit d55c555

Please sign in to comment.