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')))