diff --git a/API.md b/API.md index d3fb53b..0094f7d 100644 --- a/API.md +++ b/API.md @@ -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:** diff --git a/README.md b/README.md index f4369c7..bf8cebb 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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 `` ``s and `', + afterTrElement: '', beforeDivElement: '
extra element
', afterDivElement: '
extra element
' }
`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. @@ -161,7 +179,7 @@ $scope.sortableOptions = { ``` -#### Canceling +### Canceling Inside the `update` callback, you can check the item that is dragged and cancel the sorting. diff --git a/src/sortable.js b/src/sortable.js index 9b6bf4e..1076c85 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -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' ){ @@ -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 @@ -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'; @@ -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; } @@ -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')); } @@ -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 = { @@ -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, @@ -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); diff --git a/test/sortable.e2e.directiveoptions.spec.js b/test/sortable.e2e.directiveoptions.spec.js index 0b4b174..8fefffb 100644 --- a/test/sortable.e2e.directiveoptions.spec.js +++ b/test/sortable.e2e.directiveoptions.spec.js @@ -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'; @@ -32,6 +34,7 @@ describe('uiSortable', function() { if (!useExtraElements) { beforeLiElement = afterLiElement = ''; + beforeTrElement = afterTrElement = ''; } })); @@ -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( + '', + '', + '', + beforeTrElement, + '', + '', + '', + afterTrElement, + '', + '
{{ item }}
' + ))($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('
'); + + 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( + '', + '', + '', + beforeTrElement, + '', + '', + '', + afterTrElement, + '', + '
{{ item }}
' + ))($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('
'); + + 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( + '', + '', + '', + beforeTrElement, + '', + '', + '', + afterTrElement, + '', + '
{{ item }}
' + ))($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('
'); + + 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){ diff --git a/test/sortable.test-helper.js b/test/sortable.test-helper.js index 38721b4..6f5dd7b 100644 --- a/test/sortable.test-helper.js +++ b/test/sortable.test-helper.js @@ -102,6 +102,8 @@ angular.module('ui.sortable.testHelper', []) extraElements: { beforeLiElement: '
  • extra element
  • ', afterLiElement: '
  • extra element
  • ', + beforeTrElement: '
    extra element
    extra element