1 YUI.add('moodle-course-toolboxes', function(Y) {
2 WAITICON = {'pix':"i/loading_small",'component':'moodle'};
3 // The CSS selectors we use
5 ACTIVITYLI : 'li.activity',
6 COMMANDSPAN : 'span.commands',
7 SPINNERCOMMANDSPAN : 'span.commands',
8 CONTENTAFTERLINK : 'div.contentafterlink',
9 DELETE : 'a.editing_delete',
11 DIMMEDTEXT : 'dimmed_text',
12 EDITTITLE : 'a.editing_title',
13 EDITTITLECLASS : 'edittitle',
14 GENERICICONCLASS : 'iconsmall',
15 GROUPSNONE : 'a.editing_groupsnone',
16 GROUPSSEPARATE : 'a.editing_groupsseparate',
17 GROUPSVISIBLE : 'a.editing_groupsvisible',
19 HIDE : 'a.editing_hide',
20 HIGHLIGHT : 'a.editing_highlight',
21 INSTANCENAME : 'span.instancename',
22 LIGHTBOX : 'lightbox',
23 MODINDENTCOUNT : 'mod-indent-',
24 MODINDENTDIV : 'div.mod-indent',
25 MODINDENTHUGE : 'mod-indent-huge',
26 MODULEIDPREFIX : 'module-',
27 MOVELEFT : 'a.editing_moveleft',
28 MOVELEFTCLASS : 'editing_moveleft',
29 MOVERIGHT : 'a.editing_moveright',
30 PAGECONTENT : 'div#page-content',
31 RIGHTDIV : 'div.right',
32 SECTIONHIDDENCLASS : 'hidden',
33 SECTIONIDPREFIX : 'section-',
34 SECTIONLI : 'li.section',
35 SHOW : 'a.editing_show',
36 SHOWHIDE : 'a.editing_showhide'
42 * TOOLBOX is a generic class which should never be directly instantiated
43 * RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources
44 * SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections
46 var TOOLBOX = function() {
47 TOOLBOX.superclass.constructor.apply(this, arguments);
50 Y.extend(TOOLBOX, Y.Base, {
52 * Replace the button click at the selector with the specified
55 * @param toolboxtarget The selector of the working area
56 * @param selector The 'button' to replace
57 * @param callback The callback to apply
58 * @param cursor An optional cursor style to apply
60 replace_button : function(toolboxtarget, selector, callback, cursor) {
62 // Set the default cursor type to pointer to match the
66 var button = Y.one(toolboxtarget).all(selector)
67 .setStyle('cursor', cursor);
69 // on isn't chainable and will return an event
70 button.on('click', callback, this);
75 * Toggle the visibility and availability for the specified
76 * resource show/hide button
78 toggle_hide_resource_ui : function(button) {
79 var element = button.ancestor(CSS.ACTIVITYLI);
80 var hideicon = button.one('img');
84 if (this.is_label(element)) {
85 toggle_class = CSS.DIMMEDTEXT;
86 dimarea = element.one(CSS.MODINDENTDIV + ' div');
88 toggle_class = CSS.DIMCLASS;
89 dimarea = element.one('a');
94 if (dimarea.hasClass(toggle_class)) {
103 dimarea.toggleClass(toggle_class);
104 // We need to toggle dimming on the description too
105 element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);
106 var newstring = M.util.get_string(status, 'moodle');
110 'src' : M.util.image_url('t/' + status)
112 button.set('title', newstring);
113 button.set('className', 'editing_'+status);
118 * Send a request using the REST API
120 * @param data The data to submit
121 * @param statusspinner (optional) A statusspinner which may contain a section loader
122 * @param optionalconfig (optional) Any additional configuration to submit
123 * @return response responseText field from responce
125 send_request : function(data, statusspinner, optionalconfig) {
126 // Default data structure
130 // Handle any variables which we must pass back through to
131 var pageparams = this.get('config').pageparams;
132 for (varname in pageparams) {
133 data[varname] = pageparams[varname];
136 data.sesskey = M.cfg.sesskey;
137 data.courseId = this.get('courseid');
139 var uri = M.cfg.wwwroot + this.get('ajaxurl');
141 // Define the configuration to send with the request
142 var responsetext = [];
147 success: function(tid, response) {
149 responsetext = Y.JSON.parse(response.responseText);
150 if (responsetext.error) {
151 new M.core.ajaxException(responsetext);
155 window.setTimeout(function(e) {
156 statusspinner.hide();
160 failure : function(tid, response) {
162 statusspinner.hide();
164 new M.core.ajaxException(response);
171 // Apply optional config
172 if (optionalconfig) {
173 for (varname in optionalconfig) {
174 config[varname] = optionalconfig[varname];
179 statusspinner.show();
186 is_label : function(target) {
187 return target.hasClass(CSS.HASLABEL);
190 * Return the module ID for the specified element
192 * @param element The <li> element to determine a module-id number for
193 * @return string The module ID
195 get_element_id : function(element) {
196 return element.get('id').replace(CSS.MODULEIDPREFIX, '');
199 * Return the module ID for the specified element
201 * @param element The <li> element to determine a module-id number for
202 * @return string The module ID
204 get_section_id : function(section) {
205 return section.get('id').replace(CSS.SECTIONIDPREFIX, '');
209 NAME : 'course-toolbox',
211 // The ID of the current course
226 var RESOURCETOOLBOX = function() {
227 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
230 Y.extend(RESOURCETOOLBOX, TOOLBOX, {
237 * Initialize the resource toolbox
239 * Updates all span.commands with relevant handlers and other required changes
241 initializer : function(config) {
242 this.setup_for_resource();
243 M.course.coursebase.register_module(this);
247 * Update any span.commands within the scope of the specified
248 * selector with AJAX equivelants
250 * @param baseselector The selector to limit scope to
253 setup_for_resource : function(baseselector) {
255 var baseselector = CSS.PAGECONTENT + ' ' + CSS.ACTIVITYLI;;
258 Y.all(baseselector).each(this._setup_for_resource, this);
260 _setup_for_resource : function(toolboxtarget) {
262 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.EDITTITLE, this.edit_resource_title);
264 // Move left and right
265 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVELEFT, this.move_left);
266 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVERIGHT, this.move_right);
269 this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.DELETE, this.delete_resource);
272 var showhide = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.HIDE, this.toggle_hide_resource);
273 var shown = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.SHOW, this.toggle_hide_resource);
275 showhide = showhide.concat(shown);
276 showhide.each(function(node) {
277 var section = node.ancestor(CSS.SECTIONLI);
278 if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
279 node.setStyle('cursor', 'auto');
285 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE, this.toggle_groupmode);
286 groups.setAttribute('groupmode', this.GROUPS_NONE);
288 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE, this.toggle_groupmode);
289 groups.setAttribute('groupmode', this.GROUPS_SEPARATE);
291 groups = this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE, this.toggle_groupmode);
292 groups.setAttribute('groupmode', this.GROUPS_VISIBLE);
294 move_left : function(e) {
295 this.move_leftright(e, -1);
297 move_right : function(e) {
298 this.move_leftright(e, 1);
300 move_leftright : function(e, direction) {
301 // Prevent the default button action
304 // Get the element we're working on
305 var element = e.target.ancestor(CSS.ACTIVITYLI);
307 // And we need to determine the current and new indent level
308 var indentdiv = element.one(CSS.MODINDENTDIV);
309 var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/);
312 var oldindent = parseInt(indent[1]);
313 var newindent = Math.max(0, (oldindent + parseInt(direction)));
314 indentdiv.removeClass(indent[0]);
321 indentdiv.addClass(CSS.MODINDENTCOUNT + newindent);
323 'class' : 'resource',
326 'id' : this.get_element_id(element)
328 var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
329 this.send_request(data, spinner);
331 // Handle removal/addition of the moveleft button
332 if (newindent == 0) {
333 element.one(CSS.MOVELEFT).remove();
334 } else if (newindent == 1 && oldindent == 0) {
335 this.add_moveleft(element);
338 // Handle massive indentation to match non-ajax display
339 var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE);
340 if (newindent > 15 && !hashugeclass) {
341 indentdiv.addClass(CSS.MODINDENTHUGE);
342 } else if (newindent <= 15 && hashugeclass) {
343 indentdiv.removeClass(CSS.MODINDENTHUGE);
346 delete_resource : function(e) {
347 // Prevent the default button action
350 // Get the element we're working on
351 var element = e.target.ancestor(CSS.ACTIVITYLI);
353 var confirmstring = '';
354 if (this.is_label(element)) {
355 // Labels are slightly different to other activities
357 type : M.util.get_string('pluginname', 'label')
359 confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata)
362 type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]),
363 name : element.one(CSS.INSTANCENAME).get('firstChild').get('data')
365 confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata);
368 // Confirm element removal
369 if (!confirm(confirmstring)) {
373 // Actually remove the element
376 'class' : 'resource',
378 'id' : this.get_element_id(element)
380 this.send_request(data);
382 toggle_hide_resource : function(e) {
383 // Prevent the default button action
386 // Return early if the current section is hidden
387 var section = e.target.ancestor(CSS.SECTIONLI);
388 if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
392 // Get the element we're working on
393 var element = e.target.ancestor(CSS.ACTIVITYLI);
395 var button = e.target.ancestor('a', true);
397 var value = this.toggle_hide_resource_ui(button);
401 'class' : 'resource',
404 'id' : this.get_element_id(element)
406 var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
407 this.send_request(data, spinner);
409 toggle_groupmode : function(e) {
410 // Prevent the default button action
413 // Get the element we're working on
414 var element = e.target.ancestor(CSS.ACTIVITYLI);
416 var button = e.target.ancestor('a', true);
417 var icon = button.one('img');
420 var groupmode = button.getAttribute('groupmode');
425 button.setAttribute('groupmode', groupmode);
430 case this.GROUPS_NONE:
431 newtitle = 'groupsnone';
432 iconsrc = M.util.image_url('t/groupn');
434 case this.GROUPS_SEPARATE:
435 newtitle = 'groupsseparate';
436 iconsrc = M.util.image_url('t/groups');
438 case this.GROUPS_VISIBLE:
439 newtitle = 'groupsvisible';
440 iconsrc = M.util.image_url('t/groupv');
443 newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle',
444 M.util.get_string(newtitle, 'moodle'));
452 button.setAttribute('title', newtitle);
454 // And send the request
456 'class' : 'resource',
457 'field' : 'groupmode',
459 'id' : this.get_element_id(element)
461 var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN));
462 this.send_request(data, spinner);
465 * Add the moveleft button
466 * This is required after moving left from an initial position of 0
468 * @param target The encapsulating <li> element
470 add_moveleft : function(target) {
471 var left_string = M.util.get_string('moveleft', 'moodle');
472 var newicon = Y.Node.create('<img />')
473 .addClass(CSS.GENERICICONCLASS)
475 'src' : M.util.image_url('t/left', 'moodle'),
476 'title' : left_string,
479 var moveright = target.one(CSS.MOVERIGHT);
480 var newlink = moveright.getAttribute('href').replace('indent=1', 'indent=-1');
481 var anchor = new Y.Node.create('<a />')
482 .setStyle('cursor', 'pointer')
483 .addClass(CSS.MOVELEFTCLASS)
484 .setAttribute('href', newlink)
485 .setAttribute('title', left_string);
486 anchor.appendChild(newicon);
487 anchor.on('click', this.move_left, this);
488 moveright.insert(anchor, 'before');
491 * Edit the title for the resource
493 edit_resource_title : function(e) {
494 // Get the element we're working on
495 var element = e.target.ancestor(CSS.ACTIVITYLI);
496 var instancename = element.one(CSS.INSTANCENAME);
497 var currenttitle = instancename.get('firstChild');
498 var oldtitle = currenttitle.get('data');
499 var titletext = oldtitle;
500 var editbutton = element.one('a.' + CSS.EDITTITLECLASS + ' img');
502 // Disable the current href to prevent redirections when editing
503 var anchor = instancename.ancestor('a');
504 anchor.setAttribute('oldhref', anchor.getAttribute('href'));
505 anchor.removeAttribute('href');
508 'class' : 'resource',
509 'field' : 'gettitle',
510 'id' : this.get_element_id(element)
513 // Try to retrieve the existing string from the server
514 var response = this.send_request(data, editbutton);
515 if (response.instancename) {
516 titletext = response.instancename;
519 // Create the editor and submit button
520 var editor = Y.Node.create('<input />')
524 'autocomplete' : 'off'
526 .addClass('titleeditor');
527 var editform = Y.Node.create('<form />')
528 .setStyle('padding', '0')
529 .setStyle('display', 'inline')
530 .setAttribute('action', '#');
532 var editinstructions = Y.Node.create('<span />')
533 .addClass('editinstructions')
534 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
536 // Clear the existing content and put the editor in
537 currenttitle.set('data', '');
538 editform.appendChild(editor);
539 instancename.appendChild(editform);
540 element.appendChild(editinstructions);
543 // Focus and select the editor text
544 editor.focus().select();
546 // Handle cancellation of the editor
547 editor.on('blur', function(e) {
548 // Detach the blur event before removing as some actions trigger multiple blurs in
550 editor.detach('blur');
552 editinstructions.remove();
554 // Set the title and anchor back to their previous settings
555 currenttitle.set('data', oldtitle);
556 anchor.setAttribute('href', anchor.getAttribute('oldhref'));
557 anchor.removeAttribute('oldhref');
560 // Handle form submission
561 editform.on('submit', function(e) {
562 // We don't actually want to submit anything
565 // Detach the handlers to prevent multiple submissions
566 editform.detach('submit');
567 editor.detach('blur');
569 // We only accept strings which have valid content
570 var newtitle = Y.Lang.trim(editor.get('value'));
571 if (newtitle != null && newtitle != "" && newtitle != titletext) {
573 'class' : 'resource',
574 'field' : 'updatetitle',
576 'id' : this.get_element_id(element)
578 var response = this.send_request(data, editbutton);
579 if (response.instancename) {
580 currenttitle.set('data', response.instancename);
583 // Invalid content. Set the title back to it's original contents
584 currenttitle.set('data', oldtitle);
588 editinstructions.remove();
590 // We need a timeout here otherwise hitting return to save in some browsers triggers
592 setTimeout(function(e) {
593 anchor.setAttribute('href', anchor.getAttribute('oldhref'));
594 anchor.removeAttribute('oldhref');
599 NAME : 'course-resource-toolbox',
610 var SECTIONTOOLBOX = function() {
611 SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
614 Y.extend(SECTIONTOOLBOX, TOOLBOX, {
616 * Initialize the toolboxes module
618 * Updates all span.commands with relevant handlers and other required changes
620 initializer : function(config) {
621 this.setup_for_section();
622 M.course.coursebase.register_module(this);
625 * Update any section areas within the scope of the specified
626 * selector with AJAX equivelants
628 * @param baseselector The selector to limit scope to
631 setup_for_section : function(baseselector) {
633 var baseselector = CSS.PAGECONTENT;
636 Y.all(baseselector).each(this._setup_for_section, this);
638 _setup_for_section : function(toolboxtarget) {
639 // Section Highlighting
640 this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.HIGHLIGHT, this.toggle_highlight);
642 // Section Visibility
643 this.replace_button(toolboxtarget, CSS.RIGHTDIV + ' ' + CSS.SHOWHIDE, this.toggle_hide_section);
645 toggle_hide_section : function(e) {
646 // Prevent the default button action
649 // Get the section we're working on
650 var section = e.target.ancestor(CSS.SECTIONLI);
651 var button = e.target.ancestor('a', true);
652 var hideicon = button.one('img');
654 // The value to submit
656 // The status text for strings and images
659 if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
660 section.addClass(CSS.SECTIONHIDDENCLASS);
665 section.removeClass(CSS.SECTIONHIDDENCLASS);
670 var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format'));
674 'src' : M.util.image_url('i/' + status)
676 button.set('title', newstring);
678 // Change the highlight status
682 'id' : this.get_section_id(section),
686 var lightbox = M.util.add_lightbox(Y, section);
689 var response = this.send_request(data, lightbox);
691 var activities = section.all(CSS.ACTIVITYLI);
692 activities.each(function(node) {
693 if (node.one(CSS.SHOW)) {
694 var button = node.one(CSS.SHOW);
696 var button = node.one(CSS.HIDE);
698 var activityid = this.get_element_id(node);
700 if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
701 this.toggle_hide_resource_ui(button);
705 button.setStyle('cursor', 'auto');
707 button.setStyle('cursor', 'pointer');
711 toggle_highlight : function(e) {
712 // Prevent the default button action
715 // Get the section we're working on
716 var section = e.target.ancestor(CSS.SECTIONLI);
717 var button = e.target.ancestor('a', true);
718 var buttonicon = button.one('img');
720 // Determine whether the marker is currently set
721 var togglestatus = section.hasClass('current');
724 // Set the current highlighted item text
725 var old_string = M.util.get_string('markthistopic', 'moodle');
726 Y.one(CSS.PAGECONTENT)
727 .all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT)
728 .set('title', old_string);
729 Y.one(CSS.PAGECONTENT)
730 .all(CSS.SECTIONLI + '.current ' + CSS.HIGHLIGHT + ' img')
731 .set('title', old_string)
732 .set('alt', old_string)
733 .set('src', M.util.image_url('i/marker'));
735 // Remove the highlighting from all sections
736 var allsections = Y.one(CSS.PAGECONTENT).all(CSS.SECTIONLI)
737 .removeClass('current');
739 // Then add it if required to the selected section
741 section.addClass('current');
742 value = this.get_section_id(section);
743 var new_string = M.util.get_string('markedthistopic', 'moodle');
745 .set('title', new_string);
747 .set('title', new_string)
748 .set('alt', new_string)
749 .set('src', M.util.image_url('i/marked'));
752 // Change the highlight status
758 var lightbox = M.util.add_lightbox(Y, section);
760 this.send_request(data, lightbox);
763 NAME : 'course-section-toolbox',
774 M.course = M.course || {};
776 M.course.init_resource_toolbox = function(config) {
777 return new RESOURCETOOLBOX(config);
780 M.course.init_section_toolbox = function(config) {
781 return new SECTIONTOOLBOX(config);
786 requires : ['base', 'node', 'io', 'moodle-course-coursebase']