Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Svelte 5 runes support #59

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/package": "^2.2.4",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"cypress": "^12.4.1",
Expand All @@ -47,13 +47,13 @@
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"prettier-plugin-svelte": "^3.3.2",
"query-string": "^8.1.0",
"start-server-and-test": "^1.15.3",
"svelte-check": "^3.4.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vite": "^6.0.0",
"vitest": "^1.0.0"
},
"peerDependencies": {
Expand All @@ -74,6 +74,6 @@
"!dist/**/*.spec.*"
],
"dependencies": {
"svelte": "^4.2.8"
"svelte": "^5.12.0"
}
}
110 changes: 66 additions & 44 deletions src/lib/Cropper.svelte
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import type { HTMLImgAttributes } from 'svelte/elements'
import { onDestroy, onMount } from 'svelte'
import * as helpers from './helpers'
import type { CropShape, DispatchEvents, ImageSize, Point, Size } from './types'

export let image: string
export let crop: Point = { x: 0, y: 0 }
export let zoom = 1
export let aspect = 4 / 3
export let minZoom = 1
export let maxZoom = 3
export let cropSize: Size | null = null
export let cropShape: CropShape = 'rect'
export let showGrid = true
export let zoomSpeed = 1
export let crossOrigin: HTMLImgAttributes['crossorigin'] = null
export let restrictPosition = true
export let tabindex: number | undefined = undefined

let cropperSize: Size | null = null
let imageSize: ImageSize = { width: 0, height: 0, naturalWidth: 0, naturalHeight: 0 }
let containerEl: HTMLDivElement | null = null
let containerRect: DOMRect | null = null
let imgEl: HTMLImageElement | null = null
let dragStartPosition: Point = { x: 0, y: 0 }
let dragStartCrop: Point = { x: 0, y: 0 }
let lastPinchDistance = 0
let rafDragTimeout: number | null = null
let rafZoomTimeout: number | null = null

const dispatch = createEventDispatcher<DispatchEvents>()
import type { ImageSize, Point, CropperProps, Size } from './types'


let {
image,
crop = $bindable({ x: 0, y: 0 }),
zoom = $bindable(1),
minZoom = $bindable(1),
maxZoom = $bindable(3),
aspect = 4 / 3,
cropSize = null,
cropShape = 'rect',
showGrid = true,
zoomSpeed = 1,
crossOrigin = null,
restrictPosition = true,
tabindex = undefined,
oncropcomplete,
}: Partial<CropperProps> = $props()

let cropperSize = $state<Size | null>(null)
let imageSize = $state<ImageSize>({ width: 0, height: 0, naturalWidth: 0, naturalHeight: 0 })
let containerEl = $state<HTMLDivElement | null>(null)
let containerRect = $state<DOMRect | null>(null)
let imgEl = $state<HTMLImageElement | null>(null)
let dragStartPosition = $state<Point>({ x: 0, y: 0 })
let dragStartCrop = $state<Point>({ x: 0, y: 0 })
let lastPinchDistance = $state(0)
let rafDragTimeout = $state<number | null>(null)
let rafZoomTimeout = $state<number | null>(null)

onMount(() => {
// when rendered via SSR, the image can already be loaded and its onLoad callback will never be called
Expand Down Expand Up @@ -229,34 +230,55 @@
restrictPosition
)

dispatch('cropcomplete', {
oncropcomplete?.({
percent: croppedAreaPercentages,
pixels: croppedAreaPixels,
})
}

// ------ Reactive statement ------
//when aspect changes, we reset the cropperSize
$: if (imgEl) {
cropperSize = cropSize ? cropSize : helpers.getCropSize(imgEl.width, imgEl.height, aspect)
}
$effect(() => {
if (imgEl) {
cropperSize = cropSize ? cropSize : helpers.getCropSize(imgEl.width, imgEl.height, aspect)
}
})

// when zoom changes, we recompute the cropped area
$: zoom && emitCropData()
let lastZoom = $state(zoom);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not need to track the last zoom value ourselves, Svelte should do it automatically as long as we access zoom in the effect.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can be removed safely without causing loops. Just added it for testing and forgot to remove

$effect(() => {
if (zoom !== lastZoom) {
lastZoom = zoom;
emitCropData()
}
})

$: if(aspect) {
computeSizes()
emitCropData()
}
let lastAspect = $state(aspect);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this state should not be needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lastAspect is required here, when checking for just the aspect variable, it enters an infinite loop.

$effect(() => {
if (aspect !== lastAspect) {
lastAspect = aspect;
computeSizes()
emitCropData()
}
})
</script>

<svelte:window on:resize={computeSizes} />
<div
class="svelte-easy-crop-container"
bind:this={containerEl}
on:mousedown|preventDefault={onMouseDown}
on:touchstart|nonpassive|preventDefault={onTouchStart}
ValentinH marked this conversation as resolved.
Show resolved Hide resolved
on:wheel|nonpassive|preventDefault={onWheel}
onmousedown={(e) => {
e.preventDefault()
onMouseDown(e)
}}
ontouchstart={(e) => {
e.preventDefault()
onTouchStart(e)
}}
onwheel={(e) => {
e.preventDefault();
onWheel(e)
}}
{tabindex}
role="button"
data-testid="container"
Expand All @@ -265,7 +287,7 @@
bind:this={imgEl}
class="svelte-easy-crop-image"
src={image}
on:load={onImgLoad}
onload={onImgLoad}
alt=""
style="transform: translate({crop.x}px, {crop.y}px) scale({zoom});"
crossorigin={crossOrigin}
Expand All @@ -277,7 +299,7 @@
class:svelte-easy-crop-grid={showGrid}
style="width: {cropperSize.width}px; height: {cropperSize.height}px;"
data-testid="cropper"
/>
></div>
{/if}
</div>

Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Cropper from './Cropper.svelte'

export * from "./types"
export default Cropper
19 changes: 19 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import type { HTMLImgAttributes } from 'svelte/elements'

export type CropShape = 'rect' | 'round'

export type CropperProps = {
image: string
crop: Point
zoom: number
aspect: number
minZoom: number
maxZoom: number
cropSize: Size | null
cropShape: CropShape
showGrid: boolean
zoomSpeed: number
crossOrigin: HTMLImgAttributes['crossorigin']
restrictPosition: boolean
tabindex: number | undefined
oncropcomplete: (event: DispatchEvents['cropcomplete']) => void
}

export interface Size {
width: number
height: number
Expand Down
10 changes: 5 additions & 5 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import queryString from 'query-string'
import Cropper from '../lib'

let crop = { x: 0, y: 0 }
let zoom = 1
let minZoom = 0.5;
let maxZoom = 3;
let crop = $state({ x: 0, y: 0 })
let zoom = $state(1)
let minZoom = $state(0.5)
let maxZoom = $state(3)

const urlArgs = typeof window !== 'undefined' ? queryString.parse(window.location.search) : null
let image = typeof urlArgs?.img === 'string' ? urlArgs.img : '/images/dog.jpeg' // so we can change the image from our tests
Expand All @@ -17,5 +17,5 @@
bind:zoom
bind:minZoom
bind:maxZoom
on:cropcomplete={e => console.log(e.detail)}
oncropcomplete={(e) => console.log(e)}
/>
Loading