Skip to content
This repository was archived by the owner on Mar 10, 2024. It is now read-only.

Feature/prevent ui flicker on dragstart #379

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions angular-drag-and-drop-lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
// Check whether the element is draggable, since dragstart might be triggered on a child.
if (element.attr('draggable') == 'false') return true;

var parentIsList = element.parent().length && element.parent()[0].hasAttribute('dnd-list');

// Initialize global state.
dndState.isDragging = true;
dndState.itemType = attr.dndType && scope.$eval(attr.dndType).toLowerCase();
Expand Down Expand Up @@ -129,15 +131,28 @@

// Add CSS classes. See documentation above.
element.addClass("dndDragging");
$timeout(function() { element.addClass("dndDraggingSource"); }, 0);

// We'll set this now and add the class 'dndDraggingSource' to it when we start moving the item.
// This needs to be in sync with the process of insert the placeholder into the list

if(parentIsList){
dndState.currentDragItem = element;
}else{
$timeout(function() { element.addClass("dndDraggingSource"); }, 0);
}

// Try setting a proper drag image if triggered on a dnd-handle (won't work in IE).
if (event._dndHandle && event.dataTransfer.setDragImage) {
event.dataTransfer.setDragImage(element[0], 0, 0);
}

// Invoke dragstart callback and prepare extra callback for dropzone.
// Invoke dragstart callback
$parse(attr.dndDragstart)(scope, {event: event});

// we need to schedule a digest cycle to make sure other bits like ngHide fire
scope.$evalAsync();

