4 function isDnDsSupported() {
5 return 'ondrag' in document.createElement('a');
8 function determineEffectAllowed(e) {
9 // Chrome doesn't set dropEffect, so we have to work it out ourselves
10 if (e.dataTransfer && e.dataTransfer.dropEffect === 'none') {
11 if (e.dataTransfer.effectAllowed === 'copy' ||
12 e.dataTransfer.effectAllowed === 'move') {
13 e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed;
14 } else if (e.dataTransfer.effectAllowed === 'copyMove' || e.dataTransfer.effectAllowed === 'copymove') {
15 e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
20 if (!isDnDsSupported()) {
21 angular.module('ang-drag-drop', []);
25 if (window.jQuery && (-1 === window.jQuery.event.props.indexOf('dataTransfer'))) {
26 window.jQuery.event.props.push('dataTransfer');
29 var module = angular.module('ang-drag-drop', []);
31 module.directive('uiDraggable', ['$parse', '$rootScope', '$dragImage', function($parse, $rootScope, $dragImage) {
32 return function(scope, element, attrs) {
33 var isDragHandleUsed = false,
35 draggingClass = attrs.draggingClass || 'on-dragging',
38 element.attr('draggable', false);
40 scope.$watch(attrs.uiDraggable, function(newValue) {
42 element.attr('draggable', newValue);
43 element.bind('dragend', dragendHandler);
44 element.bind('dragstart', dragstartHandler);
47 element.removeAttr('draggable');
48 element.unbind('dragend', dragendHandler);
49 element.unbind('dragstart', dragstartHandler);
54 if (angular.isString(attrs.dragHandleClass)) {
55 isDragHandleUsed = true;
56 dragHandleClass = attrs.dragHandleClass.trim() || 'drag-handle';
58 element.bind('mousedown', function(e) {
59 dragTarget = e.target;
63 function dragendHandler(e) {
64 setTimeout(function() {
65 element.unbind('$destroy', dragendHandler);
67 var sendChannel = attrs.dragChannel || 'defaultchannel';
68 $rootScope.$broadcast('ANGULAR_DRAG_END', e, sendChannel);
70 determineEffectAllowed(e);
72 if (e.dataTransfer && e.dataTransfer.dropEffect !== 'none') {
73 if (attrs.onDropSuccess) {
74 var onDropSuccessFn = $parse(attrs.onDropSuccess);
75 scope.$evalAsync(function() {
76 onDropSuccessFn(scope, {$event: e});
79 if (attrs.onDropFailure) {
80 var onDropFailureFn = $parse(attrs.onDropFailure);
81 scope.$evalAsync(function() {
82 onDropFailureFn(scope, {$event: e});
87 element.removeClass(draggingClass);
90 function dragstartHandler(e) {
91 var isDragAllowed = !isDragHandleUsed || dragTarget.classList.contains(dragHandleClass);
94 var sendChannel = attrs.dragChannel || 'defaultchannel';
97 dragData = scope.$eval(attrs.drag);
100 var dragImage = attrs.dragImage || null;
102 element.addClass(draggingClass);
103 element.bind('$destroy', dragendHandler);
105 //Code to make sure that the setDragImage is available. IE 10, 11, and Opera do not support setDragImage.
106 var hasNativeDraggable = !(document.uniqueID || window.opera);
108 //If there is a draggable image passed in, then set the image to be dragged.
109 if (dragImage && hasNativeDraggable) {
110 var dragImageFn = $parse(attrs.dragImage);
111 scope.$apply(function() {
112 var dragImageParameters = dragImageFn(scope, {$event: e});
113 if (dragImageParameters) {
114 if (angular.isString(dragImageParameters)) {
115 dragImageParameters = $dragImage.generate(dragImageParameters);
117 if (dragImageParameters.image) {
118 var xOffset = dragImageParameters.xOffset || 0,
119 yOffset = dragImageParameters.yOffset || 0;
120 e.dataTransfer.setDragImage(dragImageParameters.image, xOffset, yOffset);
126 var transferDataObject = {data: dragData, channel: sendChannel};
127 var transferDataText = angular.toJson(transferDataObject);
129 e.dataTransfer.setData('text', transferDataText);
130 e.dataTransfer.effectAllowed = 'copyMove';
132 $rootScope.$broadcast('ANGULAR_DRAG_START', e, sendChannel, transferDataObject);
142 module.directive('uiOnDrop', ['$parse', '$rootScope', function($parse, $rootScope) {
143 return function(scope, element, attr) {
144 var dragging = 0; //Ref. http://stackoverflow.com/a/10906204
145 var dropChannel = attr.dropChannel || 'defaultchannel';
146 var dragChannel = '';
147 var dragEnterClass = attr.dragEnterClass || 'on-drag-enter';
148 var dragHoverClass = attr.dragHoverClass || 'on-drag-hover';
149 var customDragEnterEvent = $parse(attr.onDragEnter);
150 var customDragLeaveEvent = $parse(attr.onDragLeave);
152 function onDragOver(e) {
153 if (e.preventDefault) {
154 e.preventDefault(); // Necessary. Allows us to drop.
157 if (e.stopPropagation) {
161 var uiOnDragOverFn = $parse(attr.uiOnDragOver);
162 scope.$evalAsync(function() {
163 uiOnDragOverFn(scope, {$event: e, $channel: dropChannel});
169 function onDragLeave(e) {
170 if (e.preventDefault) {
174 if (e.stopPropagation) {
179 if (dragging === 0) {
180 scope.$evalAsync(function() {
181 customDragLeaveEvent(scope, {$event: e, $channel: dropChannel});
183 element.addClass(dragEnterClass);
184 element.removeClass(dragHoverClass);
187 var uiOnDragLeaveFn = $parse(attr.uiOnDragLeave);
188 scope.$evalAsync(function() {
189 uiOnDragLeaveFn(scope, {$event: e, $channel: dropChannel});
193 function onDragEnter(e) {
194 if (e.preventDefault) {
198 if (e.stopPropagation) {
202 if (dragging === 0) {
203 scope.$evalAsync(function() {
204 customDragEnterEvent(scope, {$event: e, $channel: dropChannel});
206 element.removeClass(dragEnterClass);
207 element.addClass(dragHoverClass);
211 var uiOnDragEnterFn = $parse(attr.uiOnDragEnter);
212 scope.$evalAsync(function() {
213 uiOnDragEnterFn(scope, {$event: e, $channel: dropChannel});
216 $rootScope.$broadcast('ANGULAR_HOVER', dragChannel);
220 if (e.preventDefault) {
221 e.preventDefault(); // Necessary. Allows us to drop.
223 if (e.stopPropagation) {
224 e.stopPropagation(); // Necessary. Allows us to drop.
227 var sendData = e.dataTransfer.getData('text');
228 sendData = angular.fromJson(sendData);
230 determineEffectAllowed(e);
232 var uiOnDropFn = $parse(attr.uiOnDrop);
233 scope.$evalAsync(function() {
234 uiOnDropFn(scope, {$data: sendData.data, $event: e, $channel: sendData.channel});
236 element.removeClass(dragEnterClass);
240 function isDragChannelAccepted(dragChannel, dropChannel) {
241 if (dropChannel === '*') {
245 var channelMatchPattern = new RegExp('(\\s|[,])+(' + dragChannel + ')(\\s|[,])+', 'i');
247 return channelMatchPattern.test(',' + dropChannel + ',');
250 function preventNativeDnD(e) {
251 if (e.preventDefault) {
254 if (e.stopPropagation) {
257 e.dataTransfer.dropEffect = 'none';
261 var deregisterDragStart = $rootScope.$on('ANGULAR_DRAG_START', function(_, e, channel, transferDataObject) {
262 dragChannel = channel;
266 if (!isDragChannelAccepted(channel, dropChannel)) {
270 if (valid && attr.dropValidate) {
271 var validateFn = $parse(attr.dropValidate);
272 valid = validateFn(scope, {
273 $drop: {scope: scope, element: element},
275 $data: transferDataObject.data,
276 $channel: transferDataObject.channel
281 element.bind('dragover', onDragOver);
282 element.bind('dragenter', onDragEnter);
283 element.bind('dragleave', onDragLeave);
284 element.bind('drop', onDrop);
286 element.addClass(dragEnterClass);
288 element.bind('dragover', preventNativeDnD);
289 element.bind('dragenter', preventNativeDnD);
290 element.bind('dragleave', preventNativeDnD);
291 element.bind('drop', preventNativeDnD);
293 element.removeClass(dragEnterClass);
299 var deregisterDragEnd = $rootScope.$on('ANGULAR_DRAG_END', function() {
300 element.unbind('dragover', onDragOver);
301 element.unbind('dragenter', onDragEnter);
302 element.unbind('dragleave', onDragLeave);
304 element.unbind('drop', onDrop);
305 element.removeClass(dragHoverClass);
306 element.removeClass(dragEnterClass);
308 element.unbind('dragover', preventNativeDnD);
309 element.unbind('dragenter', preventNativeDnD);
310 element.unbind('dragleave', preventNativeDnD);
311 element.unbind('drop', preventNativeDnD);
314 scope.$on('$destroy', function() {
315 deregisterDragStart();
320 attr.$observe('dropChannel', function(value) {
331 module.constant('$dragImageConfig', {
335 font: 'bold 11px Arial',
336 fontColor: '#eee8d5',
337 backgroundColor: '#93a1a1',
342 module.service('$dragImage', ['$dragImageConfig', function(defaultConfig) {
345 function fitString(canvas, text, config) {
346 var width = canvas.measureText(text).width;
347 if (width < config.width) {
350 while (width + config.padding > config.width) {
351 text = text.substring(0, text.length - 1);
352 width = canvas.measureText(text + ELLIPSIS).width;
354 return text + ELLIPSIS;
357 this.generate = function(text, options) {
358 var config = angular.extend({}, defaultConfig, options || {});
359 var el = document.createElement('canvas');
361 el.height = config.height;
362 el.width = config.width;
364 var canvas = el.getContext('2d');
366 canvas.fillStyle = config.backgroundColor;
367 canvas.fillRect(0, 0, config.width, config.height);
368 canvas.font = config.font;
369 canvas.fillStyle = config.fontColor;
371 var title = fitString(canvas, text, config);
372 canvas.fillText(title, 4, config.padding + 4);
374 var image = new Image();
375 image.src = el.toDataURL();
379 xOffset: config.xOffset,
380 yOffset: config.yOffset