The blue square supports drag and drop. Try dragging it over and dropping it into one of the squares.

This demonstrates several simultaneous state machines using generators and recursion to transition from state to state. The draggable object gets its own draggable-object state machine. Each of the droppable areas gets their own instance of the drop-target state machine.

See code below (or even better look at source or devtools)


const {gimgen, runGimgen, domEventToSignal, anySignal, createSignalFactory} = window.gimgen

const makeDraggable = (el, dropTargets) => {
	const targetMouseDown = domEventToSignal(el, 'mousedown')
	const mouseMoved = domEventToSignal(document, 'mousemove')
	const mouseUp = domEventToSignal(document, 'mouseup')
	const targetsMouseEnters = dropTargets.map(t => domEventToSignal(t, 'mouseenter'))
	const targetsMouseLeaves = dropTargets.map(t => domEventToSignal(t, 'mouseleave'))

	//Helper functiosn
	const isOrdered = (v1, v2, v3)  => v1 <= v2 && v2 <= v3
	const isOverlapping = (r, t) =>
		(isOrdered(t.left, r.left, t.right) || isOrdered(t.left, r.right, t.right)) &&
		(isOrdered(t.top, r.top, t.bottom) || isOrdered(t.top, r.bottom, t.bottom))
	const overlappedTargets = () => {
		const elRect = el.getBoundingClientRect()
		return dropTargets.filter(target => isOverlapping(elRect, target.getBoundingClientRect()) )
	}
	const moveTarget = ({clientX, clientY}) =>
		Object.assign(el.style, {left: `${clientX}px`, top: `${clientY}px`})
	const targetToMouseMove  = sig =>
		(mouseMoved === sig) && moveTarget(mouseMoved.getLastEvent())
	const fixPosition = () => Object.assign(el.style, {position: 'fixed', zIndex: 100 })
	const neverSignal = createSignalFactory('neverSignal', () => new Promise(() => {}))() //neverSignal signal

	/*************
	Draggable State Machine:
		NotDragging -> Dragging, DraggingOver
		Dragging -> NotDragging, DraggingOver
		DraggingOver -> Dragging, Dropped
		Dropped -> NotDragging
	**************/
	runGimgen(notDragging)

	function * notDragging(preTransition) {
		yield targetMouseDown
		const ev = targetMouseDown.getLastEvent()
		fixPosition()
		moveTarget(ev)
		preTransition && preTransition()
		if(overlappedTargets().length)
			yield * draggingOver()
		else
			yield * dragging()
	}

	function * dragging() {
		fixPosition()
		while(true) {
			const {signal: recieved} = yield anySignal(mouseMoved, mouseUp)
			if(mouseUp == recieved)
				yield * notDragging()
			targetToMouseMove(recieved)
			if(overlappedTargets().length)
				yield * draggingOver()
		}
	}

	function * draggingOver() {
		fixPosition()
		el.classList.add('over')
		while(true) {
			const {signal: recieved} = yield anySignal(mouseMoved, mouseUp)
			if(mouseUp === recieved) {
				el.classList.remove('over')
				yield * dropped()
			}
			targetToMouseMove(recieved)
			if(!overlappedTargets().length) {
				el.classList.remove('over')
				yield * dragging()
			}
		}
	}

	function * dropped() {
		el.classList.add('dropped')
		yield * notDragging(() => el.classList.remove('dropped') )
	}


	/********************
	Drop Target State Machine:
		Idle -> Targetted
		Targetted -> Idle, Selected
		Selected -> Targetted
	********************/

	const startIdle = gimgen(idle)
	for(let t of dropTargets)
		startIdle(t)

	function * idle(target) {
		while(true) {
			yield mouseMoved
			if(isOverlapping(el.getBoundingClientRect(), target.getBoundingClientRect()))
				yield * targetted(target) }
	}

	function * targetted(target) {
		target.classList.add('targetted')
		while(true) {
			const {signal: recieved} = yield anySignal(mouseMoved, mouseUp)
			if(!isOverlapping(el.getBoundingClientRect(), target.getBoundingClientRect())) {
				target.classList.remove('targetted')
				yield * idle(target)
			}
			if(mouseUp === recieved) {
				target.classList.remove('targetted')
				yield * selected(target)
			}
		}
	}

	function * selected(target) {
		target.classList.add('selected')
		while(true)
			yield neverSignal
	}

}

makeDraggable(document.querySelector('.draggable'), Array.from(document.querySelectorAll('.drop-target')))