102 lines
3.0 KiB
JavaScript
102 lines
3.0 KiB
JavaScript
import { inject, ref } from 'vue';
|
|
|
|
/**
|
|
* Shared image upload composable used by both the left-panel ImageDropZone
|
|
* and the right-side preview block drop zones.
|
|
*
|
|
* @param {() => string | null} getUploadUrl A getter (or plain string) for the upload endpoint.
|
|
* @param {(url: string) => void} onSuccess Called immediately with a blob URL, then again with the server URL.
|
|
*/
|
|
export const useImageUpload = (getUploadUrl, onSuccess) => {
|
|
const isDragOver = ref(false);
|
|
const isUploading = ref(false);
|
|
const error = ref(null);
|
|
|
|
// Plugs into the parent editor's pending-upload counter so the form submit
|
|
// can be blocked while this upload is in flight.
|
|
const pendingUploads = inject('editorPendingUploads', null);
|
|
|
|
const uploadFile = async (file) => {
|
|
if (!file || !file.type.startsWith('image/')) {
|
|
return;
|
|
}
|
|
|
|
const uploadUrl = typeof getUploadUrl === 'function' ? getUploadUrl() : getUploadUrl;
|
|
|
|
if (!uploadUrl) {
|
|
return;
|
|
}
|
|
|
|
// Emit blob URL immediately so the preview renders at once
|
|
const blobUrl = URL.createObjectURL(file);
|
|
onSuccess(blobUrl);
|
|
|
|
isUploading.value = true;
|
|
error.value = null;
|
|
if (pendingUploads) pendingUploads.value++;
|
|
|
|
try {
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '';
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
const response = await fetch(uploadUrl, {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-TOKEN': csrfToken },
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
URL.revokeObjectURL(blobUrl);
|
|
onSuccess(data.url);
|
|
} catch {
|
|
error.value = 'Upload failed. Please try again.';
|
|
URL.revokeObjectURL(blobUrl);
|
|
onSuccess('');
|
|
} finally {
|
|
isUploading.value = false;
|
|
if (pendingUploads) pendingUploads.value--;
|
|
}
|
|
};
|
|
|
|
const onDragOver = (event) => {
|
|
event.preventDefault();
|
|
isDragOver.value = true;
|
|
};
|
|
|
|
const onDragLeave = () => {
|
|
isDragOver.value = false;
|
|
};
|
|
|
|
const onDrop = (event) => {
|
|
event.preventDefault();
|
|
isDragOver.value = false;
|
|
const file = event.dataTransfer?.files?.[0];
|
|
|
|
if (file) {
|
|
uploadFile(file);
|
|
}
|
|
};
|
|
|
|
const fileInputRef = ref(null);
|
|
|
|
const openPicker = () => {
|
|
if (!fileInputRef.value) return;
|
|
fileInputRef.value.value = '';
|
|
fileInputRef.value.click();
|
|
};
|
|
|
|
const onFileSelected = (event) => {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
uploadFile(file);
|
|
}
|
|
};
|
|
|
|
return { isDragOver, isUploading, error, fileInputRef, onDragOver, onDragLeave, onDrop, openPicker, onFileSelected, uploadFile };
|
|
};
|