Merge branch 'MDL-67818-check-api-fixes' of https://github.com/brendanheywood/moodle
[moodle.git] / course / yui / build / moodle-course-dragdrop / moodle-course-dragdrop.js
CommitLineData
fd03a33c
AN
1YUI.add('moodle-course-dragdrop', function (Y, NAME) {
2
ad3f8cd1 3/* eslint-disable no-unused-vars */
fd03a33c
AN
4/**
5 * Drag and Drop for course sections and course modules.
6 *
7 * @module moodle-course-dragdrop
8 */
9
10var CSS = {
11 ACTIONAREA: '.actions',
12 ACTIVITY: 'activity',
13 ACTIVITYINSTANCE: 'activityinstance',
14 CONTENT: 'content',
15 COURSECONTENT: 'course-content',
16 EDITINGMOVE: 'editing_move',
17 ICONCLASS: 'iconsmall',
18 JUMPMENU: 'jumpmenu',
19 LEFT: 'left',
20 LIGHTBOX: 'lightbox',
21 MOVEDOWN: 'movedown',
22 MOVEUP: 'moveup',
23 PAGECONTENT: 'page-content',
24 RIGHT: 'right',
25 SECTION: 'section',
26 SECTIONADDMENUS: 'section_add_menus',
27 SECTIONHANDLE: 'section-handle',
28 SUMMARY: 'summary',
29 SECTIONDRAGGABLE: 'sectiondraggable'
30};
31
32M.course = M.course || {};
33/**
34 * Section drag and drop.
35 *
36 * @class M.course.dragdrop.section
37 * @constructor
38 * @extends M.core.dragdrop
39 */
40var DRAGSECTION = function() {
41 DRAGSECTION.superclass.constructor.apply(this, arguments);
42};
43Y.extend(DRAGSECTION, M.core.dragdrop, {
44 sectionlistselector: null,
45
46 initializer: function() {
47 // Set group for parent class
3a0bc0fd 48 this.groups = [CSS.SECTIONDRAGGABLE];
fd03a33c
AN
49 this.samenodeclass = M.course.format.get_sectionwrapperclass();
50 this.parentnodeclass = M.course.format.get_containerclass();
b08323a7
NM
51 // Detect the direction of travel.
52 this.detectkeyboarddirection = true;
fd03a33c
AN
53
54 // Check if we are in single section mode
55 if (Y.Node.one('.' + CSS.JUMPMENU)) {
56 return false;
57 }
58 // Initialise sections dragging
59 this.sectionlistselector = M.course.format.get_section_wrapper(Y);
60 if (this.sectionlistselector) {
61 this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
62
63 this.setup_for_section(this.sectionlistselector);
64
65 // Make each li element in the lists of sections draggable
66 var del = new Y.DD.Delegate({
67 container: '.' + CSS.COURSECONTENT,
68 nodes: '.' + CSS.SECTIONDRAGGABLE,
69 target: true,
70 handles: ['.' + CSS.LEFT],
71 dragConfig: {groups: this.groups}
72 });
73 del.dd.plug(Y.Plugin.DDProxy, {
74 // Don't move the node at the end of the drag
75 moveOnEnd: false
76 });
77 del.dd.plug(Y.Plugin.DDConstrained, {
78 // Keep it inside the .course-content
79 constrain: '#' + CSS.PAGECONTENT,
80 stickY: true
81 });
82 del.dd.plug(Y.Plugin.DDWinScroll);
83 }
84 },
85
86 /**
87 * Apply dragdrop features to the specified selector or node that refers to section(s)
88 *
89 * @method setup_for_section
90 * @param {String} baseselector The CSS selector or node to limit scope to
91 */
92 setup_for_section: function(baseselector) {
93 Y.Node.all(baseselector).each(function(sectionnode) {
94 // Determine the section ID
95 var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
96
97 // We skip the top section as it is not draggable
98 if (sectionid > 0) {
99 // Remove move icons
100 var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
101 var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
102
103 // Add dragger icon
104 var title = M.util.get_string('movesection', 'moodle', sectionid);
105 var cssleft = sectionnode.one('.' + CSS.LEFT);
106
107 if ((movedown || moveup) && cssleft) {
108 cssleft.setStyle('cursor', 'move');
109 cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
110
111 if (moveup) {
60cf0742
S
112 if (moveup.previous('br')) {
113 moveup.previous('br').remove();
114 } else if (moveup.next('br')) {
115 moveup.next('br').remove();
116 }
117
63e4df60
DW
118 if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') {
119 moveup.ancestor().remove();
60cf0742
S
120 } else {
121 moveup.remove();
122 }
fd03a33c
AN
123 }
124 if (movedown) {
60cf0742
S
125 if (movedown.previous('br')) {
126 movedown.previous('br').remove();
127 } else if (movedown.next('br')) {
128 movedown.next('br').remove();
129 }
130
63e4df60
DW
131 var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase();
132 if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') {
133 movedown.ancestor().remove();
60cf0742
S
134 } else {
135 movedown.remove();
136 }
fd03a33c
AN
137 }
138
139 // This section can be moved - add the class to indicate this to Y.DD.
140 sectionnode.addClass(CSS.SECTIONDRAGGABLE);
141 }
142 }
143 }, this);
144 },
145
146 /*
147 * Drag-dropping related functions
148 */
149 drag_start: function(e) {
150 // Get our drag object
151 var drag = e.target;
b5c9fb4a
NM
152 // This is the node that the user started to drag.
153 var node = drag.get('node');
154 // This is the container node that will follow the mouse around,
155 // or during a keyboard drag and drop the original node.
156 var dragnode = drag.get('dragNode');
157 if (node === dragnode) {
158 return;
159 }
fd03a33c 160 // Creat a dummy structure of the outer elemnents for clean styles application
557f44d9
AN
161 var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
162 '></' + M.course.format.get_containernode() + '>');
fd03a33c 163 containernode.addClass(M.course.format.get_containerclass());
557f44d9
AN
164 var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() +
165 '></' + M.course.format.get_sectionwrappernode() + '>');
3a0bc0fd 166 sectionnode.addClass(M.course.format.get_sectionwrapperclass());
fd03a33c 167 sectionnode.setStyle('margin', 0);
b5c9fb4a 168 sectionnode.setContent(node.get('innerHTML'));
fd03a33c 169 containernode.appendChild(sectionnode);
b5c9fb4a
NM
170 dragnode.setContent(containernode);
171 dragnode.addClass(CSS.COURSECONTENT);
fd03a33c
AN
172 },
173
174 drag_dropmiss: function(e) {
175 // Missed the target, but we assume the user intended to drop it
176 // on the last last ghost node location, e.drag and e.drop should be
177 // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
178 this.drop_hit(e);
179 },
180
181 get_section_index: function(node) {
182 var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y),
183 sectionList = Y.all(sectionlistselector),
184 nodeIndex = sectionList.indexOf(node),
185 zeroIndex = sectionList.indexOf(Y.one('#section-0'));
186
187 return (nodeIndex - zeroIndex);
188 },
189
190 drop_hit: function(e) {
191 var drag = e.drag;
192
193 // Get references to our nodes and their IDs.
194 var dragnode = drag.get('node'),
195 dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
196 loopstart = dragnodeid,
197
198 dropnodeindex = this.get_section_index(dragnode),
199 loopend = dropnodeindex;
200
201 if (dragnodeid === dropnodeindex) {
202 return;
203 }
204
205
206 if (loopstart > loopend) {
207 // If we're going up, we need to swap the loop order
208 // because loops can't go backwards.
209 loopstart = dropnodeindex;
210 loopend = dragnodeid;
211 }
212
213 // Get the list of nodes.
214 drag.get('dragNode').removeClass(CSS.COURSECONTENT);
215 var sectionlist = Y.Node.all(this.sectionlistselector);
216
217 // Add a lightbox if it's not there.
218 var lightbox = M.util.add_lightbox(Y, dragnode);
219
220 // Handle any variables which we must pass via AJAX.
221 var params = {},
222 pageparams = this.get('config').pageparams,
223 varname;
224
225 for (varname in pageparams) {
226 if (!pageparams.hasOwnProperty(varname)) {
227 continue;
228 }
229 params[varname] = pageparams[varname];
230 }
231
232 // Prepare request parameters
233 params.sesskey = M.cfg.sesskey;
234 params.courseId = this.get('courseid');
235 params['class'] = 'section';
236 params.field = 'move';
237 params.id = dragnodeid;
238 params.value = dropnodeindex;
239
240 // Perform the AJAX request.
241 var uri = M.cfg.wwwroot + this.get('ajaxurl');
242 Y.io(uri, {
243 method: 'POST',
244 data: params,
245 on: {
246 start: function() {
247 lightbox.show();
248 },
249 success: function(tid, response) {
250 // Update section titles, we can't simply swap them as
251 // they might have custom title
252 try {
253 var responsetext = Y.JSON.parse(response.responseText);
254 if (responsetext.error) {
255 new M.core.ajaxException(responsetext);
256 }
257 M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
3a0bc0fd
DP
258 } catch (e) {
259 // Ignore.
260 }
fd03a33c
AN
261
262 // Update all of the section IDs - first unset them, then set them
263 // to avoid duplicates in the DOM.
264 var index;
265
266 // Classic bubble sort algorithm is applied to the section
267 // nodes between original drag node location and the new one.
268 var swapped = false;
269 do {
270 swapped = false;
271 for (index = loopstart; index <= loopend; index++) {
272 if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
273 Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
274 // Swap section id.
275 var sectionid = sectionlist.item(index - 1).get('id');
276 sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
277 sectionlist.item(index).set('id', sectionid);
278
279 // See what format needs to swap.
280 M.course.format.swap_sections(Y, index - 1, index);
281
282 // Update flag.
283 swapped = true;
284 }
285 }
286 loopend = loopend - 1;
287 } while (swapped);
288
289 window.setTimeout(function() {
290 lightbox.hide();
291 }, 250);
292 },
293
294 failure: function(tid, response) {
295 this.ajax_failure(response);
296 lightbox.hide();
297 }
298 },
3a0bc0fd 299 context: this
fd03a33c
AN
300 });
301 }
302
303}, {
304 NAME: 'course-dragdrop-section',
305 ATTRS: {
306 courseid: {
307 value: null
308 },
309 ajaxurl: {
310 value: 0
311 },
312 config: {
313 value: 0
314 }
315 }
316});
317
318M.course = M.course || {};
319M.course.init_section_dragdrop = function(params) {
320 new DRAGSECTION(params);
321};
322/**
323 * Resource drag and drop.
324 *
325 * @class M.course.dragdrop.resource
326 * @constructor
327 * @extends M.core.dragdrop
328 */
329var DRAGRESOURCE = function() {
330 DRAGRESOURCE.superclass.constructor.apply(this, arguments);
331};
332Y.extend(DRAGRESOURCE, M.core.dragdrop, {
333 initializer: function() {
334 // Set group for parent class
335 this.groups = ['resource'];
336 this.samenodeclass = CSS.ACTIVITY;
337 this.parentnodeclass = CSS.SECTION;
34bcc6a9
AN
338
339 this.samenodelabel = {
340 identifier: 'afterresource',
341 component: 'moodle'
342 };
343 this.parentnodelabel = {
344 identifier: 'totopofsection',
345 component: 'moodle'
346 };
fd03a33c
AN
347
348 // Go through all sections
349 var sectionlistselector = M.course.format.get_section_selector(Y);
350 if (sectionlistselector) {
351 sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
352 this.setup_for_section(sectionlistselector);
353
354 // Initialise drag & drop for all resources/activities
355 var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
356 var del = new Y.DD.Delegate({
357 container: '.' + CSS.COURSECONTENT,
358 nodes: nodeselector,
359 target: true,
360 handles: ['.' + CSS.EDITINGMOVE],
361 dragConfig: {groups: this.groups}
362 });
363 del.dd.plug(Y.Plugin.DDProxy, {
364 // Don't move the node at the end of the drag
365 moveOnEnd: false,
366 cloneNode: true
367 });
368 del.dd.plug(Y.Plugin.DDConstrained, {
369 // Keep it inside the .course-content
370 constrain: '#' + CSS.PAGECONTENT
371 });
372 del.dd.plug(Y.Plugin.DDWinScroll);
373
374 M.course.coursebase.register_module(this);
375 M.course.dragres = this;
376 }
377 },
378
379 /**
380 * Apply dragdrop features to the specified selector or node that refers to section(s)
381 *
382 * @method setup_for_section
383 * @param {String} baseselector The CSS selector or node to limit scope to
384 */
385 setup_for_section: function(baseselector) {
386 Y.Node.all(baseselector).each(function(sectionnode) {
387 var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
388 // See if resources ul exists, if not create one
389 if (!resources) {
390 resources = Y.Node.create('<ul></ul>');
391 resources.addClass(CSS.SECTION);
392 sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
393 }
394 resources.setAttribute('data-draggroups', this.groups.join(' '));
395 // Define empty ul as droptarget, so that item could be moved to empty list
396 new Y.DD.Drop({
397 node: resources,
398 groups: this.groups,
399 padding: '20 0 20 0'
400 });
401
402 // Initialise each resource/activity in this section
403 this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
404 }, this);
405 },
406
407 /**
408 * Apply dragdrop features to the specified selector or node that refers to resource(s)
409 *
410 * @method setup_for_resource
411 * @param {String} baseselector The CSS selector or node to limit scope to
412 */
413 setup_for_resource: function(baseselector) {
414 Y.Node.all(baseselector).each(function(resourcesnode) {
2fafa90b
AN
415 var draggroups = resourcesnode.getData('draggroups');
416 if (!draggroups) {
417 // This Drop Node has not been set up. Configure it now.
418 resourcesnode.setAttribute('data-draggroups', this.groups.join(' '));
419 // Define empty ul as droptarget, so that item could be moved to empty list
420 new Y.DD.Drop({
421 node: resourcesnode,
422 groups: this.groups,
423 padding: '20 0 20 0'
424 });
425 }
426
fd03a33c
AN
427 // Replace move icons
428 var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
429 if (move) {
4ead6593 430 var sr = move.getData('sectionreturn');
e6bf10c7
DW
431 move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'),
432 CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr));
fd03a33c
AN
433 }
434 }, this);
435 },
436
437 drag_start: function(e) {
438 // Get our drag object
439 var drag = e.target;
b5c9fb4a
NM
440 if (drag.get('dragNode') === drag.get('node')) {
441 // We do not want to modify the contents of the real node.
442 // They will be the same during a keyboard drag and drop.
443 return;
444 }
fd03a33c
AN
445 drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
446 drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
447 },
448
449 drag_dropmiss: function(e) {
450 // Missed the target, but we assume the user intended to drop it
451 // on the last last ghost node location, e.drag and e.drop should be
452 // prepared by global_drag_dropmiss parent so simulate drop_hit(e).
453 this.drop_hit(e);
454 },
455
456 drop_hit: function(e) {
457 var drag = e.drag;
458 // Get a reference to our drag node
459 var dragnode = drag.get('node');
460 var dropnode = e.drop.get('node');
461
462 // Add spinner if it not there
463 var actionarea = dragnode.one(CSS.ACTIONAREA);
464 var spinner = M.util.add_spinner(Y, actionarea);
465
466 var params = {};
467
468 // Handle any variables which we must pass back through to
469 var pageparams = this.get('config').pageparams;
470 var varname;
471 for (varname in pageparams) {
472 params[varname] = pageparams[varname];
473 }
474
475 // Prepare request parameters
476 params.sesskey = M.cfg.sesskey;
477 params.courseId = this.get('courseid');
478 params['class'] = 'resource';
479 params.field = 'move';
480 params.id = Number(Y.Moodle.core_course.util.cm.getId(dragnode));
481 params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true));
482
483 if (dragnode.next()) {
484 params.beforeId = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next()));
485 }
486
487 // Do AJAX request
488 var uri = M.cfg.wwwroot + this.get('ajaxurl');
489
490 Y.io(uri, {
491 method: 'POST',
492 data: params,
493 on: {
494 start: function() {
495 this.lock_drag_handle(drag, CSS.EDITINGMOVE);
496 spinner.show();
497 },
498 success: function(tid, response) {
499 var responsetext = Y.JSON.parse(response.responseText);
500 var params = {element: dragnode, visible: responsetext.visible};
501 M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
502 this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
503 window.setTimeout(function() {
504 spinner.hide();
505 }, 250);
506 },
507 failure: function(tid, response) {
508 this.ajax_failure(response);
509 this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
510 spinner.hide();
511 // TODO: revert nodes location
512 }
513 },
3a0bc0fd 514 context: this
fd03a33c
AN
515 });
516 }
517}, {
518 NAME: 'course-dragdrop-resource',
519 ATTRS: {
520 courseid: {
521 value: null
522 },
523 ajaxurl: {
524 value: 0
525 },
526 config: {
527 value: 0
528 }
529 }
530});
531
532M.course = M.course || {};
533M.course.init_resource_dragdrop = function(params) {
534 new DRAGRESOURCE(params);
535};
536
537
538}, '@VERSION@', {
539 "requires": [
540 "base",
541 "node",
542 "io",
543 "dom",
544 "dd",
545 "dd-scroll",
546 "moodle-core-dragdrop",
547 "moodle-core-notification",
548 "moodle-course-coursebase",
549 "moodle-course-util"
550 ]
551});