1 YUI.add('moodle-block_navigation-navigation', function(Y){
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.
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.
12 * Todo: This could be centralised, a similar Event is defined in blocks/dock.js
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',
25 //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
30 _keyHandler: function (e, notifier, args) {
32 var actObj = {collapse:true, expand:true, toggle:true, enter:true};
34 var actObj = args.actions;
36 if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
37 e.action = this._keys[e.keyCode];
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) {
46 sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
48 sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
52 detach: function (node, sub, notifier) {
53 //detach our _detacher handle of the subscription made in on()
54 sub._detacher.detach();
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) {
61 sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
63 sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
67 detachDelegate: function (node, sub, notifier) {
68 sub._delegateDetacher.detach();
72 var EXPANSIONLIMIT_EVERYTHING = 0,
73 EXPANSIONLIMIT_COURSE = 20,
74 EXPANSIONLIMIT_SECTION = 30,
75 EXPANSIONLIMIT_ACTIVITY = 40;
78 * Mappings for the different types of nodes coming from the navigation.
79 * Copied from lib/navigationlib.php navigation_node constants.
83 /** @type int Root node = 0 */
85 /** @type int System context = 1 */
87 /** @type int Course category = 10 */
89 /** @type int MYCATEGORY = 11 */
91 /** @type int Course = 20 */
93 /** @type int Course section = 30 */
95 /** @type int Activity (course module) = 40 */
97 /** @type int Resource (course module = 50 */
99 /** @type int Custom node (could be anything) = 60 */
101 /** @type int Setting = 70 */
103 /** @type int User context = 80 */
105 /** @type int Container = 90 */
110 * Navigation tree class.
112 * This class establishes the tree initially, creating expandable branches as
113 * required, and delegating the expand/collapse event.
115 var TREE = function(config) {
116 TREE.superclass.constructor.apply(this, arguments);
120 * The tree's ID, normally its block instance id.
124 * Initialise the tree object when its first created.
126 initializer : function(config) {
129 var node = Y.one('#inst'+config.id);
131 // Can't find the block instance within the page
136 // Delegate event to toggle expansion
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.
143 if (config.expansions) {
144 expansions = config.expansions;
145 } else if (window['navtreeexpansions'+config.id]) {
146 expansions = window['navtreeexpansions'+config.id];
148 // Establish each expandable branch as a tree branch.
149 for (var i in expansions) {
152 branchobj:expansions[i],
159 M.block_navigation.expandablebranchcount++;
162 // Call the generic blocks init method to add all the generic stuff
163 if (this.get('candock')) {
164 this.initialise_block(Y, node);
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.
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.
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')
190 // Toggle expand/collapse providing its not a root level branch.
191 if (!target.hasClass('depth_1')) {
192 if (e.type == 'actionkey') {
195 target.removeClass('collapsed');
196 target.set('aria-expanded', true);
199 target.addClass('collapsed');
200 target.set('aria-expanded', false);
203 target.toggleClass('collapsed');
204 target.set('aria-expanded', !target.hasClass('collapsed'));
208 target.toggleClass('collapsed');
209 target.set('aria-expanded', !target.hasClass('collapsed'));
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);
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();
229 panel.correctWidth();
234 // The tree extends the YUI base foundation.
235 Y.extend(TREE, Y.Base, TREE.prototype, {
236 NAME : 'navigation-tree',
242 validator : Y.Lang.isBool,
246 validator : Y.Lang.isBool,
251 setter : function(val) {
252 return parseInt(val);
257 if (M.core_dock && M.core_dock.genericblock) {
258 Y.augment(TREE, M.core_dock.genericblock);
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.
266 var BRANCH = function(config) {
267 BRANCH.superclass.constructor.apply(this, arguments);
271 * The node for this branch (p)
275 * A reference to the ajax load event handlers when created.
277 event_ajaxload : null,
278 event_ajaxload_actionkey : null,
280 * Initialises the branch when it is first created.
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]);
288 var children = this.get('children');
289 this.set('haschildren', (children.length > 0));
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]);
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);
308 * Draws the branch within the tree.
310 * This function creates a DOM structure for the branch and then injects
311 * it into the navigation tree at the correct point.
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'));
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');
325 branchli.addClass('collapsed').addClass('contains_branch');
326 branchli.set('aria-expanded', false);
327 branchp.addClass('branch');
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');
338 branchicon.setAttribute('alt', icon.alt);
341 branchicon.setAttribute('title', icon.title);
344 for (var i in icon.classes) {
345 branchicon.addClass(icon.classes[i]);
352 branchp.appendChild(branchicon);
354 branchp.append(this.get('name'));
356 var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
358 branchlink.appendChild(branchicon);
360 branchlink.append(this.get('name'));
361 if (this.get('hidden')) {
362 branchlink.addClass('dimmed');
364 branchp.appendChild(branchlink);
367 branchli.appendChild(branchp);
368 element.appendChild(branchli);
373 * Attaches required events to the branch structure.
376 this.node = this.node || Y.one('#'+this.get('id'));
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);
387 * Gets the UL element that children for this branch should be inserted into.
389 getChildrenUL : function() {
390 var ul = this.node.next('ul');
392 ul = Y.Node.create('<ul></ul>');
393 this.node.ancestor().append(ul);
398 * Load the content of the branch via AJAX.
400 * This function calls ajaxProcessResponse with the result of the AJAX
403 ajaxLoad : function(e) {
404 if (e.type == 'actionkey' && e.action != 'enter') {
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
415 if (this.node.hasClass('loadingbranch')) {
419 this.node.addClass('loadingbranch');
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')
429 Y.io(M.cfg.wwwroot+'/lib/ajax/getnavbranch.php', {
431 data: build_querystring(params),
433 complete: this.ajaxProcessResponse
440 * Processes an AJAX request to load the content of this branch through
443 ajaxProcessResponse : function(tid, outcome) {
444 this.node.removeClass('loadingbranch');
445 this.event_ajaxload.detach();
446 this.event_ajaxload_actionkey.detach();
448 var object = Y.JSON.parse(outcome.responseText);
449 if (object.children && object.children.length > 0) {
451 for (var i in object.children) {
452 if (typeof(object.children[i])=='object') {
453 if (object.children[i].type == NODETYPE.COURSE) {
456 this.addChild(object.children[i]);
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);
463 this.get('tree').toggleExpansion({target:this.node});
467 // If we got here then there was an error parsing the result
469 // The branch is empty so class it accordingly
470 this.node.replaceClass('branch', 'emptybranch');
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.
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())) {
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) {
488 if (typeof(children[i])=='object') {
489 branch.addChild(children[i]);
492 if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
493 && count >= M.block_navigation.courselimit) {
494 this.addViewAllCoursesChild(branch);
501 * Add a link to view all courses in a category
503 addViewAllCoursesChild: function(branch) {
505 if (branch.get('type') == NODETYPE.ROOTNODE) {
506 if (branch.get('key') === 'mycourses') {
507 url = M.cfg.wwwroot + '/my';
509 url = M.cfg.wwwroot + '/course/index.php';
512 url = M.cfg.wwwroot+'/course/category.php?id=' + branch.get('key');
515 name : M.str.moodle.viewallcourses,
516 title : M.str.moodle.viewallcourses,
519 icon : {'pix':"i/navigationitem",'component':'moodle'}
523 Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
524 NAME : 'navigation-branch',
527 validator : Y.Lang.isObject
531 validator : Y.Lang.isString,
532 setter : function(val) {
533 return val.replace(/\n/g, '<br />');
538 validator : Y.Lang.isString
542 validator : Y.Lang.isString,
543 getter : function(val) {
545 val = 'expandable_branch_'+M.block_navigation.expandablebranchcount;
546 M.block_navigation.expandablebranchcount++;
562 validator : Y.Lang.isObject
566 validator : Y.Lang.isBool
570 validator : Y.Lang.isBool
574 validator : Y.Lang.isBool
578 validator : Y.Lang.isArray
584 * This namespace will contain all of the contents of the navigation blocks
585 * global navigation and settings.
588 M.block_navigation = M.block_navigation || {
589 /** The number of expandable branches in existence */
590 expandablebranchcount:1,
594 * Add new instance of navigation tree to tree collection
596 init_add_tree:function(properties) {
597 if (properties.courselimit) {
598 this.courselimit = properties.courselimit;
603 new TREE(properties);
607 }, '@VERSION@', {requires:['base', 'core_dock', 'io-base', 'node', 'dom', 'event-custom', 'event-delegate', 'json-parse']});