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( + '', + '
'))($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 []; }