Last active
August 29, 2015 14:08
-
-
Save bgerm/55eb3c8528c46c1f5a63 to your computer and use it in GitHub Desktop.
Drag and drop with baconjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$(function() { | |
var grid = $("#box-container"); | |
/*-------------------------------------------------------------------------- | |
* Constants | |
*--------------------------------------------------------------------------*/ | |
var REQUIRED_MOUSE_MOVE_DISTANCE = 4; | |
var GRID_HEIGHT = 900; // from the css, just hard-coded for now | |
var MIN_CELL_HEIGHT = 15; | |
var GRID_INNER_HEIGHT = grid.innerHeight(); | |
/*-------------------------------------------------------------------------- | |
* Helper Functions | |
*--------------------------------------------------------------------------*/ | |
var mouseMovedEnough = function(startOffsetY, offsetY) { | |
return (Math.abs(offsetY - startOffsetY)) > REQUIRED_MOUSE_MOVE_DISTANCE; | |
}; | |
var topPosInGrid = function(targetId) { | |
return parseInt($(cardSelector(targetId)).css("top").replace("px", ""), 10); | |
} | |
var yPosInGrid = function(mouseInfo) { | |
return mouseInfo.mouseY - grid.offset().top + grid.scrollTop(); | |
} | |
// distance of mouse grab position from top of box | |
var mouseGrabYOffset = function(targetId, mouseY) { | |
return yPosInGrid({mouseY: mouseY}) - topPosInGrid(targetId); | |
} | |
var boundedPosY = function(topPos, height) { | |
var maxY = GRID_HEIGHT - height; | |
if (topPos < 0) { | |
return 0; | |
} else if (topPos > maxY) { | |
return maxY; | |
} | |
return topPos; | |
} | |
var boundedHeight = function(height, topPos) { | |
return Math.min(Math.max(height, MIN_CELL_HEIGHT), GRID_HEIGHT - topPos) | |
} | |
/* Int -> Unit */ | |
var scrollTo = function(pos) { | |
grid.scrollTop(pos); | |
} | |
var preventDefault = function(e) { e.preventDefault(); e.stopPropagation(); return e; }; | |
var cardSelector = function(id) { return "#box" + id; } | |
var cardId = function(evt) { return $(evt.target).data('boxId'); }; | |
var toDragEvent = function(evt) { return {action: 'drag', evt: evt, targetId: cardId(evt)}; }; | |
var toResizeEvent = function(evt) { return {action: 'resize', evt: evt, targetId: cardId(evt)}; }; | |
/*-------------------------------------------------------------------------- | |
* Initial State | |
*--------------------------------------------------------------------------*/ | |
var initialState = function() { | |
return { | |
dragging: false, | |
mouseDownTargetId: null, | |
topPos: null, | |
startHeight: null, | |
height: null, | |
startOffsetY: null, | |
offsetY: null, | |
showInfoBox: false | |
} | |
}; | |
/*-------------------------------------------------------------------------- | |
* Signal Data to State | |
*--------------------------------------------------------------------------*/ | |
var cardMouseDownToState = function(state, data) { | |
return $.extend(state, { | |
startOffsetY: data.startOffsetY, | |
mouseDownTargetId: data.mouseDownTargetId, | |
height: data.height, | |
startHeight: data.height, | |
topPos: data.topPos | |
}); | |
} | |
var cardMouseUpToState = function(state) { | |
if (state.dragging) { | |
console.log("PRETEND: Update backend side-effect."); | |
return initialState(); | |
} else { | |
// render info box | |
return $.extend(state, {showInfoBox: true, dragging: false}); | |
} | |
}; | |
var cardDragToState = function(state, mouseInfo) { | |
if (state.dragging || mouseMovedEnough(state.startOffsetY, mouseGrabYOffset(state.mouseDownTargetId, mouseInfo.mouseY))) { | |
offsetY = (state.offsetY == null) ? mouseGrabYOffset(state.mouseDownTargetId, mouseInfo.mouseY) : state.offsetY; | |
return $.extend(state, { | |
dragging: true, | |
topPos: boundedPosY(yPosInGrid(mouseInfo) - offsetY, state.height), | |
offsetY: offsetY, | |
showInfoBox: false | |
}); | |
} else { | |
return state; | |
} | |
} | |
var cardResizeToState = function(state, mouseInfo) { | |
if (state.dragging || mouseMovedEnough(state.startOffsetY, mouseGrabYOffset(state.mouseDownTargetId, mouseInfo.mouseY))) { | |
offsetY = (state.offsetY == null) ? mouseGrabYOffset(state.mouseDownTargetId, mouseInfo.mouseY) : state.offsetY; | |
return $.extend(state, { | |
dragging: true, | |
height: boundedHeight(yPosInGrid(mouseInfo) + (state.startHeight - offsetY) - state.topPos, state.topPos), | |
offsetY: offsetY, | |
showInfoBox: false | |
}); | |
} else { | |
return state; | |
} | |
} | |
var closeInfoBoxToState = function(state) { | |
return $.extend(state, {showInfoBox: false}); | |
} | |
var mouseInfoToState = function(state, payload) { | |
if (payload.action == "stop-drag" || payload.action == "stop-resize") { | |
return cardMouseUpToState(state); | |
} else if (payload.action == "drag") { | |
return cardDragToState(state, payload.data); | |
} else if (payload.action == "start-drag" || payload.action == "start-resize") { | |
return cardMouseDownToState(state, payload.data); | |
} else if (payload.action == "resize") { | |
return cardResizeToState(state, payload.data); | |
} else if (payload.action == "closeInfoBox") { | |
return closeInfoBoxToState(state); | |
} | |
return state; | |
} | |
/*-------------------------------------------------------------------------- | |
* Event Streams | |
*--------------------------------------------------------------------------*/ | |
var cardMouseDownStream = $(".box").asEventStream('mousedown').doAction(preventDefault).map(toDragEvent); | |
var resizeMouseDownStream = $(".resizer").asEventStream('mousedown').doAction(preventDefault).map(toResizeEvent); | |
var mouseUpStream = $("html").asEventStream('mouseup'); | |
var mouseMoveStream = $("html").asEventStream('mousemove'); | |
var cardDrag = cardMouseDownStream.merge(resizeMouseDownStream).flatMap(function(data) { | |
var id = data.targetId; | |
var startOffsetY = mouseGrabYOffset(id, data.evt.pageY); | |
var startHeight = $(cardSelector(id)).height(); | |
var topPos = topPosInGrid(id); | |
var mousedown = Bacon.once( | |
{ action: "start-" + data.action, | |
data: { mouseDownTargetId: id, | |
topPos: topPos, | |
startOffsetY: startOffsetY, | |
height: startHeight } | |
} | |
); | |
var mousemoves = mouseMoveStream.map(function (mm) { | |
(mm.preventDefault) ? mm.preventDefault() : event.returnValue = false; | |
return { | |
action: data.action, | |
data: { | |
mouseY: mm.pageY, | |
mouseDownTargetId: id | |
} | |
}; | |
}).takeUntil(mouseUpStream) | |
return mousedown.concat(mousemoves).concat(Bacon.once({action: "stop-" + data.action, data: {}})); | |
}); | |
var infoboxCloseSignal = $("#infobox-button").asEventStream("click").map(function(x) { | |
return {action: "closeInfoBox", data: {}}; | |
}); | |
var stateSignal = cardDrag.merge(infoboxCloseSignal).scan(initialState(), function(state, payload) { | |
return mouseInfoToState(state, payload); | |
}); | |
var autoScrollSignal = cardMouseDownStream.merge(resizeMouseDownStream).flatMap(function(x) { | |
return mouseMoveStream.sampledBy(stateSignal.sample(45)).takeUntil(mouseUpStream); | |
}); | |
/*-------------------------------------------------------------------------- | |
* Event Stream Subscribers | |
*--------------------------------------------------------------------------*/ | |
stateSignal.onValue(function(state) { | |
$("#state").html(JSON.stringify(state, undefined, 2)); | |
if (state.dragging) { | |
$(cardSelector(state.mouseDownTargetId)).css({top: state.topPos + "px", height: state.height + "px"}); | |
} | |
$("#infobox").toggle(state.showInfoBox); | |
$("#infobox-number").html(state.mouseDownTargetId); | |
}); | |
autoScrollSignal.onValue(function(mm) { | |
var mousePosY = yPosInGrid({mouseY: mm.pageY}); | |
var scrollTop = grid.scrollTop(); | |
var scrollBottom = GRID_HEIGHT - GRID_INNER_HEIGHT + scrollTop; | |
if (scrollTop > 0 && (mousePosY - scrollTop < 20)) { | |
scrollTo(scrollTop - 10); | |
} else if (scrollBottom > 0 && (GRID_INNER_HEIGHT + scrollTop - mousePosY < 20)) { | |
scrollTo(scrollTop + 10); | |
} | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>Test Bacon DND</title> | |
<style type="text/css"> | |
* { margin: 0; padding: 0;} | |
#box-container { | |
height: 450px; | |
width: 320px; | |
border: 1px solid #ccc; | |
position: relative; | |
float: left; | |
margin: 10px 0 0 10px; | |
overflow-y: scroll; | |
/* don't know why but this seems to eliminate the box | |
* artficats left behind on auto-scroll | |
*/ | |
-webkit-transform: translateZ(0); | |
} | |
.box { | |
width: 100px; | |
height: 100px; | |
position: absolute; | |
background: #CCCCCC; | |
z-index: 2; | |
} | |
#box1 { | |
left: 10px; | |
background: #F3412A; | |
top: 10px; | |
} | |
#box2 { | |
left: 150px; | |
background: #A3912F; | |
top: 10px; | |
} | |
.resizer { | |
position: absolute; | |
bottom: 0; | |
height: 7px; | |
background-color: #000000; | |
opacity: 0.6; | |
cursor: ns-resize; | |
width: 100%; | |
} | |
#right { | |
float: right; | |
margin: 10px 10px 0 0; | |
} | |
#state { | |
width: 500px; | |
background: #eee; | |
padding: 5px; | |
} | |
#infoxbox { | |
width: 100px; | |
border: 1px solid #999; | |
padding: 5px; | |
margin: 10px 0 0 0; | |
display: none; | |
} | |
#infobox-button { | |
padding: 2px 10px; | |
} | |
#box-sizer { | |
height: 900px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="box-container"> | |
<div id="box1" class="box" data-box-id="1"><div class="resizer" data-box-id="1"></div></div> | |
<div id="box2" class="box" data-box-id="2"><div class="resizer" data-box-id="2"></div></div> | |
<div id="box-sizer"></div> | |
</div> | |
<div id="right"> | |
<pre id="state"></pre> | |
<div id="infobox"> | |
<h3>Infobox</h3> | |
<p id="infobox-number">0</p> | |
<button id="infobox-button">Close</button> | |
</div> | |
</div> | |
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> | |
<script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/bacon.js/0.7.25/Bacon.min.js"></script> | |
<script type="text/javascript" src="dnd.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment