Skip to content

Instantly share code, notes, and snippets.

@bgerm
Last active August 29, 2015 14:08
Show Gist options
  • Save bgerm/55eb3c8528c46c1f5a63 to your computer and use it in GitHub Desktop.
Save bgerm/55eb3c8528c46c1f5a63 to your computer and use it in GitHub Desktop.
Drag and drop with baconjs
$(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);
}
});
});
<!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