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