42

THE PROBLEM

I'm having a minor problem dragging elements onto a scalable div container.

Once the element is actually in the container, the elements drag fine and work the way they are supposed to.

Larger elements that are dragged onto the scalable container don't have too much of an issue.

But when smaller elements are dragged, you can see that the mouse is no longer attached to said element and when it is dropped, it drops a little off where it is supposed to drop.

I'm trying to find a solution that my mouse stays on the element and it drops where it is supposed to drop.

I've solved problems bit by bit and you can see below but this is the last piece of the puzzle that's driving me mad. If anyone has the time to lend a hand, it would be greatly appreciated.

Here is a codepen - click and drag the two blue elements onto the white container to try it out

Codepen

Full Screen View

Short GIF in Action


This wil help making sure that the droppable area works with a scaled container.

$.ui.ddmanager.prepareOffsets = function(t, event) {
  var i, j, m = $.ui.ddmanager.droppables[t.options.scope] || [],
    type = event ? event.type : null,
    list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack();
  droppablesLoop: for (i = 0; i < m.length; i++) {
    if (m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0], (t.currentItem || t.element)))) {
      continue;
    }
    for (j = 0; j < list.length; j++) {
      if (list[j] === m[i].element[0]) {
        m[i].proportions().height = 0;
        continue droppablesLoop;
      }
    }
    m[i].visible = m[i].element.css("display") !== "none";
    if (!m[i].visible) {
      continue;
    }
    if (type === "mousedown") {
      m[i]._activate.call(m[i], event);
    }
    m[i].offset = m[i].element.offset();
    m[i].proportions({
      width: m[i].element[0].offsetWidth * percent,
      height: m[i].element[0].offsetHeight * percent
    });
  }
};

Enable the element to be resizable on a scaled container

function resizeFix(event, ui) {
  var changeWidth = ui.size.width - ui.originalSize.width,
    newWidth = ui.originalSize.width + changeWidth / percent,
    changeHeight = ui.size.height - ui.originalSize.height,
    newHeight = ui.originalSize.height + changeHeight / percent;
  ui.size.width = newWidth;
  ui.size.height = newHeight;
}

Makes it so drag works on a scaled container

function dragFix(event, ui) { 
    var containmentArea = $("#documentPage_"+ui.helper.parent().parent().attr('id').replace(/^(\w+)_/, "")),
        contWidth = containmentArea.width(), contHeight = containmentArea.height();
    ui.position.left = Math.max(0, Math.min(ui.position.left / percent , contWidth - ui.helper.width()));
    ui.position.top = Math.max(0, Math.min(ui.position.top  / percent,  contHeight- ui.helper.height()));
}

Creating a draggable element that I can drag onto the box.

.directive('draggableTypes', function() {
  return {
    restrict:'A',
    link: function(scope, element, attrs) {
      element.draggable({
        zIndex:3000, 
        appendTo: 'body', 
        helper: function(e, ui){ 
          var formBox = angular.element($("#formBox"));
          percent = formBox.width() / scope.templateData.pdf_width;
          if(element.attr('id') == 'textbox_item')
              return $('<div class="text" style="text-align:left;font-size:14px;width:200px;height:20px;line-height:20px;">New Text Box.</div>').css({ 'transform': 'scale(' + percent + ')', '-moz-transform': 'scale(' + percent + ')', '-webkit-transform': 'scale(' + percent + ')', '-ms-transform': 'scale(' + percent + ')'});
          if(element.attr('id') == 'sm_textbox_item')
              return $('<div class="text" style="text-align:left;font-size:14px;width:5px;height:5px;line-height:20px;"></div>').css({ 'transform': 'scale(' + percent + ')', '-moz-transform': 'scale(' + percent + ')', '-webkit-transform': 'scale(' + percent + ')', '-ms-transform': 'scale(' + percent + ')'});
          }
      });
    }
  };
})

Create draggable/resizable elements that may already be in the box and applying the drag/resize fix to these

.directive('textboxDraggable', function() {
  return {
    restrict:'A',
    link: function(scope, element, attrs) {

        element.draggable({ 
            cursor: "move",
            drag: dragFix,
            start: function(event, ui) {
                var activeId = element.attr('id');
                scope.activeElement.id = activeId;
                scope.activeElement.name = scope.templateItems[activeId].info.name;
                scope.$apply();
            }
        });

        element.resizable({
            minWidth: 25,
            minHeight: 25,
            resize: resizeFix,
            stop: function( event, ui ) {

                var activeId = element.attr('id');

                scope.activeElement.duplicateName = false;
                scope.activeElement.id = activeId;
                scope.activeElement.name = scope.templateItems[activeId].info.name;

                scope.templateItems[activeId]['style']['width'] = element.css('width');
                scope.templateItems[activeId]['style']['height'] = element.css('height');

                scope.$apply();
            }
        })

    }
  };
})

What happens when an item is dropped

