16

I am working on how to show guides when moving boxes like it is in Google Docs Drawing. I would prefer an open-source code or any type of guide before starting writing my own.

  1. I do not need drag-n-drop across multiple browser windows so i don't need HTML5 Drag-n-Drop.
  2. Also i am using jquery-ui-draggable for boxes.

enter image description here

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Imran Naqvi
  • 2,202
  • 5
  • 26
  • 53

4 Answers4

32

jquery ui has this already built in, see this demo: http://jqueryui.com/demos/draggable/#snap-to

if you insist on the guidlines you would maybe have to fork jqueryui or look at the source and see if you can extend it.


alternatively you could just add your own snapping-functions on top of jQuery ui, i've played with it for a bit, and while it doesn't seem like fun at least it also doesn't seem to be very hard.

you can view the example on jsfiddle: http://jsfiddle.net/x7uMh/103/ update: this works ~ jQuery 1.9 + jQueryUI 1.9. it breaks in newest jquery+ui. couldn't be bothered to see what exactly the problem is, typically its only minor problems though. just in case that site ever goes down, here's the code:

css

body{
    font-family: courier new, courier; 
    font-size: 12px; 
}

.draggable{
    border: 1px solid #ccc; 
    display: inline-block; 
    cursor: move;
    position: absolute;         
}

.guide{
    display: none; 
    position: absolute; 
    left: 0; 
    top: 0; 
}

#guide-h{
    border-top: 1px dashed #55f; 
    width: 100%; 
}

#guide-v{
    border-left: 1px dashed #55f; 
    height: 100%; 
}
​

html

<div class="draggable">drag me!</div>
<div class="draggable">you can drag me too, if you like</div>
<div class="draggable">hep hep</div>

<div id="guide-h" class="guide"></div>
<div id="guide-v" class="guide"></div>
​

javascript (make sure to include jquery + jquery ui)

var MIN_DISTANCE = 10; // minimum distance to "snap" to a guide
var guides = []; // no guides available ... 
var innerOffsetX, innerOffsetY; // we'll use those during drag ... 

$( ".draggable" ).draggable({
    start: function( event, ui ) {
        guides = $.map( $( ".draggable" ).not( this ), computeGuidesForElement );
        innerOffsetX = event.originalEvent.offsetX;
        innerOffsetY = event.originalEvent.offsetY;
    }, 
    drag: function( event, ui ){
        // iterate all guides, remember the closest h and v guides
        var guideV, guideH, distV = MIN_DISTANCE+1, distH = MIN_DISTANCE+1, offsetV, offsetH; 
        var chosenGuides = { top: { dist: MIN_DISTANCE+1 }, left: { dist: MIN_DISTANCE+1 } }; 
        var $t = $(this); 
        var pos = { top: event.originalEvent.pageY - innerOffsetY, left: event.originalEvent.pageX - innerOffsetX }; 
        var w = $t.outerWidth() - 1; 
        var h = $t.outerHeight() - 1; 
        var elemGuides = computeGuidesForElement( null, pos, w, h ); 
        $.each( guides, function( i, guide ){
            $.each( elemGuides, function( i, elemGuide ){
                if( guide.type == elemGuide.type ){
                    var prop = guide.type == "h"? "top":"left"; 
                    var d = Math.abs( elemGuide[prop] - guide[prop] ); 
                    if( d < chosenGuides[prop].dist ){
                        chosenGuides[prop].dist = d; 
                        chosenGuides[prop].offset = elemGuide[prop] - pos[prop]; 
                        chosenGuides[prop].guide = guide; 
                    }
                }
            } ); 
        } );

        if( chosenGuides.top.dist <= MIN_DISTANCE ){
            $( "#guide-h" ).css( "top", chosenGuides.top.guide.top ).show(); 
            ui.position.top = chosenGuides.top.guide.top - chosenGuides.top.offset;
        }
        else{
            $( "#guide-h" ).hide(); 
            ui.position.top = pos.top; 
        }

        if( chosenGuides.left.dist <= MIN_DISTANCE ){
            $( "#guide-v" ).css( "left", chosenGuides.left.guide.left ).show(); 
            ui.position.left = chosenGuides.left.guide.left - chosenGuides.left.offset; 
        }
        else{
            $( "#guide-v" ).hide(); 
            ui.position.left = pos.left; 
        }
    }, 
    stop: function( event, ui ){
        $( "#guide-v, #guide-h" ).hide(); 
    }
});


function computeGuidesForElement( elem, pos, w, h ){
    if( elem != null ){
        var $t = $(elem); 
        pos = $t.offset(); 
        w = $t.outerWidth() - 1; 
        h = $t.outerHeight() - 1; 
    }

    return [
        { type: "h", left: pos.left, top: pos.top }, 
        { type: "h", left: pos.left, top: pos.top + h }, 
        { type: "v", left: pos.left, top: pos.top }, 
        { type: "v", left: pos.left + w, top: pos.top },
        // you can add _any_ other guides here as well (e.g. a guide 10 pixels to the left of an element)
        { type: "h", left: pos.left, top: pos.top + h/2 },
        { type: "v", left: pos.left + w/2, top: pos.top } 
    ]; 
}

​

hope that helps, best, hansi.

kritzikratzi
  • 19,662
  • 1
  • 29
  • 40
  • 2
    for reference: i played with it a bit more, there is a much more elegant version to solve this inner "guide finding loop" by just using the same computeGuides() function on the current drag target. it allows to add more markers (e.g. align via centers, baselines, etc.) much more easily and with less redundancy. check it out here: http://jsfiddle.net/kritzikratzi/x7uMh/1/ – kritzikratzi Apr 20 '12 at 14:46
  • Looks like it will work but jsfiddle link does not. But I will give it a try for sure. – Imran Naqvi Apr 21 '12 at 06:21
  • i only tested on the latest chrome, you might have to make some adoptions in other browsers ... – kritzikratzi Apr 23 '12 at 15:46
  • +1 Yep, its good. The code which i have written is showing 6 lines, 3 vertical lines (left, center and right) and 3 horizontal line (top, middle and bottom). your and mine code is almost a like but there is a common problem ! We are calculating each and everything in in drag event of draggable so it makes the animation bulky and eats up a lot of cpu, specially mine code because i am showing 3 times more lines which means 3 times more calculations. Is there any solution for that? any trick? – Imran Naqvi Apr 24 '12 at 14:05
  • the positions of the guides are computed _only_ when the dragging starts. after that only the distance from the element to the guides is calculated. i suppose you could use smarter algorithms to figure out which guide to pick, e.g. sorting beforehand and then doing a binary search would speed things up a bit. – kritzikratzi Apr 25 '12 at 04:39
  • Thanks, i appropriated your help. – Imran Naqvi Apr 25 '12 at 06:12
  • My god! Thank you for your help! IT WORKS AWESOME!! :) had to do a change on mine tho, but it works soooo great! – msqar Mar 31 '14 at 19:54
  • @kritzikratzi - this code is awesome. Just a quick question though: If you put elements inside the draggable, it jumps when first being dragged. Example: http://jsfiddle.net/x7uMh/149/ - any suggestions? – Andrew May 04 '15 at 19:20
  • 1
    In case anyone cares, to fix the issue with my previous comment, I used this code on start: var offsetPos = $(this).offset(); innerOffsetX = e.pageX - offsetPos.left; innerOffsetY = e.pageY - offsetPos.top; – Andrew May 05 '15 at 08:21
  • @kritzikratzi Is there any chance it will work if the `position` of the parent is `relative` ? – Yusril Maulidan Raji Oct 05 '16 at 15:32
  • Well, I've tried this and still have no idea how to fix it. https://jsfiddle.net/yusrilmaulidanraji/yfb1n53a/2/ *still trying. – Yusril Maulidan Raji Oct 05 '16 at 16:04
  • @YusrilMaulidanRaji you are not taking the body margin into account. add a style `body{margin:0}` and it'll work fine. – kritzikratzi Oct 11 '16 at 22:55
  • 1
    @kritzikratzi I've tried that (with the code that I shared on jsfiddle), but untortunately still didn't work. But no problem, I already got the answer for my case. Here is the result: http://stackoverflow.com/a/39899249/3097810 , perhaps it will be useful for the others. – Yusril Maulidan Raji Oct 12 '16 at 15:27
  • Totally awesome! How would you solve it if the draggable divs were within a div with a zoom for let's say 50% ? I've tried to combine your solution with this: https://stackoverflow.com/a/5258373/1427930 Any ideas @kritzikratzi ? – user1427930 Dec 01 '20 at 11:15
11

I have created a simple example with just borders line beside the draggable box. It shows up when we drag on the box. View demo here

HTML:

<div id="canvas">
    <div id="box">
        <span class="topline"></span>
        <span class="rightline"></span>
        <span class="botline"></span>
        <span class="leftline"></span>
    </div>
</div>

CSS:

#canvas {width: 1000px;height: 800px;}
.topline{
    position:absolute;
    width: 1000%;
    border-top:1px red dotted;
    display:none;
    vertical-align::middle;
    margin-top:-7px;
    margin-left:-250%;
}
.botline{
    position:absolute;
    width: 1000%;
    bottom:-2px;
    border-bottom:1px red dotted;
    display:none;
    vertical-align::middle;
    margin-top:500px;
    margin-left:-250%;
}
.leftline{
    position:absolute;
    height: 1000%;
    left:-2px;
    border-left:1px red dotted;
    display:none;
    vertical-align::middle;
    margin-top:-250%;

}
.rightline{
    position:absolute;
    height: 1000%;
    right:-2px;
    border-right:1px red dotted;
    display:none;
    vertical-align::middle;
    margin-top:-250%;

}
#box {
    cursor: move;
    border:1px solid black;
    width:150px;
    height:100px;
    min-width:80px;
    min-height:80px;
    padding:5px;
    background-color:#1196c1;

}

JavaScript:

$(function() {
    $("#box").draggable({
        containment: "#canvas",
        drag: function() {
            $(this).find($('.topline')).css('display', 'block');
            $(this).find($('.rightline')).css('display', 'block');
            $(this).find($('.botline')).css('display', 'block');
            $(this).find($('.leftline')).css('display', 'block');
        },
        start: function() {
            $(this).find($('.topline')).css('display', 'block');
            $(this).find($('.rightline')).css('display', 'block');
            $(this).find($('.botline')).css('display', 'block');
            $(this).find($('.leftline')).css('display', 'block');
        },
        stop: function() {
            $(this).find($('.topline')).css('display', 'none');
            $(this).find($('.rightline')).css('display', 'none');
            $(this).find($('.botline')).css('display', 'none');
            $(this).find($('.leftline')).css('display', 'none');
        }
    });
});
Scott
  • 21,211
  • 8
  • 65
  • 72
Jnickz
  • 143
  • 1
  • 6
1

for those who still trying to find a way of doing this, i create a fiddle

i used snap and $(this).data('draggable').snapElements;

drag: function(event, ui) 
        { 
            //var snapped = $(this).data('ui-draggable').snapElements; //## for new version of jquery UI
            var snapped = $(this).data('draggable').snapElements;
            /* Pull out only the snap targets that are "snapping": */
            var snappedTo = $.map(snapped, function(element) {
                //return element.snapping ? element.item : null;
                return element.snapping ? element : null;
            });
            if((snappedTo[0].left + snappedTo[0].width) == $(this).offset().left)
            {
                console.log('right of snapped item');
                $('#guide-v').css({'left': $(this).offset().left}).show();
            }else
            if((snappedTo[0].left)  == $(this).offset().left)
            {
                console.log('left  of snapped item');
                $('#guide-v').css({'left': $(this).offset().left}).show();
            }else $('#guide-v').hide();

            if((snappedTo[0].top)  == $(this).offset().top)
            {
                console.log('top  of snapped item');
                $('#guide-h').css({'top': $(this).offset().top}).show();
            }else
            if((snappedTo[0].top + snappedTo[0].height)  == $(this).offset().top)
            {
                console.log('bottom  of snapped item');
                $('#guide-h').css({'top': $(this).offset().top}).show();
            }else $('#guide-h').hide();
          }
      });

http://jsfiddle.net/j6zqN/1/

h0mayun
  • 3,466
  • 31
  • 40
1

I got an answer from this question: Javascript drag/drop - Illustrator style 'smart guides'

I think this is what are looking for. I also improved it by supporting the line for the same side. Here is the JsFiddle: http://jsfiddle.net/yusrilmaulidanraji/A6CpP/120/

HTML

<div id="parent">
    <div class="object1 dropped" style="left:0px;top:300px;background:#a00;"></div>
    <div class="object2 dropped"></div>
    <div class="object3 dropped" style="left:400px;top:20px;"></div>
    <div class="objectx"></div>
    <div class="objecty"></div>
</div>

CSS:

#parent{
    width:600px;
    height:500px;
    border:1px solid #000;
    position:relative;
}
.object1{
    background:#aaa;
    width:100px;
    height:100px;
    display:block;
    position:absolute;
    left:140px;
    top:50px;
}
.object2{
    background:#aaa;
    width:100px;
    height:150px;
    display:block;
    position:absolute;
    left:140px;
    top:50px;
}
.object3{
    background:#aaa;
    width:150px;
    height:100px;
    display:block;
    position:absolute;
    left:140px;
    top:50px;
}
.objectx{
    display:none;
    //background:#fff;
    width:0px;
    height:100%;
    position:absolute;
    top:0px;
    left:10px;
    border-left: 1px solid yellow;
}
.objecty{
    display:none;
    //background:#fff;
    width:100%;
    height:0px;
    position:absolute;
    top:10px;
    left:0px;
    border-bottom: 1px solid yellow;
}

