3

I am using a drag and drop interface for a game I am developing. I am currently using jQuery draggable and droppable to manage my reverting, limitations, grid snap, etc, but I also want to make make it such that if a user right clicks while dragging, the game piece rotates but does not stop dragging. However, the right click of the mouse automatically ends the drag event, even if I put an event listener with event.stopPropagation() and event.preventDefault() on right click. Any ideas how to achieve this?

I have a half working snippet here, where a right click does rotate the game piece but causes the drag to stop. Note that copying it from my source messed with some of the scaling and offsetting but functionality for this demos's purpose is still intact.

var board = function(parentDIV, size, options = {}) {
    this.ship = this.ship.bind(this);
    this.flip = this.flip.bind(this);
    this.update = this.update.bind(this);
    this.ajaxExport = this.ajaxExport.bind(this);
    this.scale = this.scale.bind(this);
    
    this.size = size / 10;
    this.callback = {
        onvalid: options.onvalid,
        oninvalid: options.oninvalid,
        ondrag: options.ondrag,
        onstart: options.onstart,
        ondrop: options.ondrop
    };
    
    this.containerDIV = $("<div>").css({"position": "relative", "padding": "-1px"}).attr("id", "board-container");
    this.boardDIV = $("<div>").css({"display": "table", "border-collapse": "collapse"}).on("contextmenu", function() { event.stopPropagation; event.preventDefault(); });
    var rowDIV = $("<div>").css({"display": "table-row"});
    var cellDIV = $("<div>").css({"display": "table-cell", "border": "2px #000 solid", "padding": "-1px", "width": this.size.toString() + "px", "height": this.size.toString() + "px"})
    for(var i = 0; i < 10; i++) {
        var cloneRow = rowDIV.clone();
        for(var ii = 0; ii < 10; ii++) cloneRow.append(cellDIV.clone());
        this.boardDIV.append(cloneRow);
    }
    
    this.ships = [
        this.ship("carrier", 1, 5, 1, 1),
        this.ship("battleship", 4, 1, 4, 3),
        this.ship("cruiser", 1, 3, 8, 6),
        this.ship("submarine", 3, 1, 2, 8),
        this.ship("destroyer", 1, 2, 0, 8)
    ];
    

    this.containerDIV.append(this.boardDIV);
    $(parentDIV).append(this.containerDIV);
    

    this.update();
};
board.prototype.update = function() {
    this.layout = [
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0]
    ];
    
    for(var i = 0; i < this.ships.length; i++) {
        var width = depx(this.ships[i].css("width")) / this.size;
        var height = depx(this.ships[i].css("height")) / this.size;
        var left = depx(this.ships[i].css("left")) / this.size;
        var top = depx(this.ships[i].css("top")) / this.size;
        
        if(width == 1) for(var ii = top; ii < top + height; ii++) this.layout[ii][left] = i + 1;
        if(height == 1) for(var ii = left; ii < left + width; ii++) this.layout[top][ii] = i + 1;
    }
    
    var pattern = new RegExp(/(?=^[^1]*(?:1,1,1,1,1|1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1)[^1]*$)(?=^[^2]*(?:2,2,2,2|2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2)[^2]*$)(?=^[^3]*(?:3,3,3|3\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+3\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+3)[^3]*$)(?=^[^4]*(?:4,4,4|4\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+4\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+4)[^4]*$)(?=^[^5]*(?:5,5|5\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+5)[^5]*$)^\[\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\]\]$/);
    
    if(pattern.test(JSON.stringify(this.layout))) if(this.callback.onvalid) this.callback.onvalid();
    else if(this.callback.oninvalid) this.callback.oninvalid();
    
    
    function depx(str) {
        return Number(str.substring(0, str.length - 2));
    }
};
board.prototype.ajaxExport = function() {
    var inputs = {
        type: "uploadGameBoard",
        game: null,
        board: this.layout
    };
    AJAXrequest("GET", inputs, function(response) {
        if(response == "success") {
            
        }
    });
};
board.prototype.random = function() {
    
};
board.prototype.default = function() {
    
};
board.prototype.scale = function() {
    console.log(this.containerDIV.parent());
};
board.prototype.set = function() {
    
};
board.prototype.ship = function(name, width, height, left, top) {
    return $("<div>")
        .attr("id", name)
        .addClass("ship")
        .draggable({
            containment: "parent",
            preventCollision: true,
            grid: [this.size, this.size], 
            cursor: "none", 
            start: function() { },
            stop: this.update,
            drag: this.validate })
        .css({
            "position": "absolute", 
            "width": (width * this.size).toString() + "px", 
            "height": (height * this.size).toString() + "px", 
            "background-color": "#000", 
            "top": (top * this.size).toString() + "px", 
            "left": (left * this.size).toString() + "px"})
        .on("contextmenu", function() { event.stopPropagation(); event.preventDefault(); })
        .on("mousedown", this.flip)
        .appendTo(this.boardDIV);
};
board.prototype.validate = function() {

};
board.prototype.flip = function() {
    if(event.which == 3) { 
        var ship = $(event.target); 
        var width = ship.css("height"); 
        var height = ship.css("width");
        var left = ship.css("left");
        var top = ship.css("top");
        
        ship.css({"width": width, "height": height}); 
        
        var offsetx = depx(width) + depx(left);
        var offsety = depx(height) + depx(top);
        var size = this.size * 10;
        
        if(offsetx > size) ship.css("left", String(size - depx(width)) + "px");
        else if(offsety > size) ship.css("top", String(size - depx(height)) + "px");
        
        this.update();
    }
    
    function depx(str) {
        return Number(str.substring(0, str.length - 2));
    }
};
board.prototype.disable = function() {
    for(var i = 0; i < this.ships.length; i++) this.ships[i].draggable( "option", "disabled", true );
};
board.prototype.enable = function() {
    for(var i = 0; i < this.ships.length; i++) this.ships[i].draggable( "option", "enable", true );
};

