diff --git a/.travis.yml b/.travis.yml index c6d6631..7900299 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js node_js: - '0.10' +addons: + firefox: "latest" before_install: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start diff --git a/README.md b/README.md index cf060eb..f20543e 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,10 @@ update: function(event, ui) { **Single sortable** [demo](http://codepen.io/thgreasi/pen/QKmWGj) ``` +create + +/* dragging starts */ +helper start activate @@ -174,6 +178,11 @@ stop **Connected sortables** [demo](http://codepen.io/thgreasi/pen/YGazpJ) ``` +list A: create +list B: create + +/* dragging starts from sortable A to B */ +list A: helper list A: start list B: activate list A: activate @@ -199,6 +208,30 @@ list A: stop For more details about the events check the [jQueryUI API documentation](http://api.jqueryui.com/sortable/). +## Integrating with directives doing transclusion +Wrap the transclusion directive element with the ui-sortable directive and set the `items` to target your `ng-repeat`ed elements. Following best practices, it is also highly recommended that you add a `track by` expression to your `ng-repeat`. [Angular Meterial example](http://codepen.io/thgreasi/pen/NbyLVK). + +```js +myAppModule.controller('MyController', function($scope) { + $scope.items = ["One", "Two", "Three"]; + + $scope.sortableOptions = { + items: '.sortable-item' + // It is suggested to use the most specific cssselector you can, + // after analyzing the DOM elements generated by the transclusion directive + // eg: items: '> .transclusionLvl1 > .transclusionLvl2 > .sortable-item' + }; +}); +``` + +```html +
+ +
{{ item }}
+
+
+``` + ## Examples - [Simple Demo](http://codepen.io/thgreasi/pen/wzmvgw) @@ -224,6 +257,7 @@ For more details about the events check the [jQueryUI API documentation](http:// ## Integrations - [firebase](http://codepen.io/thgreasi/pen/repEZg?editors=0010) - [ui.bootstrap.accordion](http://plnkr.co/edit/TGIeeEbbvJwpJ3WRqo2z?p=preview) +- [Angular Meterial](http://codepen.io/thgreasi/pen/NbyLVK) (thanks yenoh2) - [Asynchronous loading jQuery+jQueryUI with crisbeto/angular-ui-sortable-loader](https://github.com/crisbeto/angular-ui-sortable-loader) ## Reporting Issues diff --git a/bower.json b/bower.json index c3b158d..494249a 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-sortable", - "version": "0.15.1", + "version": "0.16.0", "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 a52ae4c..d0df1cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-ui-sortable", - "version": "0.15.1", + "version": "0.16.0", "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 bfed173..c22c1bd 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -97,7 +97,7 @@ angular.module('ui.sortable', []) } return; } - + if (!defaultOptions) { defaultOptions = angular.element.ui.sortable().options; } @@ -186,16 +186,13 @@ angular.module('ui.sortable', []) return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); } - function getElementScope(elementScopes, element) { - var result = null; + function getElementContext(elementScopes, element) { for (var i = 0; i < elementScopes.length; i++) { - var x = elementScopes[i]; - if (x.element[0] === element[0]) { - result = x.scope; - break; + var c = elementScopes[i]; + if (c.element[0] === element[0]) { + return c; } } - return result; } function afterStop(e, ui) { @@ -266,7 +263,8 @@ angular.module('ui.sortable', []) ui.item.sortable = { model: ngModel.$modelValue[index], index: index, - source: ui.item.parent(), + source: element, + sourceList: ui.item.parent(), sourceModel: ngModel.$modelValue, cancel: function () { ui.item.sortable._isCanceled = true; @@ -283,16 +281,33 @@ angular.module('ui.sortable', []) angular.forEach(ui.item.sortable, function(value, key) { ui.item.sortable[key] = undefined; }); + }, + _connectedSortables: [], + _getElementContext: function (element) { + return getElementContext(this._connectedSortables, element); } }; }; callbacks.activate = function(e, ui) { + var isSourceContext = ui.item.sortable.source === element; + var savedNodesOrigin = isSourceContext ? + ui.item.sortable.sourceList : + element; + var elementContext = { + element: element, + scope: scope, + isSourceContext: isSourceContext, + savedNodesOrigin: savedNodesOrigin + }; + // save the directive's scope so that it is accessible from ui.item.sortable + ui.item.sortable._connectedSortables.push(elementContext); + // We need to make a copy of the current element's contents so // we can restore it after sortable has messed it up. // This is inside activate (instead of start) in order to save // both lists when dragging between connected lists. - savedNodes = element.contents(); + savedNodes = savedNodesOrigin.contents(); // If this list has a placeholder (the connected lists won't), // don't inlcude it in saved nodes. @@ -301,16 +316,6 @@ angular.module('ui.sortable', []) var excludes = getPlaceholderExcludesludes(element, placeholder); savedNodes = savedNodes.not(excludes); } - - // save the directive's scope so that it is accessible from ui.item.sortable - var connectedSortables = ui.item.sortable._connectedSortables || []; - - connectedSortables.push({ - element: element, - scope: scope - }); - - ui.item.sortable._connectedSortables = connectedSortables; }; callbacks.update = function(e, ui) { @@ -319,11 +324,12 @@ angular.module('ui.sortable', []) // the value will be overwritten with the old value if(!ui.item.sortable.received) { ui.item.sortable.dropindex = getItemIndex(ui.item); - var droptarget = ui.item.parent(); + var droptarget = ui.item.closest('[ui-sortable]'); ui.item.sortable.droptarget = droptarget; + ui.item.sortable.droptargetList = ui.item.parent(); - var droptargetScope = getElementScope(ui.item.sortable._connectedSortables, droptarget); - ui.item.sortable.droptargetModel = droptargetScope.ngModel; + var droptargetContext = ui.item.sortable._getElementContext(droptarget); + ui.item.sortable.droptargetModel = droptargetContext.scope.ngModel; // Cancel the sort (let ng-repeat do the sort for us) // Don't cancel if this is the received list because it has @@ -343,7 +349,8 @@ angular.module('ui.sortable', []) // That way it will be garbage collected. savedNodes = savedNodes.not(sortingHelper); } - savedNodes.appendTo(element); + var elementContext = ui.item.sortable._getElementContext(element); + savedNodes.appendTo(elementContext.savedNodesOrigin); // If this is the target connected list then // it's safe to clear the restored nodes since: @@ -392,7 +399,8 @@ angular.module('ui.sortable', []) // That way it will be garbage collected. savedNodes = savedNodes.not(sortingHelper); } - savedNodes.appendTo(element); + var elementContext = ui.item.sortable._getElementContext(element); + savedNodes.appendTo(elementContext.savedNodesOrigin); } // It's now safe to clear the savedNodes @@ -433,7 +441,8 @@ angular.module('ui.sortable', []) item.sortable = { model: ngModel.$modelValue[index], index: index, - source: item.parent(), + source: element, + sourceList: item.parent(), sourceModel: ngModel.$modelValue, _restore: function () { angular.forEach(item.sortable, function(value, key) { @@ -459,7 +468,7 @@ angular.module('ui.sortable', []) var sortableWidgetInstance = getSortableWidgetInstance(element); if (!!sortableWidgetInstance) { var optsDiff = patchUISortableOptions(newVal, oldVal, sortableWidgetInstance); - + if (optsDiff) { element.sortable('option', optsDiff); } @@ -475,7 +484,7 @@ angular.module('ui.sortable', []) } else { $log.info('ui.sortable: ngModel not provided!', element); } - + // Create sortable element.sortable(opts); } diff --git a/test/sortable.e2e.directives.spec.js b/test/sortable.e2e.directives.spec.js index 23375c4..193af85 100644 --- a/test/sortable.e2e.directives.spec.js +++ b/test/sortable.e2e.directives.spec.js @@ -13,14 +13,18 @@ describe('uiSortable', function() { beforeEach(module('ui.sortable.testHelper')); beforeEach(module('ui.sortable.testDirectives')); - var EXTRA_DY_PERCENTAGE, listContent, listInnerContent, beforeLiElement, afterLiElement; + var EXTRA_DY_PERCENTAGE, listContent, listFindContent, listInnerContent, simulateElementDrag, beforeLiElement, afterLiElement, beforeDivElement, afterDivElement; beforeEach(inject(function (sortableTestHelper) { EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE; listContent = sortableTestHelper.listContent; + listFindContent = sortableTestHelper.listFindContent; listInnerContent = sortableTestHelper.listInnerContent; + simulateElementDrag = sortableTestHelper.simulateElementDrag; beforeLiElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.beforeLiElement; afterLiElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.afterLiElement; + beforeDivElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.beforeDivElement; + afterDivElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.afterDivElement; })); tests.description = 'Inner directives related'; @@ -34,6 +38,7 @@ describe('uiSortable', function() { if (!useExtraElements) { beforeLiElement = afterLiElement = ''; + beforeDivElement = afterDivElement = ''; } })); @@ -112,6 +117,243 @@ describe('uiSortable', function() { }); }); + it('should work when the items are inside a transcluded directive', function() { + inject(function($compile, $rootScope) { + var element; + element = $compile(''.concat( + '
', + '', + beforeLiElement, + '
', + '{{ item }}', + '
', + afterLiElement, + '', + '
'))($rootScope); + + $rootScope.$apply(function() { + $rootScope.opts = { + items: '> * .sortable-item' + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element); + + var li = element.find('.sortable-item:eq(1)'); + var dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Three', 'Two']); + expect($rootScope.items).toEqual(listFindContent(element)); + + li = element.find('.sortable-item:eq(1)'); + dy = -(1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['Three', 'One', 'Two']); + expect($rootScope.items).toEqual(listFindContent(element)); + + $(element).remove(); + }); + }); + + it('should properly cancel() when the items are inside a transcluded directive', function() { + inject(function($compile, $rootScope) { + var element; + element = $compile(''.concat( + '
', + '', + beforeLiElement, + '
', + '{{ item }}', + '
', + afterLiElement, + '', + '
'))($rootScope); + + $rootScope.$apply(function() { + $rootScope.opts = { + items: '> * .sortable-item', + update: function(e, ui) { + if (ui.item.sortable.model === 'Two') { + ui.item.sortable.cancel(); + } + } + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element); + + var li = element.find('.sortable-item: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(listFindContent(element)); + // try again + li = element.find('.sortable-item:eq(1)'); + dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Two', 'Three']); + expect($rootScope.items).toEqual(listFindContent(element)); + // try again + li = element.find('.sortable-item:eq(1)'); + dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Two', 'Three']); + expect($rootScope.items).toEqual(listFindContent(element)); + + li = element.find('.sortable-item: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(listFindContent(element)); + + li = element.find('.sortable-item:eq(2)'); + dy = -(1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['Two', 'Three', 'One']); + expect($rootScope.items).toEqual(listFindContent(element)); + + + $(element).remove(); + }); + }); + + it('should update model when the items are inside a transcluded directive and sorting between sortables', function() { + inject(function($compile, $rootScope) { + var elementTop, elementBottom; + elementTop = $compile(''.concat( + '
', + '', + beforeDivElement, + '
{{ item }}
', + afterDivElement, + '', + '
'))($rootScope); + elementBottom = $compile(''.concat( + '
', + '', + beforeDivElement, + '
{{ item }}
', + afterDivElement, + '', + '
'))($rootScope); + $rootScope.$apply(function() { + $rootScope.itemsTop = ['Top One', 'Top Two', 'Top Three']; + $rootScope.itemsBottom = ['Bottom One', 'Bottom Two', 'Bottom Three']; + $rootScope.opts = { + connectWith: '.cross-sortable', + items: '> * .sortable-item' + }; + }); + + host.append(elementTop).append(elementBottom).append('
'); + + var li1 = elementTop.find('.sortable-item:eq(0)'); + var li2 = elementBottom.find('.sortable-item:eq(2)'); + simulateElementDrag(li1, li2, { place: 'above', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Top One', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + + // it seems that it ony likes the last spot + li1 = elementBottom.find('.sortable-item:eq(2)'); + li2 = elementTop.find('.sortable-item:eq(1)'); + simulateElementDrag(li1, li2, { place: 'below', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three', 'Top One']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + + $(elementTop).remove(); + $(elementBottom).remove(); + }); + }); + + it('should properly cancel() when the items are inside a transcluded directive and sorting between sortables', function() { + inject(function($compile, $rootScope) { + var elementTop, elementBottom; + elementTop = $compile(''.concat( + '
', + '', + beforeDivElement, + '
{{ item }}
', + afterDivElement, + '', + '
'))($rootScope); + elementBottom = $compile(''.concat( + '
', + '', + beforeDivElement, + '
{{ item }}
', + afterDivElement, + '', + '
'))($rootScope); + $rootScope.$apply(function() { + $rootScope.itemsTop = ['Top One', 'Top Two', 'Top Three']; + $rootScope.itemsBottom = ['Bottom One', 'Bottom Two', 'Bottom Three']; + $rootScope.opts = { + connectWith: '.cross-sortable', + items: '> * .sortable-item', + update: function(e, ui) { + if (ui.item.sortable.model && + (typeof ui.item.sortable.model === 'string') && + ui.item.sortable.model.indexOf('Two') >= 0) { + ui.item.sortable.cancel(); + } + } + }; + }); + + host.append(elementTop).append(elementBottom).append('
'); + + var li1 = elementTop.find('.sortable-item:eq(1)'); + var li2 = elementBottom.find('.sortable-item:eq(0)'); + simulateElementDrag(li1, li2, { place: 'below', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + // try again + li1 = elementBottom.find('.sortable-item:eq(1)'); + li2 = elementTop.find('.sortable-item:eq(1)'); + simulateElementDrag(li1, li2, { place: 'above', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + // try again + li1 = elementBottom.find('.sortable-item:eq(1)'); + li2 = elementTop.find('.sortable-item:eq(1)'); + simulateElementDrag(li1, li2, { place: 'above', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + + li1 = elementTop.find('.sortable-item:eq(0)'); + li2 = elementBottom.find('.sortable-item:eq(2)'); + simulateElementDrag(li1, li2, { place: 'above', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Top One', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + + // it seems that it ony likes the last spot + li1 = elementBottom.find('.sortable-item:eq(2)'); + li2 = elementTop.find('.sortable-item:eq(1)'); + simulateElementDrag(li1, li2, { place: 'below', moves: 100 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three', 'Top One']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listFindContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listFindContent(elementBottom)); + + $(elementTop).remove(); + $(elementBottom).remove(); + }); + }); + } [0, 1].forEach(function(useExtraElements){ diff --git a/test/sortable.test-directives.js b/test/sortable.test-directives.js index f555ada..9ee801f 100644 --- a/test/sortable.test-directives.js +++ b/test/sortable.test-directives.js @@ -34,5 +34,21 @@ angular.module('ui.sortable.testDirectives', []) } }; } + ).directive('uiSortableTransclusionTestDirective', + function() { + return { + restrict: 'E', + transclude: true, + scope: true, + template: '
' + + '

Transclusion Directive

' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + }; + } ); diff --git a/test/sortable.test-helper.js b/test/sortable.test-helper.js index ec91db9..38721b4 100644 --- a/test/sortable.test-helper.js +++ b/test/sortable.test-helper.js @@ -15,6 +15,17 @@ angular.module('ui.sortable.testHelper', []) return []; } + function listFindContent (list, contentSelector) { + if (!contentSelector) { + contentSelector = '.sortable-item'; + } + + if (list && list.length) { + return list.find(contentSelector).map(function(){ return this.innerHTML; }).toArray(); + } + return []; + } + function listInnerContent (list, contentSelector) { if (!contentSelector) { contentSelector = '.itemContent'; @@ -35,11 +46,12 @@ angular.module('ui.sortable.testHelper', []) }; if (options === 'above') { - dragOptions.dy -= EXTRA_DY_PERCENTAGE * draggedElement.outerHeight(); + options = { place: 'above' }; } else if (options === 'below') { - dragOptions.dy += EXTRA_DY_PERCENTAGE * draggedElement.outerHeight(); - } else if (typeof options === 'object') { + options = { place: 'below' }; + } + if (typeof options === 'object') { if ('place' in options) { if (options.place === 'above') { dragOptions.dy -= EXTRA_DY_PERCENTAGE * draggedElement.outerHeight(); @@ -62,6 +74,10 @@ angular.module('ui.sortable.testHelper', []) if (isFinite(options.extradx)) { dragOptions.dx += options.extradx; } + + if (isFinite(options.moves) && options.moves > 0) { + dragOptions.moves = options.moves; + } } draggedElement.simulate(dragOptions.action, dragOptions); @@ -79,12 +95,15 @@ angular.module('ui.sortable.testHelper', []) return { EXTRA_DY_PERCENTAGE: EXTRA_DY_PERCENTAGE, listContent: listContent, + listFindContent: listFindContent, listInnerContent: listInnerContent, simulateElementDrag: simulateElementDrag, hasUndefinedProperties: hasUndefinedProperties, extraElements: { beforeLiElement: '
  • extra element
  • ', - afterLiElement: '
  • extra element
  • ' + afterLiElement: '
  • extra element
  • ', + beforeDivElement: '
    extra element
    ', + afterDivElement: '
    extra element
    ' } }; })