import { createStoreContext } from '@o/use-store' import { FullScreen, FullScreenProps, gloss } from 'gloss' import { RefObject, useEffect, useState } from 'react' type OnDropCb = (item?: any, position?: [number, number]) => void class DraggableStore { item = null position = [0, 0] activeDropCb: OnDropCb | null = null private initialPosition = [0, 0] setDragging = (item: any, e: MouseEvent) => { ActiveDraggables.cancelAll() this.item = item this.initialPosition = [e.clientX, e.clientY] this.position = [e.clientX, e.clientY] window.addEventListener('mousemove', this.handleDragMove) window.addEventListener('mouseup', this.handleDragEnd) } clearDragging = () => { this.item = null this.activeDropCb = null } setActiveDrop = (cb: OnDropCb) => { this.activeDropCb = cb } private handleDragMove = e => { console.log('move', e.clientX, e.clientY, this.initialPosition) this.initialPosition this.position = [e.clientX, e.clientY] } private handleDragEnd = () => { window.removeEventListener('mousemove', this.handleDragMove) window.removeEventListener('mouseup', this.handleDragEnd) if (this.activeDropCb) { this.activeDropCb(this.item) } this.clearDragging() } } const Context = createStoreContext(DraggableStore) export const ProvideDraggable = Context.Provider export function useCurrentDraggable() { const store = Context.useStore() return { item: store.item, position: store.position, } } type UseDraggableProps = { item: any ref: RefObject enabled: boolean delay?: number onDragStart?: (...args: any[]) => any onDragEnd?: () => any } export function useDraggable({ enabled, item, ref, delay = 800, onDragStart }: UseDraggableProps) { const store = Context.useStore() // starts dragging if you hold for `delay` time and dont move mouse useEffect(() => { const node = ref.current if (!node || !enabled) return let downTm = null const clearDrag = () => { clearTimeout(downTm) } const mouseUp = () => { clearDrag() window.removeEventListener('mousemove', clearDrag) } const onMouseDown = e => { window.addEventListener('mousemove', clearDrag) window.addEventListener('mouseup', mouseUp) downTm = setTimeout(() => { window.removeEventListener('mousemove', clearDrag) window.removeEventListener('mouseup', mouseUp) store.setDragging(item, e) onDragStart && onDragStart(e, item) }, delay) } node.addEventListener('mousedown', onMouseDown) return () => { node.removeEventListener('mousedown', onMouseDown) window.removeEventListener('mousemove', clearDrag) window.removeEventListener('mouseup', mouseUp) clearTimeout(downTm) } }, [enabled, ref, delay]) } type UseDroppableProps = { ref: RefObject acceptsItem?: (item: any) => boolean onDrop?: OnDropCb } export const trueFn = _ => true export function useDroppable(props: UseDroppableProps): boolean { const store = Context.useStore() const { ref, acceptsItem = trueFn } = props const [isDroppable, setIsDroppable] = useState(false) useEffect(() => { const node = ref.current if (!node) return const isValid = () => { return !!store.item && acceptsItem(store.item) && store.position } const mouseEnter = () => { if (isValid()) { setIsDroppable(true) store.setActiveDrop(props.onDrop) node.addEventListener('mouseleave', mouseLeave) } } const mouseLeave = () => { if (store.activeDropCb === props.onDrop) { store.setActiveDrop(null) } setIsDroppable(false) } node.addEventListener('mouseenter', mouseEnter) return () => { node.removeEventListener('mouseenter', mouseEnter) node.removeEventListener('mouseleave', mouseLeave) } }, [ref]) return isDroppable } export const DropOverlay = gloss(FullScreen, { background: 'transparent', conditional: { isDropping: { background: [255, 0, 0, 0.25], }, }, }) export const ActiveDraggables = { items: new Set(), add(item: Function) { this.items.add(item) }, remove(item: Function) { this.items.delete(item) }, cancelAll() { for (const cancel of this.items) { cancel() } }, }