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