<!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.