MDL-35715 course dragdrop Fix the ability to drag back to the empty section
[moodle.git] / course / yui / dragdrop / dragdrop.js
1 YUI.add('moodle-course-dragdrop', function(Y) {
3     var CSS = {
4         ACTIVITY : 'activity',
5         COMMANDSPAN : 'span.commands',
6         CONTENT : 'content',
7         COURSECONTENT : 'course-content',
8         EDITINGMOVE : 'editing_move',
9         ICONCLASS : 'iconsmall',
10         JUMPMENU : 'jumpmenu',
11         LEFT : 'left',
12         LIGHTBOX : 'lightbox',
13         MOVEDOWN : 'movedown',
14         MOVEUP : 'moveup',
15         PAGECONTENT : 'page-content',
16         RIGHT : 'right',
17         SECTION : 'section',
18         SECTIONADDMENUS : 'section_add_menus',
19         SECTIONHANDLE : 'section-handle',
20         SUMMARY : 'summary'
21     };
23     var DRAGSECTION = function() {
24         DRAGSECTION.superclass.constructor.apply(this, arguments);
25     };
26     Y.extend(DRAGSECTION, M.core.dragdrop, {
27         sectionlistselector : null,
29         initializer : function(params) {
30             // Set group for parent class
31             this.groups = ['section'];
32             this.samenodeclass = M.course.format.get_sectionwrapperclass();
33             this.parentnodeclass = M.course.format.get_containerclass();
35             // Check if we are in single section mode
36             if (Y.Node.one('.'+CSS.JUMPMENU)) {
37                 return false;
38             }
39             // Initialise sections dragging
40             this.sectionlistselector = M.course.format.get_section_wrapper(Y);
41             if (this.sectionlistselector) {
42                 this.sectionlistselector = '.'+CSS.COURSECONTENT+' '+this.sectionlistselector;
43                 this.setup_for_section(this.sectionlistselector);
45                 // Make each li element in the lists of sections draggable
46                 var nodeselector = this.sectionlistselector.slice(CSS.COURSECONTENT.length+2);
47                 var del = new Y.DD.Delegate({
48                     container: '.'+CSS.COURSECONTENT,
49                     nodes: nodeselector,
50                     target: true,
51                     handles: ['.'+CSS.LEFT],
52                     dragConfig: {groups: this.groups}
53                 });
54                 del.dd.plug(Y.Plugin.DDProxy, {
55                     // Don't move the node at the end of the drag
56                     moveOnEnd: false
57                 });
58                 del.dd.plug(Y.Plugin.DDConstrained, {
59                     // Keep it inside the .course-content
60                     constrain: '#'+CSS.PAGECONTENT,
61                     stickY: true
62                 });
63                 del.dd.plug(Y.Plugin.DDWinScroll);
64             }
65         },
67          /**
68          * Apply dragdrop features to the specified selector or node that refers to section(s)
69          *
70          * @param baseselector The CSS selector or node to limit scope to
71          * @return void
72          */
73         setup_for_section : function(baseselector) {
74             Y.Node.all(baseselector).each(function(sectionnode) {
75                 // Determine the section ID
76                 var sectionid = this.get_section_id(sectionnode);
78                 // We skip the top section as it is not draggable
79                 if (sectionid > 0) {
80                     // Remove move icons
81                     var movedown = sectionnode.one('.'+CSS.RIGHT+' a.'+CSS.MOVEDOWN);
82                     var moveup = sectionnode.one('.'+CSS.RIGHT+' a.'+CSS.MOVEUP);
84                     // Add dragger icon
85                     var title = M.util.get_string('movesection', 'moodle', sectionid);
86                     var cssleft = sectionnode.one('.'+CSS.LEFT);
88                     if ((movedown || moveup) && cssleft) {
89                         cssleft.setStyle('cursor', 'move');
90                         cssleft.appendChild(Y.Node.create('<br />'));
91                         cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE));
93                         if (moveup) {
94                             moveup.remove();
95                         }
96                         if (movedown) {
97                             movedown.remove();
98                         }
99                     }
100                 }
101             }, this);
102         },
104         get_section_id : function(node) {
105             return Number(node.get('id').replace(/section-/i, ''));
106         },
108         /*
109          * Drag-dropping related functions
110          */
111         drag_start : function(e) {
112             // Get our drag object
113             var drag = e.target;
114             // Creat a dummy structure of the outer elemnents for clean styles application
115             var containernode = Y.Node.create('<'+M.course.format.get_containernode()+'></'+M.course.format.get_containernode()+'>');
116             containernode.addClass(M.course.format.get_containerclass());
117             var sectionnode = Y.Node.create('<'+ M.course.format.get_sectionwrappernode()+'></'+ M.course.format.get_sectionwrappernode()+'>');
118             sectionnode.addClass( M.course.format.get_sectionwrapperclass());
119             sectionnode.setStyle('margin', 0);
120             sectionnode.setContent(drag.get('node').get('innerHTML'));
121             containernode.appendChild(sectionnode);
122             drag.get('dragNode').setContent(containernode);
123             drag.get('dragNode').addClass(CSS.COURSECONTENT);
124         },
126         drag_dropmiss : function(e) {
127             // Missed the target, but we assume the user intended to drop it
128             // on the last last ghost node location, e.drag and e.drop should be
129             // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
130             this.drop_hit(e);
131         },
133         drop_hit : function(e) {
134             var drag = e.drag;
135             // Get a reference to our drag node
136             var dragnode = drag.get('node');
137             var dropnode = e.drop.get('node');
138             // Prepare some variables
139             var dragnodeid = Number(this.get_section_id(dragnode));
140             var dropnodeid = Number(this.get_section_id(dropnode));
142             var loopstart = dragnodeid;
143             var loopend = dropnodeid;
145             if (this.goingup) {
146                 loopstart = dropnodeid;
147                 loopend = dragnodeid;
148             }
150             // Get the list of nodes
151             drag.get('dragNode').removeClass(CSS.COURSECONTENT);
152             var sectionlist = Y.Node.all(this.sectionlistselector);
154             // Add lightbox if it not there
155             var lightbox = M.util.add_lightbox(Y, dragnode);
157             var params = {};
159             // Handle any variables which we must pass back through to
160             var pageparams = this.get('config').pageparams;
161             for (varname in pageparams) {
162                 params[varname] = pageparams[varname];
163             }
165             // Prepare request parameters
166             params.sesskey = M.cfg.sesskey;
167             params.courseId = this.get('courseid');
168             params['class'] = 'section';
169             params.field = 'move';
170             params.id = dragnodeid;
171             params.value = dropnodeid;
173             // Do AJAX request
174             var uri = M.cfg.wwwroot + this.get('ajaxurl');
176             Y.io(uri, {
177                 method: 'POST',
178                 data: params,
179                 on: {
180                     start : function(tid) {
181                         lightbox.show();
182                     },
183                     success: function(tid, response) {
184                         // Update section titles, we can't simply swap them as
185                         // they might have custom title
186                         try {
187                             var responsetext = Y.JSON.parse(response.responseText);
188                             if (responsetext.error) {
189                                 new M.core.ajaxException(responsetext);
190                             }
191                             M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
192                         } catch (e) {}
194                         // Classic bubble sort algorithm is applied to the section
195                         // nodes between original drag node location and the new one.
196                         do {
197                             var swapped = false;
198                             for (var i = loopstart; i <= loopend; i++) {
199                                 if (this.get_section_id(sectionlist.item(i-1)) > this.get_section_id(sectionlist.item(i))) {
200                                     // Swap section id
201                                     var sectionid = sectionlist.item(i-1).get('id');
202                                     sectionlist.item(i-1).set('id', sectionlist.item(i).get('id'));
203                                     sectionlist.item(i).set('id', sectionid);
204                                     // See what format needs to swap
205                                     M.course.format.swap_sections(Y, i-1, i);
206                                     // Update flag
207                                     swapped = true;
208                                 }
209                             }
210                             loopend = loopend - 1;
211                         } while (swapped);
213                         // Finally, hide the lightbox
214                         window.setTimeout(function(e) {
215                             lightbox.hide();
216                         }, 250);
217                     },
218                     failure: function(tid, response) {
219                         this.ajax_failure(response);
220                         lightbox.hide();
221                     }
222                 },
223                 context:this
224             });
225         }
227     }, {
228         NAME : 'course-dragdrop-section',
229         ATTRS : {
230             courseid : {
231                 value : null
232             },
233             ajaxurl : {
234                 'value' : 0
235             },
236             config : {
237                 'value' : 0
238             }
239         }
240     });
242     var DRAGRESOURCE = function() {
243         DRAGRESOURCE.superclass.constructor.apply(this, arguments);
244     };
245     Y.extend(DRAGRESOURCE, M.core.dragdrop, {
246         initializer : function(params) {
247             // Set group for parent class
248             this.groups = ['resource'];
249             this.samenodeclass = CSS.ACTIVITY;
250             this.parentnodeclass = CSS.SECTION;
251             this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS);
253             // Go through all sections
254             var sectionlistselector = M.course.format.get_section_selector(Y);
255             if (sectionlistselector) {
256                 sectionlistselector = '.'+CSS.COURSECONTENT+' '+sectionlistselector;
257                 this.setup_for_section(sectionlistselector);
259                 // Initialise drag & drop for all resources/activities
260                 var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length+2)+' li.'+CSS.ACTIVITY;
261                 var del = new Y.DD.Delegate({
262                     container: '.'+CSS.COURSECONTENT,
263                     nodes: nodeselector,
264                     target: true,
265                     handles: ['.' + CSS.EDITINGMOVE],
266                     dragConfig: {groups: this.groups}
267                 });
268                 del.dd.plug(Y.Plugin.DDProxy, {
269                     // Don't move the node at the end of the drag
270                     moveOnEnd: false
271                 });
272                 del.dd.plug(Y.Plugin.DDConstrained, {
273                     // Keep it inside the .course-content
274                     constrain: '#'+CSS.PAGECONTENT
275                 });
276                 del.dd.plug(Y.Plugin.DDWinScroll);
278                 M.course.coursebase.register_module(this);
279                 M.course.dragres = this;
280             }
281         },
283          /**
284          * Apply dragdrop features to the specified selector or node that refers to section(s)
285          *
286          * @param baseselector The CSS selector or node to limit scope to
287          * @return void
288          */
289         setup_for_section : function(baseselector) {
290             Y.Node.all(baseselector).each(function(sectionnode) {
291                 var resources = sectionnode.one('.'+CSS.CONTENT+' ul.'+CSS.SECTION);
292                 // See if resources ul exists, if not create one
293                 if (!resources) {
294                     var resources = Y.Node.create('<ul></ul>');
295                     resources.addClass(CSS.SECTION);
296                     sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
297                 }
298                 // Define empty ul as droptarget, so that item could be moved to empty list
299                 var tar = new Y.DD.Drop({
300                     node: resources,
301                     groups: this.groups,
302                     padding: '20 0 20 0'
303                 });
305                 // Initialise each resource/activity in this section
306                 this.setup_for_resource('#'+sectionnode.get('id')+' li.'+CSS.ACTIVITY);
307             }, this);
308         },
309         /**
310          * Apply dragdrop features to the specified selector or node that refers to resource(s)
311          *
312          * @param baseselector The CSS selector or node to limit scope to
313          * @return void
314          */
315         setup_for_resource : function(baseselector) {
316             Y.Node.all(baseselector).each(function(resourcesnode) {
317                 // Replace move icons
318                 var move = resourcesnode.one('a.'+CSS.EDITINGMOVE);
319                 if (move) {
320                     move.replace(this.resourcedraghandle.cloneNode(true));
321                 }
322             }, this);
323         },
325         get_section_id : function(node) {
326             return Number(node.get('id').replace(/section-/i, ''));
327         },
329         get_resource_id : function(node) {
330             return Number(node.get('id').replace(/module-/i, ''));
331         },
333         drag_start : function(e) {
334             // Get our drag object
335             var drag = e.target;
336             drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
337             drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
338         },
340         drag_dropmiss : function(e) {
341             // Missed the target, but we assume the user intended to drop it
342             // on the last last ghost node location, e.drag and e.drop should be
343             // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
344             this.drop_hit(e);
345         },
347         drop_hit : function(e) {
348             var drag = e.drag;
349             // Get a reference to our drag node
350             var dragnode = drag.get('node');
351             var dropnode = e.drop.get('node');
353             // Add spinner if it not there
354             var spinner = M.util.add_spinner(Y, dragnode.one(CSS.COMMANDSPAN));
356             var params = {};
358             // Handle any variables which we must pass back through to
359             var pageparams = this.get('config').pageparams;
360             for (varname in pageparams) {
361                 params[varname] = pageparams[varname];
362             }
364             // Prepare request parameters
365             params.sesskey = M.cfg.sesskey;
366             params.courseId = this.get('courseid');
367             params['class'] = 'resource';
368             params.field = 'move';
369             params.id = Number(this.get_resource_id(dragnode));
370             params.sectionId = this.get_section_id(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
372             if (dragnode.next()) {
373                 params.beforeId = Number(this.get_resource_id(dragnode.next()));
374             }
376             // Do AJAX request
377             var uri = M.cfg.wwwroot + this.get('ajaxurl');
379             Y.io(uri, {
380                 method: 'POST',
381                 data: params,
382                 on: {
383                     start : function(tid) {
384                         this.lock_drag_handle(drag, CSS.EDITINGMOVE);
385                         spinner.show();
386                     },
387                     success: function(tid, response) {
388                         this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
389                         window.setTimeout(function(e) {
390                             spinner.hide();
391                         }, 250);
392                     },
393                     failure: function(tid, response) {
394                         this.ajax_failure(response);
395                         this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
396                         spinner.hide();
397                         // TODO: revert nodes location
398                     }
399                 },
400                 context:this
401             });
402         }
403     }, {
404         NAME : 'course-dragdrop-resource',
405         ATTRS : {
406             courseid : {
407                 value : null
408             },
409             ajaxurl : {
410                 'value' : 0
411             },
412             config : {
413                 'value' : 0
414             }
415         }
416     });
418     M.course = M.course || {};
419     M.course.init_resource_dragdrop = function(params) {
420         new DRAGRESOURCE(params);
421     }
422     M.course.init_section_dragdrop = function(params) {
423         new DRAGSECTION(params);
424     }
425 }, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification', 'moodle-course-coursebase']});