.directive('droppable', function($compile) {
  return {
    restrict: 'A',
    link: function(scope,element,attrs){
        element.droppable({
            drop:function(event,ui) {
                 var draggable = angular.element(ui.draggable),
                    draggable_parent = draggable.parent().parent(),
                    drag_type = draggable.attr('id'),
                    documentBg = element,
                    x = ui.offset.left,
                    y = ui.offset.top,
                    element_top = (y - documentBg.offset().top - draggable.height() * (percent - 1) / 2) / percent,
                    element_left = (x - documentBg.offset().left - draggable.width() * (percent - 1) / 2) / percent,
                    timestamp = new Date().getTime();

                    //just get the document page of where the mouse is if its a new element
                    if(draggable_parent.attr('id') == 'template_builder_box_container' || draggable_parent.attr('id') == 'template_builder_container')
                        var documentPage = documentBg.parent().parent().attr('id').replace(/^(\w+)_/, "");
                    //if you are dragging an element that was already on the page, get parent of draggable and not parent of where mouse is
                    else var documentPage = draggable_parent.parent().parent().attr('id').replace(/^(\w+)_/, "");

                    if(drag_type == "textbox_item")
                    {
                        scope.activeElement.id = scope.templateItems.push({
                            info: {'page': documentPage,'name': 'textbox_'+timestamp, 'type': 'text'},
                            style: {'text-align':'left','font-size':'14px','top':element_top+'px','left':element_left+'px', 'width':'200px', 'height':'20px'}
                        }) - 1;

                        scope.activeElement.name = 'textbox_'+timestamp;
                    }
                    else if(drag_type == "sm_textbox_item")
                    {
                        scope.activeElement.id = scope.templateItems.push({
                            info: {'page': documentPage,'name': '', 'type': 'text'},
                            style: {'text-align':'left','font-size':'14px','top':element_top+'px','left':element_left+'px', 'width':'5px', 'height':'5px'}
                        }) - 1;

                        scope.activeElement.name = 'textbox_'+timestamp;
                    }
                    else {
                        scope.templateItems[scope.activeElement.id]['style']['top'] = draggable.css('top');
                        scope.templateItems[scope.activeElement.id]['style']['left'] = draggable.css('left');
                    }

                scope.$apply();
            }
        });
    }
  };
})

last but not least, my controller

.controller('testing', function($scope, $rootScope, $state, $stateParams) {
  $scope.templateItems = [];
  $scope.activeElement = { id: undefined, name: undefined };
  $scope.templateData = {"id":"12345", "max_pages":1,"pdf_width":385,"pdf_height":800};
  $scope.clickElement = function(index)   { $scope.activeElement = { id: index, name: $scope.templateItems[index].info.name } }

});

Here is the basis of my html

<div id="formBox" ng-style="formbox(templateData.pdf_width)" zoom>
    <div class="trimSpace" ng-style="trimSpace(templateData.pdf_width)" zoom>
        <div id="formScale" ng-style="formScale(templateData.pdf_width)" zoom>
            <form action="#" id="{{ templateData.id }}_form">
                <div ng-repeat="key in [] | range:templateData.max_pages">              
                    <div class="formContainer" id="{{ templateData.id + '_' + (key+1) }}" ng-style="{width: templateData.pdf_width+'px', height: templateData.pdf_height+'px'}">
                        <div class="formContent">
                            <div class="formBackground" id="documentPage_{{ (key+1) }}" droppable>
                                <div ng-hide="preview" ng-repeat="item in templateItems">
                                    <div ng-if="item.info.page == (key+1) && item.info.type == 'text'" id="{{ $index }}" data-type="{{ item.info.type }}" ng-click="clickElement($index)" class="text" ng-style="item.style" textbox-draggable>{{ item.info.name }}</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>          
            </form>
        </div>
    </div>
</div>
bryan
  • 8,879
  • 18
  • 83
  • 166
  • I think the issue originates from the fact that when you are dragging the blue boxes from the right column, the duplicated element that is moving with the mouse is positioned relatively to the top left corner of the element, ignoring the automatic left/right margins. – Terry Oct 16 '16 at 23:36
  • @Terry any suggestions on how I could fix this? – bryan Oct 18 '16 at 00:52
  • I am not understanding your actual problem. Can u elaborate – Aravind Oct 18 '16 at 06:06
  • @bryan have you checked my answer? – Nedev Oct 24 '16 at 22:14
  • 1
    No problem, glad I could help some. Please note that your question will do better if you pare it down to the essentials. There is a lot of superfluous code in the codepen making it difficult to wade through – K Scandrett Mar 15 '17 at 22:02
  • @bryan I'm not sure how much this helps, but I had a similar issue recently and all I had to do to fix was set display:block on the element being dragged. I tried that in your codepen and it did not work, so that's probably not it. If you are using JQuery there are also some more options that can help with this. IE I set "tolerance" to "touch". Again, not sure it will help, but any insight might bet you closer to an answer. – garek007 May 24 '17 at 17:56

6 Answers6

5

For the cursor position while dragging, see this answer : Make Cursor position in center for ui.helper in jquery-ui draggable method

Basically, you can control the cursor position of the instance, allowing to have something more dynamic that cursorAt. Like this:

start: function(event, ui){
    $(this).draggable('instance').offset.click = {
        left: Math.floor(ui.helper.width() / 2),
        top: Math.floor(ui.helper.height() / 2)
    }
},

