Search code examples
jqueryjquery-uidraggablejquery-ui-droppable

dragging a droppable div to a specific point on a background image


I am trying to create a simple educational game where a flag is dragged and dropped to the correct place on a background image of a map. So I know how to make the flag(s) dragable and droppable, but don't know how to make the droppable area that specific point on the map.

Here is what I have so far:

    <!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>temperate journeys</title>

<style>
    #flags{float:left; height:100px;width:35px; padding:2px;margin-right: 10px;}
    #china{height:20px; width:25px; background-color:red; border: 1px solid #B7191C;padding:3px;}
    .china{height:22px; width:27px; background-color:green; border: 1px solid green;}
    #australia {height:20px; width:25px; background-color:blue; border: 1px solid #3324AF;padding:3px;}
    </style>
 <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
  <script>
  $( function() {
    $( "#china" ).draggable();
     $( "#australia" ).draggable(); 
     $( ".china" ).droppable({accept:"#china"});  
  } );
  </script>
</head>

<body>

    <div id="flags">
    <div id="china"> Ch </div>
    <div id="australia">Au</div>    


    </div>
<img src="map.gif" alt="" width="520" height="289" usemap="#Map"/>
<map name="Map"><area class="china" shape="rect" coords="346,118,397,153" href="#" target="_self">

  <area shape="rect" coords="409,209,461,240" class="australia" target="_self">
</map>
</body>

Solution

  • Trying to use an Image-Map with Droppable is not going to be fun nor will it work out the way you want.

    The <area> tag defines an area inside an image-map (an image-map is an image with clickable areas).

    The <area> element is always nested inside a tag.

    Note: The usemap attribute in the <img> tag is associated with the <map> element's name attribute, and creates a relationship between the image and the map.

    What does this mean when it comes to our UI? Well, it sort of suggests that the <area> element does not qualify as an element in the same sense as an <img> or <div>. It's more of a reference of coordinates to be used to check if an area has been clicked or not. It is in the DOM, yet it's box model is the same as the image it is associated with. So $("map .china") will have the same width and position as the image.

    This does not mean it cannot be helpful. Each <area> element still has attributes that define the coordinates. So $("map .china").attr("coords") will result in "346,118,397,153" and we can do something with this String!

    Goals

    • Allow User to Drag and Drop a flag to the map
    • Upon drop, identify if flag is within the coordinates
    • Indicate to the user if they hit or miss the target
    • (Revert the flag to original position?)

    Example

    https://jsfiddle.net/Twisty/saufz8tg/

    HTML

    <div class="drag-area">
      <div id="flags">
        <div id="china" class="flag" title="China">Ch</div>
        <div id="australia" class="flag" title="Autstralia">Au</div>
      </div>
      <img class="map-image" src="https://www.wpclipart.com/dl.php?img=/geography/world_maps/world_map_basic_color.png" alt="" width="520" height="289" usemap="#Map" />
      <map name="Map">
        <area class="china" shape="rect" coords="346,118,397,153" href="#" target="_self">
        <area shape="rect" coords="409,209,461,240" class="australia" target="_self">
      </map>
    </div>
    

    Not a lot of changes here. A <div> to help wrap and contain things. A few more classes to help organize and assign styling.

    CSS

    #flags {
      float: left;
      height: 100px;
      width: 35px;
      padding: 2px;
      margin-right: 10px;
    }
    
    .flag {
      height: 20px;
      width: 25px;
      margin-bottom: 1px;
      border: 1px solid #B7191C;
      border-radius: 3px;
      padding: 3px;
    }
    
    #china {
      background-color: red;
    }
    
    #australia {
      background-color: blue;
    }
    

    Again, no major changes. Simply cleanup and easier organization, less repeated code.

    JavaScript

    $(function() {
      function getObjCoords(o) {
        var p = o.position();
        var d = {
          width: o.width(),
          height: o.height()
        };
        var coords = [
          p.left, // X1
          p.top, // Y1
          p.left + d.width, // X2
          p.top + d.height // Y2
        ];
        return coords;
      }
    
      function getObjCenter(o) {
        var p = o.position();
        var d = {
          width: o.width(),
          height: o.height()
        }
        var c = [
          parseInt(p.left + (d.width / 2)),
          parseInt(p.top + (d.height / 2))
        ];
        return c;
      }
    
      function hit(o, t, off) {
        var x, y;
        var center = getObjCenter(o);
        var hit = false;
        if (center[0] > (t[0] + off.left) && center[0] < (t[2] + off.left)) {
          x = true
        } else {
          x = false;
        }
        if (center[1] > (t[1] + off.top) && center[1] < (t[3] + off.top)) {
          y = true;
        } else {
          y = false;
        }
        if (x && y) {
          hit = true;
        }
        return hit;
      }
    
      var $mapArea = $("map[name='Map']");
      var imgOff = $(".map-image").position();
      console.log("Image Position:", imgOff);
    
      $(".flag").draggable({
        containment: ".drag-area"
      });
      $(".flag").disableSelection();
      $(".map-image").droppable({
        drop: function(e, ui) {
          var itemCenter = getObjCenter(ui.draggable);
          console.log("Drop Center:", itemCenter);
          var t = ui.draggable.attr("id");
          var tPos = $mapArea.find("." + t).attr("coords").split(",");
          $.each(tPos, function(k, v) {
            tPos[k] = parseInt(v);
          });
          console.log("Target Coords:", tPos);
          console.log("Target Offset: [ " + (tPos[0] + imgOff.left) + ", " + (tPos[1] + imgOff.top) + ", " + (tPos[2] + imgOff.left) + ", " + (tPos[3] + imgOff.top) + " ]");
          if (hit(ui.draggable, tPos, imgOff)) {
            console.log("Hit!");
            return true;
          } else {
            console.log("Miss.");
            return false;
          }
        }
      });
    });
    

    I lay my JavaScript/jQuery out like so: Functions, Constants, and UI Definition / Setup. We have three helper functions: getObjCoords(), getObjCenter(), and hit(). I liken this script to the game Battleship, so I used a lot of the same terminology.

    I suspect you're going to have a lot more flags and areas to drop them. This is where these helper functions come into play. We'll leave the heavier lifting to them and just work with our more basic objects in the Drag and Drop areas.

    getObjCoords( jQuery Object )

    Accepts a jQuery Object and returns an array of [x1, y1, x2, y2] based on the objects position and dimensions.

    getObjCenter( jQuery Object )

    Accepts a jQuery Object and returns an array of [x, y] defining the center of the object's box.

    hit( jQuery Object, Array Coords, Array Offset )

    Determines if an Objects Center is within the 4 area coordinates.

    Notes

    • As you did not define what to do with a Hit or Miss, I simply send this to the console when a flag is dropped. Right now, a flag can be dropped and then moved again.
    • .split() is used to turn the string into an array, yet it will be an array of Strings. JavaScript will generally accept this, yet for proper comparisons, I have converted them to Integers.
    • getObjCenter() can use Math to round out the values. Remember that dividing an odd integer will produce a long (3 / 2 = 1.5) and HTML does not like half a Pixel. You can use Math.round() or Math.floor() but parseInt() does the job too. Pick your poison.

    Hope that helps. Also, this is not the only way to do this and you will have to decide if this is going to be useful for your script.