MDL-31096 Convert course resource/section javascript to YUI3 Module
[moodle.git] / course / yui / toolboxes / toolboxes.js
CommitLineData
ebaa29d1
ARN
1YUI.add('moodle-course-toolboxes', function(Y) {
2 WAITICON = {'pix':"i/loading_small",'component':'moodle'};
3 // The CSS selectors we use
4 var CSS = {
5 ACTIVITYLI : 'li.activity',
6 COMMANDSPAN : 'span.commands',
7 DELETE : 'a.editing_delete',
8 DIMCLASS : 'dimmed',
9 DIMMEDLABELCLASS : 'dimmed_text',
10 EDITTITLECLASS : 'edittitle',
11 GENERICICONCLASS : 'iconsmall',
12 GROUPSNONE : 'a.editing_groupsnone',
13 GROUPSSEPARATE : 'a.editing_groupsseparate',
14 GROUPSVISIBLE : 'a.editing_groupsvisible',
15 HASLABEL : 'label',
16 HIDE : 'a.editing_hide',
17 HIGHLIGHT : 'a.editing_highlight',
18 INSTANCENAME : 'span.instancename',
19 LIGHTBOX : 'lightbox',
20 MODINDENTCOUNT : 'mod-indent-',
21 MODINDENTDIV : 'div.mod-indent',
22 MODULEIDPREFIX : 'module-',
23 MOVELEFT : 'a.editing_moveleft',
24 MOVELEFTCLASS : 'editing_moveleft',
25 MOVERIGHT : 'a.editing_moveright',
26 PAGECONTENT : 'div#page-content',
27 RIGHTDIV : 'div.right',
28 SECTIONHIDDENCLASS : 'hidden',
29 SECTIONIDPREFIX : 'section-',
30 SECTIONLI : 'li.section',
31 SHOW : 'a.editing_show',
32 SHOWHIDE : 'a.editing_showhide'
33 };
34
35 /**
36 * The toolbox classes
37 *
38 * TOOLBOX is a generic class which should never be directly instantiated
39 * RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources
40 * SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections
41 */
42 var TOOLBOX = function() {
43 TOOLBOX.superclass.constructor.apply(this, arguments);
44 }
45
46 Y.extend(TOOLBOX, Y.Base, {
47 /**
48 * Replace the button click at the selector with the specified
49 * callback
50 *
51 * @param toolboxtarget The selector of the working area
52 * @param selector The 'button' to replace
53 * @param callback The callback to apply
54 * @param cursor An optional cursor style to apply
55 */
56 replace_button : function(toolboxtarget, selector, callback, cursor) {
57 if (!cursor) {
58 // Set the default cursor type to pointer to match the
59 // anchor
60 cursor = 'pointer';
61 }
62 var button = Y.one(toolboxtarget).all(selector)
63 .removeAttribute('href')
64 .setStyle('cursor', cursor);
65
66 // on isn't chainable and will return an event
67 button.on('click', callback, this);
68
69 return button;
70 },
71 /**
72 * Toggle the visibility and availability for the specified
73 * resource show/hide button
74 */
75 toggle_hide_resource_ui : function(button) {
76 var element = button.ancestor(CSS.ACTIVITYLI);
77 var hideicon = button.one('img');
78
79 var dimarea;
80 var toggle_class;
81 if (this.is_label(element)) {
82 toggle_class = CSS.DIMMEDLABELCLASS;
83 dimarea = element.one(CSS.MODINDENTDIV + ' div');
84 } else {
85 toggle_class = CSS.DIMCLASS;
86 dimarea = element.one('a');
87 }
88
89 var status = '';
90 var value;
91 if (dimarea.hasClass(toggle_class)) {
92 status = 'hide';
93 value = 1;
94 } else {
95 status = 'show';
96 value = 0;
97 }
98
99 // Change the UI
100 dimarea.toggleClass(toggle_class);
101 var newstring = M.util.get_string(status, 'moodle');
102 hideicon.setAttrs({
103 'alt' : newstring,
104 'title' : newstring,
105 'src' : M.util.image_url('t/' + status)
106 });
107 button.set('title', newstring);
108 button.set('className', 'editing_'+status);
109
110 return value;
111 },
112 /**
113 * Send a request using the REST API
114 *
115 * @param data The data to submit
116 * @param loadingiconat (optional) Show the loading icon spinner at the specified location (replaces text)
117 * @param lightbox (optional) A lightbox which may contain a section loader
118 * @param optionalconfig (optional) Any additional configuration to submit
119 * @return response responseText field from responce
120 */
121 send_request : function(data, loadingiconat, lightbox, optionalconfig) {
122 // Default data structure
123 if (!data) {
124 data = {};
125 }
126 // Handle any variables which we must pass back through to
127 var pageparams = this.get('config').pageparams;
128 for (varname in pageparams) {
129 data[varname] = pageparams[varname];
130 }
131
132 // Make a note of the icon for displaying the loadingicon spinner
133 var originalicon;
134 if (loadingiconat) {
135 originalicon = loadingiconat.getAttribute('src');
136 }
137
138 data.sesskey = M.cfg.sesskey;
139 data.courseId = this.get('courseid');
140
141 var uri = M.cfg.wwwroot + this.get('ajaxurl');
142
143 // Define the configuration to send with the request
144 var responsetext = [];
145 var config = {
146 method: 'POST',
147 data: data,
148 on: {
149 success: function(tid, response) {
150 try {
151 responsetext = Y.JSON.parse(response.responseText);
152 if (responsetext.error) {
153 new M.core.ajaxException(responsetext);
154 }
155 } catch (e) {}
156 if (originalicon) {
157 // Replace the spinner with the original icon We use a pause to give
158 // positive feedback that something is happening
159 window.setTimeout(function(e) {
160 loadingiconat.setAttribute('src', originalicon);
161 }, 250);
162 }
163 if (lightbox) {
164 window.setTimeout(function(e) {
165 lightbox.hide();
166 }, 250);
167 }
168 },
169 failure : function(tid, response) {
170 if (originalicon) {
171 loadingiconat.setAttribute('src', originalicon);
172 }
173 if (lightbox) {
174 lightbox.hide();
175 }
176 new M.core.ajaxException(response);
177 }
178 },
179 context: this,
180 sync: true
181 }
182
183 // Apply optional config
184 if (optionalconfig) {
185 for (varname in optionalconfig) {
186 config[varname] = optionalconfig[varname];
187 }
188 }
189
190 if (loadingiconat) {
191 loadingiconat.removeAttribute('innerHTML');
192 loadingiconat.set('src', M.util.image_url(WAITICON.pix, WAITICON.component));
193 }
194
195 // Send the request
196 Y.io(uri, config);
197 return responsetext;
198 },
199 is_label : function(target) {
200 return target.hasClass(CSS.HASLABEL);
201 },
202 /**
203 * Return the module ID for the specified element
204 *
205 * @param element The <li> element to determine a module-id number for
206 * @return string The module ID
207 */
208 get_element_id : function(element) {
209 return element.get('id').replace(CSS.MODULEIDPREFIX, '');
210 },
211 /**
212 * Return the module ID for the specified element
213 *
214 * @param element The <li> element to determine a module-id number for
215 * @return string The module ID
216 */
217 get_section_id : function(section) {
218 return section.get('id').replace(CSS.SECTIONIDPREFIX, '');
219 },
220 add_lightbox: function(node) {
221 node.setStyle('position', 'relative');
222 var waiticon = Y.Node.create('<img />')
223 .setAttrs({
224 'src' : M.util.image_url(WAITICON.pix, WAITICON.component),
225 'title' : M.str.moodle.move,
226 'hspace' : '3'
227 })
228 .setStyles({
229 'position' : 'relative',
230 'top' : '50%'
231 });
232
233 var lightbox = Y.Node.create('<div></div>')
234 .setStyles({
235 'opacity' : '.75',
236 'position' : 'absolute',
237 'width' : '100%',
238 'height' : '100%',
239 'top' : 0,
240 'left' : 0,
241 'backgroundColor' : 'white',
242 'text-align' : 'center'
243 })
244 .setAttribute('class', 'lightbox')
245 .hide();
246 lightbox.appendChild(waiticon);
247 node.append(lightbox);
248 return lightbox;
249 }
250 },
251 {
252 NAME : 'course-toolbox',
253 ATTRS : {
254 // The ID of the current course
255 courseid : {
256 'value' : 0
257 },
258 ajaxurl : {
259 'value' : 0
260 },
261 config : {
262 'value' : 0
263 }
264 }
265 }
266 );
267
268
269 var RESOURCETOOLBOX = function() {
270 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
271 }
272
273 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
274 // Variables
275 GROUPS_NONE : 0,
276 GROUPS_SEPARATE : 1,
277 GROUPS_VISIBLE : 2,
278
279 /**
280 * Initialize the resource toolbox
281 *
282 * Updates all span.commands with relevant handlers and other required changes
283 */
284 initializer : function(config) {
285 this.setup_for_resource();
286 M.course.coursebase.register_module(this);
287 },
288
289 /**
290 * Update any span.commands within the scope of the specified
291 * selector with AJAX equivelants
292 *
293 * @param baseselector The selector to limit scope to
294 * @return void
295 */
296 setup_for_resource : function(baseselector) {
297 if (!baseselector) {
298 var baseselector = CSS.PAGECONTENT;
299 }
300
301 Y.all(baseselector).each(this._setup_for_resource, this);
302 },
303 _setup_for_resource : function(toolboxtarget) {
304 // Move left and right
305 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVELEFT, this.move_left);
306 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVERIGHT, this.move_right);
307
308 // Delete
309 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.DELETE, this.delete_resource);
310
311 // Show/Hide
312 var showhide = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.HIDE, this.toggle_hide_resource);
313 var shown = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.SHOW, this.toggle_hide_resource);
314
315 showhide = showhide.concat(shown);
316 showhide.each(function(node) {
317 var section = node.ancestor(CSS.SECTIONLI);
318 if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
319 node.setStyle('cursor', 'auto');
320 }
321 });
322
323 // Change Group Mode
324 var groups;
325 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE, this.toggle_groupmode);
326 groups.setAttribute('groupmode', this.GROUPS_NONE);
327
328 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE, this.toggle_groupmode);
329 groups.setAttribute('groupmode', this.GROUPS_SEPARATE);
330
331 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE, this.toggle_groupmode);
332 groups.setAttribute('groupmode', this.GROUPS_VISIBLE);
333 },
334 move_left : function(e) {
335 this.move_leftright(e, -1, CSS.MOVELEFT);
336 },
337 move_right : function(e) {
338 this.move_leftright(e, 1, CSS.MOVERIGHT);
339 },
340 move_leftright : function(e, direction, buttonselector) {
341 // Get the element we're working on
342 var element = e.target.ancestor(CSS.ACTIVITYLI);
343
344 // And we need to determine the current and new indent level
345 var indentdiv = element.one(CSS.MODINDENTDIV);
346 var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/);
347
348 if (indent) {
349 var oldindent = parseInt(indent[1]);
350 var newindent = Math.max(0, (oldindent + parseInt(direction)));
351 indentdiv.removeClass(indent[0]);
352 } else {
353 var oldindent = 0;
354 var newindent = 1;
355 }
356
357 // Perform the move
358 indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
359 var data = {
360 'class' : 'resource',
361 'field' : 'indent',
362 'value' : newindent,
363 'id' : this.get_element_id(element)
364 };
365 var editbutton = element.one(buttonselector + ' img');
366 this.send_request(data, editbutton);
367
368 // Handle removal/addition of the moveleft button
369 if (newindent == 0) {
370 window.setTimeout(function(e) {
371 element.one(CSS.MOVELEFT).remove();
372 }, 250);
373 } else if (newindent == 1 && oldindent == 0) {
374 this.add_moveleft(element);
375 }
376 },
377 delete_resource : function(e) {
378 // Get the element we're working on
379 var element = e.target.ancestor(CSS.ACTIVITYLI);
380
381 var confirmstring = '';
382 if (this.is_label(element)) {
383 // Labels are slightly different to other activities
384 var plugindata = {
385 type : M.util.get_string('pluginname', 'label')
386 }
387 confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
388 } else {
389 var plugindata = {
390 type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]),
391 name : element.one(CSS.INSTANCENAME).get('firstChild').get('data')
392 }
393 confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
394 }
395
396 // Confirm element removal
397 if (!confirm(confirmstring)) {
398 return false;
399 }
400
401 // Actually remove the element
402 element.remove();
403 var data = {
404 'class' : 'resource',
405 'action' : 'DELETE',
406 'id' : this.get_element_id(element)
407 };
408 this.send_request(data);
409 },
410 toggle_hide_resource : function(e) {
411 // Return early if the current section is hidden
412 var section = e.target.ancestor(CSS.SECTIONLI);
413 if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
414 return;
415 }
416
417 // Get the element we're working on
418 var element = e.target.ancestor(CSS.ACTIVITYLI);
419
420 var button = e.target.ancestor('a', true);
421
422 var value = this.toggle_hide_resource_ui(button);
423
424 // Send the request
425 var data = {
426 'class' : 'resource',
427 'field' : 'visible',
428 'value' : value,
429 'id' : this.get_element_id(element)
430 };
431 this.send_request(data, button.one('img'));
432 },
433 toggle_groupmode : function(e) {
434 // Get the element we're working on
435 var element = e.target.ancestor(CSS.ACTIVITYLI);
436
437 var button = e.target.ancestor('a', true);
438 var icon = button.one('img');
439
440 // Current Mode
441 var groupmode = button.getAttribute('groupmode');
442 groupmode++;
443 if (groupmode > 2) {
444 groupmode = 0;
445 }
446 button.setAttribute('groupmode', groupmode);
447
448 var newtitle = '';
449 var iconsrc = '';
450 switch (groupmode) {
451 case this.GROUPS_NONE:
452 newtitle = 'groupsnone';
453 iconsrc = M.util.image_url('t/groupn');
454 break;
455 case this.GROUPS_SEPARATE:
456 newtitle = 'groupsseparate';
457 iconsrc = M.util.image_url('t/groups');
458 break;
459 case this.GROUPS_VISIBLE:
460 newtitle = 'groupsvisible';
461 iconsrc = M.util.image_url('t/groupv');
462 break;
463 }
464 newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle',
465 M.util.get_string(newtitle, 'moodle'));
466
467 // Change the UI
468 icon.setAttrs({
469 'alt' : newtitle,
470 'title' : newtitle,
471 'src' : iconsrc
472 });
473 button.setAttribute('title', newtitle);
474
475 // And send the request
476 var data = {
477 'class' : 'resource',
478 'field' : 'groupmode',
479 'value' : groupmode,
480 'id' : this.get_element_id(element)
481 };
482 this.send_request(data, icon);
483 },
484 /**
485 * Add the moveleft button
486 * This is required after moving left from an initial position of 0
487 *
488 * @param target The encapsulating <li> element
489 */
490 add_moveleft : function(target) {
491 var left_string = M.util.get_string('moveleft', 'moodle');
492 var newicon = Y.Node.create('<img />')
493 .addClass(CSS.GENERICICONCLASS)
494 .setAttrs({
495 'src' : M.util.image_url('t/left', 'moodle'),
496 'title' : left_string,
497 'alt' : left_string
498 });
499 var anchor = new Y.Node.create('<a />')
500 .setStyle('cursor', 'pointer')
501 .addClass(CSS.MOVELEFTCLASS)
502 .set('title', left_string);
503 anchor.appendChild(newicon);
504 anchor.on('click', this.move_left, this);
505 target.one(CSS.MOVERIGHT).insert(anchor, 'before');
506 }
507 }, {
508 NAME : 'course-resource-toolbox',
509 ATTRS : {
510 courseid : {
511 'value' : 0
512 },
513 format : {
514 'value' : 'topics'
515 }
516 }
517 });
518
519 var SECTIONTOOLBOX = function() {
520 SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
521 }
522
523 Y.extend(SECTIONTOOLBOX, TOOLBOX, {
524 /**
525 * Initialize the toolboxes module
526 *
527 * Updates all span.commands with relevant handlers and other required changes
528 */
529 initializer : function(config) {
530 this.setup_for_section();
531 M.course.coursebase.register_module(this);
532 },
533 /**
534 * Update any section areas within the scope of the specified
535 * selector with AJAX equivelants
536 *
537 * @param baseselector The selector to limit scope to
538 * @return void
539 */
540 setup_for_section : function(baseselector) {
541 if (!baseselector) {
542 var baseselector = CSS.PAGECONTENT;
543 }
544
545 Y.all(baseselector).each(this._setup_for_section, this);
546 },
547 _setup_for_section : function(toolboxtarget) {
548 // Section Highlighting
549 this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.HIGHLIGHT, this.toggle_highlight);
550
551 // Section Visibility
552 this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.SHOWHIDE, this.toggle_hide_section);
553 },
554 toggle_hide_section : function(e) {
555 // Get the section we're working on
556 var section = e.target.ancestor(CSS.SECTIONLI);
557 var button = e.target.ancestor('a', true);
558 var hideicon = button.one('img');
559
560 // The value to submit
561 var value;
562 // The status text for strings and images
563 var status;
564
565 if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
566 section.addClass(CSS.SECTIONHIDDENCLASS);
567 value = 0;
568 status = 'show';
569
570 } else {
571 section.removeClass(CSS.SECTIONHIDDENCLASS);
572 value = 1;
573 status = 'hide';
574 }
575
576 var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
577 hideicon.setAttrs({
578 'alt' : newstring,
579 'title' : newstring,
580 'src' : M.util.image_url('i/' + status)
581 });
582 button.set('title', newstring);
583
584 // Change the highlight status
585 var data = {
586 'class' : 'section',
587 'field' : 'visible',
588 'id' : this.get_section_id(section),
589 'value' : value
590 };
591
592 var lightbox = this.add_lightbox(section);
593 lightbox.show();
594
595 var response = this.send_request(data, null, lightbox);
596
597 var activities = section.all(CSS.ACTIVITYLI);
598 activities.each(function(node) {
599 if (node.one(CSS.SHOW)) {
600 var button = node.one(CSS.SHOW);
601 } else {
602 var button = node.one(CSS.HIDE);
603 }
604 var activityid = this.get_element_id(node);
605
606 if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
607 this.toggle_hide_resource_ui(button);
608 }
609
610 if (value == 0) {
611 button.setStyle('cursor', 'auto');
612 } else {
613 button.setStyle('cursor', 'pointer');
614 }
615 }, this);
616 },
617 toggle_highlight : function(e) {
618 // Get the section we're working on
619 var section = e.target.ancestor(CSS.SECTIONLI);
620 var button = e.target.ancestor('a', true);
621 var buttonicon = button.one('img');
622
623 // Determine whether the marker is currently set
624 var togglestatus = section.hasClass('current');
625 var value = 0;
626
627 // Set the current highlighted item text
628 var old_string = M.util.get_string('markthistopic', 'moodle');
629 Y.one(CSS.PAGECONTENT)
630 .all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT)
631 .set('title', old_string);
632 Y.one(CSS.PAGECONTENT)
633 .all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT + ' img')
634 .set('title', old_string)
635 .set('alt', old_string)
636 .set('src', M.util.image_url('i/marker'));
637
638 // Remove the highlighting from all sections
639 var allsections = Y.one(CSS.PAGECONTENT).all(CSS.SECTIONLI)
640 .removeClass('current');
641
642 // Then add it if required to the selected section
643 if (!togglestatus) {
644 section.addClass('current');
645 value = this.get_section_id(section);
646 var new_string = M.util.get_string('markedthistopic', 'moodle');
647 button
648 .set('title', new_string);
649 buttonicon
650 .set('title', new_string)
651 .set('alt', new_string)
652 .set('src', M.util.image_url('i/marked'));
653 }
654
655 // Change the highlight status
656 var data = {
657 'class' : 'course',
658 'field' : 'marker',
659 'value' : value
660 };
661 var lightbox = this.add_lightbox(section);
662 lightbox.show();
663 this.send_request(data, null, lightbox);
664 }
665 }, {
666 NAME : 'course-section-toolbox',
667 ATTRS : {
668 courseid : {
669 'value' : 0
670 },
671 format : {
672 'value' : 'topics'
673 }
674 }
675 });
676
677 M.course = M.course || {};
678
679 M.course.init_resource_toolbox = function(config) {
680 return new RESOURCETOOLBOX(config);
681 };
682
683 M.course.init_section_toolbox = function(config) {
684 return new SECTIONTOOLBOX(config);
685 };
686
687},
688'@VERSION@', {
689 requires : ['base', 'node', 'io', 'moodle-course-coursebase']
690}
691);