Update
This commit is contained in:
101
resources/js/projects-renderer/composables/useImageUpload.js
Normal file
101
resources/js/projects-renderer/composables/useImageUpload.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
export const useIntersectionActivation = (options = { rootMargin: '180px 0px' }) => {
|
||||
const target = ref(null);
|
||||
const isActive = ref(false);
|
||||
let observer;
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
isActive.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
isActive.value = true;
|
||||
observer?.disconnect();
|
||||
});
|
||||
}, options);
|
||||
|
||||
if (target.value) {
|
||||
observer.observe(target.value);
|
||||
} else {
|
||||
isActive.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
target,
|
||||
isActive,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user