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

feat: add ui-preserve-size option #543

Merged
merged 2 commits into from
Nov 26, 2017
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
11 changes: 11 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# ui.item.sortable API documentation

This refers to the additional properties that are exposed through the `ui` parameter in the provided callback hooks. eg:
```js
$scope.sortableOptions = {
update: function(e, ui) {
if (ui.item.sortable.model == "can't be moved") {
ui.item.sortable.cancel();
}
}
};
```

## Properties

**Note:**
Expand Down
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,15 @@ 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
#### ui-floating

**ui-floating** (default: undefined)
Description: Enables a workaround for smooth horizontal sorting.
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.

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).
Expand Down Expand Up @@ -118,14 +126,24 @@ $scope.sortableOptions = {
```


**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.
#### ui-model-items

**ui-model-items** (default: `> [ng-repeat],> [data-ng-repeat],> [x-ng-repeat]`)
Description: Defines which elements should be considered as part of your model.
Type: [CSS selector](http://api.jquery.com/Types/#Selector)/[String](http://api.jquery.com/Types/#String)

This is the model related counterpart option of [jQuery's items option](http://api.jqueryui.com/sortable/#option-items).

#### ui-preserve-size

**ui-preserve-size** (default: undefined)
Description: Set's the size of the sorting helper to the size of the original element before the sorting.
Type: [Boolean](http://api.jquery.com/Types/#Boolean)/`undefined`

This is useful for elements that their size is dependent to other page characteristics.
A representative example of such cases are `<table>` `<tr>`s and `<td>`s.

#### Attributes For Event Handling
### Attributes For Event Handling

To handle events with html bindings just define any expression to listed event attributes.
If you defined an attribute for this events and defined callback function in sortableOptions at the same time, the attribute based callback will be called first.
Expand Down Expand Up @@ -161,7 +179,7 @@ $scope.sortableOptions = {
</ul>
```

#### Canceling
### Canceling

Inside the `update` callback, you can check the item that is dragged and cancel the sorting.

Expand Down
55 changes: 44 additions & 11 deletions src/sortable.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ angular.module('ui.sortable', [])
return null;
}

function setItemChildrenWidth(item) {
item.children().each(function() {
var $el = angular.element(this);

// Preserve the with of the element
$el.width($el.width());
});
}

function dummyHelper(e, item) {
return item;
}

