$(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); } }); });