-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(maryui) envia o primeiro poc com bootstrap
- Loading branch information
1 parent
fd21554
commit 9317ebd
Showing
3 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?php | ||
|
||
namespace Agenciafmd\Ui\Traits; | ||
|
||
use Illuminate\Database\Eloquent\Model; | ||
use Illuminate\Support\Facades\Storage; | ||
use Illuminate\Support\Str; | ||
|
||
trait WithMediaSync | ||
{ | ||
// Remove media | ||
public function removeMedia(string $uuid, string $filesModelName, string $library, string $url): void | ||
{ | ||
// Updates library | ||
$this->{$library} = $this->{$library}->filter(fn($image) => $image['uuid'] != $uuid); | ||
|
||
// Remove file | ||
$name = str($url)->after('preview-file/')->before('?expires')->toString(); | ||
$this->{$filesModelName} = collect($this->{$filesModelName})->filter(fn($file) => $file->getFilename() != $name)->all(); | ||
} | ||
|
||
// Set order | ||
public function refreshMediaOrder(array $order, string $library): void | ||
{ | ||
$this->{$library} = $this->{$library}->sortBy(function ($item) use ($order) { | ||
return array_search($item['uuid'], $order); | ||
}); | ||
} | ||
|
||
// Bind temporary files with respective previews and replace existing ones, if necessary | ||
public function refreshMediaSources(string $filesModelName, string $library) | ||
{ | ||
// New files area | ||
foreach ($this->{$filesModelName}['*'] ?? [] as $key => $file) { | ||
$this->{$library} = $this->{$library}->add(['uuid' => Str::uuid()->toString(), 'url' => $file->temporaryUrl()]); | ||
|
||
$key = $this->{$library}->keys()->last(); | ||
$this->{$filesModelName}[$key] = $file; | ||
} | ||
|
||
// Reset new files area | ||
unset($this->{$filesModelName}['*']); | ||
|
||
//Replace existing files | ||
foreach ($this->{$filesModelName} as $key => $file) { | ||
$media = $this->{$library}->get($key); | ||
$media['url'] = $file->temporaryUrl(); | ||
|
||
$this->{$library} = $this->{$library}->replace([$key => $media]); | ||
} | ||
|
||
$this->validateOnly($filesModelName . '.*'); | ||
} | ||
|
||
// Storage files into permanent area and updates the model with fresh sources | ||
public function syncMedia( | ||
Model $model, | ||
string $library = 'library', | ||
string $files = 'files', | ||
string $storage_subpath = '', | ||
$model_field = 'library', | ||
string $visibility = 'public', | ||
string $disk = 'public' | ||
): void { | ||
// Store files | ||
foreach ($this->{$files} as $index => $file) { | ||
$media = $this->{$library}->get($index); | ||
$name = $this->getFileName($media); | ||
|
||
$file = Storage::disk($disk)->putFileAs($storage_subpath, $file, $name, $visibility); | ||
$url = Storage::disk($disk)->url($file); | ||
|
||
// Update library | ||
$media['url'] = $url . "?updated_at=" . time(); | ||
$media['path'] = str($storage_subpath)->finish('/')->append($name)->toString(); | ||
$this->{$library} = $this->{$library}->replace([$index => $media]); | ||
} | ||
|
||
// Delete removed files from library | ||
$diffs = $model->{$model_field}?->filter(fn($item) => $this->{$library}->doesntContain('uuid', $item['uuid'])) ?? []; | ||
|
||
foreach ($diffs as $diff) { | ||
Storage::disk($disk)->delete($diff['path']); | ||
} | ||
|
||
// Updates model | ||
$model->update([$model_field => $this->{$library}]); | ||
|
||
// Resets files | ||
$this->{$files} = []; | ||
} | ||
|
||
private function getFileName(?array $media): ?string | ||
{ | ||
$name = $media['uuid'] ?? null; | ||
$extension = str($media['url'] ?? null)->afterLast('.')->before('?expires')->toString(); | ||
|
||
return "$name.$extension"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
<?php | ||
|
||
namespace Agenciafmd\Ui\View\Components\Forms\Inputs; | ||
|
||
use Closure; | ||
use Illuminate\Contracts\View\View; | ||
use Illuminate\Support\Collection; | ||
use Illuminate\View\Component; | ||
|
||
class ImageLibrary extends Component | ||
{ | ||
public string $uuid; | ||
|
||
public string $mimes = 'image/png, image/jpeg'; | ||
|
||
public function __construct( | ||
public ?string $label = null, | ||
public ?string $hint = null, | ||
public ?bool $hideErrors = false, | ||
public ?bool $hideProgress = false, | ||
public ?string $changeText = "Change", | ||
public ?string $cropText = "Crop", | ||
public ?string $removeText = "Remove", | ||
public ?string $cropTitleText = "Crop image", | ||
public ?string $cropCancelText = "Cancel", | ||
public ?string $cropSaveText = "Crop", | ||
public ?string $addFilesText = "Add images", | ||
public ?array $cropConfig = [], | ||
public Collection $preview = new Collection(), | ||
|
||
) { | ||
$this->uuid = "mary" . md5(serialize($this)); | ||
} | ||
|
||
public function modelName(): ?string | ||
{ | ||
return $this->attributes->wire('model'); | ||
} | ||
|
||
public function libraryName(): ?string | ||
{ | ||
return $this->attributes->wire('library'); | ||
} | ||
|
||
public function validationMessage(string $message): string | ||
{ | ||
return str($message)->after('field'); | ||
} | ||
|
||
public function cropSetup(): string | ||
{ | ||
return json_encode(array_merge([ | ||
'autoCropArea' => 1, | ||
'viewMode' => 2, | ||
'dragMode' => 'move', | ||
'checkCrossOrigin' => false, | ||
'aspectRatio' => 16/9, | ||
], $this->cropConfig), JSON_THROW_ON_ERROR); | ||
} | ||
|
||
public function render(): View|Closure|string | ||
{ | ||
return <<<'HTML' | ||
<div | ||
x-data="{ | ||
progress: 0, | ||
indeterminate: false, | ||
cropper: null, | ||
imageCrop: null, | ||
croppingId: null, | ||
bsCropModal: null, | ||
init () { | ||
this.imageCrop = this.$refs.crop?.querySelector('img') | ||
/* FMD */ | ||
this.bsCropModal = bootstrap.Modal.getOrCreateInstance($refs.cropModal); | ||
$refs.cropModal.addEventListener('hidden.bs.modal', event => { | ||
this.cropper?.destroy(); | ||
console.log('destroy'); | ||
}); | ||
this.$watch('progress', value => { | ||
this.indeterminate = value > 99 | ||
}) | ||
}, | ||
get processing () { | ||
return this.progress > 0 && this.progress < 100 | ||
}, | ||
close() { | ||
this.bsCropModal.hide(); | ||
/* | ||
$refs.maryCropModal.close() | ||
this.cropper?.destroy() | ||
*/ | ||
}, | ||
change() { | ||
if (this.processing) { | ||
return | ||
} | ||
this.$refs.files.click() | ||
}, | ||
refreshImage() { | ||
}, | ||
crop(id) { | ||
this.bsCropModal.show(); | ||
this.cropper?.destroy() | ||
this.croppingId = id.split('-')[1] | ||
this.imageCrop.src = document.getElementById(id).src | ||
this.cropper = new Cropper(this.imageCrop, {{ $cropSetup() }}); | ||
/* | ||
$refs.maryCropModal.showModal() | ||
this.cropper?.destroy() | ||
this.croppingId = id.split('-')[1] | ||
this.imageCrop.src = document.getElementById(id).src | ||
this.cropper = new Cropper(this.imageCrop, {{ $cropSetup() }}); | ||
*/ | ||
}, | ||
removeMedia(uuid, url){ | ||
this.indeterminate = true | ||
$wire.removeMedia(uuid, '{{ $modelName() }}', '{{ $libraryName() }}', url).then(() => this.indeterminate = false) | ||
}, | ||
refreshMediaOrder(order){ | ||
$wire.refreshMediaOrder(order, '{{ $libraryName() }}') | ||
}, | ||
refreshMediaSources(){ | ||
this.indeterminate = true | ||
$wire.refreshMediaSources('{{ $modelName() }}', '{{ $libraryName() }}').then(() => this.indeterminate = false) | ||
}, | ||
async save() { | ||
this.bsCropModal.hide(); | ||
this.progress = 1 | ||
this.cropper.getCroppedCanvas().toBlob((blob) => { | ||
@this.upload(this.croppingId, blob, | ||
(uploadedFilename) => { this.refreshMediaSources() }, | ||
(error) => { }, | ||
(event) => { this.progress = event.detail.progress; } | ||
) | ||
}) | ||
/* | ||
$refs.maryCropModal.close(); | ||
this.progress = 1 | ||
this.cropper.getCroppedCanvas().toBlob((blob) => { | ||
@this.upload(this.croppingId, blob, | ||
(uploadedFilename) => { this.refreshMediaSources() }, | ||
(error) => { }, | ||
(event) => { this.progress = event.detail.progress; } | ||
) | ||
}) | ||
*/ | ||
} | ||
}" | ||
x-on:livewire-upload-progress="progress = $event.detail.progress;" | ||
x-on:livewire-upload-finish="refreshMediaSources()" | ||
{{ $attributes->whereStartsWith('class') }} | ||
> | ||
<!-- STANDARD LABEL --> | ||
@if($label) | ||
<x-form.label for="{{ $modelName() . $uuid }}" @class(['required' => $attributes->has('required')])> | ||
{{ str($label)->lower()->ucfirst() }} | ||
</x-form.label> | ||
@endif | ||
<!-- PREVIEW AREA --> | ||
<div | ||
:class="(processing || indeterminate) && 'opacity-50 pe-none'" | ||
@class(["card mb-2", "d-none" => $preview->count() == 0]) | ||
> | ||
<div | ||
x-data="{ sortable: null }" | ||
x-init="sortable = new Sortable($el, { animation: 150, ghostClass: 'bg-gray-300', onEnd: (ev) => refreshMediaOrder(sortable.toArray()) })" | ||
class="list-group card-list-group cursor-move" | ||
> | ||
@foreach($preview as $key => $image) | ||
<div class="list-group-item" data-id="{{ $image['uuid'] }}"> | ||
<div wire:key="preview-{{ $image['uuid'] }}" class="row g-2 align-items-center" title="{{ $changeText }}"> | ||
<div class="col-auto"> | ||
<img src="{{ $image['url'] }}" class="rounded" alt="{{ $image['url'] }}" width="40" height="40" | ||
@click="document.getElementById('file-{{ $uuid}}-{{ $key }}').click()" | ||
id="image-{{ $modelName().'.'.$key }}-{{ $uuid }}"> | ||
<!-- HIDDEN FILE INPUT --> | ||
<input | ||
type="file" | ||
id="file-{{ $uuid}}-{{ $key }}" | ||
wire:model="{{ $modelName().'.'.$key }}" | ||
accept="{{ $attributes->get('accept') ?? $mimes }}" | ||
class="d-none" | ||
@change="progress = 1" | ||
/> | ||
</div> | ||
<!-- TODO colocaremos aqui os inputs de meta --> | ||
<!--div class="col"> | ||
Górą ty | ||
<div class="text-secondary"> | ||
GOLEC UORKIESTRA, | ||
Gromee, | ||
Bedoes | ||
</div> | ||
</div--> | ||
<div class="col-auto"> | ||
<!-- ACTIONS --> | ||
<div class="absolute flex flex-col gap-2 top-3 left-3 cursor-pointer p-2 rounded-lg"> | ||
<a class="link-muted" @click="removeMedia('{{ $image['uuid'] }}', '{{ $image['url'] }}')" title="{{ $removeText }}"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg></a> | ||
<a class="link-muted" @click="crop('image-{{ $modelName().'.'.$key }}-{{ $uuid }}')" title="{{ $cropText }}"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-scissors" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 7m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M8.6 8.6l10.4 10.4" /><path d="M8.6 15.4l10.4 -10.4" /></svg></a> | ||
</div> | ||
</div> | ||
</div> | ||
<!-- VALIDATION --> | ||
@error($modelName().'.'.$key) | ||
<div class="invalid-feedback d-block">{{ $validationMessage($message) }}</div> | ||
@enderror | ||
</div> | ||
@endforeach | ||
</div> | ||
</div> | ||
<!-- CROP MODAL --> | ||
<div @click.prevent="" x-ref="crop" wire:ignore> | ||
<x-modal id="cropModal{{ $uuid }}" x-ref="cropModal" title="{{ $cropTitleText }}"> | ||
<img src="#" crossOrigin="Anonymous" /> | ||
<x-slot:footer> | ||
<x-btn label="{{ $cropCancelText }}" class="me-auto" data-bs-dismiss="modal"/> | ||
<x-btn label="{{ $cropSaveText }}" class="btn-primary" @click="save()"/> | ||
</x-slot:footer> | ||
</x-modal> | ||
</div> | ||
<!-- PROGRESS BAR --> | ||
@if(! $hideProgress && $slot->isEmpty()) | ||
<div x-cloak class="progress mb-2" :class="(!processing && !indeterminate) && 'invisible'"> | ||
<div class="progress-bar" :style="{ width: progress + '%' }"></div> | ||
</div> | ||
@endif | ||
<!-- ADD FILES --> | ||
<a @click="$refs.files.click()" class="btn btn-info w-100 mb-1" :class="(processing || indeterminate) && 'disabled'"> | ||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-upload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 9l5 -5l5 5" /><path d="M12 4l0 12" /></svg> | ||
Adicionar arquivos | ||
</a> | ||
<!-- MAIN FILE INPUT --> | ||
<input | ||
id="{{ $uuid }}" | ||
type="file" | ||
x-ref="files" | ||
class="d-none" | ||
wire:model="{{ $modelName() }}.*" | ||
accept="{{ $attributes->get('accept') ?? $mimes }}" | ||
@change="progress = 1" | ||
multiple /> | ||
<!-- ERROR --> | ||
@if (! $hideErrors) | ||
@error($libraryName()) | ||
<div class="invalid-feedback">{{ $message }}</div> | ||
@enderror | ||
@endif | ||
<!-- HINT --> | ||
<x-form.hint message="{{ $hint }}"/> | ||
</div> | ||
HTML; | ||
} | ||
} |