d2631ddadf9f9959d14407e31a3bb486bfd44a46
[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      * Initialise the tree object when its first created.
125      */
126     initializer : function(config) {
127         this.id = config.id;
129         var node = Y.one('#inst'+config.id);
131         // Can't find the block instance within the page
132         if (node === null) {
133             return;
134         }
136         // Delegate event to toggle expansion
137         var self = this;
138         Y.delegate('click', function(e){self.toggleExpansion(e);}, node.one('.block_tree'), '.tree_item.branch');
139         Y.delegate('actionkey', function(e){self.toggleExpansion(e);}, node.one('.block_tree'), '.tree_item.branch');
141         // Gather the expandable branches ready for initialisation.
142         var expansions = [];
143         if (config.expansions) {
144             expansions = config.expansions;
145         } else if (window['navtreeexpansions'+config.id]) {
146             expansions = window['navtreeexpansions'+config.id];
147         }
148         // Establish each expandable branch as a tree branch.
149         for (var i in expansions) {
150             new BRANCH({
151                 tree:this,
152                 branchobj:expansions[i],
153                 overrides : {
154                     expandable : true,
155                     children : [],
156                     haschildren : true
157                 }
158             }).wire();
159             M.block_navigation.expandablebranchcount++;
160         }
162         // Call the generic blocks init method to add all the generic stuff
163         if (this.get('candock')) {
164             this.initialise_block(Y, node);
165         }
166     },
167     /**
168      * This is a callback function responsible for expanding and collapsing the
169      * branches of the tree. It is delegated to rather than multiple event handles.
170      */
171     toggleExpansion : function(e) {
172         // First check if they managed to click on the li iteslf, then find the closest
173         // LI ancestor and use that
175         if (e.target.test('a') && (e.keyCode == 0 || e.keyCode == 13)) {
176             // A link has been clicked (or keypress is 'enter') don't fire any more events just do the default.
177             e.stopPropagation();
178             return;
179         }
181         // Makes sure we can get to the LI containing the branch.
182         var target = e.target;
183         if (!target.test('li')) {
184             target = target.ancestor('li')
185         }
186         if (!target) {
187             return;
188         }
190         // Toggle expand/collapse providing its not a root level branch.
191         if (!target.hasClass('depth_1')) {
192             if (e.type == 'actionkey') {
193                 switch (e.action) {
194                     case 'expand' :
195                         target.removeClass('collapsed');
196                         target.set('aria-expanded', true);
197                         break;
198                     case 'collapse' :
199                         target.addClass('collapsed');
200                         target.set('aria-expanded', false);
201                         break;
202                     default :
203                         target.toggleClass('collapsed');
204                         target.set('aria-expanded', !target.hasClass('collapsed'));
205                 }
206                 e.halt();
207             } else {
208                 target.toggleClass('collapsed');
209                 target.set('aria-expanded', !target.hasClass('collapsed'));
210             }
211         }
213         // If the accordian feature has been enabled collapse all siblings.
214         if (this.get('accordian')) {
215             target.siblings('li').each(function(){
216                 if (this.get('id') !== target.get('id') && !this.hasClass('collapsed')) {
217                     this.addClass('collapsed');
218                     this.set('aria-expanded', false);
219                 }
220             });
221         }
223         // If this block can dock tell the dock to resize if required and check
224         // the width on the dock panel in case it is presently in use.
225         if (this.get('candock')) {
226             M.core_dock.resize();
227             var panel = M.core_dock.getPanel();
228             if (panel.visible) {
229                 panel.correctWidth();
230             }
231         }
232     }
234 // The tree extends the YUI base foundation.
235 Y.extend(TREE, Y.Base, TREE.prototype, {
236     NAME : 'navigation-tree',
237     ATTRS : {
238         instance : {
239             value : null
240         },
241         candock : {
242             validator : Y.Lang.isBool,
243             value : false
244         },
245         accordian : {
246             validator : Y.Lang.isBool,
247             value : false
248         },
249         expansionlimit : {
250             value : 0,
251             setter : function(val) {
252                 return parseInt(val);
253             }
254         }
255     }
256 });
257 if (M.core_dock && M.core_dock.genericblock) {
258     Y.augment(TREE, M.core_dock.genericblock);
261 /**
262  * The tree branch class.
263  * This class is used to manage a tree branch, in particular its ability to load
264  * its contents by AJAX.
265  */
266 var BRANCH = function(config) {
267     BRANCH.superclass.constructor.apply(this, arguments);
269 BRANCH.prototype = {
270     /**
271      * The node for this branch (p)
272      */
273     node : null,
274     /**
275      * A reference to the ajax load event handlers when created.
276      */
277     event_ajaxload : null,
278     event_ajaxload_actionkey : null,
279     /**
280      * Initialises the branch when it is first created.
281      */
282     initializer : function(config) {
283         if (config.branchobj !== null) {
284             // Construct from the provided xml
285             for (var i in config.branchobj) {
286                 this.set(i, config.branchobj[i]);
287             }
288             var children = this.get('children');
289             this.set('haschildren', (children.length > 0));
290         }
291         if (config.overrides !== null) {
292             // Construct from the provided xml
293             for (var i in config.overrides) {
294                 this.set(i, config.overrides[i]);
295             }
296         }
297         // Get the node for this branch
298         this.node = Y.one('#', this.get('id'));
299         // Now check whether the branch is not expandable because of the expansionlimit
300         var expansionlimit = this.get('tree').get('expansionlimit');
301         var type = this.get('type');
302         if (expansionlimit != EXPANSIONLIMIT_EVERYTHING &&  type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
303             this.set('expandable', false);
304             this.set('haschildren', false);
305         }
306     },
307     /**
308      * Draws the branch within the tree.
309      *
310      * This function creates a DOM structure for the branch and then injects
311      * it into the navigation tree at the correct point.
312      */
313     draw : function(element) {
315         var isbranch = (this.get('expandable') || this.get('haschildren'));
316         var branchli = Y.Node.create('<li></li>');
317         var link = this.get('link');
318         var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
319         if (!link) {
320             //add tab focus if not link (so still one focus per menu node).
321             // it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
322             branchp.setAttribute('tabindex', '0');
323         }
324         if (isbranch) {
325             branchli.addClass('collapsed').addClass('contains_branch');
326             branchli.set('aria-expanded', false);
327             branchp.addClass('branch');
328         }
330         // Prepare the icon, should be an object representing a pix_icon
331         var branchicon = false;
332         var icon = this.get('icon');
333         if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) {
334             branchicon = Y.Node.create('<img alt="" />');
335             branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
336             branchli.addClass('item_with_icon');
337             if (icon.alt) {
338                 branchicon.setAttribute('alt', icon.alt);
339             }
340             if (icon.title) {
341                 branchicon.setAttribute('title', icon.title);
342             }
343             if (icon.classes) {
344                 for (var i in icon.classes) {
345                     branchicon.addClass(icon.classes[i]);
346                 }
347             }
348         }
350         if (!link) {
351             if (branchicon) {
352                 branchp.appendChild(branchicon);
353             }
354             branchp.append(this.get('name'));
355         } else {
356             var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
357             if (branchicon) {
358                 branchlink.appendChild(branchicon);
359             }
360             branchlink.append(this.get('name'));
361             if (this.get('hidden')) {
362                 branchlink.addClass('dimmed');
363             }
364             branchp.appendChild(branchlink);
365         }
367         branchli.appendChild(branchp);
368         element.appendChild(branchli);
369         this.node = branchp;
370         return this;
371     },
372     /**
373      * Attaches required events to the branch structure.
374      */
375     wire : function() {
376         this.node = this.node || Y.one('#'+this.get('id'));
377         if (!this.node) {
378             return false;
379         }
380         if (this.get('expandable')) {
381             this.event_ajaxload = this.node.on('ajaxload|click', this.ajaxLoad, this);
382             this.event_ajaxload_actionkey = this.node.on('actionkey', this.ajaxLoad, this);
383         }
384         return this;
385     },
386     /**
387      * Gets the UL element that children for this branch should be inserted into.
388      */
389     getChildrenUL : function() {
390         var ul = this.node.next('ul');
391         if (!ul) {
392             ul = Y.Node.create('<ul></ul>');
393             this.node.ancestor().append(ul);
394         }
395         return ul;
396     },
397     /**
398      * Load the content of the branch via AJAX.
399      *
400      * This function calls ajaxProcessResponse with the result of the AJAX
401      * request made here.
402      */
403     ajaxLoad : function(e) {
404         if (e.type == 'actionkey' && e.action != 'enter') {
405             e.halt();
406         } else {
407             e.stopPropagation();
408         }
409         if (e.type = 'actionkey' && e.action == 'enter' && e.target.test('A')) {
410             this.event_ajaxload_actionkey.detach();
411             this.event_ajaxload.detach();
412             return true; // no ajaxLoad for enter
413         }
415         if (this.node.hasClass('loadingbranch')) {
416             return true;
417         }
419         this.node.addClass('loadingbranch');
421         var params = {
422             elementid : this.get('id'),
423             id : this.get('key'),
424             type : this.get('type'),
425             sesskey : M.cfg.sesskey,
426             instance : this.get('tree').get('instance')
427         };
429         Y.io(M.cfg.wwwroot+'/lib/ajax/getnavbranch.php', {
430             method:'POST',
431             data:  build_querystring(params),
432             on: {
433                 complete: this.ajaxProcessResponse
434             },
435             context:this
436         });
437         return true;
438     },
439     /**
440      * Processes an AJAX request to load the content of this branch through
441      * AJAX.
442      */
443     ajaxProcessResponse : function(tid, outcome) {
444         this.node.removeClass('loadingbranch');
445         this.event_ajaxload.detach();
446         this.event_ajaxload_actionkey.detach();
447         try {
448             var object = Y.JSON.parse(outcome.responseText);
449             if (object.children && object.children.length > 0) {
450                 var coursecount = 0;
451                 for (var i in object.children) {
452                     if (typeof(object.children[i])=='object') {
453                         if (object.children[i].type == NODETYPE.COURSE) {
454                             coursecount++;
455                         }
456                         this.addChild(object.children[i]);
457                     }
458                 }
459                 if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY)
460                     && coursecount >= M.block_navigation.courselimit) {
461                     this.addViewAllCoursesChild(this);
462                 }
463                 this.get('tree').toggleExpansion({target:this.node});
464                 return true;
465             }
466         } catch (ex) {
467             // If we got here then there was an error parsing the result
468         }
469         // The branch is empty so class it accordingly
470         this.node.replaceClass('branch', 'emptybranch');
471         return true;
472     },
473     /**
474      * Turns the branch object passed to the method into a proper branch object
475      * and then adds it as a child of this branch.
476      */
477     addChild : function(branchobj) {
478         // Make the new branch into an object
479         var branch = new BRANCH({tree:this.get('tree'), branchobj:branchobj});
480         if (branch.draw(this.getChildrenUL())) {
481             branch.wire();
482             var count = 0, i, children = branch.get('children');
483             for (i in children) {
484                 // Add each branch to the tree
485                 if (children[i].type == NODETYPE.COURSE) {
486                     count++;
487                 }
488                 if (typeof(children[i])=='object') {
489                     branch.addChild(children[i]);
490                 }
491             }
492             if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
493                 && count >= M.block_navigation.courselimit) {
494                 this.addViewAllCoursesChild(branch);
495             }
496         }
497         return true;
498     },
500     /**
501      * Add a link to view all courses in a category
502      */
503     addViewAllCoursesChild: function(branch) {
504         var url = null;
505         if (branch.get('type') == NODETYPE.ROOTNODE) {
506             if (branch.get('key') === 'mycourses') {
507                 url = M.cfg.wwwroot + '/my';
508             } else {
509                 url = M.cfg.wwwroot + '/course/index.php';
510             }
511         } else {
512             url = M.cfg.wwwroot+'/course/category.php?id=' + branch.get('key');
513         }
514         branch.addChild({
515             name : M.str.moodle.viewallcourses,
516             title : M.str.moodle.viewallcourses,
517             link : url,
518             haschildren : false,
519             icon : {'pix':"i/navigationitem",'component':'moodle'}
520         });
521     }
523 Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
524     NAME : 'navigation-branch',
525     ATTRS : {
526         tree : {
527             validator : Y.Lang.isObject
528         },
529         name : {
530             value : '',
531             validator : Y.Lang.isString,
532             setter : function(val) {
533                 return val.replace(/\n/g, '<br />');
534             }
535         },
536         title : {
537             value : '',
538             validator : Y.Lang.isString
539         },
540         id : {
541             value : '',
542             validator : Y.Lang.isString,
543             getter : function(val) {
544                 if (val == '') {
545                     val = 'expandable_branch_'+M.block_navigation.expandablebranchcount;
546                     M.block_navigation.expandablebranchcount++;
547                 }
548                 return val;
549             }
550         },
551         key : {
552             value : null
553         },
554         type : {
555             value : null
556         },
557         link : {
558             value : false
559         },
560         icon : {
561             value : false,
562             validator : Y.Lang.isObject
563         },
564         expandable : {
565             value : false,
566             validator : Y.Lang.isBool
567         },
568         hidden : {
569             value : false,
570             validator : Y.Lang.isBool
571         },
572         haschildren : {
573             value : false,
574             validator : Y.Lang.isBool
575         },
576         children : {
577             value : [],
578             validator : Y.Lang.isArray
579         }
580     }
581 });
583 /**
584  * This namespace will contain all of the contents of the navigation blocks
585  * global navigation and settings.
586  * @namespace
587  */
588 M.block_navigation = M.block_navigation || {
589     /** The number of expandable branches in existence */
590     expandablebranchcount:1,
591     courselimit : 20,
592     instance : null,
593     /**
594      * Add new instance of navigation tree to tree collection
595      */
596     init_add_tree:function(properties) {
597         if (properties.courselimit) {
598             this.courselimit = properties.courselimit;
599         }
600         if (M.core_dock) {
601             M.core_dock.init(Y);
602         }
603         new TREE(properties);
604     }
605 };
607 }, '@VERSION@', {requires:['base', 'core_dock', 'io-base', 'node', 'dom', 'event-custom', 'event-delegate', 'json-parse']});