MDL-34209 JavaScript: Tidy up dragdrop code.
[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() {
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 = Y.Moodle.core_course.util.section.getId(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         /*
108          * Drag-dropping related functions
109          */
110         drag_start : function(e) {
111             // Get our drag object
112             var drag = e.target;
113             // Creat a dummy structure of the outer elemnents for clean styles application
114             var containernode = Y.Node.create('<'+M.course.format.get_containernode()+'></'+M.course.format.get_containernode()+'>');
115             containernode.addClass(M.course.format.get_containerclass());
116             var sectionnode = Y.Node.create('<'+ M.course.format.get_sectionwrappernode()+'></'+ M.course.format.get_sectionwrappernode()+'>');
117             sectionnode.addClass( M.course.format.get_sectionwrapperclass());
118             sectionnode.setStyle('margin', 0);
119             sectionnode.setContent(drag.get('node').get('innerHTML'));
120             containernode.appendChild(sectionnode);
121             drag.get('dragNode').setContent(containernode);
122             drag.get('dragNode').addClass(CSS.COURSECONTENT);
123         },
125         drag_dropmiss : function(e) {
126             // Missed the target, but we assume the user intended to drop it
127             // on the last last ghost node location, e.drag and e.drop should be
128             // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
129             this.drop_hit(e);
130         },
132         get_section_index: function(node) {
133             var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y),
134                 sectionList = Y.all(sectionlistselector),
135                 nodeIndex = sectionList.indexOf(node),
136                 zeroIndex = sectionList.indexOf(Y.one('#section-0'));
138             return (nodeIndex - zeroIndex);
139         },
141         drop_hit : function(e) {
142             var drag = e.drag;
144             // Get references to our nodes and their IDs.
145             var dragnode = drag.get('node'),
146                 dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
147                 loopstart = dragnodeid,
149                 dropnodeindex = this.get_section_index(dragnode),
150                 loopend = dropnodeindex;
152             if (dragnodeid === dropnodeindex) {
153                 Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
154                 return;
155             }
157             Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-course-dragdrop');
159             if (loopstart > loopend) {
160                 // If we're going up, we need to swap the loop order
161                 // because loops can't go backwards.
162                 loopstart = dropnodeindex;
163                 loopend = dragnodeid;
164             }
166             // Get the list of nodes.
167             drag.get('dragNode').removeClass(CSS.COURSECONTENT);
168             var sectionlist = Y.Node.all(this.sectionlistselector);
170             // Add a lightbox if it's not there.
171             var lightbox = M.util.add_lightbox(Y, dragnode);
173             // Handle any variables which we must pass via AJAX.
174             var params = {},
175                 pageparams = this.get('config').pageparams,
176                 varname;
178             for (varname in pageparams) {
179                 if (!pageparams.hasOwnProperty(varname)) {
180                     continue;
181                 }
182                 params[varname] = pageparams[varname];
183             }
185             // Prepare request parameters
186             params.sesskey = M.cfg.sesskey;
187             params.courseId = this.get('courseid');
188             params['class'] = 'section';
189             params.field = 'move';
190             params.id = dragnodeid;
191             params.value = dropnodeindex;
193             // Perform the AJAX request.
194             var uri = M.cfg.wwwroot + this.get('ajaxurl');
195             Y.io(uri, {
196                 method: 'POST',
197                 data: params,
198                 on: {
199                     start : function() {
200                         lightbox.show();
201                     },
202                     success: function(tid, response) {
203                         // Update section titles, we can't simply swap them as
204                         // they might have custom title
205                         try {
206                             var responsetext = Y.JSON.parse(response.responseText);
207                             if (responsetext.error) {
208                                 new M.core.ajaxException(responsetext);
209                             }
210                             M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
211                         } catch (e) {}
213                         // Update all of the section IDs - first unset them, then set them
214                         // to avoid duplicates in the DOM.
215                         var index;
217                         // Classic bubble sort algorithm is applied to the section
218                         // nodes between original drag node location and the new one.
219                         var swapped = false;
220                         do {
221                             swapped = false;
222                             for (index = loopstart; index <= loopend; index++) {
223                                 if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
224                                             Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
225                                     Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
226                                             " with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
227                                     // Swap section id.
228                                     var sectionid = sectionlist.item(index - 1).get('id');
229                                     sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
230                                     sectionlist.item(index).set('id', sectionid);
232                                     // See what format needs to swap.
233                                     M.course.format.swap_sections(Y, index - 1, index);
235                                     // Update flag.
236                                     swapped = true;
237                                 }
238                             }
239                             loopend = loopend - 1;
240                         } while (swapped);
242                         window.setTimeout(function() {
243                             lightbox.hide();
244                         }, 250);
245                     },
247                     failure: function(tid, response) {
248                         this.ajax_failure(response);
249                         lightbox.hide();
250                     }
251                 },
252                 context:this
253             });
254         }
256     }, {
257         NAME : 'course-dragdrop-section',
258         ATTRS : {
259             courseid : {
260                 value : null
261             },
262             ajaxurl : {
263                 'value' : 0
264             },
265             config : {
266                 'value' : 0
267             }
268         }
269     });
271     var DRAGRESOURCE = function() {
272         DRAGRESOURCE.superclass.constructor.apply(this, arguments);
273     };
274     Y.extend(DRAGRESOURCE, M.core.dragdrop, {
275         initializer : function(params) {
276             // Set group for parent class
277             this.groups = ['resource'];
278             this.samenodeclass = CSS.ACTIVITY;
279             this.parentnodeclass = CSS.SECTION;
280             this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS);
282             // Go through all sections
283             var sectionlistselector = M.course.format.get_section_selector(Y);
284             if (sectionlistselector) {
285                 sectionlistselector = '.'+CSS.COURSECONTENT+' '+sectionlistselector;
286                 this.setup_for_section(sectionlistselector);
288                 // Initialise drag & drop for all resources/activities
289                 var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length+2)+' li.'+CSS.ACTIVITY;
290                 var del = new Y.DD.Delegate({
291                     container: '.'+CSS.COURSECONTENT,
292                     nodes: nodeselector,
293                     target: true,
294                     handles: ['.' + CSS.EDITINGMOVE],
295                     dragConfig: {groups: this.groups}
296                 });
297                 del.dd.plug(Y.Plugin.DDProxy, {
298                     // Don't move the node at the end of the drag
299                     moveOnEnd: false,
300                     cloneNode: true
301                 });
302                 del.dd.plug(Y.Plugin.DDConstrained, {
303                     // Keep it inside the .course-content
304                     constrain: '#'+CSS.PAGECONTENT
305                 });
306                 del.dd.plug(Y.Plugin.DDWinScroll);
308                 M.course.coursebase.register_module(this);
309                 M.course.dragres = this;
310             }
311         },
313          /**
314          * Apply dragdrop features to the specified selector or node that refers to section(s)
315          *
316          * @param baseselector The CSS selector or node to limit scope to
317          * @return void
318          */
319         setup_for_section : function(baseselector) {
320             Y.Node.all(baseselector).each(function(sectionnode) {
321                 var resources = sectionnode.one('.'+CSS.CONTENT+' ul.'+CSS.SECTION);
322                 // See if resources ul exists, if not create one
323                 if (!resources) {
324                     var resources = Y.Node.create('<ul></ul>');
325                     resources.addClass(CSS.SECTION);
326                     sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
327                 }
328                 resources.setAttribute('data-draggroups', this.groups.join(' '));
329                 // Define empty ul as droptarget, so that item could be moved to empty list
330                 var tar = new Y.DD.Drop({
331                     node: resources,
332                     groups: this.groups,
333                     padding: '20 0 20 0'
334                 });
336                 // Initialise each resource/activity in this section
337                 this.setup_for_resource('#'+sectionnode.get('id')+' li.'+CSS.ACTIVITY);
338             }, this);
339         },
340         /**
341          * Apply dragdrop features to the specified selector or node that refers to resource(s)
342          *
343          * @param baseselector The CSS selector or node to limit scope to
344          * @return void
345          */
346         setup_for_resource : function(baseselector) {
347             Y.Node.all(baseselector).each(function(resourcesnode) {
348                 // Replace move icons
349                 var move = resourcesnode.one('a.'+CSS.EDITINGMOVE);
350                 if (move) {
351                     move.replace(this.resourcedraghandle.cloneNode(true));
352                 }
353             }, this);
354         },
356         drag_start : function(e) {
357             // Get our drag object
358             var drag = e.target;
359             drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
360             drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
361         },
363         drag_dropmiss : function(e) {
364             // Missed the target, but we assume the user intended to drop it
365             // on the last last ghost node location, e.drag and e.drop should be
366             // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
367             this.drop_hit(e);
368         },
370         drop_hit : function(e) {
371             var drag = e.drag;
372             // Get a reference to our drag node
373             var dragnode = drag.get('node');
374             var dropnode = e.drop.get('node');
376             // Add spinner if it not there
377             var spinner = M.util.add_spinner(Y, dragnode.one(CSS.COMMANDSPAN));
379             var params = {};
381             // Handle any variables which we must pass back through to
382             var pageparams = this.get('config').pageparams;
383             for (varname in pageparams) {
384                 params[varname] = pageparams[varname];
385             }
387             // Prepare request parameters
388             params.sesskey = M.cfg.sesskey;
389             params.courseId = this.get('courseid');
390             params['class'] = 'resource';
391             params.field = 'move';
392             params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
393             params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
395             if (dragnode.next()) {
396                 params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
397             }
399             // Do AJAX request
400             var uri = M.cfg.wwwroot + this.get('ajaxurl');
402             Y.io(uri, {
403                 method: 'POST',
404                 data: params,
405                 on: {
406                     start : function(tid) {
407                         this.lock_drag_handle(drag, CSS.EDITINGMOVE);
408                         spinner.show();
409                     },
410                     success: function(tid, response) {
411                         var responsetext = Y.JSON.parse(response.responseText);
412                         var params = {element: dragnode, visible: responsetext.visible};
413                         M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
414                         this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
415                         window.setTimeout(function(e) {
416                             spinner.hide();
417                         }, 250);
418                     },
419                     failure: function(tid, response) {
420                         this.ajax_failure(response);
421                         this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
422                         spinner.hide();
423                         // TODO: revert nodes location
424                     }
425                 },
426                 context:this
427             });
428         }
429     }, {
430         NAME : 'course-dragdrop-resource',
431         ATTRS : {
432             courseid : {
433                 value : null
434             },
435             ajaxurl : {
436                 'value' : 0
437             },
438             config : {
439                 'value' : 0
440             }
441         }
442     });
444     M.course = M.course || {};
445     M.course.init_resource_dragdrop = function(params) {
446         new DRAGRESOURCE(params);
447     }
448     M.course.init_section_dragdrop = function(params) {
449         new DRAGSECTION(params);
450     }
451 }, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification', 'moodle-course-coursebase', 'moodle-course-util']});