Javascript drag/drop - Illustrator style 'smart guides'
Asked Answered
H

5

4

I'm looking for a way to implement Adobe Illustrator style 'smart guides' when dragging/dropping in Javascript. I'm currently using jQuery UI's draggable:

$('.object').draggable({
    containment: 'parent',
    snap: '.other-objects',
    snapTolerance: 5
})

This does 90% of what I want - I can drag .object around within it's parent, and it will snap it's edges to .other-objects when it gets close enough.

What I want, however, is for a line of some kind (or a guide of some kind) to appear, if it's in line with the edge of another object, so I can snap stuff in a row without them being directly next to each other.

Does anybody know if this is possible, or how I'd go about doing it?

Halfhardy answered 19/11, 2013 at 15:6 Comment(1)
This is a good question, but your last line makes it tough to tell what an acceptable answer will look like. Maybe suggest a potential solution, e.g. "What if I create a div that's 1px in one dimension and 100% in the other dimension, would that work as the guide?" (Better still, try it out, and then we can suggest ways to refine it so it works.)Holyhead
K
4

I started messing around with a jsFiddle. It's not perfect, but it should get you started.

The bulk of the logic is within jQuery UI's drag event handler:

function (event, ui) {

        // You'll want to debounce this function so that it doesn't run every mouse move (e.g. see Ben Alman's site @ http://tinyurl.com/37dyjug)
        var debounceTime = 200; // milliseconds
        setTimeout(function () {

            // Loop through all 'other-object's and see if we're lined up
            $(".other-object").each(function (idx, other) {
                var $other = $(other);

                // Determine whether we're "close enough" to display the line
                var padding = 1;
                var closeToLeft = Math.abs($other.offset().left - ui.offset.left) < padding;
                var closeToTop = Math.abs($other.offset().top - ui.offset.top) < padding;
                // You can add closeToRight/closeToBottom, but you may need to do some calculation, e.g. right = left + width

                // If we're close, display a line, otherwise remove that same line
                // TODO: Find a better way of tagging which 'other-object' this line belongs to, using IDs or something more stable than the index of the jQuery each() function!
                var id = 'leftOther' + idx;
                if (closeToLeft) {
                    console.debug(idx, 'left');
                    $('.parent').not(':has(#' + id + ')').append('<div id="' + id + '" class="line vertical" style="left: ' + $other.offset().left + 'px;"/>');
                } else {
                    $('#' + id).remove();
                }

                id = 'topOther' + idx;
                if (closeToTop) {
                    console.debug(idx, 'top');
                    $('.parent').not(':has(#' + id + ')').append('<div id="topOther' + idx + '" class="line horizontal" style="top: ' + $other.offset().top + 'px;"/>');
                } else {
                    $('#' + id).remove();
                }
            }); // End of 'other-object' loop

        }, debounceTime); // End of setTimeout
    } // End of drag function

If I have some time later I'll come back and give it some more thought, but figured you'd appreciate a semi-answer so start you off for now =)

Khachaturian answered 19/11, 2013 at 16:45 Comment(2)
BTW, you can see other examples here: #10026734Khachaturian
This is great, and gives me a good start - thanks! Those other examples are good too. I'll see what I can do. Thanks :)Halfhardy
H
8

I forked the fiddle above, added support for midlines.

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 ls = Math.abs(l - x2) <= d;
                var rs = Math.abs(r - x1) <= d;
                var ts = Math.abs(t - y2) <= d;
                var bs = Math.abs(b - y1) <= d;
                var hs = Math.abs(hc - xc) <= d;
                var vs = Math.abs(vc - yc) <= d; 
            if(ls) {
                ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left;
                $(".objectx").css({"left":l-d-4,"display":"block"});
            }
            if(rs) {
                ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left;
                 $(".objectx").css({"left":r-d-4,"display":"block"});
            }

            if(ts) {
                ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top;
                $(".objecty").css({"top":t-d-4,"display":"block"});
            }
            if(bs) {
                ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top;
                $(".objecty").css({"top":b-d-4,"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":hc-d-4,"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":vc-d-4,"display":"block"});
            }


        };
    }

This way it would be easier to align elements in a row/column.

Check the fiddle below:
http://jsfiddle.net/elin/A6CpP/

