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
+
+```
+
## 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
'
}
};
})