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