Hemline answered 27/2, 2014 at 7:42 Comment(2)
why you do - 4 every time. e.g : $(".objectx").css({"left":l-d-4,"display":"block"});Chelseachelsey
@Eddie Thanks for the answer, well, my structure is causing a small issue in the smart guides. Maybe you could help me on this - jsfiddle.net/A6CpP/109Peptone
K
4

I started messing around with a jsFiddle. It's not perfect, but it should get you started.

The bulk of the logic is within jQuery UI's drag event handler:

function (event, ui) {

        // You'll want to debounce this function so that it doesn't run every mouse move (e.g. see Ben Alman's site @ http://tinyurl.com/37dyjug)
        var debounceTime = 200; // milliseconds
        setTimeout(function () {

            // Loop through all 'other-object's and see if we're lined up
            $(".other-object").each(function (idx, other) {
                var $other = $(other);

                // Determine whether we're "close enough" to display the line
                var padding = 1;
                var closeToLeft = Math.abs($other.offset().left - ui.offset.left) < padding;
                var closeToTop = Math.abs($other.offset().top - ui.offset.top) < padding;
                // You can add closeToRight/closeToBottom, but you may need to do some calculation, e.g. right = left + width

                // If we're close, display a line, otherwise remove that same line
                // TODO: Find a better way of tagging which 'other-object' this line belongs to, using IDs or something more stable than the index of the jQuery each() function!
                var id = 'leftOther' + idx;
                if (closeToLeft) {
                    console.debug(idx, 'left');
                    $('.parent').not(':has(#' + id + ')').append('<div id="' + id + '" class="line vertical" style="left: ' + $other.offset().left + 'px;"/>');
                } else {
                    $('#' + id).remove();
                }

                id = 'topOther' + idx;
                if (closeToTop) {
                    console.debug(idx, 'top');
                    $('.parent').not(':has(#' + id + ')').append('<div id="topOther' + idx + '" class="line horizontal" style="top: ' + $other.offset().top + 'px;"/>');
                } else {
                    $('#' + id).remove();
                }
            }); // End of 'other-object' loop

        }, debounceTime); // End of setTimeout
    } // End of drag function

If I have some time later I'll come back and give it some more thought, but figured you'd appreciate a semi-answer so start you off for now =)

Khachaturian answered 19/11, 2013 at 16:45 Comment(2)
BTW, you can see other examples here: #10026734Khachaturian
This is great, and gives me a good start - thanks! Those other examples are good too. I'll see what I can do. Thanks :)Halfhardy
M
3

you can try to create a plugin like this

$.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
        });
    });
},
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;
        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;
                var ls = Math.abs(l - x2) <= d;
                var rs = Math.abs(r - x1) <= d;
                var ts = Math.abs(t - y2) <= d;
                var bs = Math.abs(b - y1) <= d;
            if(ls) {
                ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left;
                $(".objectx").css({"left":l-d-4,"display":"block"});
            }
            if(rs) {
                ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left;
                 $(".objectx").css({"left":r-d-4,"display":"block"});
            }
            if(ts) {
                ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top;
                $(".objecty").css({"top":t-d-4,"display":"block"});
            }
            if(bs) {
                ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top;
                $(".objecty").css({"top":b-d-4,"display":"block"});
            }
        };
    }
});    

and use it like this

$('.other-objects').draggable({
    containment: 'parent',
    smartguides:".other-objects",
    tolerance:5
});    

http://jsfiddle.net/Vd5X6/

Mcneil answered 19/11, 2013 at 18:37 Comment(0)
L
2

I forked the JSFiddle from above, and I improved it by supporting the same side of shaped. 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
});
Lucknow answered 6/10, 2016 at 14:50 Comment(0)
O
0

I added smart guides functionality to draggable and resizable plugins.

$('.drag')
    .draggable({
        smartGuides: '.drag:not(".selected")'
    })
    .resizable({
        handles: 'all',
        smartGuides: '.drag:not(".selected")'
    }

You can see how it works:

jsFiddle: https://jsfiddle.net/chukanov/bypr7Lt3/2/

github: https://github.com/aichukanov/smartguides

demo-site: https://ready2.net/smartguides.shtml

Ossuary answered 18/5, 2017 at 15:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.