function patchSortableOption(key, value) {
if (callbacks[key]) {
if( key === 'stop' ){
Expand All @@ -85,7 +98,8 @@ angular.module('ui.sortable', [])
return value;
}

function patchUISortableOptions(newVal, oldVal, sortableWidgetInstance) {
function patchUISortableOptions(newOpts, oldOpts, sortableWidgetInstance) {

function addDummyOptionKey(value, key) {
if (!(key in opts)) {
// add the key in the opts object so that
Expand All @@ -100,11 +114,11 @@ angular.module('ui.sortable', [])
// update some options of the sortable
var optsDiff = null;

if (oldVal) {
if (oldOpts) {
// reset deleted options to default
var defaultOptions;
angular.forEach(oldVal, function(oldValue, key) {
if (!newVal || !(key in newVal)) {
angular.forEach(oldOpts, function(oldValue, key) {
if (!newOpts || !(key in newOpts)) {
if (key in directiveOpts) {
if (key === 'ui-floating') {
opts[key] = 'auto';
Expand All @@ -129,16 +143,33 @@ angular.module('ui.sortable', [])
});
}

newOpts = angular.extend({}, newOpts);
// update changed options
angular.forEach(newVal, function(value, key) {
// if it's a custom option of the directive,
// handle it approprietly
// handle the custom option of the directive first
angular.forEach(newOpts, function(value, key) {
if (key in directiveOpts) {
if (key === 'ui-floating' && (value === false || value === true) && sortableWidgetInstance) {
sortableWidgetInstance.floating = value;
}

if (key === 'ui-preserve-size' && (value === false || value === true)) {
var userProvidedHelper = opts.helper;
newOpts.helper = function(e, item) {
if (opts['ui-preserve-size'] === true) {
setItemChildrenWidth(item);
}
return (userProvidedHelper || dummyHelper).apply(this, arguments);
};
}

opts[key] = patchSortableOption(key, value);
}
});

// handle the normal option of the directive
angular.forEach(newOpts, function(value, key) {
if (key in directiveOpts) {
// the custom option of the directive are already handled
return;
}

Expand Down Expand Up @@ -197,7 +228,7 @@ angular.module('ui.sortable', [])
}

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

Expand Down Expand Up @@ -228,7 +259,8 @@ angular.module('ui.sortable', [])
// directive specific options
var directiveOpts = {
'ui-floating': undefined,
'ui-model-items': uiSortableConfig.items
'ui-model-items': uiSortableConfig.items,
'ui-preserve-size': undefined
};

var callbacks = {
Expand Down Expand Up @@ -480,6 +512,7 @@ angular.module('ui.sortable', [])
return function (e, item) {
var oldItemSortable = item.sortable;
var index = getItemIndex(item);

item.sortable = {
model: ngModel.$modelValue[index],
index: index,
Expand All @@ -504,12 +537,12 @@ angular.module('ui.sortable', [])
return inner;
};

scope.$watchCollection('uiSortable', function(newVal, oldVal) {
scope.$watchCollection('uiSortable', function(newOpts, oldOpts) {
// ensure that the jquery-ui-sortable widget instance
// is still bound to the directive's element
var sortableWidgetInstance = getSortableWidgetInstance(element);
if (!!sortableWidgetInstance) {
var optsDiff = patchUISortableOptions(newVal, oldVal, sortableWidgetInstance);
var optsDiff = patchUISortableOptions(newOpts, oldOpts, sortableWidgetInstance);

if (optsDiff) {
element.sortable('option', optsDiff);
Expand Down
146 changes: 145 additions & 1 deletion test/sortable.e2e.directiveoptions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ describe('uiSortable', function() {
beforeEach(module('ui.sortable'));
beforeEach(module('ui.sortable.testHelper'));

var EXTRA_DY_PERCENTAGE, listContent, beforeLiElement, afterLiElement;
var EXTRA_DY_PERCENTAGE, listContent, beforeLiElement, afterLiElement, beforeTrElement, afterTrElement;

beforeEach(inject(function (sortableTestHelper) {
EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE;
listContent = sortableTestHelper.listContent;
beforeLiElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.beforeLiElement;
afterLiElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.afterLiElement;
beforeTrElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.beforeTrElement;
afterTrElement = sortableTestHelper.extraElements && sortableTestHelper.extraElements.afterTrElement;
}));

tests.description = 'Custom directive options related';
Expand All @@ -32,6 +34,7 @@ describe('uiSortable', function() {

if (!useExtraElements) {
beforeLiElement = afterLiElement = '';
beforeTrElement = afterTrElement = '';
}
}));

Expand Down Expand Up @@ -393,6 +396,147 @@ describe('uiSortable', function() {
});
});

it('should work when the "ui-preserve-size" option is used', function() {
inject(function($compile, $rootScope) {
var width = '200px';
var element;
element = $compile(''.concat(
'<table>',
'<tbody><tr><td style="width: ' + width + ';"></td></tr></tbody>',
'<tbody ui-sortable="opts" ng-model="items">',
beforeTrElement,
'<tr ng-repeat="item in items" id="s-{{$index}}">',
'<td class="sortable-item">{{ item }}</td>',
'</tr>',
afterTrElement,
'</tbody>',
'</table>'
))($rootScope);

var itemsSelector = '.sortable-item';
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-preserve-size': true,
stop: function(e, ui) {
expect(ui.item.children().css('width')).toEqual(width);
}
};
$rootScope.items = ['One', 'Two', 'Three'];
});

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

var tr = element.find(itemsSelector + ':eq(1)');
var dy = (1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['One', 'Three', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

tr = element.find(itemsSelector + ':eq(1)');
dy = -(1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['Three', 'One', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

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

it('should work when the "ui-preserve-size" option is false', function() {
inject(function($compile, $rootScope) {
var width = '200px';
var element;
element = $compile(''.concat(
'<table>',
'<tbody><tr><td style="width: ' + width + ';"></td></tr></tbody>',
'<tbody ui-sortable="opts" ng-model="items">',
beforeTrElement,
'<tr ng-repeat="item in items" id="s-{{$index}}">',
'<td class="sortable-item">{{ item }}</td>',
'</tr>',
afterTrElement,
'</tbody>',
'</table>'
))($rootScope);

var itemsSelector = '.sortable-item';
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-preserve-size': false,
stop: function(e, ui) {
expect(ui.item.children().attr('style')).toEqual(undefined);
}
};
$rootScope.items = ['One', 'Two', 'Three'];
});

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

var tr = element.find(itemsSelector + ':eq(1)');
var dy = (1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['One', 'Three', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

tr = element.find(itemsSelector + ':eq(1)');
dy = -(1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['Three', 'One', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

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

it('should work when the "ui-preserve-size" & helper options are used', function() {
inject(function($compile, $rootScope) {
var width = '200px';
var element;
element = $compile(''.concat(
'<table>',
'<tbody><tr><td style="width: ' + width + ';"></td></tr></tbody>',
'<tbody ui-sortable="opts" ng-model="items">',
beforeTrElement,
'<tr ng-repeat="item in items" id="s-{{$index}}">',
'<td class="sortable-item">{{ item }}</td>',
'</tr>',
afterTrElement,
'</tbody>',
'</table>'
))($rootScope);

var itemsSelector = '.sortable-item';
$rootScope.$apply(function() {
$rootScope.opts = {
'ui-preserve-size': true,
helper: function (e, item) {
return item.clone();
},
stop: function(e, ui) {
expect(ui.item.children().css('width')).toEqual(width);
}
};
$rootScope.items = ['One', 'Two', 'Three'];
});

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

var tr = element.find(itemsSelector + ':eq(1)');
var dy = (1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['One', 'Three', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

tr = element.find(itemsSelector + ':eq(1)');
dy = -(1 + EXTRA_DY_PERCENTAGE) * tr.outerHeight();
tr.simulate('drag', { dy: dy });
expect($rootScope.items).toEqual(['Three', 'One', 'Two']);
expect($rootScope.items).toEqual(listContent(element.find('tbody')).map($).map($.text));

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

}

[0, 1].forEach(function(useExtraElements){
Expand Down
2 changes: 2 additions & 0 deletions test/sortable.test-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ angular.module('ui.sortable.testHelper', [])
extraElements: {
beforeLiElement: '<li>extra element</li>',
afterLiElement: '<li>extra element</li>',
beforeTrElement: '<tr><td>extra element</td></tr>',
afterTrElement: '<tr><td>extra element</td></tr>',
beforeDivElement: '<div>extra element</div>',
afterDivElement: '<div>extra element</div>'
}
Expand Down