a5085d276d707e572b4dd970e7d4b32176c5b7f1
[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 = 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 lightbox if it not there
171             var lightbox = M.util.add_lightbox(Y, dragnode);
173             var params = {};
175             // Handle any variables which we must pass back through to
176             var pageparams = this.get('config').pageparams;
177             for (varname in pageparams) {
178                 params[varname] = pageparams[varname];
179             }
181             // Prepare request parameters
182             params.sesskey = M.cfg.sesskey;
183             params.courseId = this.get('courseid');
184             params['class'] = 'section';
185             params.field = 'move';
186             params.id = dragnodeid;
187             params.value = dropnodeindex;
189             // Do AJAX request
190             var uri = M.cfg.wwwroot + this.get('ajaxurl');
192             Y.io(uri, {
193                 method: 'POST',
194                 data: params,
195                 on: {
196                     start : function(tid) {
197                         lightbox.show();
198                     },
199                     success: function(tid, response) {
200                         // Update section titles, we can't simply swap them as
201                         // they might have custom title
202                         try {
203                             var responsetext = Y.JSON.parse(response.responseText);
204                             if (responsetext.error) {
205                                 new M.core.ajaxException(responsetext);
206                             }
207                             M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
208                         } catch (e) {}
210                         // Update all of the section IDs - first unset them, then set them
211                         // to avoid duplicates in the DOM.
212                         var index;
214                         // Classic bubble sort algorithm is applied to the section
215                         // nodes between original drag node location and the new one.
216                         var swapped = false;
217                         do {
218                             swapped = false;
219                             for (index = loopstart; index <= loopend; index++) {
220                                 if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
221                                             Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
222                                     Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
223                                             " with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)));
224                                     // Swap section id.
225                                     var sectionid = sectionlist.item(index - 1).get('id');
226                                     sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
227                                     sectionlist.item(index).set('id', sectionid);
229                                     // See what format needs to swap.
230                                     M.course.format.swap_sections(Y, index - 1, index);
232                                     // Update flag.
233                                     swapped = true;
234                                 }
235                             }
236                             loopend = loopend - 1;
237                         } while (swapped);
239                         window.setTimeout(function() {
240                             lightbox.hide();
241                         }, 250);
242                     },
243                     failure: function(tid, response) {
244                         this.ajax_failure(response);
245                         lightbox.hide();
246                     }
247                 },
248                 context:this
249             });
250         }
252     }, {
253         NAME : 'course-dragdrop-section',
254         ATTRS : {
255             courseid : {
256                 value : null
257             },
258             ajaxurl : {
259                 'value' : 0
260             },
261             config : {
262                 'value' : 0
263             }
264         }
265     });
267     var DRAGRESOURCE = function() {
268         DRAGRESOURCE.superclass.constructor.apply(this, arguments);
269     };
270     Y.extend(DRAGRESOURCE, M.core.dragdrop, {
271         initializer : function(params) {
272             // Set group for parent class
273             this.groups = ['resource'];
274             this.samenodeclass = CSS.ACTIVITY;
275             this.parentnodeclass = CSS.SECTION;
276             this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS);
278             // Go through all sections
279             var sectionlistselector = M.course.format.get_section_selector(Y);
280             if (sectionlistselector) {
281                 sectionlistselector = '.'+CSS.COURSECONTENT+' '+sectionlistselector;
282                 this.setup_for_section(sectionlistselector);
284                 // Initialise drag & drop for all resources/activities
285                 var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length+2)+' li.'+CSS.ACTIVITY;
286                 var del = new Y.DD.Delegate({
287                     container: '.'+CSS.COURSECONTENT,
288                     nodes: nodeselector,
289                     target: true,
290                     handles: ['.' + CSS.EDITINGMOVE],
291                     dragConfig: {groups: this.groups}
292                 });
293                 del.dd.plug(Y.Plugin.DDProxy, {
294                     // Don't move the node at the end of the drag
295                     moveOnEnd: false,
296                     cloneNode: true
297                 });
298                 del.dd.plug(Y.Plugin.DDConstrained, {
299                     // Keep it inside the .course-content
300                     constrain: '#'+CSS.PAGECONTENT
301                 });
302                 del.dd.plug(Y.Plugin.DDWinScroll);
304                 M.course.coursebase.register_module(this);
305                 M.course.dragres = this;
306             }
307         },
309          /**
310          * Apply dragdrop features to the specified selector or node that refers to section(s)
311          *
312          * @param baseselector The CSS selector or node to limit scope to
313          * @return void
314          */
315         setup_for_section : function(baseselector) {
316             Y.Node.all(baseselector).each(function(sectionnode) {
317                 var resources = sectionnode.one('.'+CSS.CONTENT+' ul.'+CSS.SECTION);
318                 // See if resources ul exists, if not create one
319                 if (!resources) {
320                     var resources = Y.Node.create('<ul></ul>');
321                     resources.addClass(CSS.SECTION);
322                     sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
323                 }
324                 resources.setAttribute('data-draggroups', this.groups.join(' '));
325                 // Define empty ul as droptarget, so that item could be moved to empty list
326                 var tar = new Y.DD.Drop({
327                     node: resources,
328                     groups: this.groups,
329                     padding: '20 0 20 0'
330                 });
332                 // Initialise each resource/activity in this section
333                 this.setup_for_resource('#'+sectionnode.get('id')+' li.'+CSS.ACTIVITY);
334             }, this);
335         },
336         /**
337          * Apply dragdrop features to the specified selector or node that refers to resource(s)
338          *
339          * @param baseselector The CSS selector or node to limit scope to
340          * @return void
341          */
342         setup_for_resource : function(baseselector) {
343             Y.Node.all(baseselector).each(function(resourcesnode) {
344                 // Replace move icons
345                 var move = resourcesnode.one('a.'+CSS.EDITINGMOVE);
346                 if (move) {
347                     move.replace(this.resourcedraghandle.cloneNode(true));
348                 }
349             }, this);
350         },
352         drag_start : function(e) {
353             // Get our drag object
354             var drag = e.target;
355             drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
356             drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
357         },
359         drag_dropmiss : function(e) {
360             // Missed the target, but we assume the user intended to drop it
361             // on the last last ghost node location, e.drag and e.drop should be
362             // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
363             this.drop_hit(e);
364         },
366         drop_hit : function(e) {
367             var drag = e.drag;
368             // Get a reference to our drag node
369             var dragnode = drag.get('node');
370             var dropnode = e.drop.get('node');
372             // Add spinner if it not there
373             var spinner = M.util.add_spinner(Y, dragnode.one(CSS.COMMANDSPAN));
375             var params = {};
377             // Handle any variables which we must pass back through to
378             var pageparams = this.get('config').pageparams;
379             for (varname in pageparams) {
380                 params[varname] = pageparams[varname];
381             }
383             // Prepare request parameters
384             params.sesskey = M.cfg.sesskey;
385             params.courseId = this.get('courseid');
386             params['class'] = 'resource';
387             params.field = 'move';
388             params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
389             params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
391             if (dragnode.next()) {
392                 params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
393             }
395             // Do AJAX request
396             var uri = M.cfg.wwwroot + this.get('ajaxurl');
398             Y.io(uri, {
399                 method: 'POST',
400                 data: params,
401                 on: {
402                     start : function(tid) {
403                         this.lock_drag_handle(drag, CSS.EDITINGMOVE);
404                         spinner.show();
405                     },
406                     success: function(tid, response) {
407                         var responsetext = Y.JSON.parse(response.responseText);
408                         var params = {element: dragnode, visible: responsetext.visible};
409                         M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
410                         this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
411                         window.setTimeout(function(e) {
412                             spinner.hide();
413                         }, 250);
414                     },
415                     failure: function(tid, response) {
416                         this.ajax_failure(response);
417                         this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
418                         spinner.hide();
419                         // TODO: revert nodes location
420                     }
421                 },
422                 context:this
423             });
424         }
425     }, {
426         NAME : 'course-dragdrop-resource',
427         ATTRS : {
428             courseid : {
429                 value : null
430             },
431             ajaxurl : {
432                 'value' : 0
433             },
434             config : {
435                 'value' : 0
436             }
437         }
438     });
440     M.course = M.course || {};
441     M.course.init_resource_dragdrop = function(params) {
442         new DRAGRESOURCE(params);
443     }
444     M.course.init_section_dragdrop = function(params) {
445         new DRAGSECTION(params);
446     }
447 }, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification', 'moodle-course-coursebase', 'moodle-course-util']});