window.onload = function() {new board(document.body, 500); };
<!doctype html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <style>
      body {
        margin 0;
       }
    </style>
  </head>
  <body>
  </body>
</html>
  • Does the user start with a left click and then add a right click? Kind of confused about the actions in play. – Twisty Dec 20 '17 at 18:06
  • I know its a little quirky but yes. If you play with the fiddle, you can see the pieces rotate 90 deg on right click and drag on left click. – Benjamin Brownlee Dec 20 '17 at 18:09
  • When I run the snippet, I get console errors. Would address those first. – Twisty Dec 20 '17 at 23:03
  • Running it through jshint, I am seeing a number of `event.stopPropagation;` that are alerted for, should be `event.stopPropagation();` – Twisty Dec 20 '17 at 23:27
  • After addressing all other alerts, I am left with two: Line 1 `'default parameters' is only available in ES6 (use 'esversion: 6').` Line 187 `Do not use 'new' for side effects.` – Twisty Dec 20 '17 at 23:29
  • https://stackoverflow.com/questions/4495626/making-custom-right-click-context-menus-for-my-web-app – Twisty Dec 21 '17 at 00:24
  • Thanks for your help. I fixed the propagation function, but I am unsure what the other alerts are. Your link gave me resources on how to prevent the contextmenu, which `event.preventDefault()` handles, but does not show me how to prevent jQuery from stopping the drag or manually restarting it when the event is fired. – Benjamin Brownlee Dec 21 '17 at 05:35
  • I been working on it. Need to do some testing cause `dragstop` is triggered when `contextmenu` is fired. Both work separately, just not together. – Twisty Dec 21 '17 at 15:40
  • Can see the events here: https://jsfiddle.net/Twisty/ue2qpp2z/4/ – Twisty Dec 21 '17 at 16:34
  • I also found this: https://stackoverflow.com/questions/40464357/jquery-ui-immediate-draggable-on-mousedown which may be helpful. – Twisty Dec 21 '17 at 18:03

1 Answers1

1

