1/* 2 jQuery UI Sortable plugin wrapper 3 4 @param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config 5 */ 6angular.module('ui.sortable', []) 7 .value('uiSortableConfig',{}) 8 .directive('uiSortable', [ 9 'uiSortableConfig', '$timeout', '$log', 10 function(uiSortableConfig, $timeout, $log) { 11 return { 12 require: '?ngModel', 13 scope: { 14 ngModel: '=', 15 uiSortable: '=' 16 }, 17 link: function(scope, element, attrs, ngModel) { 18 var savedNodes; 19 20 function combineCallbacks(first,second){ 21 if(second && (typeof second === 'function')) { 22 return function() { 23 first.apply(this, arguments); 24 second.apply(this, arguments); 25 }; 26 } 27 return first; 28 } 29 30 function getSortableWidgetInstance(element) { 31 // this is a fix to support jquery-ui prior to v1.11.x 32 // otherwise we should be using `element.sortable('instance')` 33 var data = element.data('ui-sortable'); 34 if (data && typeof data === 'object' && data.widgetFullName === 'ui-sortable') { 35 return data; 36 } 37 return null; 38 } 39 40 function hasSortingHelper (element, ui) { 41 var helperOption = element.sortable('option','helper'); 42 return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed()); 43 } 44 45 // thanks jquery-ui 46 function isFloating (item) { 47 return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); 48 } 49 50 function getElementScope(elementScopes, element) { 51 var result = null; 52 for (var i = 0; i < elementScopes.length; i++) { 53 var x = elementScopes[i]; 54 if (x.element[0] === element[0]) { 55 result = x.scope; 56 break; 57 } 58 } 59 return result; 60 } 61 62 function afterStop(e, ui) { 63 ui.item.sortable._destroy(); 64 } 65 66 var opts = {}; 67 68 // directive specific options 69 var directiveOpts = { 70 'ui-floating': undefined 71 }; 72 73 var callbacks = { 74 receive: null, 75 remove:null, 76 start:null, 77 stop:null, 78 update:null 79 }; 80 81 var wrappers = { 82 helper: null 83 }; 84 85 angular.extend(opts, directiveOpts, uiSortableConfig, scope.uiSortable); 86 87 if (!angular.element.fn || !angular.element.fn.jquery) { 88 $log.error('ui.sortable: jQuery should be included before AngularJS!'); 89 return; 90 } 91 92 if (ngModel) { 93 94 // When we add or remove elements, we need the sortable to 'refresh' 95 // so it can find the new/removed elements. 96 scope.$watch('ngModel.length', function() { 97 // Timeout to let ng-repeat modify the DOM 98 $timeout(function() { 99 // ensure that the jquery-ui-sortable widget instance 100 // is still bound to the directive's element 101 if (!!getSortableWidgetInstance(element)) { 102 element.sortable('refresh'); 103 } 104 }, 0, false); 105 }); 106 107 callbacks.start = function(e, ui) { 108 if (opts['ui-floating'] === 'auto') { 109 // since the drag has started, the element will be 110 // absolutely positioned, so we check its siblings 111 var siblings = ui.item.siblings(); 112 var sortableWidgetInstance = getSortableWidgetInstance(angular.element(e.target)); 113 sortableWidgetInstance.floating = isFloating(siblings); 114 } 115 116 // Save the starting position of dragged item 117 ui.item.sortable = { 118 model: ngModel.$modelValue[ui.item.index()], 119 index: ui.item.index(), 120 source: ui.item.parent(), 121 sourceModel: ngModel.$modelValue, 122 cancel: function () { 123 ui.item.sortable._isCanceled = true; 124 }, 125 isCanceled: function () { 126 return ui.item.sortable._isCanceled; 127 }, 128 isCustomHelperUsed: function () { 129 return !!ui.item.sortable._isCustomHelperUsed; 130 }, 131 _isCanceled: false, 132 _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed, 133 _destroy: function () { 134 angular.forEach(ui.item.sortable, function(value, key) { 135 ui.item.sortable[key] = undefined; 136 }); 137 } 138 }; 139 }; 140 141 callbacks.activate = function(e, ui) { 142 // We need to make a copy of the current element's contents so 143 // we can restore it after sortable has messed it up. 144 // This is inside activate (instead of start) in order to save 145 // both lists when dragging between connected lists. 146 savedNodes = element.contents(); 147 148 // If this list has a placeholder (the connected lists won't), 149 // don't inlcude it in saved nodes. 150 var placeholder = element.sortable('option','placeholder'); 151 152 // placeholder.element will be a function if the placeholder, has 153 // been created (placeholder will be an object). If it hasn't 154 // been created, either placeholder will be false if no 155 // placeholder class was given or placeholder.element will be 156 // undefined if a class was given (placeholder will be a string) 157 if (placeholder && placeholder.element && typeof placeholder.element === 'function') { 158 var phElement = placeholder.element(); 159 // workaround for jquery ui 1.9.x, 160 // not returning jquery collection 161 phElement = angular.element(phElement); 162 163 // exact match with the placeholder's class attribute to handle 164 // the case that multiple connected sortables exist and 165 // the placehoilder option equals the class of sortable items 166 var excludes = element.find('[class="' + phElement.attr('class') + '"]:not([ng-repeat], [data-ng-repeat])'); 167 168 savedNodes = savedNodes.not(excludes); 169 } 170 171 // save the directive's scope so that it is accessible from ui.item.sortable 172 var connectedSortables = ui.item.sortable._connectedSortables || []; 173 174 connectedSortables.push({ 175 element: element, 176 scope: scope 177 }); 178 179 ui.item.sortable._connectedSortables = connectedSortables; 180 }; 181 182 callbacks.update = function(e, ui) { 183 // Save current drop position but only if this is not a second 184 // update that happens when moving between lists because then 185 // the value will be overwritten with the old value 186 if(!ui.item.sortable.received) { 187 ui.item.sortable.dropindex = ui.item.index(); 188 var droptarget = ui.item.parent(); 189 ui.item.sortable.droptarget = droptarget; 190
191 var droptargetScope = getElementScope(ui.item.sortable._connectedSortables, droptarget); 192 ui.item.sortable.droptargetModel = droptargetScope.ngModel; 193 194 // Cancel the sort (let ng-repeat do the sort for us) 195 // Don't cancel if this is the received list because it has 196 // already been canceled in the other list, and trying to cancel 197 // here will mess up the DOM. 198 element.sortable('cancel'); 199 } 200 201 // Put the nodes back exactly the way they started (this is very 202 // important because ng-repeat uses comment elements to delineate 203 // the start and stop of repeat sections and sortable doesn't 204 // respect their order (even if we cancel, the order of the 205 // comments are still messed up). 206 if (hasSortingHelper(element, ui) && !ui.item.sortable.received && 207 element.sortable( 'option', 'appendTo' ) === 'parent') { 208 // restore all the savedNodes except .ui-sortable-helper element 209 // (which is placed last). That way it will be garbage collected. 210 savedNodes = savedNodes.not(savedNodes.last()); 211 } 212 savedNodes.appendTo(element); 213 214 // If this is the target connected list then 215 // it's safe to clear the restored nodes since: 216 // update is currently running and 217 // stop is not called for the target list. 218 if(ui.item.sortable.received) { 219 savedNodes = null; 220 } 221 222 // If received is true (an item was dropped in from another list) 223 // then we add the new item to this list otherwise wait until the 224 // stop event where we will know if it was a sort or item was 225 // moved here from another list 226 if(ui.item.sortable.received && !ui.item.sortable.isCanceled()) { 227 scope.$apply(function () { 228 ngModel.$modelValue.splice(ui.item.sortable.dropindex, 0, 229 ui.item.sortable.moved); 230 }); 231 } 232 }; 233 234 callbacks.stop = function(e, ui) { 235 // If the received flag hasn't be set on the item, this is a 236 // normal sort, if dropindex is set, the item was moved, so move 237 // the items in the list. 238 if(!ui.item.sortable.received && 239 ('dropindex' in ui.item.sortable) && 240 !ui.item.sortable.isCanceled()) { 241 242 scope.$apply(function () { 243 ngModel.$modelValue.splice( 244 ui.item.sortable.dropindex, 0, 245 ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]); 246 }); 247 } else { 248 // if the item was not moved, then restore the elements 249 // so that the ngRepeat's comment are correct. 250 if ((!('dropindex' in ui.item.sortable) || ui.item.sortable.isCanceled()) && 251 !hasSortingHelper(element, ui)) { 252 savedNodes.appendTo(element); 253 } 254 } 255 256 // It's now safe to clear the savedNodes 257 // since stop is the last callback. 258 savedNodes = null; 259 }; 260 261 callbacks.receive = function(e, ui) { 262 // An item was dropped here from another list, set a flag on the 263 // item. 264 ui.item.sortable.received = true; 265 }; 266 267 callbacks.remove = function(e, ui) { 268 // Workaround for a problem observed in nested connected lists. 269 // There should be an 'update' event before 'remove' when moving 270 // elements. If the event did not fire, cancel sorting. 271 if (!('dropindex' in ui.item.sortable)) { 272 element.sortable('cancel'); 273 ui.item.sortable.cancel(); 274 } 275 276 // Remove the item from this list's model and copy data into item, 277 // so the next list can retrive it 278 if (!ui.item.sortable.isCanceled()) { 279 scope.$apply(function () { 280 ui.item.sortable.moved = ngModel.$modelValue.splice( 281 ui.item.sortable.index, 1)[0]; 282 }); 283 } 284 }; 285 286 wrappers.helper = function (inner) { 287 if (inner && typeof inner === 'function') { 288 return function (e, item) { 289 var innerResult = inner.apply(this, arguments); 290 item.sortable._isCustomHelperUsed = item !== innerResult; 291 return innerResult; 292 }; 293 } 294 return inner; 295 }; 296 297 scope.$watch('uiSortable', function(newVal /*, oldVal*/) { 298 // ensure that the jquery-ui-sortable widget instance 299 // is still bound to the directive's element 300 var sortableWidgetInstance = getSortableWidgetInstance(element); 301 if (!!sortableWidgetInstance) { 302 angular.forEach(newVal, function(value, key) { 303 // if it's a custom option of the directive, 304 // handle it approprietly 305 if (key in directiveOpts) { 306 if (key === 'ui-floating' && (value === false || value === true)) { 307 sortableWidgetInstance.floating = value; 308 } 309 310 opts[key] = value; 311 return; 312 } 313 314 if (callbacks[key]) { 315 if( key === 'stop' ){ 316 // call apply after stop 317 value = combineCallbacks( 318 value, function() { scope.$apply(); }); 319 320 value = combineCallbacks(value, afterStop); 321 } 322 // wrap the callback 323 value = combineCallbacks(callbacks[key], value); 324 } else if (wrappers[key]) { 325 value = wrappers[key](value); 326 } 327 328 opts[key] = value; 329 element.sortable('option', key, value); 330 }); 331 } 332 }, true); 333 334 angular.forEach(callbacks, function(value, key) { 335 opts[key] = combineCallbacks(value, opts[key]); 336 if( key === 'stop' ){ 337 opts[key] = combineCallbacks(opts[key], afterStop); 338 } 339 }); 340 341 } else { 342 $log.info('ui.sortable: ngModel not provided!', element); 343 } 344 345 // Create sortable 346 element.sortable(opts); 347 } 348 }; 349 } 350 ]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.