89 lines
2.4 KiB
Svelte
89 lines
2.4 KiB
Svelte
<script lang="ts">
|
|
import { createEventDispatcher } from 'svelte';
|
|
|
|
export let open = false;
|
|
export let imageSrc = '';
|
|
|
|
const dispatch = createEventDispatcher<{
|
|
crop: Blob;
|
|
cancel: void;
|
|
}>();
|
|
|
|
let cropperInstance: import('cropperjs').default | null = null;
|
|
let isProcessing = false;
|
|
|
|
// Svelte action — only runs in the browser, so dynamic import is safe here
|
|
function initCropper(node: HTMLImageElement) {
|
|
import('cropperjs').then(({ default: Cropper }) => {
|
|
cropperInstance = new Cropper(node, {
|
|
aspectRatio: 1,
|
|
viewMode: 0, // no restriction — canvas can move freely
|
|
dragMode: 'move', // drag moves the image, not the crop box
|
|
cropBoxMovable: false,
|
|
cropBoxResizable: false,
|
|
toggleDragModeOnDblclick: false,
|
|
autoCropArea: 0.8, // crop box = 80% so image extends beyond it and can be dragged
|
|
});
|
|
});
|
|
|
|
return {
|
|
destroy() {
|
|
cropperInstance?.destroy();
|
|
cropperInstance = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
function handleApply() {
|
|
if (!cropperInstance) return;
|
|
isProcessing = true;
|
|
|
|
const canvas = cropperInstance.getCroppedCanvas({ width: 500, height: 500 });
|
|
canvas.toBlob(
|
|
(blob) => {
|
|
if (blob) dispatch('crop', blob);
|
|
isProcessing = false;
|
|
},
|
|
'image/jpeg',
|
|
0.92
|
|
);
|
|
}
|
|
|
|
function handleCancel() {
|
|
dispatch('cancel');
|
|
}
|
|
</script>
|
|
|
|
{#if open}
|
|
<div class="modal modal-open z-50">
|
|
<div class="modal-box max-w-2xl w-full">
|
|
<h3 class="font-bold text-lg mb-1">Crop Logo</h3>
|
|
<p class="text-sm text-base-content/50 mb-4">Drag to reposition • Scroll or pinch to zoom</p>
|
|
|
|
<div class="w-full h-80 bg-base-300 rounded-lg overflow-hidden">
|
|
<img
|
|
src={imageSrc}
|
|
alt="Crop preview"
|
|
use:initCropper
|
|
class="block max-w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div class="modal-action mt-6">
|
|
<button class="btn btn-ghost" on:click={handleCancel} disabled={isProcessing}>
|
|
Cancel
|
|
</button>
|
|
<button class="btn btn-primary" on:click={handleApply} disabled={isProcessing}>
|
|
{#if isProcessing}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
Processing...
|
|
{:else}
|
|
Apply Crop
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop bg-black/60" on:click={handleCancel} role="presentation"></div>
|
|
</div>
|
|
{/if}
|