(function(angular) { 'use strict'; function isDnDsSupported() { return 'ondrag' in document.createElement('a'); } function determineEffectAllowed(e) { if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } // Chrome doesn't set dropEffect, so we have to work it out ourselves if (typeof e.dataTransfer !== 'undefined' && e.dataTransfer.dropEffect === 'none') { if (e.dataTransfer.effectAllowed === 'copy' || e.dataTransfer.effectAllowed === 'move') { e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed; } else if (e.dataTransfer.effectAllowed === 'copyMove' || e.dataTransfer.effectAllowed === 'copymove') { e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move'; } } } if (!isDnDsSupported()) { angular.module('ang-drag-drop', []); return; } var module = angular.module('ang-drag-drop', []); module.directive('uiDraggable', ['$parse', '$rootScope', '$dragImage', function($parse, $rootScope, $dragImage) { return function(scope, element, attrs) { var isDragHandleUsed = false, dragHandleClass, draggingClass = attrs.draggingClass || 'on-dragging', dragTarget; element.attr('draggable', false); scope.$watch(attrs.uiDraggable, function(newValue) { if (newValue) { element.attr('draggable', newValue); element.bind('dragend', dragendHandler); element.bind('dragstart', dragstartHandler); } else { element.removeAttr('draggable'); element.unbind('dragend', dragendHandler); element.unbind('dragstart', dragstartHandler); } }); if (angular.isString(attrs.dragHandleClass)) { isDragHandleUsed = true; dragHandleClass = attrs.dragHandleClass.trim() || 'drag-handle'; element.bind('mousedown', function(e) { dragTarget = e.target; }); } function dragendHandler(e) { if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } setTimeout(function() { element.unbind('$destroy', dragendHandler); }, 0); var sendChannel = attrs.dragChannel || 'defaultchannel'; $rootScope.$broadcast('ANGULAR_DRAG_END', e, sendChannel); determineEffectAllowed(e); if (e.dataTransfer && e.dataTransfer.dropEffect !== 'none') { if (attrs.onDropSuccess) { var onDropSuccessFn = $parse(attrs.onDropSuccess); scope.$evalAsync(function() { onDropSuccessFn(scope, {$event: e}); }); } }else if (e.dataTransfer && e.dataTransfer.dropEffect === 'none'){ if (attrs.onDropFailure) { var onDropFailureFn = $parse(attrs.onDropFailure); scope.$evalAsync(function() { onDropFailureFn(scope, {$event: e}); }); } } element.removeClass(draggingClass); } function setDragElement(e, dragImageElementId) { var dragImageElementFn; if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } dragImageElementFn = $parse(dragImageElementId); scope.$apply(function() { var elementId = dragImageElementFn(scope, {$event: e}), dragElement; if (!(elementId && angular.isString(elementId))) { return; } dragElement = document.getElementById(elementId); if (!dragElement) { return; } e.dataTransfer.setDragImage(dragElement, 0, 0); }); } function dragstartHandler(e) { if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } var isDragAllowed = !isDragHandleUsed || dragTarget.classList.contains(dragHandleClass); if (isDragAllowed) { var sendChannel = attrs.dragChannel || 'defaultchannel'; var dragData = ''; if (attrs.drag) { dragData = scope.$eval(attrs.drag); } var dragImage = attrs.dragImage || null; element.addClass(draggingClass); element.bind('$destroy', dragendHandler); //Code to make sure that the setDragImage is available. IE 10, 11, and Opera do not support setDragImage. var hasNativeDraggable = !(document.uniqueID || window.opera); //If there is a draggable image passed in, then set the image to be dragged. if (dragImage && hasNativeDraggable) { var dragImageFn = $parse(attrs.dragImage); scope.$apply(function() { var dragImageParameters = dragImageFn(scope, {$event: e}); if (dragImageParameters) { if (angular.isString(dragImageParameters)) { dragImageParameters = $dragImage.generate(dragImageParameters); } if (dragImageParameters.image) { var xOffset = dragImageParameters.xOffset || 0, yOffset = dragImageParameters.yOffset || 0; e.dataTransfer.setDragImage(dragImageParameters.image, xOffset, yOffset); } } }); } else if (attrs.dragImageElementId) { setDragElement(e, attrs.dragImageElementId); } var offset = {x: e.offsetX, y: e.offsetY}; var transferDataObject = {data: dragData, channel: sendChannel, offset: offset}; var transferDataText = angular.toJson(transferDataObject); e.dataTransfer.setData('text', transferDataText); e.dataTransfer.effectAllowed = 'copyMove'; if (attrs.onDragStart) { var onDragStartFn = $parse(attrs.onDragStart); scope.$evalAsync(function () { onDragStartFn(scope, { $event: e }); }); } $rootScope.$broadcast('ANGULAR_DRAG_START', e, sendChannel, transferDataObject); } else { e.preventDefault(); } } }; } ]); module.directive('uiOnDrop', ['$parse', '$rootScope', function($parse, $rootScope) { return function(scope, element, attr) { var dragging = 0; //Ref. http://stackoverflow.com/a/10906204 var dropChannel = attr.dropChannel || 'defaultchannel'; var dragChannel = ''; var dragEnterClass = attr.dragEnterClass || 'on-drag-enter'; var dragHoverClass = attr.dragHoverClass || 'on-drag-hover'; var customDragEnterEvent = $parse(attr.onDragEnter); var customDragLeaveEvent = $parse(attr.onDragLeave); var uiOnDragOverFn = $parse(attr.uiOnDragOver); function calculateDropOffset(e) { var offset = { x: e.offsetX, y: e.offsetY }; var target = e.target; while (target !== element[0]) { offset.x = offset.x + target.offsetLeft; offset.y = offset.y + target.offsetTop; target = target.offsetParent; if (!target) { return null; } } return offset; } function onDragOver(e) { if (e.preventDefault) { e.preventDefault(); // Necessary. Allows us to drop. } if (e.stopPropagation) { e.stopPropagation(); } if (attr.uiOnDragOver) { scope.$evalAsync(function() { uiOnDragOverFn(scope, {$event: e, $channel: dropChannel}); }); } return false; } function onDragLeave(e) { if (e.preventDefault) { e.preventDefault(); } if (e.stopPropagation) { e.stopPropagation(); } dragging--; if (dragging === 0) { scope.$evalAsync(function() { customDragLeaveEvent(scope, {$event: e, $channel: dropChannel}); }); element.addClass(dragEnterClass); element.removeClass(dragHoverClass); } if (attr.uiOnDragLeave) { var uiOnDragLeaveFn = $parse(attr.uiOnDragLeave); scope.$evalAsync(function() { uiOnDragLeaveFn(scope, {$event: e, $channel: dropChannel}); }); } } function onDragEnter(e) { if (e.preventDefault) { e.preventDefault(); } if (e.stopPropagation) { e.stopPropagation(); } if (dragging === 0) { scope.$evalAsync(function() { customDragEnterEvent(scope, {$event: e, $channel: dropChannel}); }); element.removeClass(dragEnterClass); element.addClass(dragHoverClass); } dragging++; if (attr.uiOnDragEnter) { var uiOnDragEnterFn = $parse(attr.uiOnDragEnter); scope.$evalAsync(function() { uiOnDragEnterFn(scope, {$event: e, $channel: dropChannel}); }); } $rootScope.$broadcast('ANGULAR_HOVER', dragChannel); } function onDrop(e) { if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } if (e.preventDefault) { e.preventDefault(); // Necessary. Allows us to drop. } if (e.stopPropagation) { e.stopPropagation(); // Necessary. Allows us to drop. } var sendData = e.dataTransfer.getData('text'); sendData = angular.fromJson(sendData); var dropOffset = calculateDropOffset(e); var position = dropOffset ? { x: dropOffset.x - sendData.offset.x, y: dropOffset.y - sendData.offset.y } : null; determineEffectAllowed(e); var uiOnDropFn = $parse(attr.uiOnDrop); scope.$evalAsync(function() { uiOnDropFn(scope, {$data: sendData.data, $event: e, $channel: sendData.channel, $position: position}); }); element.removeClass(dragEnterClass); dragging = 0; } function isDragChannelAccepted(dragChannel, dropChannel) { if (dropChannel === '*') { return true; } var channelMatchPattern = new RegExp('(\\s|[,])+(' + dragChannel + ')(\\s|[,])+', 'i'); return channelMatchPattern.test(',' + dropChannel + ','); } function preventNativeDnD(e) { if(e.originalEvent) { e.dataTransfer = e.originalEvent.dataTransfer; } if (e.preventDefault) { e.preventDefault(); } if (e.stopPropagation) { e.stopPropagation(); } e.dataTransfer.dropEffect = 'none'; return false; } var deregisterDragStart = $rootScope.$on('ANGULAR_DRAG_START', function(_, e, channel, transferDataObject) { dragChannel = channel; var valid = true; if (!isDragChannelAccepted(channel, dropChannel)) { valid = false; } if (valid && attr.dropValidate) { var validateFn = $parse(attr.dropValidate); valid = validateFn(scope, { $drop: {scope: scope, element: element}, $event: e, $data: transferDataObject.data, $channel: transferDataObject.channel }); } if (valid) { element.bind('dragover', onDragOver); element.bind('dragenter', onDragEnter); element.bind('dragleave', onDragLeave); element.bind('drop', onDrop); element.addClass(dragEnterClass); } else { element.bind('dragover', preventNativeDnD); element.bind('dragenter', preventNativeDnD); element.bind('dragleave', preventNativeDnD); element.bind('drop', preventNativeDnD); element.removeClass(dragEnterClass); } }); var deregisterDragEnd = $rootScope.$on('ANGULAR_DRAG_END', function() { element.unbind('dragover', onDragOver); element.unbind('dragenter', onDragEnter); element.unbind('dragleave', onDragLeave); element.unbind('drop', onDrop); element.removeClass(dragHoverClass); element.removeClass(dragEnterClass); element.unbind('dragover', preventNativeDnD); element.unbind('dragenter', preventNativeDnD); element.unbind('dragleave', preventNativeDnD); element.unbind('drop', preventNativeDnD); }); scope.$on('$destroy', function() { deregisterDragStart(); deregisterDragEnd(); }); attr.$observe('dropChannel', function(value) { if (value) { dropChannel = value; } }); }; } ]); module.constant('$dragImageConfig', { height: 20, width: 200, padding: 10, font: 'bold 11px Arial', fontColor: '#eee8d5', backgroundColor: '#93a1a1', xOffset: 0, yOffset: 0 }); module.service('$dragImage', ['$dragImageConfig', function(defaultConfig) { var ELLIPSIS = '…'; function fitString(canvas, text, config) { var width = canvas.measureText(text).width; if (width < config.width) { return text; } while (width + config.padding > config.width) { text = text.substring(0, text.length - 1); width = canvas.measureText(text + ELLIPSIS).width; } return text + ELLIPSIS; } this.generate = function(text, options) { var config = angular.extend({}, defaultConfig, options || {}); var el = document.createElement('canvas'); el.height = config.height; el.width = config.width; var canvas = el.getContext('2d'); canvas.fillStyle = config.backgroundColor; canvas.fillRect(0, 0, config.width, config.height); canvas.font = config.font; canvas.fillStyle = config.fontColor; var title = fitString(canvas, text, config); canvas.fillText(title, 4, config.padding + 4); var image = new Image(); image.src = el.toDataURL(); return { image: image, xOffset: config.xOffset, yOffset: config.yOffset }; }; } ]); }(angular));