diff --git a/README.md b/README.md index 426f172..cc537c8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Use the dnd-draggable directive to make your element draggable **Attributes** * `dnd-draggable` Required attribute. The value has to be an object that represents the data of the element. In case of a drag and drop operation the object will be serialized and unserialized on the receiving end. +* `dnd-class-for-dragging` Optional name of class to add to element while it's being dragged. Defaults to dndDragging. +* `dnd-class-for-dragging-source` Optional name of class to add to element after drag operation has started. See 'CSS classes' for more details. Defaults to dndDraggingSource. * `dnd-effect-allowed` Use this attribute to limit the operations that can be performed. Valid options are `move`, `copy` and `link`, as well as `all`, `copyMove`, `copyLink` and `linkMove`, while `move` is the default value. The semantics of these operations are up to you and have to be implemented using the callbacks described below. If you allow multiple options, the user can choose between them by using the modifier keys (OS specific). The cursor will be changed accordingly, expect for IE and Edge, where this is not supported. Note that the implementation of this attribute is very buggy in IE9. This attribute works together with `dnd-external-sources` except on Safari and IE, where the restriction will be lost when dragging accross browser tabs. [Design document](https://github.com/marceljuenemann/angular-drag-and-drop-lists/wiki/Drop-Effects-Design) [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) * `dnd-type` Use this attribute if you have different kinds of items in your application and you want to limit which items can be dropped into which lists. Combine with dnd-allowed-types on the dnd-list(s). This attribute must be a lower case string. Upper case characters can be used, but will be converted to lower case automatically. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) * `dnd-disable-if` You can use this attribute to dynamically disable the draggability of the element. This is useful if you have certain list items that you don't want to be draggable, or if you want to disable drag & drop completely without having two different code branches (e.g. only allow for admins). [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) @@ -41,8 +43,8 @@ Use the dnd-draggable directive to make your element draggable * `dnd-callback` Custom callback that is passed to dropzone callbacks and can be used to communicate between source and target scopes. The dropzone can pass user defined variables to this callback. This can be used to transfer objects without serialization, see [Demo](https://jsfiddle.net/Ldxffyod/1/). **CSS classes** -* `dndDragging` This class will be added to the element while the element is being dragged. It will affect both the element you see while dragging and the source element that stays at it's position. Do not try to hide the source element with this class, because that will abort the drag operation. -* `dndDraggingSource` This class will be added to the element after the drag operation was started, meaning it only affects the original element that is still at it's source position, and not the "element" that the user is dragging with his mouse pointer +* `dndDragging` This class will be added to the element while the element is being dragged. It will affect both the element you see while dragging and the source element that stays at it's position. Do not try to hide the source element with this class, because that will abort the drag operation. Class name can be overridden (see above). +* `dndDraggingSource` This class will be added to the element after the drag operation was started, meaning it only affects the original element that is still at it's source position, and not the "element" that the user is dragging with his mouse pointer. Class name can be overridden (see above). ## dnd-list directive @@ -51,6 +53,8 @@ Use the dnd-list attribute to make your list element a dropzone. Usually you wil **Attributes** * `dnd-list` Required attribute. The value has to be the array in which the data of the dropped element should be inserted. The value can be blank if used with a custom dnd-drop handler that handles the insertion on its own. * `dnd-allowed-types` Optional array of allowed item types. When used, only items that had a matching dnd-type attribute will be dropable. Upper case characters will automatically be converted to lower case. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) +* `dnd-class-for-dragover` Optional name of class to add to list while element is being dragged over it. Defaults to dndDragover. +* `dnd-class-for-placeholder` Optional name of class to add to placeholder element. Defaults to dndPlaceholder. * `dnd-effect-allowed` Optional string expression that limits the drop effects that can be performed on the list. See dnd-effect-allowed on dnd-draggable for more details on allowed options. The default value is `all`. * `dnd-disable-if` Optional boolean expression. When it evaluates to true, no dropping into the list is possible. Note that this also disables rearranging items inside the list. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/types) * `dnd-horizontal-list` Optional boolean expression. When it evaluates to true, the positioning algorithm will use the left and right halfs of the list items instead of the upper and lower halfs. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) @@ -72,8 +76,8 @@ Use the dnd-list attribute to make your list element a dropzone. Usually you wil * `dnd-inserted` Optional expression that is invoked after a drop if the element was actually inserted into the list. The same local variables as for `dnd-drop` will be available. Note that for reorderings inside the same list the old element will still be in the list due to the fact that `dnd-moved` was not called yet. [Demo](http://marceljuenemann.github.io/angular-drag-and-drop-lists/demo/#/advanced) **CSS classes** -* `dndPlaceholder` When an element is dragged over the list, a new placeholder child element will be added. This element is of type `li` and has the class `dndPlaceholder` set. Alternatively, you can define your own placeholder by creating a child element with `dndPlaceholder` class. -* `dndDragover` This class will be added to the list while an element is being dragged over the list. +* `dndPlaceholder` When an element is dragged over the list, a new placeholder child element will be added. This element is of type `li` and has the class `dndPlaceholder` set. Alternatively, you can define your own placeholder by creating a child element with `dndPlaceholder` class. Class name can be overridden (see above). +* `dndDragover` This class will be added to the list while an element is being dragged over the list. Class name can be overridden (see above). ## dnd-nodrag directive diff --git a/angular-drag-and-drop-lists.js b/angular-drag-and-drop-lists.js index 8a07ce3..f8a5915 100644 --- a/angular-drag-and-drop-lists.js +++ b/angular-drag-and-drop-lists.js @@ -26,6 +26,11 @@ * - dnd-draggable Required attribute. The value has to be an object that represents the data * of the element. In case of a drag and drop operation the object will be * serialized and unserialized on the receiving end. + * - dnd-class-for-dragging + * Optional name of class to add to element while it's being dragged. Defaults to dndDragging. + * - dnd-class-for-dragging-source + * Optional name of class to add to element after drag operation has started. See 'CSS classes' + * for more details. Defaults to dndDraggingSource. * - dnd-effect-allowed Use this attribute to limit the operations that can be performed. Valid * options are "move", "copy" and "link", as well as "all", "copyMove", * "copyLink" and "linkMove". The semantics of these operations are up to you @@ -77,6 +82,24 @@ */ dndLists.directive('dndDraggable', ['$parse', '$timeout', function($parse, $timeout) { return function(scope, element, attr) { + var classForDragging = 'dndDragging'; + var classForDraggingSource = 'dndDraggingSource'; + + // If the dnd-class* attribute is set, we'll parse the current value and watch for future changes + if (attr.dndClassForDragging) { + classForDragging = attr.dndClassForDragging; + scope.$watch(attr.dndClassForDragging, function(){ + classForDragging = attr.dndClassForDragging; + }); + } + + if (attr.dndClassForDraggingSource) { + classForDraggingSource = attr.dndClassForDraggingSource; + scope.$watch(attr.dndClassForDraggingSource, function(){ + classForDraggingSource = attr.dndClassForDraggingSource; + }); + } + // Set the HTML5 draggable attribute on the element. element.attr("draggable", "true"); @@ -128,8 +151,8 @@ } // Add CSS classes. See documentation above. - element.addClass("dndDragging"); - $timeout(function() { element.addClass("dndDraggingSource"); }, 0); + element.addClass(classForDragging); + $timeout(function() { element.addClass(classForDraggingSource); }, 0); // Try setting a proper drag image if triggered on a dnd-handle (won't work in IE). if (event._dndHandle && event.dataTransfer.setDragImage) { @@ -168,12 +191,12 @@ // Clean up dndState.isDragging = false; dndState.callback = undefined; - element.removeClass("dndDragging"); - element.removeClass("dndDraggingSource"); + element.removeClass(classForDragging); + element.removeClass(classForDraggingSource); event.stopPropagation(); // In IE9 it is possible that the timeout from dragstart triggers after the dragend handler. - $timeout(function() { element.removeClass("dndDraggingSource"); }, 0); + $timeout(function() { element.removeClass(classForDraggingSource); }, 0); }); /** @@ -211,6 +234,11 @@ * - dnd-list Required attribute. The value has to be the array in which the data of * the dropped element should be inserted. The value can be blank if used * with a custom dnd-drop handler that always returns true. + * - dnd-class-for-dragover + * Optional name of class to add to list while element is being dragged over it. + * Defaults to dndDragover. + * - dnd-class-for-placeholder + * Optional name of class to add to placeholder element. Defaults to dndPlaceholder. * - dnd-allowed-types Optional array of allowed item types. When used, only items that had a * matching dnd-type attribute will be dropable. Upper case characters will * automatically be converted to lower case. @@ -271,11 +299,29 @@ */ dndLists.directive('dndList', ['$parse', function($parse) { return function(scope, element, attr) { + var classForPlaceholder = 'dndPlaceholder'; + var classForDragover = 'dndDragover'; + + // If the dnd-class* attribute is set, we'll parse the current value and watch for future changes + if (attr.dndClassForPlaceholder) { + classForPlaceholder = attr.dndClassForPlaceholder; + scope.$watch(attr.dndClassForPlaceholder, function(){ + classForPlaceholder = attr.dndClassForPlaceholder; + }); + } + + if (attr.dndClassForDragover) { + classForDragover = attr.dndClassForDragover; + scope.$watch(attr.dndClassForDragover, function(){ + classForDragover = attr.dndClassForDragover; + }); + } + // While an element is dragged over the list, this placeholder element is inserted // at the location where the element would be inserted after dropping. var placeholder = getPlaceholderElement(); placeholder.remove(); - + var placeholderNode = placeholder[0]; var listNode = element[0]; var listSettings = {}; @@ -361,7 +407,7 @@ event.dataTransfer.dropEffect = dropEffect; } - element.addClass("dndDragover"); + element.addClass(classForDragover); event.stopPropagation(); return false; }); @@ -519,7 +565,7 @@ */ function stopDragover() { placeholder.remove(); - element.removeClass("dndDragover"); + element.removeClass(classForDragover); return true; } @@ -554,12 +600,13 @@ var placeholder; angular.forEach(element.children(), function(childNode) { var child = angular.element(childNode); - if (child.hasClass('dndPlaceholder')) { + if (child.hasClass(classForPlaceholder)) { placeholder = child; } }); - return placeholder || angular.element("
  • "); + return placeholder || angular.element("
  • "); } + }; }]); diff --git a/angular-drag-and-drop-lists.min.js b/angular-drag-and-drop-lists.min.js index 0d957a6..99f39c4 100644 --- a/angular-drag-and-drop-lists.min.js +++ b/angular-drag-and-drop-lists.min.js @@ -7,43 +7,46 @@ * * License: MIT */ -!function(e){function n(e,n){return"all"==n?e:e.filter(function(e){return-1!=n.toLowerCase().indexOf(e)})}var a="application/x-dnd",r="application/json",t="Text",d=["move","copy","link"] -e.directive("dndDraggable",["$parse","$timeout",function(e,i){return function(l,f,c){f.attr("draggable","true"),c.dndDisableIf&&l.$watch(c.dndDisableIf,function(e){f.attr("draggable",!e)}),f.on("dragstart",function(s){if(s=s.originalEvent||s,"false"==f.attr("draggable"))return!0 -o.isDragging=!0,o.itemType=c.dndType&&l.$eval(c.dndType).toLowerCase(),o.dropEffect="none",o.effectAllowed=c.dndEffectAllowed||d[0],s.dataTransfer.effectAllowed=o.effectAllowed -var g=l.$eval(c.dndDraggable),u=a+(o.itemType?"-"+o.itemType:"") -try{s.dataTransfer.setData(u,angular.toJson(g))}catch(p){var v=angular.toJson({item:g,type:o.itemType}) -try{s.dataTransfer.setData(r,v)}catch(p){var D=n(d,o.effectAllowed) -s.dataTransfer.effectAllowed=D[0],s.dataTransfer.setData(t,v)}}if(f.addClass("dndDragging"),i(function(){f.addClass("dndDraggingSource")},0),s._dndHandle&&s.dataTransfer.setDragImage&&s.dataTransfer.setDragImage(f[0],0,0),e(c.dndDragstart)(l,{event:s}),c.dndCallback){var y=e(c.dndCallback) -o.callback=function(e){return y(l,e||{})}}s.stopPropagation()}),f.on("dragend",function(n){n=n.originalEvent||n,l.$apply(function(){var a=o.dropEffect,r={copy:"dndCopied",link:"dndLinked",move:"dndMoved",none:"dndCanceled"} -e(c[r[a]])(l,{event:n}),e(c.dndDragend)(l,{event:n,dropEffect:a})}),o.isDragging=!1,o.callback=void 0,f.removeClass("dndDragging"),f.removeClass("dndDraggingSource"),n.stopPropagation(),i(function(){f.removeClass("dndDraggingSource")},0)}),f.on("click",function(n){c.dndSelected&&(n=n.originalEvent||n,l.$apply(function(){e(c.dndSelected)(l,{event:n})}),n.stopPropagation())}),f.on("selectstart",function(){this.dragDrop&&this.dragDrop()})}}]),e.directive("dndList",["$parse",function(e){return function(i,l,f){function c(e){if(!e)return t -for(var n=0;n")}var T=y() +!function(n){function e(n,e){return"all"==e?n:n.filter(function(n){return e.toLowerCase().indexOf(n)!=-1})}var a="application/x-dnd",r="application/json",t="Text",d=["move","copy","link"] +n.directive("dndDraggable",["$parse","$timeout",function(n,l){return function(i,s,c){var f="dndDragging",g="dndDraggingSource" +c.dndClassForDragging&&(f=c.dndClassForDragging,i.$watch(c.dndClassForDragging,function(){f=c.dndClassForDragging})),c.dndClassForDraggingSource&&(g=c.dndClassForDraggingSource,i.$watch(c.dndClassForDraggingSource,function(){g=c.dndClassForDraggingSource})),s.attr("draggable","true"),c.dndDisableIf&&i.$watch(c.dndDisableIf,function(n){s.attr("draggable",!n)}),s.on("dragstart",function(u){if(u=u.originalEvent||u,"false"==s.attr("draggable"))return!0 +o.isDragging=!0,o.itemType=c.dndType&&i.$eval(c.dndType).toLowerCase(),o.dropEffect="none",o.effectAllowed=c.dndEffectAllowed||d[0],u.dataTransfer.effectAllowed=o.effectAllowed +var v=i.$eval(c.dndDraggable),p=a+(o.itemType?"-"+o.itemType:"") +try{u.dataTransfer.setData(p,angular.toJson(v))}catch(n){var D=angular.toJson({item:v,type:o.itemType}) +try{u.dataTransfer.setData(r,D)}catch(n){var y=e(d,o.effectAllowed) +u.dataTransfer.effectAllowed=y[0],u.dataTransfer.setData(t,D)}}if(s.addClass(f),l(function(){s.addClass(g)},0),u._dndHandle&&u.dataTransfer.setDragImage&&u.dataTransfer.setDragImage(s[0],0,0),n(c.dndDragstart)(i,{event:u}),c.dndCallback){var h=n(c.dndCallback) +o.callback=function(n){return h(i,n||{})}}u.stopPropagation()}),s.on("dragend",function(e){e=e.originalEvent||e,i.$apply(function(){var a=o.dropEffect,r={copy:"dndCopied",link:"dndLinked",move:"dndMoved",none:"dndCanceled"} +n(c[r[a]])(i,{event:e}),n(c.dndDragend)(i,{event:e,dropEffect:a})}),o.isDragging=!1,o.callback=void 0,s.removeClass(f),s.removeClass(g),e.stopPropagation(),l(function(){s.removeClass(g)},0)}),s.on("click",function(e){c.dndSelected&&(e=e.originalEvent||e,i.$apply(function(){n(c.dndSelected)(i,{event:e})}),e.stopPropagation())}),s.on("selectstart",function(){this.dragDrop&&this.dragDrop()})}}]),n.directive("dndList",["$parse",function(n){return function(l,i,s){function c(n){if(!n)return t +for(var e=0;e")}var h="dndPlaceholder",C="dndDragover" +s.dndClassForPlaceholder&&(h=s.dndClassForPlaceholder,l.$watch(s.dndClassForPlaceholder,function(){h=s.dndClassForPlaceholder})),s.dndClassForDragover&&(C=s.dndClassForDragover,l.$watch(s.dndClassForDragover,function(){C=s.dndClassForDragover})) +var T=y() T.remove() -var h=T[0],m=l[0],E={} -l.on("dragenter",function(e){e=e.originalEvent||e -var n=f.dndAllowedTypes&&i.$eval(f.dndAllowedTypes) -E={allowedTypes:angular.isArray(n)&&n.join("|").toLowerCase().split("|"),disabled:f.dndDisableIf&&i.$eval(f.dndDisableIf),externalSources:f.dndExternalSources&&i.$eval(f.dndExternalSources),horizontal:f.dndHorizontalList&&i.$eval(f.dndHorizontalList)} -var a=c(e.dataTransfer.types) -return a&&g(s(a))?void e.preventDefault():!0}),l.on("dragover",function(e){e=e.originalEvent||e -var n=c(e.dataTransfer.types),a=s(n) -if(!n||!g(a))return!0 -if(h.parentNode!=m&&l.append(T),e.target!=m){for(var r=e.target;r.parentNode!=m&&r.parentNode;)r=r.parentNode -if(r.parentNode==m&&r!=h){var d=r.getBoundingClientRect() -if(E.horizontal)var o=e.clientX'; + element = compileAndLink(htmlWithClass); + + Dragstart.on(element); + + expect(element.hasClass('dnd-dragging')).toBe(true); + expect(element.hasClass('dnd-dragging-source')).toBe(false); + + $timeout.flush(0); + expect(element.hasClass('dnd-dragging-source')).toBe(true); + })); + it('invokes dnd-dragstart callback', function() { element = compileAndLink('
    '); Dragstart.on(element); @@ -126,6 +140,22 @@ describe('dndDraggable', function() { expect(element.hasClass('dndDraggingSource')).toBe(false); })); + it('removes overridden CSS classes from element', inject(function($timeout) { + var htmlWithClass = '
    '; + element = compileAndLink(htmlWithClass); + dragstart = Dragstart.on(element); + + $timeout.flush(0); + + expect(element.hasClass('dnd-dragging')).toBe(true); + expect(element.hasClass('dnd-dragging-source')).toBe(true); + + dragstart.dragend(element); + expect(element.hasClass('dnd-dragging')).toBe(false); + expect(element.hasClass('dnd-dragging-source')).toBe(false); + })); + it('removes dndDraggingSource after a timeout', inject(function($timeout) { // IE 9 might not flush the $timeout before invoking the dragend handler. expect(element.hasClass('dndDragging')).toBe(true); diff --git a/test/dndListSpec.js b/test/dndListSpec.js index b77a31c..5ff9cd7 100644 --- a/test/dndListSpec.js +++ b/test/dndListSpec.js @@ -118,12 +118,26 @@ describe('dndList', function() { expect(element.hasClass('dndDragover')).toBe(true); }); + it('adds custom dndDragover CSS class', function() { + element = compileAndLink('
    '); + Dragstart.on(source).dragover(element); + expect(element.hasClass('dnd-dragover')).toBe(true); + }); + it('adds placeholder element', function() { Dragstart.on(source).dragover(element); expect(element.children().length).toBe(1); expect(element.children()[0].tagName).toBe('LI'); }); + it('adds custom dndPlaceholder CSS class', function() { + element = compileAndLink('
    '); + Dragstart.on(source).dragover(element); + + var placeholder = element.find('li'); + expect(placeholder.hasClass('dnd-placeholder')).toBe(true); + }); + it('reuses custom placeholder element if it exists', function() { element = compileAndLink(''); Dragstart.on(source).dragover(element);