Skip to content

Commit

Permalink
feat: implement handling of drag events (file drag & drop)
Browse files Browse the repository at this point in the history
Apart from the usual canvas dragenter & dragleave, this commits adds
special handlers: onDragOverEnter, onDragOverLeave & onDragOverMissed.
These are fired when dragover events intersect with objects in a scene
or miss all of them, similar to how onPointerMissed already works.
onDrop and onDropMissed are other additions. These can come  handy
when working on editor UIs etc. that need to attribute different drag &
drop actions to different objects.
  • Loading branch information
haywirez committed Jul 20, 2022
1 parent 0921539 commit ec958d9
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 9 deletions.
9 changes: 8 additions & 1 deletion docs/API/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ nav: 8

Additionally, there's a special `onUpdate` that is called every time the object gets fresh props, which is good for things like `self => (self.verticesNeedUpdate = true)`.

Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes.
Also notice the `onPointerMissed` on the canvas element, which fires on clicks that haven't hit _any_ meshes. Similarly, `onDragOverMissed` and `onDropMissed` can handle actions that need to be taken when file drag & drop drag events are not hitting items in the scene.

```jsx
<mesh
onClick={(e) => console.log('click')}
onContextMenu={(e) => console.log('context menu')}
onDoubleClick={(e) => console.log('double click')}
onDragEnter={(e) => console.log('drag enter')}
onDragLeave={(e) => console.log('drag leave')}
onDragOverEnter={(e) => console.log('dragover enter')}
onDragOverLeave={(e) => console.log('dragover leave')}
onDragOverMissed={(e) => console.log('dragover missed')}
onDrop={(e) => console.log('dropped')}
onDropMissed={(e) => console.log('drop missed')}
onWheel={(e) => console.log('wheel spins')}
onPointerUp={(e) => console.log('up')}
onPointerDown={(e) => console.log('down')}
Expand Down
53 changes: 53 additions & 0 deletions example/src/demos/FileDragDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { SyntheticEvent, useState } from 'react'
import { Canvas } from '@react-three/fiber'
import { a, useSpring } from '@react-spring/three'
import { OrbitControls } from '@react-three/drei'

export default function Box() {
const [active, setActive] = useState(0)
const [activeBg, setActiveBg] = useState(0)
// create a common spring that will be used later to interpolate other values
const { spring } = useSpring({
spring: active,
config: { mass: 5, tension: 400, friction: 50, precision: 0.0001 },
})
// interpolate values from commong spring
const scale = spring.to([0, 1], [1, 2])
const rotation = spring.to([0, 1], [0, Math.PI])
const color = active ? spring.to([0, 1], ['#6246ea', '#e45858']) : spring.to([0, 1], ['#620000', '#e40000'])
const bgColor = activeBg ? 'lightgreen' : 'lightgray'
const preventDragDropDefaults = {
onDrop: (e: SyntheticEvent) => e.preventDefault(),
onDragEnter: (e: SyntheticEvent) => e.preventDefault(),
onDragOver: (e: SyntheticEvent) => e.preventDefault(),
}
return (
<Canvas
{...preventDragDropDefaults}
onDropMissed={(e) => {
console.log('drop missed!')
setActiveBg(0)
}}
onDragOverMissed={(e) => setActiveBg(1)}
onDragLeave={() => setActiveBg(0)}>
<color attach="background" args={[bgColor]} />
<a.mesh
rotation-y={rotation}
scale-x={scale}
scale-z={scale}
onDrop={(e) => {
console.log('dropped!')
setActive(0)
}}
onDragOverEnter={() => {
setActive(1)
setActiveBg(0)
}}
onDragOverLeave={() => setActive(0)}>
<boxBufferGeometry />
<a.meshBasicMaterial color={color} />
</a.mesh>
<OrbitControls />
</Canvas>
)
}
2 changes: 2 additions & 0 deletions example/src/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Animation = { Component: lazy(() => import('./Animation')) }
const AutoDispose = { Component: lazy(() => import('./AutoDispose')) }
const ClickAndHover = { Component: lazy(() => import('./ClickAndHover')) }
const ContextMenuOverride = { Component: lazy(() => import('./ContextMenuOverride')) }
const FileDragDrop = { Component: lazy(() => import('./FileDragDrop')) }
const Gestures = { Component: lazy(() => import('./Gestures')) }
const Gltf = { Component: lazy(() => import('./Gltf')) }
const Inject = { Component: lazy(() => import('./Inject')) }
Expand All @@ -30,6 +31,7 @@ export {
AutoDispose,
ClickAndHover,
ContextMenuOverride,
FileDragDrop,
Gestures,
Gltf,
Inject,
Expand Down
74 changes: 68 additions & 6 deletions packages/fiber/src/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export type Events = {
onClick: EventListener
onContextMenu: EventListener
onDoubleClick: EventListener
onDragEnter: EventListener
onDragLeave: EventListener
onDragOverEnter: EventListener
onDragOverLeave: EventListener
onDrop: EventListener
onDropMissed: EventListener
onWheel: EventListener
onPointerDown: EventListener
onPointerUp: EventListener
Expand All @@ -54,6 +60,13 @@ export type EventHandlers = {
onClick?: (event: ThreeEvent<MouseEvent>) => void
onContextMenu?: (event: ThreeEvent<MouseEvent>) => void
onDoubleClick?: (event: ThreeEvent<MouseEvent>) => void
onDragEnter?: (event: ThreeEvent<DragEvent>) => void
onDragLeave?: (event: ThreeEvent<DragEvent>) => void
onDragOverEnter?: (event: ThreeEvent<DragEvent>) => void
onDragOverLeave?: (event: ThreeEvent<DragEvent>) => void
onDragOverMissed?: (event: DragEvent) => void
onDrop?: (event: ThreeEvent<DragEvent>) => void
onDropMissed?: (event: DragEvent) => void
onPointerUp?: (event: ThreeEvent<PointerEvent>) => void
onPointerDown?: (event: ThreeEvent<PointerEvent>) => void
onPointerOver?: (event: ThreeEvent<PointerEvent>) => void
Expand Down Expand Up @@ -105,10 +118,14 @@ export function getEventPriority() {
case 'click':
case 'contextmenu':
case 'dblclick':
case 'dragenter':
case 'dragleave':
case 'drop':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
return DiscreteEventPriority
case 'dragover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
Expand Down Expand Up @@ -171,10 +188,14 @@ export function createEvents(store: UseBoundStore<RootState>) {

/** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
function filterPointerEvents(objects: THREE.Object3D[]) {
return objects.filter((obj) =>
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
),
return objects.filter(
(obj) =>
['Move', 'Over', 'Enter', 'Out', 'Leave'].some(
(name) => (obj as unknown as Instance).__r3f?.handlers[('onPointer' + name) as keyof EventHandlers],
) ||
['Over', 'Enter', 'Leave'].some(
(name) => (obj as unknown as Instance).__r3f?.handlers[('onDrag' + name) as keyof EventHandlers],
),
)
}

Expand Down Expand Up @@ -377,6 +398,8 @@ export function createEvents(store: UseBoundStore<RootState>) {
const data = { ...hoveredObj, intersections }
handlers.onPointerOut?.(data as ThreeEvent<PointerEvent>)
handlers.onPointerLeave?.(data as ThreeEvent<PointerEvent>)
// @ts-ignore
handlers.onDragOverLeave?.(data)
}
}
})
Expand All @@ -387,6 +410,7 @@ export function createEvents(store: UseBoundStore<RootState>) {
switch (name) {
case 'onPointerLeave':
case 'onPointerCancel':
case 'onDragLeave':
return () => cancelPointer([])
case 'onLostPointerCapture':
return (event: DomEvent) => {
Expand All @@ -402,13 +426,15 @@ export function createEvents(store: UseBoundStore<RootState>) {

// Any other pointer goes here ...
return (event: DomEvent) => {
const { onPointerMissed, internal } = store.getState()
const { onPointerMissed, onDragOverMissed, onDropMissed, internal } = store.getState()

//prepareRay(event)
internal.lastEvent.current = event

// Get fresh intersects
const isPointerMove = name === 'onPointerMove'
const isDragOver = name === 'onDragOver'
const isDrop = name === 'onDrop'
const isClickEvent = name === 'onClick' || name === 'onContextMenu' || name === 'onDoubleClick'
const filter = isPointerMove ? filterPointerEvents : undefined
//const hits = patchIntersects(intersect(filter), event)
Expand All @@ -429,8 +455,17 @@ export function createEvents(store: UseBoundStore<RootState>) {
if (onPointerMissed) onPointerMissed(event)
}
}
if (isDragOver && !hits.length) {
dragOverMissed(event as DragEvent, internal.interaction)
if (onDragOverMissed) onDragOverMissed(event as DragEvent)
}
if (isDrop && !hits.length) {
dropMissed(event as DragEvent, internal.interaction)
if (onDropMissed) onDropMissed(event as DragEvent)
}

// Take care of unhover
if (isPointerMove) cancelPointer(hits)
if (isPointerMove || isDragOver) cancelPointer(hits)

handleIntersects(hits, event, delta, (data: ThreeEvent<DomEvent>) => {
const eventObject = data.eventObject
Expand All @@ -457,6 +492,23 @@ export function createEvents(store: UseBoundStore<RootState>) {
}
// Call mouse move
handlers.onPointerMove?.(data as ThreeEvent<PointerEvent>)
} else if (isDragOver) {
// When enter or out is present take care of hover-state
const id = makeId(data)
const hoveredItem = internal.hovered.get(id)
if (!hoveredItem) {
// If the object wasn't previously hovered, book it and call its handler
internal.hovered.set(id, data)
handlers.onDragOverEnter?.(data as ThreeEvent<DragEvent>)
} else if (hoveredItem.stopped) {
// If the object was previously hovered and stopped, we shouldn't allow other items to proceed
data.stopPropagation()
} else if (internal.initialHits.includes(eventObject)) {
dragOverMissed(
event as DragEvent,
internal.interaction.filter((object) => !internal.initialHits.includes(object)),
)
}
} else {
// All other events ...
const handler = handlers[name as keyof EventHandlers] as (event: ThreeEvent<PointerEvent>) => void
Expand Down Expand Up @@ -492,5 +544,15 @@ export function createEvents(store: UseBoundStore<RootState>) {
)
}

function dragOverMissed(event: DragEvent, objects: THREE.Object3D[]) {
objects.forEach((object: THREE.Object3D) =>
(object as unknown as Instance).__r3f?.handlers.onDragOverMissed?.(event),
)
}

function dropMissed(event: DragEvent, objects: THREE.Object3D[]) {
objects.forEach((object: THREE.Object3D) => (object as unknown as Instance).__r3f?.handlers.onDropMissed?.(event))
}

return { handlePointer }
}
10 changes: 10 additions & 0 deletions packages/fiber/src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export type RenderProps<TCanvas extends Element> = {
onCreated?: (state: RootState) => void
/** Response for pointer clicks that have missed any target */
onPointerMissed?: (event: MouseEvent) => void
/** Response for dragover events that have missed any target */
onDragOverMissed?: (event: DragEvent) => void
/** Response for drop events that have missed any target */
onDropMissed?: (event: DragEvent) => void
}

const createRendererInstance = <TElement extends Element>(gl: GLProps, canvas: TElement): THREE.WebGLRenderer => {
Expand Down Expand Up @@ -169,6 +173,8 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
raycaster: raycastOptions,
camera: cameraOptions,
onPointerMissed,
onDragOverMissed,
onDropMissed,
} = props

let state = store.getState()
Expand Down Expand Up @@ -282,6 +288,10 @@ function createRoot<TCanvas extends Element>(canvas: TCanvas): ReconcilerRoot<TC
if (state.frameloop !== frameloop) state.setFrameloop(frameloop)
// Check pointer missed
if (!state.onPointerMissed) state.set({ onPointerMissed })
// Check dragover missed
if (!state.onDragOverMissed) state.set({ onDragOverMissed })
// Check drop missed
if (!state.onDropMissed) state.set({ onDropMissed })
// Check performance
if (performance && !is.equ(performance, state.performance, shallowLoose))
state.set((state) => ({ performance: { ...state.performance, ...performance } }))
Expand Down
6 changes: 6 additions & 0 deletions packages/fiber/src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export type RootState = {
setFrameloop: (frameloop?: 'always' | 'demand' | 'never') => void
/** When the canvas was clicked but nothing was hit */
onPointerMissed?: (event: MouseEvent) => void
/** When the canvas was dragover but nothing was hit */
onDragOverMissed?: (event: DragEvent) => void
/** When the canvas was dropped but nothing was hit */
onDropMissed?: (event: DragEvent) => void
/** If this state model is layerd (via createPortal) then this contains the previous layer */
previousRoot?: UseBoundStore<RootState, StoreApi<RootState>>
/** Internals */
Expand Down Expand Up @@ -209,6 +213,8 @@ const createStore = (

frameloop: 'always',
onPointerMissed: undefined,
onDragOverMissed: undefined,
onDropMissed: undefined,

performance: {
current: 1,
Expand Down
3 changes: 2 additions & 1 deletion packages/fiber/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export function diffProps(
// When props match bail out
if (is.equ(value, previous[key])) return
// Collect handlers and bail out
if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) return changes.push([key, value, true, []])
if (/^on(Pointer|DragOver|Drop|Click|DoubleClick|ContextMenu|Wheel)/.test(key))
return changes.push([key, value, true, []])
// Split dashed props
let entries: string[] = []
if (key.includes('-')) entries = key.split('-')
Expand Down
6 changes: 6 additions & 0 deletions packages/fiber/src/web/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
raycaster,
camera,
onPointerMissed,
onDragOverMissed,
onDropMissed,
onCreated,
...props
},
Expand All @@ -57,6 +59,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
React.useImperativeHandle(forwardedRef, () => canvasRef.current)

const handlePointerMissed = useMutableCallback(onPointerMissed)
const handleDragOverMissed = useMutableCallback(onDragOverMissed)
const handleDropMissed = useMutableCallback(onDropMissed)
const [block, setBlock] = React.useState<SetBlock>(false)
const [error, setError] = React.useState<any>(false)

Expand Down Expand Up @@ -85,6 +89,8 @@ export const Canvas = /*#__PURE__*/ React.forwardRef<HTMLCanvasElement, Props>(f
size: { width, height },
// Pass mutable reference to onPointerMissed so it's free to update
onPointerMissed: (...args) => handlePointerMissed.current?.(...args),
onDragOverMissed: (...args) => handleDragOverMissed.current?.(...args),
onDropMissed: (...args) => handleDropMissed.current?.(...args),
onCreated: (state) => {
state.events.connect?.(divRef.current)
onCreated?.(state)
Expand Down
4 changes: 4 additions & 0 deletions packages/fiber/src/web/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const DOM_EVENTS = {
onClick: ['click', false],
onContextMenu: ['contextmenu', false],
onDoubleClick: ['dblclick', false],
onDragEnter: ['dragenter', false],
onDragLeave: ['dragleave', false],
onDragOver: ['dragover', false],
onDrop: ['drop', false],
onWheel: ['wheel', true],
onPointerDown: ['pointerdown', true],
onPointerUp: ['pointerup', true],
Expand Down
Loading

0 comments on commit ec958d9

Please sign in to comment.