When adding wire:model="deal_photos" to my file input, each image uploaded is displayed twice. Clicking to select versus drag-n-drop makes no difference. The alpine files object contains exactly the number of objects related to the number of images I select. If I drag and drop images to sort them, each image the dragged image is hovering also highlights its duplicate.
If I remove wire:model from the input then Alpine displays the correct number of temp images. What am I missing?
<!-- blade file -->
<div>
<div class="w-full bg-white rounded">
<div id="dndBlock" x-data="dataFileDnD()" class="relative flex flex-col p-4 text-gray-400 rounded">
<div x-ref="dnd"
class="relative flex flex-col text-gray-400 border border-gray-200 border-dashed rounded cursor-pointer">
<input wire:model="deal_photos" type="file" multiple
class="absolute inset-0 z-50 w-full h-full p-0 m-0 outline-none opacity-0 cursor-pointer"
@change="addFiles($event)"
@dragover="$refs.dnd.classList.add('border-[#8a1a5b]');"
@dragleave="$refs.dnd.classList.remove('border-[#8a1a5b]');"
@drop="$refs.dnd.classList.remove('border-[#8a1a5b]');"
title="" accept="image/jpg,image/jpeg,image/png" />
<div class="flex flex-col items-center justify-center py-10 text-center">
<svg class="w-6 h-6 mr-1 text-current-50" 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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="m-0">Drag your files here or click in this area.</p>
</div>
</div>
<template x-if="files.length > 0">
<div class="grid grid-cols-2 gap-4 mt-4 md:grid-cols-6" @drop.prevent="drop($event)"
@dragover.prevent="$event.dataTransfer.dropEffect = 'move'">
<template x-for="(file, index) in files">
<div class="relative flex flex-col items-center overflow-hidden text-center bg-gray-100 border rounded cursor-move select-none" style="padding-top: 100%;" @dragstart="dragstart($event)" @dragend="fileDragging = null" :class="{'border-blue-600': fileDragging == index}" draggable="true" :data-index="index">
<button class="absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none" type="button" @click="remove(index)">
<svg class="w-4 h-4 text-gray-700" 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 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<template x-if="files[index].type.includes('image/')">
<img class="absolute inset-0 z-0 object-cover w-full h-full border-4 border-white preview"
x-bind:src="loadFile(files[index])" />
</template>
<div class="absolute bottom-0 left-0 right-0 flex flex-col p-2 text-xs bg-white bg-opacity-50">
<span class="w-full font-bold text-gray-900 truncate"
x-text="files[index].name">Loading</span>
<span class="text-xs text-gray-900" x-text="humanFileSize(files[index].size)">...</span>
</div>
<div class="absolute inset-0 z-40 transition-colors duration-300" @dragenter="dragenter($event)"
@dragleave="fileDropping = null"
:class="{'bg-blue-200 bg-opacity-80': fileDropping == index && fileDragging != index}">
</div>
</div>
</template>
</div>
</template>
<!-- Temporary -->
<span x-text="files.length"></span>
</div>
</div>
<button wire:click="test"
class="mt-4 px-4 py-2 rounded bg-black text-white">Test</button>
<script src="https://unpkg.com/create-file-list"></script>
<script>
function dataFileDnD() {
return {
files: [],
fileDragging: null,
fileDropping: null,
humanFileSize(size) {
const i = Math.floor(Math.log(size) / Math.log(1024));
return (
(size / Math.pow(1024, i)).toFixed(2) * 1 +
" " +
["B", "kB", "MB", "GB", "TB"][i]
);
},
remove(index) {
let files = [...this.files];
files.splice(index, 1);
this.files = createFileList(files);
},
drop(e) {
let removed, add;
let files = [...this.files];
removed = files.splice(this.fileDragging, 1);
files.splice(this.fileDropping, 0, ...removed);
this.files = createFileList(files);
this.fileDropping = null;
this.fileDragging = null;
},
dragenter(e) {
let targetElem = e.target.closest("[draggable]");
this.fileDropping = targetElem.getAttribute("data-index");
},
dragstart(e) {
this.fileDragging = e.target
.closest("[draggable]")
.getAttribute("data-index");
e.dataTransfer.effectAllowed = "move";
},
loadFile(file) {
const preview = document.querySelectorAll(".preview");
const blobUrl = URL.createObjectURL(file);
preview.forEach(elem => {
elem.onload = () => {
URL.revokeObjectURL(elem.src); // free memory
};
});
return blobUrl;
},
addFiles(e) {
this.files = createFileList([...this.files], [...e.target.files]);
console.log(this.files);
}
};
}
</script>
</div>
<?php
namespace App\Http\Livewire\Deals;
use Livewire\Component;
use Livewire\WithFileUploads;
class DealBuilder extends Component
{
use WithFileUploads;
public $deal_photos = [];
public function test()
{
dd($this->deal_photos);
}
public function render()
{
return view('livewire.deals.deal-builder');
}
}