3
3
jQuery UI Sortable plugin wrapper
4
4
5
5
@param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config
6
- */
6
+ */
7
7
angular . module ( 'ui.sortable' , [ ] ) . value ( 'uiSortableConfig' , { } ) . directive ( 'uiSortable' , [
8
8
'uiSortableConfig' ,
9
+ '$timeout' ,
9
10
'$log' ,
10
- function ( uiSortableConfig , log ) {
11
+ function ( uiSortableConfig , $timeout , $ log) {
11
12
return {
12
13
require : '?ngModel' ,
13
14
link : function ( scope , element , attrs , ngModel ) {
15
+ var savedNodes ;
14
16
function combineCallbacks ( first , second ) {
15
17
if ( second && typeof second === 'function' ) {
16
18
return function ( e , ui ) {
@@ -28,71 +30,143 @@ angular.module('ui.sortable', []).value('uiSortableConfig', {}).directive('uiSor
28
30
stop : null ,
29
31
update : null
30
32
} ;
31
- var apply = function ( e , ui ) {
32
- if ( ui . item . sortable . resort || ui . item . sortable . relocate ) {
33
- scope . $apply ( ) ;
34
- }
35
- } ;
36
33
angular . extend ( opts , uiSortableConfig ) ;
37
34
if ( ngModel ) {
38
- ngModel . $render = function ( ) {
39
- element . sortable ( 'refresh' ) ;
40
- } ;
35
+ // When we add or remove elements, we need the sortable to 'refresh'
36
+ // so it can find the new/removed elements.
37
+ scope . $watch ( attrs . ngModel + '.length' , function ( ) {
38
+ // Timeout to let ng-repeat modify the DOM
39
+ $timeout ( function ( ) {
40
+ element . sortable ( 'refresh' ) ;
41
+ } ) ;
42
+ } ) ;
41
43
callbacks . start = function ( e , ui ) {
42
- // Save position of dragged item
43
- ui . item . sortable = { index : ui . item . index ( ) } ;
44
+ // Save the starting position of dragged item
45
+ ui . item . sortable = {
46
+ index : ui . item . index ( ) ,
47
+ cancel : function ( ) {
48
+ ui . item . sortable . _isCanceled = true ;
49
+ } ,
50
+ isCanceled : function ( ) {
51
+ return ui . item . sortable . _isCanceled ;
52
+ } ,
53
+ _isCanceled : false
54
+ } ;
44
55
} ;
45
- callbacks . update = function ( e , ui ) {
46
- // For some reason the reference to ngModel in stop() is wrong
47
- ui . item . sortable . resort = ngModel ;
56
+ callbacks . activate = function ( ) {
57
+ // We need to make a copy of the current element's contents so
58
+ // we can restore it after sortable has messed it up.
59
+ // This is inside activate (instead of start) in order to save
60
+ // both lists when dragging between connected lists.
61
+ savedNodes = element . contents ( ) ;
62
+ // If this list has a placeholder (the connected lists won't),
63
+ // don't inlcude it in saved nodes.
64
+ var placeholder = element . sortable ( 'option' , 'placeholder' ) ;
65
+ // placeholder.element will be a function if the placeholder, has
66
+ // been created (placeholder will be an object). If it hasn't
67
+ // been created, either placeholder will be false if no
68
+ // placeholder class was given or placeholder.element will be
69
+ // undefined if a class was given (placeholder will be a string)
70
+ if ( placeholder && placeholder . element && typeof placeholder . element === 'function' ) {
71
+ var phElement = placeholder . element ( ) ;
72
+ // workaround for jquery ui 1.9.x,
73
+ // not returning jquery collection
74
+ if ( ! phElement . jquery ) {
75
+ phElement = angular . element ( phElement ) ;
76
+ }
77
+ // exact match with the placeholder's class attribute to handle
78
+ // the case that multiple connected sortables exist and
79
+ // the placehoilder option equals the class of sortable items
80
+ var excludes = element . find ( '[class="' + phElement . attr ( 'class' ) + '"]' ) ;
81
+ savedNodes = savedNodes . not ( excludes ) ;
82
+ }
48
83
} ;
49
- callbacks . receive = function ( e , ui ) {
50
- ui . item . sortable . relocate = true ;
51
- // if the item still exists (it has not been cancelled)
52
- if ( 'moved' in ui . item . sortable ) {
53
- // added item to array into correct position and set up flag
54
- ngModel . $modelValue . splice ( ui . item . index ( ) , 0 , ui . item . sortable . moved ) ;
84
+ callbacks . update = function ( e , ui ) {
85
+ // Save current drop position but only if this is not a second
86
+ // update that happens when moving between lists because then
87
+ // the value will be overwritten with the old value
88
+ if ( ! ui . item . sortable . received ) {
89
+ ui . item . sortable . dropindex = ui . item . index ( ) ;
90
+ ui . item . sortable . droptarget = ui . item . parent ( ) ;
91
+ // Cancel the sort (let ng-repeat do the sort for us)
92
+ // Don't cancel if this is the received list because it has
93
+ // already been canceled in the other list, and trying to cancel
94
+ // here will mess up the DOM.
95
+ element . sortable ( 'cancel' ) ;
96
+ }
97
+ // Put the nodes back exactly the way they started (this is very
98
+ // important because ng-repeat uses comment elements to delineate
99
+ // the start and stop of repeat sections and sortable doesn't
100
+ // respect their order (even if we cancel, the order of the
101
+ // comments are still messed up).
102
+ savedNodes . detach ( ) ;
103
+ if ( element . sortable ( 'option' , 'helper' ) === 'clone' ) {
104
+ // first detach all the savedNodes and then restore all of them
105
+ // except .ui-sortable-helper element (which is placed last).
106
+ // That way it will be garbage collected.
107
+ savedNodes = savedNodes . not ( savedNodes . last ( ) ) ;
108
+ }
109
+ savedNodes . appendTo ( element ) ;
110
+ // If received is true (an item was dropped in from another list)
111
+ // then we add the new item to this list otherwise wait until the
112
+ // stop event where we will know if it was a sort or item was
113
+ // moved here from another list
114
+ if ( ui . item . sortable . received && ! ui . item . sortable . isCanceled ( ) ) {
115
+ scope . $apply ( function ( ) {
116
+ ngModel . $modelValue . splice ( ui . item . sortable . dropindex , 0 , ui . item . sortable . moved ) ;
117
+ } ) ;
55
118
}
56
119
} ;
57
- callbacks . remove = function ( e , ui ) {
58
- // copy data into item
59
- if ( ngModel . $modelValue . length === 1 ) {
60
- ui . item . sortable . moved = ngModel . $modelValue . splice ( 0 , 1 ) [ 0 ] ;
120
+ callbacks . stop = function ( e , ui ) {
121
+ // If the received flag hasn't be set on the item, this is a
122
+ // normal sort, if dropindex is set, the item was moved, so move
123
+ // the items in the list.
124
+ if ( ! ui . item . sortable . received && 'dropindex' in ui . item . sortable && ! ui . item . sortable . isCanceled ( ) ) {
125
+ scope . $apply ( function ( ) {
126
+ ngModel . $modelValue . splice ( ui . item . sortable . dropindex , 0 , ngModel . $modelValue . splice ( ui . item . sortable . index , 1 ) [ 0 ] ) ;
127
+ } ) ;
61
128
} else {
62
- ui . item . sortable . moved = ngModel . $modelValue . splice ( ui . item . sortable . index , 1 ) [ 0 ] ;
129
+ // if the item was not moved, then restore the elements
130
+ // so that the ngRepeat's comment are correct.
131
+ if ( ( ! ( 'dropindex' in ui . item . sortable ) || ui . item . sortable . isCanceled ( ) ) && element . sortable ( 'option' , 'helper' ) !== 'clone' ) {
132
+ savedNodes . detach ( ) . appendTo ( element ) ;
133
+ }
63
134
}
64
135
} ;
65
- callbacks . stop = function ( e , ui ) {
66
- // digest all prepared changes
67
- if ( ui . item . sortable . resort && ! ui . item . sortable . relocate ) {
68
- // Fetch saved and current position of dropped element
69
- var end , start ;
70
- start = ui . item . sortable . index ;
71
- end = ui . item . index ( ) ;
72
- // Reorder array and apply change to scope
73
- ui . item . sortable . resort . $modelValue . splice ( end , 0 , ui . item . sortable . resort . $modelValue . splice ( start , 1 ) [ 0 ] ) ;
136
+ callbacks . receive = function ( e , ui ) {
137
+ // An item was dropped here from another list, set a flag on the
138
+ // item.
139
+ ui . item . sortable . received = true ;
140
+ } ;
141
+ callbacks . remove = function ( e , ui ) {
142
+ // Remove the item from this list's model and copy data into item,
143
+ // so the next list can retrive it
144
+ if ( ! ui . item . sortable . isCanceled ( ) ) {
145
+ scope . $apply ( function ( ) {
146
+ ui . item . sortable . moved = ngModel . $modelValue . splice ( ui . item . sortable . index , 1 ) [ 0 ] ;
147
+ } ) ;
74
148
}
75
149
} ;
76
150
scope . $watch ( attrs . uiSortable , function ( newVal ) {
77
151
angular . forEach ( newVal , function ( value , key ) {
78
152
if ( callbacks [ key ] ) {
79
- // wrap the callback
80
- value = combineCallbacks ( callbacks [ key ] , value ) ;
81
153
if ( key === 'stop' ) {
82
154
// call apply after stop
83
- value = combineCallbacks ( value , apply ) ;
155
+ value = combineCallbacks ( value , function ( ) {
156
+ scope . $apply ( ) ;
157
+ } ) ;
84
158
}
159
+ // wrap the callback
160
+ value = combineCallbacks ( callbacks [ key ] , value ) ;
85
161
}
86
162
element . sortable ( 'option' , key , value ) ;
87
163
} ) ;
88
164
} , true ) ;
89
165
angular . forEach ( callbacks , function ( value , key ) {
90
166
opts [ key ] = combineCallbacks ( value , opts [ key ] ) ;
91
167
} ) ;
92
- // call apply after stop
93
- opts . stop = combineCallbacks ( opts . stop , apply ) ;
94
168
} else {
95
- log . info ( 'ui.sortable: ngModel not provided!' , element ) ;
169
+ $ log. info ( 'ui.sortable: ngModel not provided!' , element ) ;
96
170
}
97
171
// Create sortable
98
172
element . sortable ( opts ) ;
0 commit comments