I got it to work... I'm not sure what I changed exactly, but when I switched the .on() to this, it started working as expected for the most part:

  .on({
    contextmenu: function(event) {
      console.log("EVENT: " + event.type, event.target.id);
      event.preventDefault();
      event.stopImmediatePropagation();
    },
    mousedown: me.flip,
    mouseup: function(event) {
      console.log("EVENT: " + event.type, event.target.id);
      console.log(event);
      if (event.which === 3) {
        return false;
      }
    }
  })

Basically, what I could see happening was that events like mousedown, contextmenu, and mouseup would trigger dragstop. It made sense with mouseup since draggable might be looking for that event to trigger dragstop.

Working Test: https://jsfiddle.net/Twisty/ue2qpp2z/6/

JavaScript

function Board(parentDIV, size, options) {
  if (options == "undefined") {
    options = {};
  }

  var me = this;
  /**
  // Define Functions
  ***/
  this.ship = function(name, width, height, left, top) {
    console.log("Creating Ship: " + name, width, height, left, top);
    return $("<div>", {
        id: name,
        class: "ship"
      })
      .draggable({
        containment: "parent",
        preventCollision: true,
        grid: [me.size, me.size],
        cursor: "none",
        start: function(event, ui) {
          console.log("EVENT: " + event.type, event.target.id);
          console.log(event);
          me.dragging = true;
        },
        stop: function(event, ui) {
          console.log("EVENT: " + event.type, event.target.id);
          me.dragging = false;
          me.update.apply(me);
        },
        drag: function(event, ui) {
          console.log("EVENT: " + event.type, event.target.id);;
          //me.validate();
        }
      })
      .css({
        position: "absolute",
        width: (width * me.size).toString() + "px",
        height: (height * me.size).toString() + "px",
        "background-color": "#000",
        top: (top * me.size).toString() + "px",
        left: (left * me.size).toString() + "px"
      })
      .on({
        contextmenu: function(event) {
          console.log("EVENT: " + event.type, event.target.id);
          event.preventDefault();
          event.stopImmediatePropagation();
        },
        mousedown: me.flip,
        mouseup: function(event) {
          console.log("EVENT: " + event.type, event.target.id);
          console.log(event);
          if (event.which === 3) {
            return false;
          }
        }
      })
      .appendTo(me.boardDIV);
  };

  this.flip = function(event) {
    if (event.which == 3) {
      console.log("EVENT: " + event.type, event.target.id);
      console.log("Performing Flip: ", event.target.id);
      var ship = $(event.target);
      var width = ship.css("height");
      var height = ship.css("width");
      var left = ship.css("left");
      var top = ship.css("top");

      ship.css({
        "width": width,
        "height": height
      });

      var offsetx = depx(width) + depx(left);
      var offsety = depx(height) + depx(top);
      var size = me.size * 10;

      if (offsetx > size) ship.css("left", String(size - depx(width)) + "px");
      else if (offsety > size) ship.css("top", String(size - depx(height)) + "px");
      if ($(event.target).hasClass("ui-draggable-dragging")) {
        // Restart Drag Event
        console.log("drag & mousedown, triggering `drag` again");
        $(event.target).trigger(jQuery.Event("drag"));
      }
      me.update(event);
    }

    function depx(str) {
      return Number(str.substring(0, str.length - 2));
    }
  };

  this.update = function(event) {
    console.log("Board Update");
    me.layout = [
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ];

    for (var i = 0; i < me.ships.length; i++) {
      var width = depx(me.ships[i].css("width")) / me.size;
      var height = depx(me.ships[i].css("height")) / me.size;
      var left = depx(me.ships[i].css("left")) / me.size;
      var top = depx(me.ships[i].css("top")) / me.size;
      var ii;

      if (width == 1)
        for (ii = top; ii < top + height; ii++) me.layout[ii][left] = i + 1;
      if (height == 1)
        for (ii = left; ii < left + width; ii++) me.layout[top][ii] = i + 1;
    }

    var pattern = new RegExp(/(?=^[^1]*(?:1,1,1,1,1|1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+[^1]\D+1)[^1]*$)(?=^[^2]*(?:2,2,2,2|2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+[^2]\D+2)[^2]*$)(?=^[^3]*(?:3,3,3|3\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+3\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+[^3]\D+3)[^3]*$)(?=^[^4]*(?:4,4,4|4\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+4\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+[^4]\D+4)[^4]*$)(?=^[^5]*(?:5,5|5\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+[^5]\D+5)[^5]*$)^\[\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\],\[[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5],[0-5]\]\]$/);

    if (pattern.test(JSON.stringify(me.layout)))
      console.log("Update: Calling Callbacks");
    if (me.callback.onvalid) {
      console.log("Uopdate: Calling 'onvalid'");
      me.callback.onvalid.apply(me);
    } else if (me.callback.oninvalid) {
      console.log("Update: Calling 'oninvalid'");
      me.callback.oninvalid.apply(me);
    }

    function depx(str) {
      return Number(str.substring(0, str.length - 2));
    }

    if (event) {
      console.log("Update: Event found: " + event.type);
      if (event.type == "mousedown" && me.dragging) {
        $(event.target).trigger(jQuery.Event("dragstart"));
        event.stopImmediatePropagation();
      }
    }
  };

  this.scale = function() {
    console.log(me.containerDIV.parent());
  };

  this.disable = function() {
    console.log("Disable");
    for (var i = 0; i < me.ships.length; i++) me.ships[i].draggable("option", "disabled", true);
  };

  this.enable = function() {
    console.log("Enable");
    for (var i = 0; i < me.ships.length; i++) me.ships[i].draggable("option", "enable", true);
  };

  /***
  // Define Variables
  ***/

  this.size = size / 10;
  this.callback = {
    onvalid: options.onvalid,
    oninvalid: options.oninvalid,
    ondrag: options.ondrag,
    onstart: options.onstart,
    ondrop: options.ondrop
  };
  this.dragging = false;

  this.containerDIV = $("<div>").css({
    "position": "relative",
    "padding": "-1px"
  }).attr("id", "board-container");

  this.boardDIV = $("<div>").css({
    "display": "table",
    "border-collapse": "collapse"
  }).on("contextmenu", function(event) {
    console.log("EVENT: " + event.type, event.target.id);
    if ($(event.target).not(".ui-draggable-dragging")) {
      event.stopPropagation();
      event.preventDefault();
    }
  });

  var rowDIV = $("<div>").css({
    "display": "table-row"
  });

  var cellDIV = $("<div>").css({
    "display": "table-cell",
    "border": "2px #000 solid",
    "padding": "-1px",
    "width": this.size.toString() + "px",
    "height": this.size.toString() + "px"
  });

  for (var i = 0; i < 10; i++) {
    var cloneRow = rowDIV.clone();
    for (var ii = 0; ii < 10; ii++) cloneRow.append(cellDIV.clone());
    this.boardDIV.append(cloneRow);
  }

  this.ships = [
    this.ship("carrier", 1, 5, 1, 1),
    this.ship("battleship", 4, 1, 4, 3),
    this.ship("cruiser", 1, 3, 8, 6),
    this.ship("submarine", 3, 1, 2, 8),
    this.ship("destroyer", 1, 2, 0, 8)
  ];

  this.containerDIV.append(this.boardDIV);
  $(parentDIV).append(this.containerDIV);

  this.update();
}

$(function() {
  Board(document.body, 500, {});
});

Hope that helps.

Update

I think I know where the issue lands, related to the event x and y. When the element is flipped, in some cases, the mouse is no longer over the element and I think this is causing the dragstop to fire. I will see if I can dig into jQuery UI to see if I can find how draggable handles this scenario.

Twisty
  • 30,304
  • 2
  • 26
  • 45
  • This works very well, thanks. I think I can modify the code as that the ship rotates around the cell that is clicked on, making a more fluid interface and removing the drag error all together. – Benjamin Brownlee Jan 01 '18 at 19:40
  • Another option would be to snap the cursor to the top right hand corner of any ship that is dragged, which would simply be a parameter when the draggable object is defined: `.draggable({cursorAt: {top: y, left: x}})`. – Benjamin Brownlee Jan 01 '18 at 19:45