Merge branch 'wip-MDL-36287-m25' of git://github.com/samhemelryk/moodle
[moodle.git] / blocks / navigation / yui / navigation / navigation.js
1 YUI.add('moodle-block_navigation-navigation', function(Y){
3 /**
4  * A 'actionkey' Event to help with Y.delegate().
5  * The event consists of the left arrow, right arrow, enter and space keys.
6  * More keys can be mapped to action meanings.
7  * actions: collapse , expand, toggle, enter.
8  *
9  * This event is delegated to branches in the navigation tree.
10  * The on() method to subscribe allows specifying the desired trigger actions as JSON.
11  *
12  * Todo: This could be centralised, a similar Event is defined in blocks/dock.js
13  */
14 Y.Event.define("actionkey", {
15    // Webkit and IE repeat keydown when you hold down arrow keys.
16     // Opera links keypress to page scroll; others keydown.
17     // Firefox prevents page scroll via preventDefault() on either
18     // keydown or keypress.
19     _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
21     _keys: {
22         //arrows
23         '37': 'collapse',
24         '39': 'expand',
25         //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
26         '32': 'toggle',
27         '13': 'enter'
28     },
30     _keyHandler: function (e, notifier, args) {
31         if (!args.actions) {
32             var actObj = {collapse:true, expand:true, toggle:true, enter:true};
33         } else {
34             var actObj = args.actions;
35         }
36         if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
37             e.action = this._keys[e.keyCode];
38             notifier.fire(e);
39         }
40     },
42     on: function (node, sub, notifier) {
43         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
44         if (sub.args == null) {
45             //no actions given
46             sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
47         } else {
48             sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
49         }
50     },
52     detach: function (node, sub, notifier) {
53         //detach our _detacher handle of the subscription made in on()
54         sub._detacher.detach();
55     },
57     delegate: function (node, sub, notifier, filter) {
58         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
59         if (sub.args == null) {
60             //no actions given
61             sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
62         } else {
63             sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
64         }
65     },
67     detachDelegate: function (node, sub, notifier) {
68         sub._delegateDetacher.detach();
69     }
70 });
72 var EXPANSIONLIMIT_EVERYTHING = 0,
73     EXPANSIONLIMIT_COURSE     = 20,
74     EXPANSIONLIMIT_SECTION    = 30,
75     EXPANSIONLIMIT_ACTIVITY   = 40;
77 /**
78  * Mappings for the different types of nodes coming from the navigation.
79  * Copied from lib/navigationlib.php navigation_node constants.
80  * @type object
81  */
82 var NODETYPE = {
83     /** @type int Root node = 0 */
84     ROOTNODE : 0,
85     /** @type int System context = 1 */
86     SYSTEM : 1,
87     /** @type int Course category = 10 */
88     CATEGORY : 10,
89     /** @type int MYCATEGORY = 11 */
90     MYCATEGORY : 11,
91     /** @type int Course = 20 */
92     COURSE : 20,
93     /** @type int Course section = 30 */
94     SECTION : 30,
95     /** @type int Activity (course module) = 40 */
96     ACTIVITY : 40,
97     /** @type int Resource (course module = 50 */
98     RESOURCE : 50,
99     /** @type int Custom node (could be anything) = 60 */
100     CUSTOM : 60,
101     /** @type int Setting = 70 */
102     SETTING : 70,
103     /** @type int User context = 80 */
104     USER : 80,
105     /** @type int Container = 90 */
106     CONTAINER : 90
109 /**
110  * Navigation tree class.
111  *
112  * This class establishes the tree initially, creating expandable branches as
113  * required, and delegating the expand/collapse event.
114  */
115 var TREE = function(config) {
116     TREE.superclass.constructor.apply(this, arguments);
118 TREE.prototype = {
119     /**
120      * The tree's ID, normally its block instance id.
121      */
122     id : null,
123     /**
124      * An array of initialised branches.
125      */
126     branches : [],
127     /**
128      * Initialise the tree object when its first created.
129      */
130     initializer : function(config) {
131         this.id = config.id;
133         var node = Y.one('#inst'+config.id);
135         // Can't find the block instance within the page
136         if (node === null) {
137             return;
138         }
140         // Delegate event to toggle expansion
141         Y.delegate('click', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
142         Y.delegate('actionkey', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this);
144         // Gather the expandable branches ready for initialisation.
145         var expansions = [];
146         if (config.expansions) {
147             expansions = config.expansions;
148         } else if (window['navtreeexpansions'+config.id]) {
149             expansions = window['navtreeexpansions'+config.id];
150         }
151         // Establish each expandable branch as a tree branch.
152         for (var i in expansions) {
153             var branch = new BRANCH({
154                 tree:this,
155                 branchobj:expansions[i],
156                 overrides : {
157                     expandable : true,
158                     children : [],
159                     haschildren : true
160                 }
161             }).wire();
162             M.block_navigation.expandablebranchcount++;
163             this.branches[branch.get('id')] = branch;
164         }
165         if (M.block_navigation.expandablebranchcount > 0) {
166             // Delegate some events to handle AJAX loading.
167             Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
168             Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
169         }
171         // Call the generic blocks init method to add all the generic stuff
172         if (this.get('candock')) {
173             this.initialise_block(Y, node);
174         }
175     },
176     /**
177      * Fire actions for a branch when an event occurs.
178      */
179     fire_branch_action : function(event) {
180         var id = event.currentTarget.getAttribute('id');
181         var branch = this.branches[id];
182         branch.ajaxLoad(event);
183     },
184     /**
185      * This is a callback function responsible for expanding and collapsing the
186      * branches of the tree. It is delegated to rather than multiple event handles.
187      */
188     toggleExpansion : function(e) {
189         // First check if they managed to click on the li iteslf, then find the closest
190         // LI ancestor and use that
192         if (e.target.test('a') && (e.keyCode == 0 || e.keyCode == 13)) {
193             // A link has been clicked (or keypress is 'enter') don't fire any more events just do the default.
194             e.stopPropagation();
195             return;
196         }
198         // Makes sure we can get to the LI containing the branch.
199         var target = e.target;
200         if (!target.test('li')) {
201             target = target.ancestor('li')
202         }
203         if (!target) {
204             return;
205         }
207         // Toggle expand/collapse providing its not a root level branch.
208         if (!target.hasClass('depth_1')) {
209             if (e.type == 'actionkey') {
210                 switch (e.action) {
211                     case 'expand' :
212                         target.removeClass('collapsed');
213                         target.set('aria-expanded', true);
214                         break;
215                     case 'collapse' :
216                         target.addClass('collapsed');
217                         target.set('aria-expanded', false);
218                         break;
219                     default :
220                         target.toggleClass('collapsed');
221                         target.set('aria-expanded', !target.hasClass('collapsed'));
222                 }
223                 e.halt();
224             } else {
225                 target.toggleClass('collapsed');
226                 target.set('aria-expanded', !target.hasClass('collapsed'));
227             }
228         }
230         // If the accordian feature has been enabled collapse all siblings.
231         if (this.get('accordian')) {
232             target.siblings('li').each(function(){
233                 if (this.get('id') !== target.get('id') && !this.hasClass('collapsed')) {
234                     this.addClass('collapsed');
235                     this.set('aria-expanded', false);
236                 }
237             });
238         }
240         // If this block can dock tell the dock to resize if required and check
241         // the width on the dock panel in case it is presently in use.
242         if (this.get('candock')) {
243             M.core_dock.resize();
244             var panel = M.core_dock.getPanel();
245             if (panel.visible) {
246                 panel.correctWidth();
247             }
248         }
249     }
251 // The tree extends the YUI base foundation.
252 Y.extend(TREE, Y.Base, TREE.prototype, {
253     NAME : 'navigation-tree',
254     ATTRS : {
255         instance : {
256             value : null
257         },
258         candock : {
259             validator : Y.Lang.isBool,
260             value : false
261         },
262         accordian : {
263             validator : Y.Lang.isBool,
264             value : false
265         },
266         expansionlimit : {
267             value : 0,
268             setter : function(val) {
269                 return parseInt(val);
270             }
271         }
272     }
273 });
274 if (M.core_dock && M.core_dock.genericblock) {
275     Y.augment(TREE, M.core_dock.genericblock);
278 /**
279  * The tree branch class.
280  * This class is used to manage a tree branch, in particular its ability to load
281  * its contents by AJAX.
282  */
283 var BRANCH = function(config) {
284     BRANCH.superclass.constructor.apply(this, arguments);
286 BRANCH.prototype = {
287     /**
288      * The node for this branch (p)
289      */
290     node : null,
291     /**
292      * Initialises the branch when it is first created.
293      */
294     initializer : function(config) {
295         if (config.branchobj !== null) {
296             // Construct from the provided xml
297             for (var i in config.branchobj) {
298                 this.set(i, config.branchobj[i]);
299             }
300             var children = this.get('children');
301             this.set('haschildren', (children.length > 0));
302         }
303         if (config.overrides !== null) {
304             // Construct from the provided xml
305             for (var i in config.overrides) {
306                 this.set(i, config.overrides[i]);
307             }
308         }
309         // Get the node for this branch
310         this.node = Y.one('#', this.get('id'));
311         // Now check whether the branch is not expandable because of the expansionlimit
312         var expansionlimit = this.get('tree').get('expansionlimit');
313         var type = this.get('type');
314         if (expansionlimit != EXPANSIONLIMIT_EVERYTHING &&  type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
315             this.set('expandable', false);
316             this.set('haschildren', false);
317         }
318     },
319     /**
320      * Draws the branch within the tree.
321      *
322      * This function creates a DOM structure for the branch and then injects
323      * it into the navigation tree at the correct point.
324      */
325     draw : function(element) {
327         var isbranch = (this.get('expandable') || this.get('haschildren'));
328         var branchli = Y.Node.create('<li></li>');
329         var link = this.get('link');
330         var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
331         if (!link) {
332             //add tab focus if not link (so still one focus per menu node).
333             // it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
334             branchp.setAttribute('tabindex', '0');
335         }
336         if (isbranch) {
337             branchli.addClass('collapsed').addClass('contains_branch');
338             branchli.set('aria-expanded', false);
339             branchp.addClass('branch');
340         }
342         // Prepare the icon, should be an object representing a pix_icon
343         var branchicon = false;
344         var icon = this.get('icon');
345         if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) {
346             branchicon = Y.Node.create('<img alt="" />');
347             branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
348             branchli.addClass('item_with_icon');
349             if (icon.alt) {
350                 branchicon.setAttribute('alt', icon.alt);
351             }
352             if (icon.title) {
353                 branchicon.setAttribute('title', icon.title);
354             }
355             if (icon.classes) {
356                 for (var i in icon.classes) {
357                     branchicon.addClass(icon.classes[i]);
358                 }
359             }
360         }
362         if (!link) {
363             if (branchicon) {
364                 branchp.appendChild(branchicon);
365             }
366             branchp.append(this.get('name'));
367         } else {
368             var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
369             if (branchicon) {
370                 branchlink.appendChild(branchicon);
371             }
372             branchlink.append(this.get('name'));
373             if (this.get('hidden')) {
374                 branchlink.addClass('dimmed');
375             }
376             branchp.appendChild(branchlink);
377         }
379         branchli.appendChild(branchp);
380         element.appendChild(branchli);
381         this.node = branchp;
382         return this;
383     },
384     /**
385      * Attaches required events to the branch structure.
386      */
387     wire : function() {
388         this.node = this.node || Y.one('#'+this.get('id'));
389         if (!this.node) {
390             return false;
391         }
392         if (this.get('expandable')) {
393             this.node.setAttribute('data-expandable', '1');
394             this.node.setAttribute('data-loaded', '0');
395         }
396         return this;
397     },
398     /**
399      * Gets the UL element that children for this branch should be inserted into.
400      */
401     getChildrenUL : function() {
402         var ul = this.node.next('ul');
403         if (!ul) {
404             ul = Y.Node.create('<ul></ul>');
405             this.node.ancestor().append(ul);
406         }
407         return ul;
408     },
409     /**
410      * Load the content of the branch via AJAX.
411      *
412      * This function calls ajaxProcessResponse with the result of the AJAX
413      * request made here.
414      */
415     ajaxLoad : function(e) {
416         if (e.type == 'actionkey' && e.action != 'enter') {
417             e.halt();
418         } else {
419             e.stopPropagation();
420         }
421         if (e.type = 'actionkey' && e.action == 'enter' && e.target.test('A')) {
422             // No ajaxLoad for enter.
423             this.node.setAttribute('data-expandable', '0');
424             this.node.setAttribute('data-loaded', '1');
425             return true;
426         }
428         if (this.node.hasClass('loadingbranch')) {
429             // Already loading. Just skip.
430             return true;
431         }
433         if (this.node.getAttribute('data-loaded') === '1') {
434             // We've already loaded this stuff.
435             return true;
436         }
437         this.node.addClass('loadingbranch');
439         var params = {
440             elementid : this.get('id'),
441             id : this.get('key'),
442             type : this.get('type'),
443             sesskey : M.cfg.sesskey,
444             instance : this.get('tree').get('instance')
445         };
447         Y.io(M.cfg.wwwroot+'/lib/ajax/getnavbranch.php', {
448             method:'POST',
449             data:  build_querystring(params),
450             on: {
451                 complete: this.ajaxProcessResponse
452             },
453             context:this
454         });
455         return true;
456     },
457     /**
458      * Processes an AJAX request to load the content of this branch through
459      * AJAX.
460      */
461     ajaxProcessResponse : function(tid, outcome) {
462         this.node.removeClass('loadingbranch');
463         this.node.setAttribute('data-loaded', '1');
464         try {
465             var object = Y.JSON.parse(outcome.responseText);
466             if (object.children && object.children.length > 0) {
467                 var coursecount = 0;
468                 for (var i in object.children) {
469                     if (typeof(object.children[i])=='object') {
470                         if (object.children[i].type == NODETYPE.COURSE) {
471                             coursecount++;
472                         }
473                         this.addChild(object.children[i]);
474                     }
475                 }
476                 if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY)
477                     && coursecount >= M.block_navigation.courselimit) {
478                     this.addViewAllCoursesChild(this);
479                 }
480                 return true;
481             }
482         } catch (ex) {
483             // If we got here then there was an error parsing the result
484         }
485         // The branch is empty so class it accordingly
486         this.node.replaceClass('branch', 'emptybranch');
487         return true;
488     },
489     /**
490      * Turns the branch object passed to the method into a proper branch object
491      * and then adds it as a child of this branch.
492      */
493     addChild : function(branchobj) {
494         // Make the new branch into an object
495         var branch = new BRANCH({tree:this.get('tree'), branchobj:branchobj});
496         if (branch.draw(this.getChildrenUL())) {
497             this.get('tree').branches[branch.get('id')] = branch;
498             branch.wire();
499             var count = 0, i, children = branch.get('children');
500             for (i in children) {
501                 // Add each branch to the tree
502                 if (children[i].type == NODETYPE.COURSE) {
503                     count++;
504                 }
505                 if (typeof(children[i])=='object') {
506                     branch.addChild(children[i]);
507                 }
508             }
509             if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
510                 && count >= M.block_navigation.courselimit) {
511                 this.addViewAllCoursesChild(branch);
512             }
513         }
514         return true;
515     },
517     /**
518      * Add a link to view all courses in a category
519      */
520     addViewAllCoursesChild: function(branch) {
521         var url = null;
522         if (branch.get('type') == NODETYPE.ROOTNODE) {
523             if (branch.get('key') === 'mycourses') {
524                 url = M.cfg.wwwroot + '/my';
525             } else {
526                 url = M.cfg.wwwroot + '/course/index.php';
527             }
528         } else {
529             url = M.cfg.wwwroot+'/course/category.php?id=' + branch.get('key');
530         }
531         branch.addChild({
532             name : M.str.moodle.viewallcourses,
533             title : M.str.moodle.viewallcourses,
534             link : url,
535             haschildren : false,
536             icon : {'pix':"i/navigationitem",'component':'moodle'}
537         });
538     }
540 Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
541     NAME : 'navigation-branch',
542     ATTRS : {
543         tree : {
544             validator : Y.Lang.isObject
545         },
546         name : {
547             value : '',
548             validator : Y.Lang.isString,
549             setter : function(val) {
550                 return val.replace(/\n/g, '<br />');
551             }
552         },
553         title : {
554             value : '',
555             validator : Y.Lang.isString
556         },
557         id : {
558             value : '',
559             validator : Y.Lang.isString,
560             getter : function(val) {
561                 if (val == '') {
562                     val = 'expandable_branch_'+M.block_navigation.expandablebranchcount;
563                     M.block_navigation.expandablebranchcount++;
564                 }
565                 return val;
566             }
567         },
568         key : {
569             value : null
570         },
571         type : {
572             value : null
573         },
574         link : {
575             value : false
576         },
577         icon : {
578             value : false,
579             validator : Y.Lang.isObject
580         },
581         expandable : {
582             value : false,
583             validator : Y.Lang.isBool
584         },
585         hidden : {
586             value : false,
587             validator : Y.Lang.isBool
588         },
589         haschildren : {
590             value : false,
591             validator : Y.Lang.isBool
592         },
593         children : {
594             value : [],
595             validator : Y.Lang.isArray
596         }
597     }
598 });
600 /**
601  * This namespace will contain all of the contents of the navigation blocks
602  * global navigation and settings.
603  * @namespace
604  */
605 M.block_navigation = M.block_navigation || {
606     /** The number of expandable branches in existence */
607     expandablebranchcount:1,
608     courselimit : 20,
609     instance : null,
610     /**
611      * Add new instance of navigation tree to tree collection
612      */
613     init_add_tree:function(properties) {
614         if (properties.courselimit) {
615             this.courselimit = properties.courselimit;
616         }
617         if (M.core_dock) {
618             M.core_dock.init(Y);
619         }
620         new TREE(properties);
621     }
622 };
624 }, '@VERSION@', {requires:['base', 'core_dock', 'io-base', 'node', 'dom', 'event-custom', 'event-delegate', 'json-parse']});