// Prepare extra callback for dropzone
if (attr.dndCallback) {
var callback = $parse(attr.dndCallback);
dndState.callback = function(params) { return callback(scope, params || {}); };
Expand Down Expand Up @@ -168,8 +183,10 @@
// Clean up
dndState.isDragging = false;
dndState.callback = undefined;
dndState.currentDragItem = undefined;
element.removeClass("dndDragging");
element.removeClass("dndDraggingSource");

event.stopPropagation();

// In IE9 it is possible that the timeout from dragstart triggers after the dragend handler.
Expand Down Expand Up @@ -318,6 +335,14 @@
// Make sure the placeholder is shown, which is especially important if the list is empty.
if (placeholderNode.parentNode != listNode) {
element.append(placeholder);

// We set the class here instead of in the dragstart, because if this class
// hides the original item, we want to make sure we do that at the same time
// that we put the placeholder on the dom. This prevents some UI flashing
if(dndState.currentDragItem){
dndState.currentDragItem.addClass("dndDraggingSource");
}

}

if (event.target != listNode) {
Expand Down Expand Up @@ -644,6 +669,8 @@
* - isDragging: True between dragstart and dragend. Falsy for drops from external sources.
* - itemType: The item type of the dragged element set via dnd-type. This is needed because IE
* and Edge don't support custom mime types that we can use to transfer this information.
* - currentDragItem: A reference to the current drag item. We set this on dragStart and then
* reference it when moving.
*/
var dndState = {};

Expand Down
73 changes: 37 additions & 36 deletions angular-drag-and-drop-lists.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,44 @@
*
* 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<e.length;n++)if(e[n]==t||e[n]==r||e[n].substr(0,a.length)==a)return e[n]
return null}function s(e){return o.isDragging?o.itemType||void 0:e==t||e==r?null:e&&e.substr(a.length+1)||void 0}function g(e){return E.disabled?!1:E.externalSources||o.isDragging?E.allowedTypes&&null!==e?e&&-1!=E.allowedTypes.indexOf(e):!0:!1}function u(e,a){var r=d
return a||(r=n(r,e.dataTransfer.effectAllowed)),o.isDragging&&(r=n(r,o.effectAllowed)),f.dndEffectAllowed&&(r=n(r,f.dndEffectAllowed)),r.length?e.ctrlKey&&-1!=r.indexOf("copy")?"copy":e.altKey&&-1!=r.indexOf("link")?"link":r[0]:"none"}function p(){return T.remove(),l.removeClass("dndDragover"),!0}function v(n,a,r,t,d,l){return e(n)(i,{callback:o.callback,dropEffect:r,event:a,external:!o.isDragging,index:void 0!==d?d:D(),item:l||void 0,type:t})}function D(){return Array.prototype.indexOf.call(m.children,h)}function y(){var e
return angular.forEach(l.children(),function(n){var a=angular.element(n)
a.hasClass("dndPlaceholder")&&(e=a)}),e||angular.element("<li class='dndPlaceholder'></li>")}var T=y()
T.remove()
var h=T[0],m=l[0],E={}
!function(e){function n(e,n){return"all"==n?e:e.filter(function(e){return n.toLowerCase().indexOf(e)!=-1})}var r="application/x-dnd",a="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(g){if(g=g.originalEvent||g,"false"==f.attr("draggable"))return!0
var s=f.parent().length&&f.parent()[0].hasAttribute("dnd-list")
o.isDragging=!0,o.itemType=c.dndType&&l.$eval(c.dndType).toLowerCase(),o.dropEffect="none",o.effectAllowed=c.dndEffectAllowed||d[0],g.dataTransfer.effectAllowed=o.effectAllowed
var u=l.$eval(c.dndDraggable),p=r+(o.itemType?"-"+o.itemType:"")
try{g.dataTransfer.setData(p,angular.toJson(u))}catch(e){var v=angular.toJson({item:u,type:o.itemType})
try{g.dataTransfer.setData(a,v)}catch(e){var D=n(d,o.effectAllowed)
g.dataTransfer.effectAllowed=D[0],g.dataTransfer.setData(t,v)}}if(f.addClass("dndDragging"),s?o.currentDragItem=f:i(function(){f.addClass("dndDraggingSource")},0),g._dndHandle&&g.dataTransfer.setDragImage&&g.dataTransfer.setDragImage(f[0],0,0),e(c.dndDragstart)(l,{event:g}),l.$evalAsync(),c.dndCallback){var y=e(c.dndCallback)
o.callback=function(e){return y(l,e||{})}}g.stopPropagation()}),f.on("dragend",function(n){n=n.originalEvent||n,l.$apply(function(){var r=o.dropEffect,a={copy:"dndCopied",link:"dndLinked",move:"dndMoved",none:"dndCanceled"}
e(c[a[r]])(l,{event:n}),e(c.dndDragend)(l,{event:n,dropEffect:r})}),o.isDragging=!1,o.callback=void 0,o.currentDragItem=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<e.length;n++)if(e[n]==t||e[n]==a||e[n].substr(0,r.length)==r)return e[n]
return null}function g(e){return o.isDragging?o.itemType||void 0:e==t||e==a?null:e&&e.substr(r.length+1)||void 0}function s(e){return!b.disabled&&(!(!b.externalSources&&!o.isDragging)&&(!b.allowedTypes||null===e||e&&b.allowedTypes.indexOf(e)!=-1))}function u(e,r){var a=d
return r||(a=n(a,e.dataTransfer.effectAllowed)),o.isDragging&&(a=n(a,o.effectAllowed)),f.dndEffectAllowed&&(a=n(a,f.dndEffectAllowed)),a.length?e.ctrlKey&&a.indexOf("copy")!=-1?"copy":e.altKey&&a.indexOf("link")!=-1?"link":a[0]:"none"}function p(){return m.remove(),l.removeClass("dndDragover"),!0}function v(n,r,a,t,d,l){return e(n)(i,{callback:o.callback,dropEffect:a,event:r,external:!o.isDragging,index:void 0!==d?d:D(),item:l||void 0,type:t})}function D(){return Array.prototype.indexOf.call(h.children,T)}function y(){var e
return angular.forEach(l.children(),function(n){var r=angular.element(n)
r.hasClass("dndPlaceholder")&&(e=r)}),e||angular.element("<li class='dndPlaceholder'></li>")}var m=y()
m.remove()
var T=m[0],h=l[0],b={}
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<d.left+d.width/2
else var o=e.clientY<d.top+d.height/2
m.insertBefore(h,o?r:r.nextSibling)}}var i=n==t,D=u(e,i)
return"none"==D?p():f.dndDragover&&!v(f.dndDragover,e,D,a)?p():(e.preventDefault(),i||(e.dataTransfer.dropEffect=D),l.addClass("dndDragover"),e.stopPropagation(),!1)}),l.on("drop",function(e){e=e.originalEvent||e
var n=c(e.dataTransfer.types),a=s(n)
if(!n||!g(a))return!0
b={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 r=c(e.dataTransfer.types)
return!r||!s(g(r))||void e.preventDefault()}),l.on("dragover",function(e){e=e.originalEvent||e
var n=c(e.dataTransfer.types),r=g(n)
if(!n||!s(r))return!0
if(T.parentNode!=h&&(l.append(m),o.currentDragItem&&o.currentDragItem.addClass("dndDraggingSource")),e.target!=h){for(var a=e.target;a.parentNode!=h&&a.parentNode;)a=a.parentNode
if(a.parentNode==h&&a!=T){var d=a.getBoundingClientRect()
if(b.horizontal)var i=e.clientX<d.left+d.width/2
else var i=e.clientY<d.top+d.height/2
h.insertBefore(T,i?a:a.nextSibling)}}var D=n==t,y=u(e,D)
return"none"==y?p():f.dndDragover&&!v(f.dndDragover,e,y,r)?p():(e.preventDefault(),D||(e.dataTransfer.dropEffect=y),l.addClass("dndDragover"),e.stopPropagation(),!1)}),l.on("drop",function(e){e=e.originalEvent||e
var n=c(e.dataTransfer.types),r=g(n)
if(!n||!s(r))return!0
e.preventDefault()
try{var d=JSON.parse(e.dataTransfer.getData(n))}catch(l){return p()}if((n==t||n==r)&&(a=d.type||void 0,d=d.item,!g(a)))return p()
var y=n==t,T=u(e,y)
if("none"==T)return p()
var h=D()
return f.dndDrop&&(d=v(f.dndDrop,e,T,a,h,d),!d)?p():(o.dropEffect=T,y||(e.dataTransfer.dropEffect=T),d!==!0&&i.$apply(function(){i.$eval(f.dndList).splice(h,0,d)}),v(f.dndInserted,e,T,a,h,d),p(),e.stopPropagation(),!1)}),l.on("dragleave",function(e){e=e.originalEvent||e
try{var d=JSON.parse(e.dataTransfer.getData(n))}catch(e){return p()}if((n==t||n==a)&&(r=d.type||void 0,d=d.item,!s(r)))return p()
var l=n==t,y=u(e,l)
if("none"==y)return p()
var m=D()
return f.dndDrop&&(d=v(f.dndDrop,e,y,r,m,d),!d)?p():(o.dropEffect=y,l||(e.dataTransfer.dropEffect=y),d!==!0&&i.$apply(function(){i.$eval(f.dndList).splice(m,0,d)}),v(f.dndInserted,e,y,r,m,d),p(),e.stopPropagation(),!1)}),l.on("dragleave",function(e){e=e.originalEvent||e
var n=document.elementFromPoint(e.clientX,e.clientY)
m.contains(n)&&!e._dndPhShown?e._dndPhShown=!0:p()})}}]),e.directive("dndNodrag",function(){return function(e,n,a){n.attr("draggable","true"),n.on("dragstart",function(e){e=e.originalEvent||e,e._dndHandle||(e.dataTransfer.types&&e.dataTransfer.types.length||e.preventDefault(),e.stopPropagation())}),n.on("dragend",function(e){e=e.originalEvent||e,e._dndHandle||e.stopPropagation()})}}),e.directive("dndHandle",function(){return function(e,n,a){n.attr("draggable","true"),n.on("dragstart dragend",function(e){e=e.originalEvent||e,e._dndHandle=!0})}})
var o={}}(angular.module("dndLists",[]));
h.contains(n)&&!e._dndPhShown?e._dndPhShown=!0:p()})}}]),e.directive("dndNodrag",function(){return function(e,n,r){n.attr("draggable","true"),n.on("dragstart",function(e){e=e.originalEvent||e,e._dndHandle||(e.dataTransfer.types&&e.dataTransfer.types.length||e.preventDefault(),e.stopPropagation())}),n.on("dragend",function(e){e=e.originalEvent||e,e._dndHandle||e.stopPropagation()})}}),e.directive("dndHandle",function(){return function(e,n,r){n.attr("draggable","true"),n.on("dragstart dragend",function(e){e=e.originalEvent||e,e._dndHandle=!0})}})
var o={}}(angular.module("dndLists",[]))
23 changes: 22 additions & 1 deletion test/dndDraggableSpec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
describe('dndDraggable', function() {

var SIMPLE_HTML = '<div dnd-draggable="{hello: \'world\'}"></div>';
var DRAGGABLE_INSIDE_LIST_HTML = '<div dnd-list="[]"><div dnd-draggable="{hello: \'world\'}"></div></div>';

describe('constructor', function() {
it('sets the draggable attribute', function() {
Expand Down Expand Up @@ -75,9 +76,27 @@ describe('dndDraggable', function() {
expect(Dragstart.on(element, {allowedMimeTypes: ['Text']}).effectAllowed).toBe('copy');
});

it('adds CSS classes to element', inject(function($timeout) {
it('adds dndDragging class to element', inject(function($timeout) {
Dragstart.on(element);
expect(element.hasClass('dndDragging')).toBe(true);
}));

it('adds dndDraggingSource class to element only after subsequent dragover if item is in a list', inject(function($timeout) {
var listWithDragItem = compileAndLink(DRAGGABLE_INSIDE_LIST_HTML);
element = listWithDragItem.find('[dnd-draggable]');

var dragstart = Dragstart.on(element);
expect(element.hasClass('dndDraggingSource')).toBe(false);

$timeout.flush(0);
expect(element.hasClass('dndDraggingSource')).toBe(false);

dragstart.dragover(listWithDragItem);
expect(element.hasClass('dndDraggingSource')).toBe(true);
}));

it('adds CSS classes to element immediately if item is not in a list', inject(function($timeout) {
Dragstart.on(element);
expect(element.hasClass('dndDraggingSource')).toBe(false);

$timeout.flush(0);
Expand Down Expand Up @@ -109,6 +128,7 @@ describe('dndDraggable', function() {

beforeEach(function() {
element = compileAndLink(SIMPLE_HTML);
target = compileAndLink('<div dnd-list="[]"></div>');
dragstart = Dragstart.on(element);
});

Expand All @@ -118,6 +138,7 @@ describe('dndDraggable', function() {

it('removes CSS classes from element', inject(function($timeout) {
$timeout.flush(0);
dragstart.dragover(target);
expect(element.hasClass('dndDragging')).toBe(true);
expect(element.hasClass('dndDraggingSource')).toBe(true);

Expand Down