draganddrop.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. (function(angular) {
  2. 'use strict';
  3. function isDnDsSupported() {
  4. return 'ondrag' in document.createElement('a');
  5. }
  6. function determineEffectAllowed(e) {
  7. // Chrome doesn't set dropEffect, so we have to work it out ourselves
  8. if (e.dataTransfer && e.dataTransfer.dropEffect === 'none') {
  9. if (e.dataTransfer.effectAllowed === 'copy' ||
  10. e.dataTransfer.effectAllowed === 'move') {
  11. e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed;
  12. } else if (e.dataTransfer.effectAllowed === 'copyMove' || e.dataTransfer.effectAllowed === 'copymove') {
  13. e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
  14. }
  15. }
  16. }
  17. if (!isDnDsSupported()) {
  18. angular.module('ang-drag-drop', []);
  19. return;
  20. }
  21. if (window.jQuery && (-1 === window.jQuery.event.props.indexOf('dataTransfer'))) {
  22. window.jQuery.event.props.push('dataTransfer');
  23. }
  24. var module = angular.module('ang-drag-drop', []);
  25. module.directive('uiDraggable', ['$parse', '$rootScope', '$dragImage', function($parse, $rootScope, $dragImage) {
  26. return function(scope, element, attrs) {
  27. var isDragHandleUsed = false,
  28. dragHandleClass,
  29. draggingClass = attrs.draggingClass || 'on-dragging',
  30. dragTarget;
  31. element.attr('draggable', false);
  32. scope.$watch(attrs.uiDraggable, function(newValue) {
  33. if (newValue) {
  34. element.attr('draggable', newValue);
  35. element.bind('dragend', dragendHandler);
  36. element.bind('dragstart', dragstartHandler);
  37. }
  38. else {
  39. element.removeAttr('draggable');
  40. element.unbind('dragend', dragendHandler);
  41. element.unbind('dragstart', dragstartHandler);
  42. }
  43. });
  44. if (angular.isString(attrs.dragHandleClass)) {
  45. isDragHandleUsed = true;
  46. dragHandleClass = attrs.dragHandleClass.trim() || 'drag-handle';
  47. element.bind('mousedown', function(e) {
  48. dragTarget = e.target;
  49. });
  50. }
  51. function dragendHandler(e) {
  52. setTimeout(function() {
  53. element.unbind('$destroy', dragendHandler);
  54. }, 0);
  55. var sendChannel = attrs.dragChannel || 'defaultchannel';
  56. $rootScope.$broadcast('ANGULAR_DRAG_END', e, sendChannel);
  57. determineEffectAllowed(e);
  58. if (e.dataTransfer && e.dataTransfer.dropEffect !== 'none') {
  59. if (attrs.onDropSuccess) {
  60. var onDropSuccessFn = $parse(attrs.onDropSuccess);
  61. scope.$evalAsync(function() {
  62. onDropSuccessFn(scope, {$event: e});
  63. });
  64. } else {
  65. if (attrs.onDropFailure) {
  66. var onDropFailureFn = $parse(attrs.onDropFailure);
  67. scope.$evalAsync(function() {
  68. onDropFailureFn(scope, {$event: e});
  69. });
  70. }
  71. }
  72. }
  73. element.removeClass(draggingClass);
  74. }
  75. function dragstartHandler(e) {
  76. var isDragAllowed = !isDragHandleUsed || dragTarget.classList.contains(dragHandleClass);
  77. if (dragTarget.classList.contains("resize-panel-handle")) {
  78. return;
  79. }
  80. if (isDragAllowed) {
  81. var sendChannel = attrs.dragChannel || 'defaultchannel';
  82. var dragData = '';
  83. if (attrs.drag) {
  84. dragData = scope.$eval(attrs.drag);
  85. }
  86. var dragImage = attrs.dragImage || null;
  87. element.addClass(draggingClass);
  88. element.bind('$destroy', dragendHandler);
  89. //Code to make sure that the setDragImage is available. IE 10, 11, and Opera do not support setDragImage.
  90. var hasNativeDraggable = !(document.uniqueID || window.opera);
  91. //If there is a draggable image passed in, then set the image to be dragged.
  92. if (dragImage && hasNativeDraggable) {
  93. var dragImageFn = $parse(attrs.dragImage);
  94. scope.$apply(function() {
  95. var dragImageParameters = dragImageFn(scope, {$event: e});
  96. if (dragImageParameters) {
  97. if (angular.isString(dragImageParameters)) {
  98. dragImageParameters = $dragImage.generate(dragImageParameters);
  99. }
  100. if (dragImageParameters.image) {
  101. var xOffset = dragImageParameters.xOffset || 0,
  102. yOffset = dragImageParameters.yOffset || 0;
  103. e.dataTransfer.setDragImage(dragImageParameters.image, xOffset, yOffset);
  104. }
  105. }
  106. });
  107. }
  108. var transferDataObject = {data: dragData, channel: sendChannel}
  109. var transferDataText = angular.toJson(transferDataObject);
  110. e.dataTransfer.setData('text', transferDataText);
  111. e.dataTransfer.effectAllowed = 'copyMove';
  112. $rootScope.$broadcast('ANGULAR_DRAG_START', e, sendChannel, transferDataObject);
  113. }
  114. else {
  115. e.preventDefault();
  116. }
  117. }
  118. };
  119. }
  120. ]);
  121. module.directive('uiOnDrop', ['$parse', '$rootScope', function($parse, $rootScope) {
  122. return function(scope, element, attr) {
  123. var dragging = 0; //Ref. http://stackoverflow.com/a/10906204
  124. var dropChannel = attr.dropChannel || 'defaultchannel';
  125. var dragChannel = '';
  126. var dragEnterClass = attr.dragEnterClass || 'on-drag-enter';
  127. var dragHoverClass = attr.dragHoverClass || 'on-drag-hover';
  128. var customDragEnterEvent = $parse(attr.onDragEnter);
  129. var customDragLeaveEvent = $parse(attr.onDragLeave);
  130. function onDragOver(e) {
  131. if (e.preventDefault) {
  132. e.preventDefault(); // Necessary. Allows us to drop.
  133. }
  134. if (e.stopPropagation) {
  135. e.stopPropagation();
  136. }
  137. var uiOnDragOverFn = $parse(attr.uiOnDragOver);
  138. scope.$evalAsync(function() {
  139. uiOnDragOverFn(scope, {$event: e, $channel: dropChannel});
  140. });
  141. return false;
  142. }
  143. function onDragLeave(e) {
  144. if (e.preventDefault) {
  145. e.preventDefault();
  146. }
  147. if (e.stopPropagation) {
  148. e.stopPropagation();
  149. }
  150. dragging--;
  151. if (dragging === 0) {
  152. scope.$evalAsync(function() {
  153. customDragLeaveEvent(scope, {$event: e, $channel: dropChannel});
  154. });
  155. element.addClass(dragEnterClass);
  156. element.removeClass(dragHoverClass);
  157. }
  158. var uiOnDragLeaveFn = $parse(attr.uiOnDragLeave);
  159. scope.$evalAsync(function() {
  160. uiOnDragLeaveFn(scope, {$event: e, $channel: dropChannel});
  161. });
  162. }
  163. function onDragEnter(e) {
  164. if (e.preventDefault) {
  165. e.preventDefault();
  166. }
  167. if (e.stopPropagation) {
  168. e.stopPropagation();
  169. }
  170. if (dragging === 0) {
  171. scope.$evalAsync(function() {
  172. customDragEnterEvent(scope, {$event: e, $channel: dropChannel});
  173. });
  174. element.removeClass(dragEnterClass);
  175. element.addClass(dragHoverClass);
  176. }
  177. dragging++;
  178. var uiOnDragEnterFn = $parse(attr.uiOnDragEnter);
  179. scope.$evalAsync(function() {
  180. uiOnDragEnterFn(scope, {$event: e, $channel: dropChannel});
  181. });
  182. $rootScope.$broadcast('ANGULAR_HOVER', dragChannel);
  183. }
  184. function onDrop(e) {
  185. if (e.preventDefault) {
  186. e.preventDefault(); // Necessary. Allows us to drop.
  187. }
  188. if (e.stopPropagation) {
  189. e.stopPropagation(); // Necessary. Allows us to drop.
  190. }
  191. var sendData = e.dataTransfer.getData('text');
  192. sendData = angular.fromJson(sendData);
  193. determineEffectAllowed(e);
  194. var uiOnDropFn = $parse(attr.uiOnDrop);
  195. scope.$evalAsync(function() {
  196. uiOnDropFn(scope, {$data: sendData.data, $event: e, $channel: sendData.channel});
  197. });
  198. element.removeClass(dragEnterClass);
  199. dragging = 0;
  200. }
  201. function isDragChannelAccepted(dragChannel, dropChannel) {
  202. if (dropChannel === '*') {
  203. return true;
  204. }
  205. var channelMatchPattern = new RegExp('(\\s|[,])+(' + dragChannel + ')(\\s|[,])+', 'i');
  206. return channelMatchPattern.test(',' + dropChannel + ',');
  207. }
  208. function preventNativeDnD(e) {
  209. if (e.preventDefault) {
  210. e.preventDefault();
  211. }
  212. if (e.stopPropagation) {
  213. e.stopPropagation();
  214. }
  215. e.dataTransfer.dropEffect = 'none';
  216. return false;
  217. }
  218. var deregisterDragStart = $rootScope.$on('ANGULAR_DRAG_START', function(_, e, channel, transferDataObject) {
  219. dragChannel = channel;
  220. var valid = true;
  221. if (!isDragChannelAccepted(channel, dropChannel)) {
  222. valid = false;
  223. }
  224. if (valid && attr.dropValidate) {
  225. var validateFn = $parse(attr.dropValidate);
  226. valid = validateFn(scope, {
  227. $drop: {scope: scope, element: element},
  228. $event: e,
  229. $data: transferDataObject.data,
  230. $channel: transferDataObject.channel
  231. });
  232. }
  233. if (valid) {
  234. element.bind('dragover', onDragOver);
  235. element.bind('dragenter', onDragEnter);
  236. element.bind('dragleave', onDragLeave);
  237. element.bind('drop', onDrop);
  238. element.addClass(dragEnterClass);
  239. } else {
  240. element.bind('dragover', preventNativeDnD);
  241. element.bind('dragenter', preventNativeDnD);
  242. element.bind('dragleave', preventNativeDnD);
  243. element.bind('drop', preventNativeDnD);
  244. element.removeClass(dragEnterClass);
  245. }
  246. });
  247. var deregisterDragEnd = $rootScope.$on('ANGULAR_DRAG_END', function(_, e, channel) {
  248. element.unbind('dragover', onDragOver);
  249. element.unbind('dragenter', onDragEnter);
  250. element.unbind('dragleave', onDragLeave);
  251. element.unbind('drop', onDrop);
  252. element.removeClass(dragHoverClass);
  253. element.removeClass(dragEnterClass);
  254. element.unbind('dragover', preventNativeDnD);
  255. element.unbind('dragenter', preventNativeDnD);
  256. element.unbind('dragleave', preventNativeDnD);
  257. element.unbind('drop', preventNativeDnD);
  258. });
  259. scope.$on('$destroy', function() {
  260. deregisterDragStart();
  261. deregisterDragEnd();
  262. });
  263. attr.$observe('dropChannel', function(value) {
  264. if (value) {
  265. dropChannel = value;
  266. }
  267. });
  268. };
  269. }
  270. ]);
  271. module.constant('$dragImageConfig', {
  272. height: 20,
  273. width: 200,
  274. padding: 10,
  275. font: 'bold 11px Arial',
  276. fontColor: '#eee8d5',
  277. backgroundColor: '#93a1a1',
  278. xOffset: 0,
  279. yOffset: 0
  280. });
  281. module.service('$dragImage', ['$dragImageConfig', function(defaultConfig) {
  282. var ELLIPSIS = '…';
  283. function fitString(canvas, text, config) {
  284. var width = canvas.measureText(text).width;
  285. if (width < config.width) {
  286. return text;
  287. }
  288. while (width + config.padding > config.width) {
  289. text = text.substring(0, text.length - 1);
  290. width = canvas.measureText(text + ELLIPSIS).width;
  291. }
  292. return text + ELLIPSIS;
  293. }
  294. this.generate = function(text, options) {
  295. var config = angular.extend({}, defaultConfig, options || {});
  296. var el = document.createElement('canvas');
  297. el.height = config.height;
  298. el.width = config.width;
  299. var canvas = el.getContext('2d');
  300. canvas.fillStyle = config.backgroundColor;
  301. canvas.fillRect(0, 0, config.width, config.height);
  302. canvas.font = config.font;
  303. canvas.fillStyle = config.fontColor;
  304. var title = fitString(canvas, text, config);
  305. canvas.fillText(title, 4, config.padding + 4);
  306. var image = new Image();
  307. image.src = el.toDataURL();
  308. return {
  309. image: image,
  310. xOffset: config.xOffset,
  311. yOffset: config.yOffset
  312. };
  313. };
  314. }
  315. ]);
  316. }(angular));