From 2fb4240d151b8d9c02cb4ae046b70431a85c7559 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Wed, 9 Jul 2014 00:51:06 +0300 Subject: [PATCH 01/14] feat(sortable): update local variable with active options --- src/sortable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sortable.js b/src/sortable.js index 065179c..951d4c4 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -241,6 +241,7 @@ angular.module('ui.sortable', []) value = wrappers[key](value); } + opts[key] = value; element.sortable('option', key, value); }); } From 104405bdcc8b6efc61fb14e00ab12697d90a48dc Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Wed, 9 Jul 2014 01:37:28 +0300 Subject: [PATCH 02/14] feat(sortable): add workaround for horizontal lists --- src/sortable.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/sortable.js b/src/sortable.js index 951d4c4..3353f9d 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -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, @@ -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!'); @@ -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(), @@ -229,7 +246,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( From 830831cb59c3b00c67d6e76a710e236920b9da28 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Wed, 9 Jul 2014 01:37:52 +0300 Subject: [PATCH 03/14] test(sortable): add tests for horizontal lists workaround --- test/sortable.e2e.directiveoptions.spec.js | 172 +++++++++++++++++++++ test/sortable.tests.css | 5 + 2 files changed, 177 insertions(+) create mode 100644 test/sortable.e2e.directiveoptions.spec.js diff --git a/test/sortable.e2e.directiveoptions.spec.js b/test/sortable.e2e.directiveoptions.spec.js new file mode 100644 index 0000000..bd2461c --- /dev/null +++ b/test/sortable.e2e.directiveoptions.spec.js @@ -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 = $('
'); + $('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('
  • {{ item }}
')($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('
  • {{ item }}
')($rootScope); + $rootScope.$apply(function() { + $rootScope.opts = { + 'ui-floating': true + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element).append('
'); + + 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('
  • {{ item }}
')($rootScope); + $rootScope.$apply(function() { + $rootScope.opts = { + 'ui-floating': 'auto' + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element).append('
'); + + 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('
  • {{ item }}
')($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(); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/sortable.tests.css b/test/sortable.tests.css index 0d7bb75..6601ca0 100644 --- a/test/sortable.tests.css +++ b/test/sortable.tests.css @@ -1,3 +1,8 @@ +.inline-block { + display: inline-block; +} + +.floatleft, .cross-sortable { float: left; } From 2e22b0e494e9a8a6493512a6909f1127476b85b6 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Tue, 8 Jul 2014 23:55:52 +0300 Subject: [PATCH 04/14] docs(README): add section about floating workaround & example --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index a2c445a..7574742 100644 --- a/README.md +++ b/README.md @@ -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 +
    +
  • {{ item }}
  • +
+``` + +**OR** + +```js +$scope.sortableOptions = { + 'ui-floating': true +}; +``` +```html +
    +
  • {{ item }}
  • +
+``` + + +**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. @@ -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) From 1d180297e6927096e35a99b8951b1096caa3ee2d Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sun, 31 Aug 2014 22:30:32 +0300 Subject: [PATCH 05/14] fix(sortable): clear ui.item.sortable properties after drag stop --- src/sortable.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sortable.js b/src/sortable.js index 3353f9d..1a22667 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -33,6 +33,10 @@ angular.module('ui.sortable', []) return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); } + function afterStop(e, ui) { + ui.item.sortable._destroy(); + } + var opts = {}; // directive specific options @@ -95,7 +99,14 @@ angular.module('ui.sortable', []) return !!ui.item.sortable._isCustomHelperUsed; }, _isCanceled: false, - _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed + _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed, + _destroy: function () { + for (var key in ui.item.sortable) { + if (ui.item.sortable.hasOwnProperty(key)) { + ui.item.sortable[key] = undefined; + } + } + } }; }; @@ -262,6 +273,8 @@ angular.module('ui.sortable', []) // call apply after stop value = combineCallbacks( value, function() { scope.$apply(); }); + + value = combineCallbacks(value, afterStop); } // wrap the callback value = combineCallbacks(callbacks[key], value); @@ -277,6 +290,9 @@ angular.module('ui.sortable', []) angular.forEach(callbacks, function(value, key) { opts[key] = combineCallbacks(value, opts[key]); + if( key === 'stop' ){ + opts[key] = combineCallbacks(opts[key], afterStop); + } }); } else { From 14ae8da4ab984e5f9e4e970feec8e61ce3f8a302 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sun, 31 Aug 2014 23:22:17 +0300 Subject: [PATCH 06/14] test(sortable): add tests for ui.item.sortable proper destruction --- test/sortable.e2e.callbacks.spec.js | 58 ++++++++++++++++++++- test/sortable.e2e.multi.spec.js | 80 ++++++++++++++++++++++++++++- test/sortable.test-helper.js | 12 ++++- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/test/sortable.e2e.callbacks.spec.js b/test/sortable.e2e.callbacks.spec.js index 3902792..ec49f62 100644 --- a/test/sortable.e2e.callbacks.spec.js +++ b/test/sortable.e2e.callbacks.spec.js @@ -6,11 +6,12 @@ describe('uiSortable', function() { beforeEach(module('ui.sortable')); beforeEach(module('ui.sortable.testHelper')); - var EXTRA_DY_PERCENTAGE, listContent; + var EXTRA_DY_PERCENTAGE, listContent, hasUndefinedProperties; beforeEach(inject(function (sortableTestHelper) { EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE; listContent = sortableTestHelper.listContent; + hasUndefinedProperties = sortableTestHelper.hasUndefinedProperties; })); describe('Callbacks related', function() { @@ -188,6 +189,61 @@ describe('uiSortable', function() { }); }); + it('should properly free ui.item.sortable object', function() { + inject(function($compile, $rootScope) { + var element, uiItem, uiItemSortable_Destroy; + element = $compile('
  • {{ item }}
')($rootScope); + $rootScope.$apply(function() { + $rootScope.opts = { + start: function (e, ui) { + uiItem = ui.item; + spyOn(ui.item.sortable, '_destroy').andCallThrough(); + uiItemSortable_Destroy = ui.item.sortable._destroy; + }, + update: function(e, ui) { + if (ui.item.scope().item === 'Two') { + ui.item.sortable.cancel(); + } + } + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element); + + var li = element.find(':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(listContent(element)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + + li = element.find(':eq(0)'); + dy = (2 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['Two', 'Three', 'One']); + expect($rootScope.items).toEqual(listContent(element)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + + li = element.find(':eq(2)'); + dy = -(2 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Two', 'Three']); + expect($rootScope.items).toEqual(listContent(element)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + $(element).remove(); + }); + }); + }); }); \ No newline at end of file diff --git a/test/sortable.e2e.multi.spec.js b/test/sortable.e2e.multi.spec.js index 860d349..7343276 100644 --- a/test/sortable.e2e.multi.spec.js +++ b/test/sortable.e2e.multi.spec.js @@ -6,13 +6,14 @@ describe('uiSortable', function() { beforeEach(module('ui.sortable')); beforeEach(module('ui.sortable.testHelper')); - var EXTRA_DY_PERCENTAGE, listContent, listInnerContent, simulateElementDrag; + var EXTRA_DY_PERCENTAGE, listContent, listInnerContent, simulateElementDrag, hasUndefinedProperties; beforeEach(inject(function (sortableTestHelper) { EXTRA_DY_PERCENTAGE = sortableTestHelper.EXTRA_DY_PERCENTAGE; listContent = sortableTestHelper.listContent; listInnerContent = sortableTestHelper.listInnerContent; simulateElementDrag = sortableTestHelper.simulateElementDrag; + hasUndefinedProperties = sortableTestHelper.hasUndefinedProperties; })); describe('Multiple sortables related', function() { @@ -470,6 +471,83 @@ describe('uiSortable', function() { }); }); + it('should properly free ui.item.sortable object', function() { + inject(function($compile, $rootScope) { + var elementTop, elementBottom, uiItem, uiItemSortable_Destroy; + elementTop = $compile('
  • {{ item }}
')($rootScope); + elementBottom = $compile('
  • {{ item }}
')($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', + start: function (e, ui) { + uiItem = ui.item; + spyOn(ui.item.sortable, '_destroy').andCallThrough(); + uiItemSortable_Destroy = ui.item.sortable._destroy; + }, + update: function(e, ui) { + uiItem.sortable = ui.item.sortable; + if (ui.item.scope() && + (typeof ui.item.scope().item === 'string') && + ui.item.scope().item.indexOf('Two') >= 0) { + ui.item.sortable.cancel(); + } + } + }; + }); + + host.append(elementTop).append(elementBottom).append('
'); + + var li1 = elementTop.find(':eq(1)'); + var li2 = elementBottom.find(':eq(0)'); + simulateElementDrag(li1, li2, 'below'); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + li1 = elementBottom.find(':eq(1)'); + li2 = elementTop.find(':eq(1)'); + simulateElementDrag(li1, li2, { place: 'above', extradx: -20, extrady: -10 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + li1 = elementTop.find(':eq(0)'); + li2 = elementBottom.find(':eq(0)'); + simulateElementDrag(li1, li2, 'below'); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Top One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + li1 = elementBottom.find(':eq(1)'); + li2 = elementTop.find(':eq(1)'); + simulateElementDrag(li1, li2, { place: 'above', extradx: -20, extrady: -10 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top One', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + expect(uiItemSortable_Destroy).toHaveBeenCalled(); + expect(hasUndefinedProperties(uiItem.sortable)).toBe(true); + uiItem = uiItemSortable_Destroy = undefined; + + $(elementTop).remove(); + $(elementBottom).remove(); + }); + }); + }); }); \ No newline at end of file diff --git a/test/sortable.test-helper.js b/test/sortable.test-helper.js index 2b5bf90..4da4d45 100644 --- a/test/sortable.test-helper.js +++ b/test/sortable.test-helper.js @@ -62,10 +62,20 @@ angular.module('ui.sortable.testHelper', []) draggedElement.simulate('drag', dragOptions); } + function hasUndefinedProperties(testObject) { + return testObject && Object.keys(testObject) + .filter(function(key) { + return testObject.hasOwnProperty(key) && + testObject[key] !== undefined; + }) + .length === 0; + } + return { EXTRA_DY_PERCENTAGE: EXTRA_DY_PERCENTAGE, listContent: listContent, listInnerContent: listInnerContent, - simulateElementDrag: simulateElementDrag + simulateElementDrag: simulateElementDrag, + hasUndefinedProperties: hasUndefinedProperties }; }); From d777fef79f39fbd00672523a2d5aa69e663b26b6 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Thu, 4 Sep 2014 00:59:52 +0300 Subject: [PATCH 07/14] refactor(sortable): replace for-in-hasOwnProperty with angular.forEach This also brings code coverage back to 100%. --- src/sortable.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/sortable.js b/src/sortable.js index 1a22667..cc42504 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -101,11 +101,9 @@ angular.module('ui.sortable', []) _isCanceled: false, _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed, _destroy: function () { - for (var key in ui.item.sortable) { - if (ui.item.sortable.hasOwnProperty(key)) { - ui.item.sortable[key] = undefined; - } - } + angular.forEach(ui.item.sortable, function(value, key) { + ui.item.sortable[key] = undefined; + }); } }; }; From ff25513446f0fee6adfe85e7d3a2d794eb0e5f33 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Thu, 4 Sep 2014 01:38:03 +0300 Subject: [PATCH 08/14] feat(sortable): add extra properties to ui.item.sortable --- src/sortable.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sortable.js b/src/sortable.js index cc42504..7eb5e39 100644 --- a/src/sortable.js +++ b/src/sortable.js @@ -88,7 +88,10 @@ angular.module('ui.sortable', []) // Save the starting position of dragged item ui.item.sortable = { + model: ngModel.$modelValue[ui.item.index()], index: ui.item.index(), + source: ui.item.parent(), + sourceModel: ngModel.$modelValue, cancel: function () { ui.item.sortable._isCanceled = true; }, @@ -145,7 +148,9 @@ angular.module('ui.sortable', []) // the value will be overwritten with the old value if(!ui.item.sortable.received) { ui.item.sortable.dropindex = ui.item.index(); - ui.item.sortable.droptarget = ui.item.parent(); + var droptarget = ui.item.parent(); + ui.item.sortable.droptarget = droptarget; + ui.item.sortable.droptargetModel = droptarget.scope().$eval(droptarget.attr('ng-model')); // Cancel the sort (let ng-repeat do the sort for us) // Don't cancel if this is the received list because it has From 3a845c53ad2502fd6935db1be0b8ad4e1b236c21 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Fri, 5 Sep 2014 00:10:09 +0300 Subject: [PATCH 09/14] tests(sortable): add tests for the properties of ui.item.sortable --- test/sortable.e2e.callbacks.spec.js | 72 ++++++++++++++ test/sortable.e2e.multi.spec.js | 139 ++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) diff --git a/test/sortable.e2e.callbacks.spec.js b/test/sortable.e2e.callbacks.spec.js index ec49f62..5be87d7 100644 --- a/test/sortable.e2e.callbacks.spec.js +++ b/test/sortable.e2e.callbacks.spec.js @@ -189,6 +189,78 @@ describe('uiSortable', function() { }); }); + it('should properly set ui.item.sortable properties', function() { + inject(function($compile, $rootScope) { + var element, updateCallbackExpectations; + element = $compile('
  • {{ item }}
')($rootScope); + $rootScope.$apply(function() { + $rootScope.opts = { + update: function(e, ui) { + if (ui.item.scope().item === 'Two') { + ui.item.sortable.cancel(); + } + updateCallbackExpectations(ui.item.sortable); + } + }; + $rootScope.items = ['One', 'Two', 'Three']; + }); + + host.append(element); + + $rootScope.$apply(function() { + }); + var li = element.find(':eq(1)'); + var dy = (1 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('Two'); + expect(uiItemSortable.index).toEqual(1); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[0]); + expect(uiItemSortable.sourceModel).toBe($rootScope.items); + expect(uiItemSortable.isCanceled()).toBe(true); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + }; + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Two', 'Three']); + expect($rootScope.items).toEqual(listContent(element)); + updateCallbackExpectations = undefined; + + li = element.find(':eq(0)'); + dy = (2 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('One'); + expect(uiItemSortable.index).toEqual(0); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[0]); + expect(uiItemSortable.sourceModel).toBe($rootScope.items); + expect(uiItemSortable.isCanceled()).toBe(false); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + }; + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['Two', 'Three', 'One']); + expect($rootScope.items).toEqual(listContent(element)); + updateCallbackExpectations = undefined; + + li = element.find(':eq(2)'); + dy = -(2 + EXTRA_DY_PERCENTAGE) * li.outerHeight(); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('One'); + expect(uiItemSortable.index).toEqual(2); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[0]); + expect(uiItemSortable.sourceModel).toBe($rootScope.items); + expect(uiItemSortable.isCanceled()).toBe(false); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + }; + li.simulate('drag', { dy: dy }); + expect($rootScope.items).toEqual(['One', 'Two', 'Three']); + expect($rootScope.items).toEqual(listContent(element)); + updateCallbackExpectations = undefined; + + $(element).remove(); + }); + }); + it('should properly free ui.item.sortable object', function() { inject(function($compile, $rootScope) { var element, uiItem, uiItemSortable_Destroy; diff --git a/test/sortable.e2e.multi.spec.js b/test/sortable.e2e.multi.spec.js index 7343276..a9cebae 100644 --- a/test/sortable.e2e.multi.spec.js +++ b/test/sortable.e2e.multi.spec.js @@ -471,6 +471,145 @@ describe('uiSortable', function() { }); }); + it('should properly set ui.item.sortable properties', function() { + inject(function($compile, $rootScope) { + var elementTop, elementBottom, updateCallbackExpectations, stopCallbackExpectations; + elementTop = $compile('
  • {{ item }}
')($rootScope); + elementBottom = $compile('
  • {{ item }}
')($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', + update: function(e, ui) { + if (ui.item.scope() && + (typeof ui.item.scope().item === 'string') && + ui.item.scope().item.indexOf('Two') >= 0) { + ui.item.sortable.cancel(); + } + updateCallbackExpectations(ui.item.sortable); + }, + stop: function(e, ui) { + stopCallbackExpectations(ui.item.sortable); + } + }; + }); + + host.append(elementTop).append(elementBottom).append('
'); + + var li1 = elementTop.find(':eq(1)'); + var li2 = elementBottom.find(':eq(0)'); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('Top Two'); + expect(uiItemSortable.index).toEqual(1); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[0]); + expect(uiItemSortable.sourceModel).toBe($rootScope.itemsTop); + expect(uiItemSortable.isCanceled()).toBe(true); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + + expect(uiItemSortable.dropindex).toEqual(1); + expect(uiItemSortable.droptarget.length).toBe(1); + expect(uiItemSortable.droptarget[0]).toBe(host.children()[1]); + expect(uiItemSortable.droptargetModel).toBe($rootScope.itemsBottom); + }; + stopCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.received).toBe(true); + expect(uiItemSortable.moved).toBe(undefined); + }; + simulateElementDrag(li1, li2, { place: 'below', extradx: -20, extrady: -10 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + updateCallbackExpectations = stopCallbackExpectations = undefined; + + li1 = elementBottom.find(':eq(1)'); + li2 = elementTop.find(':eq(1)'); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('Bottom Two'); + expect(uiItemSortable.index).toEqual(1); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[1]); + expect(uiItemSortable.sourceModel).toBe($rootScope.itemsBottom); + expect(uiItemSortable.isCanceled()).toBe(true); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + + expect(uiItemSortable.dropindex).toEqual(1); + expect(uiItemSortable.droptarget.length).toBe(1); + expect(uiItemSortable.droptarget[0]).toBe(host.children()[0]); + expect(uiItemSortable.droptargetModel).toBe($rootScope.itemsTop); + }; + stopCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.received).toBe(true); + expect(uiItemSortable.moved).toBe(undefined); + }; + simulateElementDrag(li1, li2, { place: 'above', extradx: -20, extrady: -10 }); + expect($rootScope.itemsTop).toEqual(['Top One', 'Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + updateCallbackExpectations = stopCallbackExpectations = undefined; + + li1 = elementTop.find(':eq(0)'); + li2 = elementBottom.find(':eq(0)'); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('Top One'); + expect(uiItemSortable.index).toEqual(0); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[0]); + expect(uiItemSortable.sourceModel).toBe($rootScope.itemsTop); + expect(uiItemSortable.isCanceled()).toBe(false); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + + expect(uiItemSortable.dropindex).toEqual(1); + expect(uiItemSortable.droptarget.length).toBe(1); + expect(uiItemSortable.droptarget[0]).toBe(host.children()[1]); + expect(uiItemSortable.droptargetModel).toBe($rootScope.itemsBottom); + }; + stopCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.received).toBe(true); + expect(uiItemSortable.moved).toBe('Top One'); + }; + simulateElementDrag(li1, li2, 'below'); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Top One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + updateCallbackExpectations = stopCallbackExpectations = undefined; + + li1 = elementBottom.find(':eq(1)'); + li2 = elementTop.find(':eq(1)'); + updateCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.model).toEqual('Top One'); + expect(uiItemSortable.index).toEqual(1); + expect(uiItemSortable.source.length).toEqual(1); + expect(uiItemSortable.source[0]).toBe(host.children()[1]); + expect(uiItemSortable.sourceModel).toBe($rootScope.itemsBottom); + expect(uiItemSortable.isCanceled()).toBe(false); + expect(uiItemSortable.isCustomHelperUsed()).toBe(false); + + expect(uiItemSortable.dropindex).toEqual(1); + expect(uiItemSortable.droptarget.length).toBe(1); + expect(uiItemSortable.droptarget[0]).toBe(host.children()[0]); + expect(uiItemSortable.droptargetModel).toBe($rootScope.itemsTop); + }; + stopCallbackExpectations = function(uiItemSortable) { + expect(uiItemSortable.received).toBe(true); + expect(uiItemSortable.moved).toBe('Top One'); + }; + simulateElementDrag(li1, li2, { place: 'above', extradx: -20, extrady: -10 }); + expect($rootScope.itemsTop).toEqual(['Top Two', 'Top One', 'Top Three']); + expect($rootScope.itemsBottom).toEqual(['Bottom One', 'Bottom Two', 'Bottom Three']); + expect($rootScope.itemsTop).toEqual(listContent(elementTop)); + expect($rootScope.itemsBottom).toEqual(listContent(elementBottom)); + updateCallbackExpectations = stopCallbackExpectations = undefined; + + $(elementTop).remove(); + $(elementBottom).remove(); + }); + }); + it('should properly free ui.item.sortable object', function() { inject(function($compile, $rootScope) { var elementTop, elementBottom, uiItem, uiItemSortable_Destroy; From 381ed628ff4d044eaf8494853255ddff55782426 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Mon, 8 Sep 2014 00:17:59 +0300 Subject: [PATCH 10/14] tests: fix configuration of custom helper function + cancel() test --- test/sortable.e2e.callbacks.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/sortable.e2e.callbacks.spec.js b/test/sortable.e2e.callbacks.spec.js index 5be87d7..50b182b 100644 --- a/test/sortable.e2e.callbacks.spec.js +++ b/test/sortable.e2e.callbacks.spec.js @@ -73,6 +73,9 @@ describe('uiSortable', function() { element = $compile('
  • {{ item }}
')($rootScope); $rootScope.$apply(function() { $rootScope.opts = { + helper: function (e, item) { + return item; + }, update: function(e, ui) { if (ui.item.scope().item === 'Two') { ui.item.sortable.cancel(); From 1da4f7ecde5e21cffee65c96abf43dbc3f98887a Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Fri, 19 Sep 2014 01:22:42 +0300 Subject: [PATCH 11/14] docs(README): change dynamic nested tree helper repo link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7574742..4a43adb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Apply the directive to your form elements: * `ui-sortable` element should only contain one `ng-repeat` and not any other elements (above or below). Otherwise the index matching of the generated DOM elements and the `ng-model`'s items will break. **In other words: The items of `ng-model` must match the indexes of the generated DOM elements.** -* `ui-sortable` lists containing many 'types' of items can be implemented by using dynamic template loading [with ng-include](http://stackoverflow.com/questions/14607879/angularjs-load-dynamic-template-html-within-directive/14621927#14621927) or a [loader directive](https://gist.github.com/thgreasi/7152499c0e91973c4820), to determine how each model item should be rendered. Also take a look at the [Tree with dynamic template](http://codepen.io/thgreasi/pen/uyHFC) example. +* `ui-sortable` lists containing many 'types' of items can be implemented by using dynamic template loading [with ng-include](http://stackoverflow.com/questions/14607879/angularjs-load-dynamic-template-html-within-directive/14621927#14621927) or a [loader directive](https://github.com/thgreasi/tg-dynamic-directive), to determine how each model item should be rendered. Also take a look at the [Tree with dynamic template](http://codepen.io/thgreasi/pen/uyHFC) example. ### Options From 837f1a67486f1ad843161b405bcfcd1be467f044 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sun, 5 Oct 2014 23:08:37 +0300 Subject: [PATCH 12/14] docs(API.md): add API.md explaining ui.item.sortable properties --- API.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 3 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..afd9223 --- /dev/null +++ b/API.md @@ -0,0 +1,57 @@ +# ui.item.sortable API documentation + +## Properties + +### dropindex +Type: [Integer](http://api.jquery.com/Types/#Integer) +Holds the index of the drop target that the dragged item was dropped. + + +### droptarget +Type: [jQuery](http://api.jquery.com/Types/#jQuery) +Holds the ui-sortable element that the dragged item was dropped on. + +### droptargetModel +Type: [Array](http://api.jquery.com/Types/#Array) +Holds the array that is specified by the `ng-model` attribute of the [`droptarget`](#droptarget) ui-sortable element. + +### index +Type: [Integer](http://api.jquery.com/Types/#Integer) +Holds the original index of the item dragged. + +### model +Type: [Object](http://api.jquery.com/Types/#Object) +Holds the JavaScript object that is used as the model of the dragged item, as specified by the ng-repeat of the [`source`](#source) ui-sortable element and the item's [`index`](#index). + +### moved +Type: [Object](http://api.jquery.com/Types/#Object)/`undefined` +Holds the model of the dragged item only when a sorting happens between two connected ui-sortable elements. +In other words: `'moved' in ui.item.sortable` will return false only when a sorting is withing the same ui-sortable element ([`source`](#source) equals to the [`droptarget`](#droptarget)). + +### received +Type: [Boolean](http://api.jquery.com/Types/#Boolean) +When sorting between two connected sortables, it will be set to true inside the `update` callback of the [`droptarget`](#droptarget). + +### source +Type: [jQuery](http://api.jquery.com/Types/#jQuery) +Holds the ui-sortable element that the dragged item originated from. + +### sourceModel +Type: [Array](http://api.jquery.com/Types/#Array) +Holds the array that is specified by the `ng-model` of the [`source`](#source) ui-sortable element. + + +## Methods + +### cancel[()](http://api.jquery.com/Types/#Function) +Returns: Nothing +Can be called inside the `update` callback, in order to prevent/revert a sorting. +Should be used instead of the [jquery-ui-sortable cancel()](http://api.jqueryui.com/sortable/#method-cancel) method. + +### isCanceled[()](http://api.jquery.com/Types/#Function) +Returns: [Boolean](http://api.jquery.com/Types/#Boolean) +Returns whether the current sorting is marked as canceled, by an earlier call to [`ui.item.sortable.cancel()`](#cancel). + +### isCustomHelperUsed[()](http://api.jquery.com/Types/#Function) +Returns: [Boolean](http://api.jquery.com/Types/#Boolean) +Returns whether the [`helper`](http://api.jqueryui.com/sortable/#option-helper) element used for the current sorting, is one of the original ui-sortable list elements. diff --git a/README.md b/README.md index 7574742..e4e70e2 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,8 @@ Apply the directive to your form elements: ### Options -All the [jQueryUI Sortable options](http://api.jqueryui.com/sortable/) can be passed through the directive. +All the [jQueryUI Sortable options](http://api.jqueryui.com/sortable/) can be passed through the directive. +Additionally, the `ui` argument of the available callbacks gets enriched with some extra properties as specified to the [API.md file](API.md#uiitemsortable-api-documentation). ```js From fb527279de4a2f5e5030451d95f8af6b36931750 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Tue, 14 Oct 2014 22:23:54 +0300 Subject: [PATCH 13/14] chore: update bower.json to allow angular#1.3 --- bower.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index e5fe9f1..cdf29e2 100644 --- a/bower.json +++ b/bower.json @@ -16,11 +16,11 @@ "package.json" ], "dependencies": { - "angular": "~1.2.x", + "angular": ">=1.2.x", "jquery-ui": ">=1.9" }, "devDependencies": { - "angular-mocks": "~1.2.x", + "angular-mocks": ">=1.2.x", "jquery-simulate": "latest" } } From 2a5f7c3bf2c6251f33f1a49a7c0ae8d09cfa8c16 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Tue, 14 Oct 2014 22:27:12 +0300 Subject: [PATCH 14/14] chore: increase version number to v0.13.0 --- bower.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index cdf29e2..b4c923b 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-sortable", - "version": "0.12.11", + "version": "0.13.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 77b2d78..b865815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-ui-sortable", - "version": "0.12.11", + "version": "0.13.0", "description": "This directive allows you to jQueryUI Sortable.", "author": "https://github.com/angular-ui/ui-sortable/graphs/contributors", "license": "MIT",