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
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,24 @@
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/package": "^2.2.4",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/kit": "^2.15.0",
"@sveltejs/package": "^2.3.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"cypress": "^12.4.1",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"prettier": "^3.4.2",
"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.5",
"vitest": "^1.0.0"
},
"peerDependencies": {
Expand All @@ -74,6 +74,6 @@
"!dist/**/*.spec.*"
],
"dependencies": {
"svelte": "^4.2.8"
"svelte": "^5.15.0"
}
}
105 changes: 61 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'
import type { Action } from 'svelte/action'

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 @@ -100,6 +101,7 @@
})

const onMouseDown = (e: MouseEvent) => {
e.preventDefault()
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onDragStopped)
onDragStart(getMousePoint(e))
Expand All @@ -108,6 +110,7 @@
const onMouseMove = (e: MouseEvent) => onDrag(getMousePoint(e))

const onTouchStart = (e: TouchEvent) => {
e.preventDefault()
document.addEventListener('touchmove', onTouchMove, { passive: false }) // iOS 11 now defaults to passive: true
document.addEventListener('touchend', onDragStopped)

Expand Down Expand Up @@ -179,6 +182,7 @@
}

const onWheel = (e: WheelEvent) => {
e.preventDefault()
const point = getMousePoint(e)
const newZoom = zoom - (e.deltaY * zoomSpeed) / 200
setNewZoom(newZoom, point)
Expand Down Expand Up @@ -229,34 +233,47 @@
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()
$effect(() => {
if (zoom) {
emitCropData()
}
})

$: if(aspect) {
computeSizes()
emitCropData()
}
const containerAction: Action<HTMLDivElement> = node => {
$effect(() => {
node.addEventListener('touchstart', onTouchStart)
node.addEventListener('mousedown', onMouseDown)
node.addEventListener('wheel', onWheel, { passive: false })

return () => {
node.removeEventListener('touchstart', onTouchStart)
node.removeEventListener('mousedown', onMouseDown)
node.removeEventListener('wheel', onWheel)
}
})
}
</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}
use:containerAction
{tabindex}
role="button"
data-testid="container"
Expand All @@ -265,7 +282,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 +294,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
22 changes: 22 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import type { HTMLImgAttributes } from 'svelte/elements'

export type CropShape = 'rect' | 'round'

export type OnCropCompleteEvent = { percent: CropArea; pixels: CropArea }
export type OnCropComplete = (event: OnCropCompleteEvent) => void

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: OnCropComplete
}

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