draganddrop.js 16 KB

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