draganddrop.js 14 KB

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