<!DOCTYPE html>
<html>

  <head>
    <script data-require="jquery@*" data-semver="2.1.1" src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
    <link href='http://fonts.googleapis.com/css?family=Oswald:300' rel='stylesheet' type='text/css'>
  </head>

  <body ng-app="scopeExample">
    <section ng-controller="ZoeZone">
      <drop-zone></drop-zone>
    </section>
    <contact-card drag-me-to-zone>
    </contact-card>
    <section ng-controller="ZachZone">
      <drop-zone></drop-zone>
    </section>
  </body>

</html>
(function init() {
  var app = angular.module('scopeExample', []);

  // Get coordinates of an element's corners
  function coordinatesOfThisElementsCorners(element) {
    var elementOffset = element.offset(),
      elementLeft = elementOffset.left,
      elementTop = elementOffset.top,
      elementCornerLocation = {
        top: elementTop,
        bottom: elementTop + element.height(),
        left: elementLeft,
        right: elementLeft + element.width()
      };
    return elementCornerLocation;
  }

  // Test whether two divs are intersecting
  function theseTwoDivsIntersecting(element1, element2) {
    var element1Sides = coordinatesOfThisElementsCorners(element1),
      element2Sides = coordinatesOfThisElementsCorners(element2);

    if (((element1Sides.top >= element2Sides.top && element1Sides.top <= element2Sides.bottom) ||
        (element1Sides.bottom >= element2Sides.top && element1Sides.bottom <= element2Sides.bottom)) &&
      ((element1Sides.left >= element2Sides.left && element1Sides.left <= element2Sides.right) ||
        (element1Sides.right >= element2Sides.left && element1Sides.right <= element2Sides.right))) {
      return true;
    }

    return false;
  }

  // A function that reassigns scope. By that, I mean it removes all the user 
  // defined properties of the assignee scope (properties that don't start with '$' 
  // and the this property), assigns all the user defined properties of the assigning 
  // scope to the assignee scope, and then calls apply on the assignee scope
  function reassignScope(assigneeScope, assigningScope) {
    angular.forEach(assigneeScope, function(value, key){
      if(key[0] !== '$' && key !== 'this'){
        delete assigneeScope[key];
      }
    });
    
    angular.forEach(assigningScope, function(value, key){
      if(key[0] !== '$' && key !== 'this'){
        assigneeScope[key] = value;
      }
    });
    
    assigneeScope.$apply();
  }

  app.directive("dragMeToZone", function(ScopeZones) {
    return {
      restrict: 'A',
      scope: true,
      link: function link(scope, element, attrs, ctrl, transclude) {

        // This dragging code is based on this elegent solution: 
        // http://upshots.org/actionscript/jquery-basics-of-dragging
        element.on('mousedown', {
          element: element,
          scopeZones: ScopeZones,
          reassignScope: reassignScope
        }, function(e) {
          var node = $(this);
          var position = node.offset();
          var initialized = {
            x: position.left - e.pageX,
            y: position.top - e.pageY
          };
          e.preventDefault(); // Prevent text selection on drag
          var handlers = {
            mousemove: function(e) {
              var inZone = false,
                  inZoneElement;
              node.css({
                left: (initialized.x + e.pageX) + 'px',
                top: (initialized.y + e.pageY) + 'px'
              });

              // loop through all scope zones and test intersection with draggable
              // and set the divs' classes to signify that they interesect
              e.data.scopeZones.getScopeZones()
                .forEach(function checkAllZonesForIntersection(element) {
                  element = element.find(".zone");
                  if (theseTwoDivsIntersecting(node, element)) {
                    inZone = inZone || true;
                    element.addClass("containsDragMeToZone");
                    inZoneElement = element;
                  } else {
                    inZone = inZone || false;
                    element.removeClass("containsDragMeToZone");
                  }
                });

              //TODO: only reassignScope when you enter or leave a zone cause
              //reassigning on every mouse movement is ineffecient also, what
              //if the zones are next to each other?
              if (inZone) {
                node.addClass("inZone");
                e.data.reassignScope(node.scope(), inZoneElement.scope());
              } else {
                node.removeClass("inZone");
                e.data.reassignScope(node.scope(), {});
              }
            },
            mouseup: function(e) {
              $(this).off(handlers);
            }
          };
          $(document).on(handlers, e.data);
        });

      }
    };
  });

  app.directive("dropZone", function(ScopeZones) {
    return {
      restrict: 'E',
      template: '<div class="zone"><pre>{{person | json}}</pre></div>',
      link: function link(scope, element) {
        ScopeZones.addScopeZone(element);
      }
    };
  });
  
  app.directive('contactCard', function(){
    return {
      restrict: 'E',
      compile: function compile(element) {
        element.append(
          '<img class="bottom" src="http://i.imgur.com/XeA39Dp.png">'+
          '<img class="top" src="http://i.imgur.com/2l1rHWi.png">'+
          '<div class="text">'+
          '<div>Name: <span ng-class="{bound:person}" ng-bind="person.name"></span></div>'+
          '<div>Address: <span ng-class="{bound:person}" ng-bind="person.address"></span></div>'+
          '<div>Phone Number: <span ng-class="{bound:person}" ng-bind="person.phoneNumber"></span></div>'+
          '<div>Email: <span ng-class="{bound:person}" ng-bind="person.email"></span></div>'+
          '</div>'
          );
      }
    }
  })

  // A service that keeps track of all the scope zone elements in the app
  app.factory("ScopeZones", function() {
    var ScopeZonesObject = {},
      scopeZones = [];

    ScopeZonesObject.addScopeZone = function(scopeZone) {
      scopeZones.push(scopeZone);
    };

    ScopeZonesObject.getScopeZones = function() {
      return scopeZones;
    };

    return ScopeZonesObject;
  });

  // Controllers that set the person associated with each zone
  app.controller("ZoeZone", function($scope) {
    $scope.person = {
      name: "Bart Simpson",
      address: "123 Fake Street, Springfield, USA",
      phoneNumber: "555-555-5555",
      email: "eatmyshorts@example.com"
    };
  });
  app.controller("ZachZone", function($scope) {
    $scope.person = {
      name: "Mabel Pines",
      address: "123 True Street, Gravity Falls, Oregon, USA",
      phoneNumber: "555-555-1234",
      email: "waddlesizdabest@example.com"
    };
  });
})();
/* Styles go here */
body {
  background-image: url(http://i.imgur.com/Bogqwm6.png);
  font-family: 'Oswald', sans-serif;
}

.zone {
  width: 400px;
  height: 300px;
  border-radius: 8px;
  background: rgba(255,255,255,0.18);
  -moz-box-shadow:    inset 0px 0px 5px 0px rgba(0,0,0,0.50);
  -webkit-box-shadow: inset 0px 0px 5px 0px rgba(0,0,0,0.50);
  box-shadow:         inset 0px 0px 5px 0px rgba(0,0,0,0.50);
  margin-bottom: 250px;
  -webkit-transition: background-color 0.3s ease-in-out;
  -moz-transition: background-color 0.3s ease-in-out;
  -o-transition: background-color 0.3s ease-in-out;
  transition: background-color 0.3s ease-in-out;
}

.zone pre {
  white-space: pre-wrap;
}

.containsDragMeToZone {
  background: rgba(255,255,255,0.5);
}

contact-card {
  padding: 10px;
  padding-left: 15px;
  width: 313px;
  height: 159px;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  position: absolute;
  cursor: move;
  top: 360px;
  left: 50px;
}

contact-card .text {
  z-index: 2;
  position: absolute;
}

contact-card span.bound{
  opacity: 1;
}
contact-card span{
  opacity: 0;
  -webkit-transition: opacity 0.3s ease-in-out;
  -moz-transition: opacity 0.3s ease-in-out;
  -o-transition: opacity 0.3s ease-in-out;
  transition: opacity 0.3s ease-in-out;
}

contact-card img {
  position: absolute;
  top: 0;
  left: 0;
  -webkit-transition: opacity 0.3s ease-in-out;
  -moz-transition: opacity 0.3s ease-in-out;
  -o-transition: opacity 0.3s ease-in-out;
  transition: opacity 0.3s ease-in-out;
}

contact-card img.bottom {
  opacity: 0;
}

contact-card.inZone img.bottom {
  opacity: 1;
}


This example shows how you can have a reusable piece of UI (in this case a contact card) that can reflect different pieces of data (as long as that data is organized as person's contact information) using Angular's $scope.