JS:

$.ui.plugin.add("draggable", "smartguides", {
    start: function(event, ui) {
        var i = $(this).data("draggable"), o = i.options;
        i.elements = [];
        $(o.smartguides.constructor != String ? ( o.smartguides.items || ':data(draggable)' ) : o.smartguides).each(function() {
            var $t = $(this); var $o = $t.offset();
            if(this != i.element[0]) i.elements.push({
                item: this,
                width: $t.outerWidth(), height: $t.outerHeight(),
                top: $o.top, left: $o.left
            });
        });
    },
    stop: function(event, ui) {
        $(".objectx").css({"display":"none"});
        $(".objecty").css({"display":"none"});
    },
    drag: function(event, ui) {
        var inst = $(this).data("draggable"), o = inst.options;
        var d = o.tolerance;
        $(".objectx").css({"display":"none"});
        $(".objecty").css({"display":"none"});
            var x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width,
                y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height,
                xc = (x1 + x2) / 2, yc = (y1 + y2) / 2;
            for (var i = inst.elements.length - 1; i >= 0; i--){
                var l = inst.elements[i].left, r = l + inst.elements[i].width,
                    t = inst.elements[i].top, b = t + inst.elements[i].height,
                    hc = (l + r) / 2, vc = (t + b) / 2;
                     var lss = Math.abs(l - x1) <= d;
                            var ls = Math.abs(l - x2) <= d;
                            var rss = Math.abs(r - x2) <= d;
                            var rs = Math.abs(r - x1) <= d;
                            var tss = Math.abs(t - y1) <= d;
                            var ts = Math.abs(t - y2) <= d;
                            var bss = Math.abs(b - y2) <= d;
                            var bs = Math.abs(b - y1) <= d;
                            var hs = Math.abs(hc - xc) <= d;
                            var vs = Math.abs(vc - yc) <= d; 
                        if(lss) {
                            ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left;
                            $(".objectx").css({"left":ui.position.left,"display":"block"});
                        }
                        if(rss) {
                            ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left;
                            $(".objectx").css({"left":ui.position.left + ui.helper.width(),"display":"block"});
                        }
                        if(ls) {
                            ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left;
                            $(".objectx").css({"left":ui.position.left + ui.helper.width(),"display":"block"});
                        }
                        if(rs) {
                            ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left;
                            $(".objectx").css({"left":ui.position.left,"display":"block"});
                        }
                        if(tss) {
                            ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top;
                            $(".objecty").css({"top":ui.position.top,"display":"block"});
                        }
                        if(ts) {
                            ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top;
                            $(".objecty").css({"top":ui.position.top + ui.helper.height(),"display":"block"});
                        }
                        if(bss) {
                            ui.position.top = inst._convertPositionTo("relative", { top: b-inst.helperProportions.height, left: 0 }).top - inst.margins.top;
                            $(".objecty").css({"top":ui.position.top + ui.helper.height(),"display":"block"});
                        }
                        if(bs) {
                            ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top;
                            $(".objecty").css({"top":ui.position.top,"display":"block"});
                        }
                        if(hs) {
                            ui.position.left = inst._convertPositionTo("relative", { top: 0, left: hc - inst.helperProportions.width/2 }).left - inst.margins.left;
                            $(".objectx").css({"left":ui.position.left + (ui.helper.width()/2),"display":"block"});
                        }
                        if(vs) {
                            ui.position.top = inst._convertPositionTo("relative", { top: vc - inst.helperProportions.height/2, left: 0 }).top - inst.margins.top;
                            $(".objecty").css({"top":ui.position.top + (ui.helper.height()/2),"display":"block"});
                        }


            };
        }
});
$('.dropped').draggable({
    containment: 'parent',
    smartguides:".dropped",
    tolerance:5
});
Community
  • 1
  • 1
Yusril Maulidan Raji
  • 1,682
  • 1
  • 21
  • 46
  • I keep getting an error at "var i = $(this).data("draggable"), o = i.options;" - "Cannot read property 'options' of undefined". – shiri Sep 25 '19 at 00:17
  • @shiri You should replace `"draggable"` with `"uiDraggable"`. – MM PP Feb 17 '20 at 10:56
  • I am trying to get the bottom of a div to snap to the center of another and hopelessly failing at this. Can anyone assist please? – Jeff Dec 17 '20 at 23:03
  • The previous request for snapping the bottom to center has been resolved. Should anyone need assistance with this, please let me know. – Jeff Dec 17 '20 at 23:55