Skip to content
This repository was archived by the owner on Sep 8, 2020. It is now read-only.

feat(sortable): add workaround for horizontal lists #258

Merged
merged 4 commits into from
Sep 1, 2014
Merged
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,41 @@ myAppModule.controller('MyController', function($scope) {
When using event callbacks ([start](http://api.jqueryui.com/sortable/#event-start)/[update](http://api.jqueryui.com/sortable/#event-update)/[stop](http://api.jqueryui.com/sortable/#event-stop)...), avoid manipulating DOM elements (especially the one with the ng-repeat attached).
The suggested pattern is to use callbacks for emmiting events and altering the scope (inside the 'Angular world').

#### Floating

To have a smooth horizontal-list reordering, jquery.ui.sortable needs to detect the orientation of the list.
This detection takes place during the initialization of the plugin (and some of the checks include: whether the first item is floating left/right or if 'axis' parameter is 'x', etc).
There is also a [known issue](bugs.jqueryui.com/ticket/7498) about initially empty horizontal lists.

To provide a solution/workaround (till jquery.ui.sortable.refresh() also tests the orientation or a more appropriate method is provided), ui-sortable directive provides a `ui-floating` option as an extra to the [jquery.ui.sortable options](http://api.jqueryui.com/sortable/).

```html
<ul ui-sortable="{ 'ui-floating': true }" ng-model="items">
<li ng-repeat="item in items">{{ item }}</li>
</ul>
```

**OR**

```js
$scope.sortableOptions = {
'ui-floating': true
};
```
```html
<ul ui-sortable="sortableOptions" ng-model="items">
<li ng-repeat="item in items">{{ item }}</li>
</ul>
```


**ui-floating** (default: undefined)
Type: [Boolean](http://api.jquery.com/Types/#Boolean)/[String](http://api.jquery.com/Types/#String)/`undefined`
* **undefined**: Relies on jquery.ui to detect the list's orientation.
* **false**: Forces jquery.ui.sortable to detect the list as vertical.
* **true**: Forces jquery.ui.sortable to detect the list as horizontal.
* **"auto"**: Detects on each drag `start` if the element is floating or not.

#### Canceling

Inside the `update` callback, you can check the item that is dragged and cancel the sorting.
Expand Down Expand Up @@ -138,6 +173,7 @@ For more details about the events check the [jQueryUI API documentation](http://
- [Filtering](http://codepen.io/thgreasi/pen/mzGbq) ([details](https://github.com/angular-ui/ui-sortable/issues/113))
- [Ordering 1](http://codepen.io/thgreasi/pen/iKEHd) & [Ordering 2](http://plnkr.co/edit/XPUzJjdvwE0QWQ6py6mQ?p=preview) ([details](https://github.com/angular-ui/ui-sortable/issues/70))
- [Cloning](http://codepen.io/thgreasi/pen/qmvhG) ([details](https://github.com/angular-ui/ui-sortable/issues/139))
- [Horizontal List](http://codepen.io/thgreasi/pen/wsfjD)
- [Tree with dynamic template](http://codepen.io/thgreasi/pen/uyHFC)
- Canceling
- [Connected Lists With Max Size](http://codepen.io/thgreasi/pen/IdvFc)
Expand Down
33 changes: 31 additions & 2 deletions src/sortable.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ angular.module('ui.sortable', [])
return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed());
}

// thanks jquery-ui
function isFloating (item) {
return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display'));
}

var opts = {};

// directive specific options
var directiveOpts = {
'ui-floating': undefined
};

var callbacks = {
receive: null,
remove:null,
Expand All @@ -42,7 +52,7 @@ angular.module('ui.sortable', [])
helper: null
};

angular.extend(opts, uiSortableConfig, scope.$eval(attrs.uiSortable));
angular.extend(opts, directiveOpts, uiSortableConfig, scope.$eval(attrs.uiSortable));

if (!angular.element.fn || !angular.element.fn.jquery) {
$log.error('ui.sortable: jQuery should be included before AngularJS!');
Expand All @@ -65,6 +75,13 @@ angular.module('ui.sortable', [])
});

callbacks.start = function(e, ui) {
if (opts['ui-floating'] === 'auto') {
// since the drag has started, the element will be
// absolutely positioned, so we check its siblings
var siblings = ui.item.siblings();
angular.element(e.target).data('ui-sortable').floating = isFloating(siblings);
}

// Save the starting position of dragged item
ui.item.sortable = {
index: ui.item.index(),
Expand Down Expand Up @@ -228,7 +245,18 @@ angular.module('ui.sortable', [])
// is still bound to the directive's element
if (!!element.data('ui-sortable')) {
angular.forEach(newVal, function(value, key) {
if(callbacks[key]) {
// if it's a custom option of the directive,
// handle it approprietly
if (key in directiveOpts) {
if (key === 'ui-floating' && (value === false || value === true)) {
element.data('ui-sortable').floating = value;
}

opts[key] = value;
return;
}

if (callbacks[key]) {
if( key === 'stop' ){
// call apply after stop
value = combineCallbacks(
Expand All @@ -240,6 +268,7 @@ angular.module('ui.sortable', [])
value = wrappers[key](value);
}

opts[key] = value;
element.sortable('option', key, value);
});
}
Expand Down
172 changes: 172 additions & 0 deletions test/sortable.e2e.directiveoptions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use strict';

describe('uiSortable', function() {

// Ensure the sortable angular module is loaded
beforeEach(module('ui.sortable'));
beforeEach(module('ui.sortable.testHelper'));

var EXTRA_DY_PERCENTAGE, listContent;

beforeEach(inject(function (sortableTestHelper) {
EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE;
listContent = sortableTestHelper.listContent;
}));

describe('Custom directive options related', function() {

var host;

beforeEach(inject(function() {
host = $('<div id="test-host"></div>');
$('body').append(host);
}));

afterEach(function() {
host.remove();
host = null;
});

it('should work when "ui-floating: false" option is used', function() {
inject(function($compile, $rootScope) {
var element;
element = $compile('<ul ui-sortable="opts" ng-model="items"><li ng-repeat="item in items" id="s-{{$index}}" class="sortable-item">{{ item }}</li></ul>')($rootScope);
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-floating': false
};
$rootScope.items = ['One', 'Two', 'Three'];
});

host.append(element);

var li = element.find(':eq(0)');
var 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 "ui-floating: true" option is used', function() {
inject(function($compile, $rootScope) {
var element;
element = $compile('<ul ui-sortable="opts" ng-model="items"><li class="floatleft" ng-repeat="item in items" id="s-{{$index}}" class="sortable-item">{{ item }}</li></ul>')($rootScope);
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-floating': true
};
$rootScope.items = ['One', 'Two', 'Three'];
});

host.append(element).append('<div class="clear"></div>');

var li = element.find(':eq(0)');
var dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx, moves: 5 });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

$(element).remove();
});
});

it('should work when "ui-floating: \'auto\'" option is used and elements are "float"ing', function() {
inject(function($compile, $rootScope) {
var element;
element = $compile('<ul ui-sortable="opts" ng-model="items"><li class="floatleft" ng-repeat="item in items" id="s-{{$index}}" class="sortable-item">{{ item }}</li></ul>')($rootScope);
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-floating': 'auto'
};
$rootScope.items = ['One', 'Two', 'Three'];
});

host.append(element).append('<div class="clear"></div>');

var li = element.find(':eq(0)');
var dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx, moves: 5 });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

$(element).remove();
});
});

it('should work when "ui-floating: \'auto\'" option is used and elements are "display: inline-block"', function() {
inject(function($compile, $rootScope) {
var element;
element = $compile('<ul ui-sortable="opts" ng-model="items"><li class="inline-block" ng-repeat="item in items" id="s-{{$index}}" class="sortable-item">{{ item }}</li></ul>')($rootScope);
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-floating': 'auto'
};
$rootScope.items = ['One', 'Two', 'Three'];
});

host.append(element);

var li = element.find(':eq(0)');
var dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx });
expect($rootScope.items).toEqual(['Two', 'Three', 'One']);
expect($rootScope.items).toEqual(listContent(element));

li = element.find(':eq(1)');
dx = (1 + EXTRA_DY_PERCENTAGE) * li.outerWidth();
li.simulate('drag', { dx: dx, moves: 5 });
expect($rootScope.items).toEqual(['Two', 'One', 'Three']);
expect($rootScope.items).toEqual(listContent(element));

$(element).remove();
});
});

});

});
5 changes: 5 additions & 0 deletions test/sortable.tests.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.inline-block {
display: inline-block;
}

.floatleft,
.cross-sortable {
float: left;
}
Expand Down