diff --git a/bower.json b/bower.json
index c95c8bb..864f19b 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "angular-ui-sortable",
- "version": "0.12.7",
+ "version": "0.12.8",
"description": "This directive allows you to jQueryUI Sortable.",
"author": "https://github.com/angular-ui/ui-sortable/graphs/contributors",
"license": "MIT",
diff --git a/package.json b/package.json
index 527926d..f5009f8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-ui-sortable",
- "version": "0.12.7",
+ "version": "0.12.8",
"description": "This directive allows you to jQueryUI Sortable.",
"author": "https://github.com/angular-ui/ui-sortable/graphs/contributors",
"license": "MIT",
diff --git a/src/sortable.js b/src/sortable.js
index 9f86dcd..d293eb8 100644
--- a/src/sortable.js
+++ b/src/sortable.js
@@ -23,9 +23,9 @@ angular.module('ui.sortable', [])
return first;
}
- function hasSortingHelper (element) {
+ function hasSortingHelper (element, ui) {
var helperOption = element.sortable('option','helper');
- return helperOption === 'clone' || typeof helperOption === 'function';
+ return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed());
}
var opts = {};
@@ -38,6 +38,10 @@ angular.module('ui.sortable', [])
update:null
};
+ var wrappers = {
+ helper: null
+ };
+
angular.extend(opts, uiSortableConfig, scope.$eval(attrs.uiSortable));
if (!angular.element.fn || !angular.element.fn.jquery) {
@@ -70,7 +74,11 @@ angular.module('ui.sortable', [])
isCanceled: function () {
return ui.item.sortable._isCanceled;
},
- _isCanceled: false
+ isCustomHelperUsed: function () {
+ return !!ui.item.sortable._isCustomHelperUsed;
+ },
+ _isCanceled: false,
+ _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed
};
};
@@ -125,7 +133,7 @@ angular.module('ui.sortable', [])
// the start and stop of repeat sections and sortable doesn't
// respect their order (even if we cancel, the order of the
// comments are still messed up).
- if (hasSortingHelper(element) && !ui.item.sortable.received) {
+ if (hasSortingHelper(element, ui) && !ui.item.sortable.received) {
// restore all the savedNodes except .ui-sortable-helper element
// (which is placed last). That way it will be garbage collected.
savedNodes = savedNodes.not(savedNodes.last());
@@ -161,7 +169,7 @@ angular.module('ui.sortable', [])
// if the item was not moved, then restore the elements
// so that the ngRepeat's comment are correct.
if ((!('dropindex' in ui.item.sortable) || ui.item.sortable.isCanceled()) &&
- !hasSortingHelper(element)) {
+ !hasSortingHelper(element, ui)) {
savedNodes.appendTo(element);
}
}
@@ -174,6 +182,14 @@ angular.module('ui.sortable', [])
};
callbacks.remove = function(e, ui) {
+ // Workaround for a problem observed in nested connected lists.
+ // There should be an 'update' event before 'remove' when moving
+ // elements. If the event did not fire, cancel sorting.
+ if (!('dropindex' in ui.item.sortable)) {
+ element.sortable('cancel');
+ ui.item.sortable.cancel();
+ }
+
// Remove the item from this list's model and copy data into item,
// so the next list can retrive it
if (!ui.item.sortable.isCanceled()) {
@@ -184,6 +200,17 @@ angular.module('ui.sortable', [])
}
};
+ wrappers.helper = function (inner) {
+ if (inner && typeof inner === 'function') {
+ return function (e, item) {
+ var innerResult = inner(e, item);
+ item.sortable._isCustomHelperUsed = item !== innerResult;
+ return innerResult;
+ };
+ }
+ return inner;
+ };
+
scope.$watch(attrs.uiSortable, function(newVal /*, oldVal*/) {
// ensure that the jquery-ui-sortable widget instance
// is still bound to the directive's element
@@ -197,6 +224,8 @@ angular.module('ui.sortable', [])
}
// wrap the callback
value = combineCallbacks(callbacks[key], value);
+ } else if (wrappers[key]) {
+ value = wrappers[key](value);
}
element.sortable('option', key, value);
diff --git a/test/sortable.e2e.callbacks.spec.js b/test/sortable.e2e.callbacks.spec.js
index 491ec5a..38b2268 100644
--- a/test/sortable.e2e.callbacks.spec.js
+++ b/test/sortable.e2e.callbacks.spec.js
@@ -66,6 +66,45 @@ describe('uiSortable', function() {
});
});
+ it('should cancel sorting of node "Two" and "helper: function" that returns a list element is used', function() {
+ inject(function($compile, $rootScope) {
+ var element;
+ element = $compile('
')($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.opts = {
+ update: function(e, ui) {
+ if (ui.item.scope().item === 'Two') {
+ ui.item.sortable.cancel();
+ }
+ }
+ };
+ $rootScope.items = ['One', 'Two', 'Three'];
+ });
+
+ host.append(element);
+
+ var li = element.find(':eq(1)');
+ var dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['One', 'Two', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(0)');
+ dy = (2 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(2)');
+ dy = -(2 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['One', 'Two', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ $(element).remove();
+ });
+ });
+
it('should cancel sorting of nodes that contain "Two"', function() {
inject(function($compile, $rootScope) {
var elementTop, elementBottom;
diff --git a/test/sortable.e2e.multi.spec.js b/test/sortable.e2e.multi.spec.js
index 3a16df7..e9c827a 100644
--- a/test/sortable.e2e.multi.spec.js
+++ b/test/sortable.e2e.multi.spec.js
@@ -6,11 +6,12 @@ describe('uiSortable', function() {
beforeEach(module('ui.sortable'));
beforeEach(module('ui.sortable.testHelper'));
- var EXTRA_DY_PERCENTAGE, listContent;
+ var EXTRA_DY_PERCENTAGE, listContent, listInnerContent;
beforeEach(inject(function (sortableTestHelper) {
EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE;
listContent = sortableTestHelper.listContent;
+ listInnerContent = sortableTestHelper.listInnerContent;
}));
describe('Multiple sortables related', function() {
@@ -346,6 +347,193 @@ describe('uiSortable', function() {
});
});
+ it('should work when "helper: function" that returns a list element is used', function() {
+ inject(function($compile, $rootScope) {
+ var elementTop, elementBottom;
+ elementTop = $compile('')($rootScope);
+ elementBottom = $compile('')($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.itemsTop = ['Top One', 'Top Two', 'Top Three'];
+ $rootScope.itemsBottom = ['Bottom One', 'Bottom Two', 'Bottom Three'];
+ $rootScope.opts = {
+ helper: function (e, item) {
+ return item;
+ },
+ connectWith: '.cross-sortable'
+ };
+ });
+
+ host.append(elementTop).append(elementBottom);
+
+ var li1 = elementTop.find(':eq(0)');
+ var li2 = elementBottom.find(':eq(0)');
+ var dy = EXTRA_DY_PERCENTAGE * li1.outerHeight() + (li2.position().top - li1.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']);
+ expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Top One', 'Bottom Two', 'Bottom Three']);
+ expect($rootScope.itemsTop).toEqual(listContent(elementTop));
+ expect($rootScope.itemsBottom).toEqual(listContent(elementBottom));
+
+ li1 = elementBottom.find(':eq(1)');
+ li2 = elementTop.find(':eq(1)');
+ dy = -EXTRA_DY_PERCENTAGE * li1.outerHeight() - (li1.position().top - li2.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.itemsTop).toEqual(['Top Two', 'Top One', 'Top Three']);
+ expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']);
+ expect($rootScope.itemsTop).toEqual(listContent(elementTop));
+ expect($rootScope.itemsBottom).toEqual(listContent(elementBottom));
+
+ $(elementTop).remove();
+ $(elementBottom).remove();
+ });
+ });
+
+ it('should work when "placeholder" and "helper: function" that returns a list element are used', function() {
+ inject(function($compile, $rootScope) {
+ var elementTop, elementBottom;
+ elementTop = $compile('')($rootScope);
+ elementBottom = $compile('')($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.itemsTop = ['Top One', 'Top Two', 'Top Three'];
+ $rootScope.itemsBottom = ['Bottom One', 'Bottom Two', 'Bottom Three'];
+ $rootScope.opts = {
+ helper: function (e, item) {
+ return item;
+ },
+ placeholder: 'sortable-item-placeholder',
+ connectWith: '.cross-sortable'
+ };
+ });
+
+ host.append(elementTop).append(elementBottom);
+
+ var li1 = elementTop.find(':eq(0)');
+ var li2 = elementBottom.find(':eq(0)');
+ var dy = EXTRA_DY_PERCENTAGE * li1.outerHeight() + (li2.position().top - li1.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']);
+ expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Top One', 'Bottom Two', 'Bottom Three']);
+ expect($rootScope.itemsTop).toEqual(listContent(elementTop));
+ expect($rootScope.itemsBottom).toEqual(listContent(elementBottom));
+
+ li1 = elementBottom.find(':eq(1)');
+ li2 = elementTop.find(':eq(1)');
+ dy = -EXTRA_DY_PERCENTAGE * li1.outerHeight() - (li1.position().top - li2.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.itemsTop).toEqual(['Top Two', 'Top One', 'Top Three']);
+ expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']);
+ expect($rootScope.itemsTop).toEqual(listContent(elementTop));
+ expect($rootScope.itemsBottom).toEqual(listContent(elementBottom));
+
+ $(elementTop).remove();
+ $(elementBottom).remove();
+ });
+ });
+
+ it('should update model when sorting between nested sortables', function() {
+ inject(function($compile, $rootScope) {
+ var elementTree, li1, li2, dy;
+
+ elementTree = $compile(''.concat(
+ '',
+ '- ',
+ '
',
+ '
{{item.text}}',
+ '
',
+ '- ',
+ '{{i.text}}',
+ '
',
+ '
',
+ '
',
+ ' ',
+ '
',
+ ''))($rootScope);
+
+ $rootScope.$apply(function() {
+ $rootScope.items = [
+ {
+ text: 'Item 1',
+ items: []
+ },
+ {
+ text: 'Item 2',
+ items: [
+ { text: 'Item 2.1', items: [] },
+ { text: 'Item 2.2', items: [] }
+ ]
+ }
+ ];
+
+ $rootScope.sortableOptions = {
+ connectWith: '.apps-container'
+ };
+ });
+
+ host.append(elementTree);
+
+ // this should drag the item out of the list and
+ // the item should return back to its original position
+ li1 = elementTree.find('.innerList:last').find(':last');
+ li1.simulate('drag', { dx: -200, moves: 30 });
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(['Item 1', 'Item 2']);
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree, '.lvl1ItemContent'));
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual([]);
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(0)'), '.lvl2ItemContent'));
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual(['Item 2.1', 'Item 2.2']);
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(1)'), '.lvl2ItemContent'));
+
+ // this should drag the item from the inner list and
+ // drop it to the outter list
+ li1 = elementTree.find('.innerList:last').find(':last');
+ li2 = elementTree.find('> li:last');
+ dy = EXTRA_DY_PERCENTAGE * li1.outerHeight() + (li2.position().top - li1.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(['Item 1', 'Item 2.2', 'Item 2']);
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree, '.lvl1ItemContent'));
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual([]);
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(0)'), '.lvl2ItemContent'));
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual([]);
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(1)'), '.lvl2ItemContent'));
+ expect($rootScope.items[2].items.map(function(x){ return x.text; }))
+ .toEqual(['Item 2.1']);
+ expect($rootScope.items[2].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(2)'), '.lvl2ItemContent'));
+
+ // this should drag the item from the outter list and
+ // drop it to the inner list
+ li1 = elementTree.find('> li:first');
+ li2 = elementTree.find('.innerList:last').find(':last');
+ dy = -EXTRA_DY_PERCENTAGE * li1.outerHeight() + (li2.position().top - li1.position().top);
+ li1.simulate('drag', { dy: dy });
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(['Item 2.2', 'Item 2']);
+ expect($rootScope.items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree, '.lvl1ItemContent'));
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual([]);
+ expect($rootScope.items[0].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(0)'), '.lvl2ItemContent'));
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual(['Item 1', 'Item 2.1']);
+ expect($rootScope.items[1].items.map(function(x){ return x.text; }))
+ .toEqual(listInnerContent(elementTree.find('.innerList:eq(1)'), '.lvl2ItemContent'));
+
+ $(elementTree).remove();
+ });
+ });
+
});
});
\ No newline at end of file
diff --git a/test/sortable.e2e.spec.js b/test/sortable.e2e.spec.js
index 77c463a..dc2cdaf 100644
--- a/test/sortable.e2e.spec.js
+++ b/test/sortable.e2e.spec.js
@@ -553,6 +553,99 @@ describe('uiSortable', function() {
});
});
+ it('should work when "helper: function" that returns a list element is used', function() {
+ inject(function($compile, $rootScope) {
+ var element;
+ element = $compile('')($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.opts = {
+ helper: function (e, item) {
+ return item;
+ }
+ };
+ $rootScope.items = ['One', 'Two', 'Three'];
+ });
+
+ host.append(element);
+
+ var li = element.find(':eq(0)');
+ var dy = (2 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('dragAndRevert', { dy: dy });
+ expect($rootScope.items).toEqual(['One', 'Two', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(0)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(1)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(1)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ $(element).remove();
+ });
+ });
+
+ it('should work when "helper: function" that returns a list element and "placeholder" options are used together.', function() {
+ inject(function($compile, $rootScope) {
+ var element;
+ element = $compile('')($rootScope);
+ $rootScope.$apply(function() {
+ $rootScope.opts = {
+ helper: function (e, item) {
+ return item;
+ },
+ placeholder: 'sortable-item'
+ };
+ $rootScope.items = ['One', 'Two', 'Three'];
+ });
+
+ host.append(element);
+
+ var li = element.find(':eq(0)');
+ var dy = (2 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('dragAndRevert', { dy: dy });
+ expect($rootScope.items).toEqual(['One', 'Two', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(0)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(1)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(1)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('dragAndRevert', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ li = element.find(':eq(1)');
+ dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight();
+ li.simulate('drag', { dy: dy });
+ expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
+ expect($rootScope.items).toEqual(listContent(element));
+
+ $(element).remove();
+ });
+ });
+
});
});
\ No newline at end of file
diff --git a/test/sortable.test-helper.js b/test/sortable.test-helper.js
index d0c298f..11141d1 100644
--- a/test/sortable.test-helper.js
+++ b/test/sortable.test-helper.js
@@ -11,9 +11,13 @@ angular.module('ui.sortable.testHelper', [])
return [];
}
- function listInnerContent (list) {
+ function listInnerContent (list, contentSelector) {
+ if (!contentSelector) {
+ contentSelector = '.itemContent';
+ }
+
if (list && list.length) {
- return list.children().map(function(){ return $(this).find('.itemContent').html(); }).toArray();
+ return list.children().map(function(){ return $(this).find(contentSelector).html(); }).toArray();
}
return [];
}