Then on the drop, you need to take into account the transform, but you can simplify by using the helper coordinates instead of the draggable. Like this:

element_top = (ui.helper.offset().top / percent) - (documentBg.offset().top / percent);
element_left = (ui.helper.offset().left / percent) - (documentBg.offset().left / percent);

Result: https://codepen.io/anon/pen/jamLBq

Julien Grégoire
  • 16,864
  • 4
  • 32
  • 57
  • Whoa! It's taken me over a year to get this issue resolved and you finally did it! Thank you so much! I don't see any issues – bryan Nov 13 '17 at 16:16
1

It looks like what is causing this to look strange is the following:

First, the small div is styled as display: block. This means that even though it looks like the div is small, that element actually stretches out to it's whole container.

Second, once you show the dragged square on the left screen, the relation between the mouse cursor and the element whole is technically centered, but you are cutting the size of the original element to a smaller one, and when the width and height get diminished, the result is rendered with the new width and height starting from the upper left corner of the original div. (If you style the small button to be display: inline, you can see what I mean. Try grabbing it from the upper left corner and the try the lower right one. You will see that the former looks fine but the latter is off).

So my suggestions are:

  1. Make the draggabble elements display: inline
  2. Make the dragged element on the left screen the exact height and width of the original element on the right screen.

Hope that helps!

WeHateNick
  • 54
  • 4
0

I've forked your codepen and played around with it.

Take a look at it HERE, and see if it helps you find the "bug".

For your draggable script, I changed the code to this, adding margin-left and margin-right:

if(element.attr('id') == 'sm_textbox_item') { /* the small draggable box */
  var el = {
    pos: element.offset(), // position of the small box
    height: element.outerHeight() + 20,
    left: 0
  }
  var deduct = $('#formBox').innerWidth() - 20; // width of the element that's left of small box's container
  el.left = el.pos.left - deduct;

  return $('<div class="text" style="text-align:left; font-size:14px; width:5px; height:5px; line-height:20px;"></div>')
    .css({
      'margin-left': el.left + 'px',
      'margin-top': el.pos.top - el.height + 'px',

      'transform': 'scale(' + percent + ')',
      '-moz-transform': 'scale(' + percent + ')',
      '-webkit-transform': 'scale(' + percent + ')',
      '-ms-transform': 'scale(' + percent + ')'
    });
}

Then, for your droppable script, I changed the formula for element_top and element_left:

// old formula
element_top = (y - documentBg.offset().top - draggable.height() * (percent - 1) / 2) / percent
element_left = (x - documentBg.offset().left - draggable.width() * (percent - 1) / 2) / percent

// new formula
element_top = (y - documentBg.offset().top) / (percent * 0.915)
element_left = (x - documentBg.offset().left) / (percent * 0.915)

It gives an "almost" accurate result, but you may be able to tweak it further to polish it. Hope this helps.

ITWitch
  • 1,729
  • 5
  • 20
  • 38
  • @bryan, were you able to compare the mouse cursor position when dragging and dropping the "small" element between my codepen and the OP's [codepen](https://codepen.io/bgold/pen/ZporGJ)? That might help you see what's been solved (although I repeat, it's not 100% accurate, hence the "tweak" and "polish" words. – ITWitch Dec 18 '17 at 20:49
0

For attaching elements with cursor during dragging you just need to use

cursorAt: { top: 6, left: -100 }

And a little change in top and left parameters of "sm_textbox_item".

top: (y - documentBg.offset().top) / (percent) + "px",
left: (x - documentBg.offset().left) / (percent) + "px",

For the large box again some tweak in top and left element is required (pen updated).

top: element_top-3, left: element_left+6.49,

I forked your pen and did some changes. I know that this is not a perfect solution, i am also trying to solve this bit by bit. You can check it here

Nidhin Chandran
  • 846
  • 8
  • 19
  • I appreciate the attempt. The larger text box doesn't seem to work with keeping the cursor on the box in a reduced window size; even with the above changes – bryan Jun 19 '17 at 15:29
  • It looks like you have the small box fixed but seem to have broke the bigger box somehow. Make the window small and you'll see. – bryan Jun 21 '17 at 16:20
0

@ITWitch is right, there have to be some bug in draggable(). Style margin: 0 auto; in #sm_textbox_item is the source of problem.

Try to add this to draggable options in your draggableType directive to correct the position:

cursorAt: {left: -parseInt(window.getComputedStyle(element[0],null,null)['margin-left'])},

0

This problem occurs when you add a transform to a element's style, then make it draggable. You'll have to make do without transform to have a perfect result. I spent 2 days debugging till I found it out, and I didn't want someone else to go through that pain.

Aanand Kainth
  • 719
  • 4
  • 18
  • Welcome to StackOverflow. Aanand, please specify: does this work ok if one makes an element draggable and then adds `transform`? An example of code in your answer would be helpful, too. Best regards – YakovL Jul 17 '17 at 17:40
  • I believe that your first suggestion doesn't work. However, apparently, using `.animate({transform: [transform]})` *does* work. – Aanand Kainth Jul 17 '17 at 18:26