Skip to content

Commit 30b6b9d

Browse files
snap to other elements too
1 parent 542e71a commit 30b6b9d

1 file changed

Lines changed: 169 additions & 78 deletions

File tree

packages/imagekit-editor-dev/src/components/editor/MoveableLayerController.tsx

Lines changed: 169 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Box } from "@chakra-ui/react"
2-
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react"
2+
import {
3+
type FC,
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from "react"
310
import Moveable from "react-moveable"
411
import type { Transformation } from "../../store"
512
import { useEditorStore } from "../../store"
@@ -41,8 +48,11 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
4148
onClick,
4249
}) => {
4350
const targetRef = useRef<HTMLImageElement>(null)
44-
const { updateTransformation, _internalState, _acknowledgeExpressionOverwrite } =
45-
useEditorStore()
51+
const {
52+
updateTransformation,
53+
_internalState,
54+
_acknowledgeExpressionOverwrite,
55+
} = useEditorStore()
4656
const [showExpressionDialog, setShowExpressionDialog] = useState(false)
4757
const [dragBlocked, setDragBlocked] = useState(false)
4858
const pendingActionRef = useRef<(() => void) | null>(null)
@@ -51,7 +61,10 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
5161
// Track the actual rendered dimensions of the single-layer image so we can
5262
// apply the correct centering offset when the layer config has no explicit
5363
// width/height (e.g. auto-sized text layers).
54-
const [naturalDims, setNaturalDims] = useState<{ w: number; h: number } | null>(null)
64+
const [naturalDims, setNaturalDims] = useState<{
65+
w: number
66+
h: number
67+
} | null>(null)
5568

5669
// Reset when the layer URL changes (layer content was re-rendered).
5770
useEffect(() => {
@@ -74,8 +87,9 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
7487
const value = layer.value as Record<string, unknown>
7588
const posConfig = extractLayerPositionConfig(value)
7689
const hasExpressions = hasExpressionCoords(posConfig)
77-
const isAcknowledged =
78-
_internalState.acknowledgedExpressionOverwrites.has(layer.id)
90+
const isAcknowledged = _internalState.acknowledgedExpressionOverwrites.has(
91+
layer.id,
92+
)
7993

8094
const checkExpressionGuard = useCallback(
8195
(action: () => void): boolean => {
@@ -196,7 +210,11 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
196210
if (posConfig.layerHeight == null) posConfig.layerHeight = naturalDims.h
197211
}
198212

199-
const rect = resolveLayerRect(posConfig, coordSpace.canvasW, coordSpace.canvasH)
213+
const rect = resolveLayerRect(
214+
posConfig,
215+
coordSpace.canvasW,
216+
coordSpace.canvasH,
217+
)
200218

201219
const left = rect.x * coordSpace.scale
202220
const top = rect.y * coordSpace.scale
@@ -220,6 +238,7 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
220238
ref={targetRef}
221239
src={layerUrl}
222240
alt=""
241+
data-layer-id={layer.id}
223242
style={{
224243
display: "block",
225244
maxWidth: "none",
@@ -254,70 +273,140 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
254273
</Box>
255274

256275
{/* Distance-to-edge overlay shown while dragging */}
257-
{isSelected && dragOffset && (() => {
258-
const canvasDisplayW = coordSpace.canvasW * coordSpace.scale
259-
const canvasDisplayH = coordSpace.canvasH * coordSpace.scale
260-
const lw = displayRect.width ?? targetRef.current?.offsetWidth ?? 0
261-
const lh = displayRect.height ?? targetRef.current?.offsetHeight ?? 0
262-
const lx = displayRect.left + dragOffset[0]
263-
const ly = displayRect.top + dragOffset[1]
264-
const lcx = lx + lw / 2
265-
const lcy = ly + lh / 2
266-
267-
const distTop = Math.round(ly / coordSpace.scale)
268-
const distBottom = Math.round((canvasDisplayH - ly - lh) / coordSpace.scale)
269-
const distLeft = Math.round(lx / coordSpace.scale)
270-
const distRight = Math.round((canvasDisplayW - lx - lw) / coordSpace.scale)
271-
272-
const lineStyle = {
273-
position: 'absolute' as const,
274-
background: '#E53E3E',
275-
zIndex: 10000,
276-
pointerEvents: 'none' as const,
277-
}
278-
const labelStyle = {
279-
position: 'absolute' as const,
280-
background: '#E53E3E',
281-
color: '#fff',
282-
fontSize: '10px',
283-
lineHeight: '14px',
284-
padding: '0 4px',
285-
borderRadius: '2px',
286-
whiteSpace: 'nowrap' as const,
287-
zIndex: 10001,
288-
pointerEvents: 'none' as const,
289-
transform: 'translate(-50%, -50%)',
290-
}
291-
292-
return (
293-
<>
294-
{ly > 1 && (
295-
<>
296-
<div style={{ ...lineStyle, left: `${lcx}px`, top: '0px', width: '1px', height: `${ly}px` }} />
297-
<div style={{ ...labelStyle, left: `${lcx}px`, top: `${ly / 2}px` }}>{distTop}px</div>
298-
</>
299-
)}
300-
{canvasDisplayH - ly - lh > 1 && (
301-
<>
302-
<div style={{ ...lineStyle, left: `${lcx}px`, top: `${ly + lh}px`, width: '1px', height: `${canvasDisplayH - ly - lh}px` }} />
303-
<div style={{ ...labelStyle, left: `${lcx}px`, top: `${ly + lh + (canvasDisplayH - ly - lh) / 2}px` }}>{distBottom}px</div>
304-
</>
305-
)}
306-
{lx > 1 && (
307-
<>
308-
<div style={{ ...lineStyle, left: '0px', top: `${lcy}px`, width: `${lx}px`, height: '1px' }} />
309-
<div style={{ ...labelStyle, left: `${lx / 2}px`, top: `${lcy}px` }}>{distLeft}px</div>
310-
</>
311-
)}
312-
{canvasDisplayW - lx - lw > 1 && (
313-
<>
314-
<div style={{ ...lineStyle, left: `${lx + lw}px`, top: `${lcy}px`, width: `${canvasDisplayW - lx - lw}px`, height: '1px' }} />
315-
<div style={{ ...labelStyle, left: `${lx + lw + (canvasDisplayW - lx - lw) / 2}px`, top: `${lcy}px` }}>{distRight}px</div>
316-
</>
317-
)}
318-
</>
319-
)
320-
})()}
276+
{isSelected &&
277+
dragOffset &&
278+
(() => {
279+
const canvasDisplayW = coordSpace.canvasW * coordSpace.scale
280+
const canvasDisplayH = coordSpace.canvasH * coordSpace.scale
281+
const lw = displayRect.width ?? targetRef.current?.offsetWidth ?? 0
282+
const lh = displayRect.height ?? targetRef.current?.offsetHeight ?? 0
283+
const lx = displayRect.left + dragOffset[0]
284+
const ly = displayRect.top + dragOffset[1]
285+
const lcx = lx + lw / 2
286+
const lcy = ly + lh / 2
287+
288+
const distTop = Math.round(ly / coordSpace.scale)
289+
const distBottom = Math.round(
290+
(canvasDisplayH - ly - lh) / coordSpace.scale,
291+
)
292+
const distLeft = Math.round(lx / coordSpace.scale)
293+
const distRight = Math.round(
294+
(canvasDisplayW - lx - lw) / coordSpace.scale,
295+
)
296+
297+
const lineStyle = {
298+
position: "absolute" as const,
299+
background: "#E53E3E",
300+
zIndex: 10000,
301+
pointerEvents: "none" as const,
302+
}
303+
const labelStyle = {
304+
position: "absolute" as const,
305+
background: "#E53E3E",
306+
color: "#fff",
307+
fontSize: "10px",
308+
lineHeight: "14px",
309+
padding: "0 4px",
310+
borderRadius: "2px",
311+
whiteSpace: "nowrap" as const,
312+
zIndex: 10001,
313+
pointerEvents: "none" as const,
314+
transform: "translate(-50%, -50%)",
315+
}
316+
317+
return (
318+
<>
319+
{ly > 1 && (
320+
<>
321+
<div
322+
style={{
323+
...lineStyle,
324+
left: `${lcx}px`,
325+
top: "0px",
326+
width: "1px",
327+
height: `${ly}px`,
328+
}}
329+
/>
330+
<div
331+
style={{
332+
...labelStyle,
333+
left: `${lcx}px`,
334+
top: `${ly / 2}px`,
335+
}}
336+
>
337+
{distTop}px
338+
</div>
339+
</>
340+
)}
341+
{canvasDisplayH - ly - lh > 1 && (
342+
<>
343+
<div
344+
style={{
345+
...lineStyle,
346+
left: `${lcx}px`,
347+
top: `${ly + lh}px`,
348+
width: "1px",
349+
height: `${canvasDisplayH - ly - lh}px`,
350+
}}
351+
/>
352+
<div
353+
style={{
354+
...labelStyle,
355+
left: `${lcx}px`,
356+
top: `${ly + lh + (canvasDisplayH - ly - lh) / 2}px`,
357+
}}
358+
>
359+
{distBottom}px
360+
</div>
361+
</>
362+
)}
363+
{lx > 1 && (
364+
<>
365+
<div
366+
style={{
367+
...lineStyle,
368+
left: "0px",
369+
top: `${lcy}px`,
370+
width: `${lx}px`,
371+
height: "1px",
372+
}}
373+
/>
374+
<div
375+
style={{
376+
...labelStyle,
377+
left: `${lx / 2}px`,
378+
top: `${lcy}px`,
379+
}}
380+
>
381+
{distLeft}px
382+
</div>
383+
</>
384+
)}
385+
{canvasDisplayW - lx - lw > 1 && (
386+
<>
387+
<div
388+
style={{
389+
...lineStyle,
390+
left: `${lx + lw}px`,
391+
top: `${lcy}px`,
392+
width: `${canvasDisplayW - lx - lw}px`,
393+
height: "1px",
394+
}}
395+
/>
396+
<div
397+
style={{
398+
...labelStyle,
399+
left: `${lx + lw + (canvasDisplayW - lx - lw) / 2}px`,
400+
top: `${lcy}px`,
401+
}}
402+
>
403+
{distRight}px
404+
</div>
405+
</>
406+
)}
407+
</>
408+
)
409+
})()}
321410

322411
{isSelected && targetRef.current && (
323412
<Moveable
@@ -327,28 +416,30 @@ export const MoveableLayerController: FC<MoveableLayerControllerProps> = ({
327416
resizable={isResizable && !dragBlocked}
328417
keepRatio
329418
snappable
330-
// snapGridWidth={10}
331-
// snapGridHeight={10}
332-
isDisplayGridGuidelines={false}
333419
horizontalGuidelines={horizontalGuidelines}
334420
verticalGuidelines={verticalGuidelines}
421+
elementGuidelines={Array.from(
422+
document.querySelectorAll<HTMLElement>("[data-layer-id]"),
423+
).filter((el) => el.dataset.layerId !== layer.id)}
424+
snapThreshold={5}
335425
snapDistFormat={(v) => `${Math.round(v)}px`}
336426
snapDirections={{
337427
top: true,
338428
left: true,
339429
bottom: true,
340430
right: true,
341-
// center: true,
342-
// middle: true,
431+
center: true,
432+
middle: true,
343433
}}
344434
elementSnapDirections={{
345435
top: true,
346436
left: true,
347437
bottom: true,
348438
right: true,
349-
// center: true,
350-
// middle: true,
439+
center: true,
440+
middle: true,
351441
}}
442+
snapGap={true}
352443
onDragStart={() => {
353444
const proceed = checkExpressionGuard(() => {
354445
// User confirmed — they can drag again now

0 commit comments

Comments
 (0)