YUI.add('moodle-block_navigation-navigation', function (Y, NAME) { /** * Navigation block JS. * * This file contains the Navigation block JS.. * * @module moodle-block_navigation-navigation */ /** * This namespace will contain all of the contents of the navigation blocks * global navigation and settings. * @class M.block_navigation * @static */ M.block_navigation = M.block_navigation || {}; /** * The number of expandable branches in existence. * * @property expandablebranchcount * @protected * @static * @type Number */ M.block_navigation.expandablebranchcount = 1; /** * The maximum number of courses to show as part of a branch. * * @property courselimit * @protected * @static * @type Number */ M.block_navigation.courselimit = 20; /** * Add new instance of navigation tree to tree collection * * @method init_add_tree * @static * @param {Object} properties */ M.block_navigation.init_add_tree = function(properties) { if (properties.courselimit) { this.courselimit = properties.courselimit; } new TREE(properties); }; /** * A 'actionkey' Event to help with Y.delegate(). * The event consists of the left arrow, right arrow, enter and space keys. * More keys can be mapped to action meanings. * actions: collapse , expand, toggle, enter. * * This event is delegated to branches in the navigation tree. * The on() method to subscribe allows specifying the desired trigger actions as JSON. * * @namespace M.block_navigation * @class ActionKey */ Y.Event.define("actionkey", { // Webkit and IE repeat keydown when you hold down arrow keys. // Opera links keypress to page scroll; others keydown. // Firefox prevents page scroll via preventDefault() on either // keydown or keypress. _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress', /** * The keys to trigger on. * @method _keys */ _keys: { //arrows '37': 'collapse', '39': 'expand', '32': 'toggle', '13': 'enter' }, /** * Handles key events * @method _keyHandler * @param {EventFacade} e * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers * @param {Object} args */ _keyHandler: function (e, notifier, args) { var actObj; if (!args.actions) { actObj = {collapse:true, expand:true, toggle:true, enter:true}; } else { actObj = args.actions; } if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) { e.action = this._keys[e.keyCode]; notifier.fire(e); } }, /** * Subscribes to events. * @method on * @param {Node} node The node this subscription was applied to. * @param {Subscription} sub The object tracking this subscription. * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers */ on: function (node, sub, notifier) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { //no actions given sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false}); } else { sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]); } }, /** * Detaches an event listener * @method detach */ detach: function (node, sub) { //detach our _detacher handle of the subscription made in on() sub._detacher.detach(); }, /** * Creates a delegated event listener. * @method delegate * @param {Node} node The node this subscription was applied to. * @param {Subscription} sub The object tracking this subscription. * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers * @param {String|function} filter Selector string or function that accpets an event object and returns null. */ delegate: function (node, sub, notifier, filter) { // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions). if (sub.args === null) { //no actions given sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false}); } else { sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]); } }, /** * Detaches a delegated event listener. * @method detachDelegate * @param {Node} node The node this subscription was applied to. * @param {Subscription} sub The object tracking this subscription. * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers * @param {String|function} filter Selector string or function that accpets an event object and returns null. */ detachDelegate: function (node, sub) { sub._delegateDetacher.detach(); } }); var EXPANSIONLIMIT_EVERYTHING = 0, EXPANSIONLIMIT_COURSE = 20, EXPANSIONLIMIT_SECTION = 30, EXPANSIONLIMIT_ACTIVITY = 40; // Mappings for the different types of nodes coming from the navigation. // Copied from lib/navigationlib.php navigation_node constants. var NODETYPE = { // @type int Root node = 0 ROOTNODE : 0, // @type int System context = 1 SYSTEM : 1, // @type int Course category = 10 CATEGORY : 10, // @type int MYCATEGORY = 11 MYCATEGORY : 11, // @type int Course = 20 COURSE : 20, // @type int Course section = 30 SECTION : 30, // @type int Activity (course module) = 40 ACTIVITY : 40, // @type int Resource (course module = 50 RESOURCE : 50, // @type int Custom node (could be anything) = 60 CUSTOM : 60, // @type int Setting = 70 SETTING : 70, // @type int site administration = 71 SITEADMIN : 71, // @type int User context = 80 USER : 80, // @type int Container = 90 CONTAINER : 90 }; /** * Navigation tree class. * * This class establishes the tree initially, creating expandable branches as * required, and delegating the expand/collapse event. * * @namespace M.block_navigation * @class Tree * @constructor * @extends Base */ var TREE = function() { TREE.superclass.constructor.apply(this, arguments); }; TREE.prototype = { /** * The tree's ID, normally its block instance id. * @property id * @type Number * @protected */ id : null, /** * An array of initialised branches. * @property branches * @type Array * @protected */ branches : [], /** * Initialise the tree object when its first created. * @method initializer * @param {Object} config */ initializer : function(config) { this.id = parseInt(config.id, 10); var node = Y.one('#inst'+config.id); // Can't find the block instance within the page if (node === null) { return; } // Delegate event to toggle expansion Y.delegate('click', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this); Y.delegate('actionkey', this.toggleExpansion, node.one('.block_tree'), '.tree_item.branch', this); // Gather the expandable branches ready for initialisation. var expansions = []; if (config.expansions) { expansions = config.expansions; } else if (window['navtreeexpansions'+config.id]) { expansions = window['navtreeexpansions'+config.id]; } // Establish each expandable branch as a tree branch. for (var i in expansions) { var branch = new BRANCH({ tree:this, branchobj:expansions[i], overrides : { expandable : true, children : [], haschildren : true } }).wire(); M.block_navigation.expandablebranchcount++; this.branches[branch.get('id')] = branch; } // Create siteadmin branch. if (window.siteadminexpansion) { var siteadminbranch = new BRANCH({ tree: this, branchobj: window.siteadminexpansion, overrides : { expandable : true, children : [], haschildren : true } }).wire(); M.block_navigation.expandablebranchcount++; this.branches[siteadminbranch.get('id')] = siteadminbranch; // Remove link on site admin with JS to keep old UI. if (siteadminbranch.node) { var siteadminlinknode = siteadminbranch.node.get('childNodes').item(0); if (siteadminlinknode) { var siteadminnode = Y.Node.create(''+siteadminlinknode.get('innerHTML')+''); siteadminbranch.node.replaceChild(siteadminnode, siteadminlinknode); } } } if (M.block_navigation.expandablebranchcount > 0) { // Delegate some events to handle AJAX loading. Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this); } }, /** * Fire actions for a branch when an event occurs. * @method fire_branch_action * @param {EventFacade} event */ fire_branch_action : function(event) { var id = event.currentTarget.getAttribute('id'); var branch = this.branches[id]; branch.ajaxLoad(event); }, /** * This is a callback function responsible for expanding and collapsing the * branches of the tree. It is delegated to rather than multiple event handles. * @method toggleExpansion * @param {EventFacade} e * @return Boolean */ toggleExpansion : function(e) { // First check if they managed to click on the li iteslf, then find the closest // LI ancestor and use that if (e.target.test('a') && (e.keyCode === 0 || e.keyCode === 13)) { // A link has been clicked (or keypress is 'enter') don't fire any more events just do the default. e.stopPropagation(); return; } // Makes sure we can get to the LI containing the branch. var target = e.target; if (!target.test('li')) { target = target.ancestor('li'); } if (!target) { return; } // Toggle expand/collapse providing its not a root level branch. if (!target.hasClass('depth_1')) { if (e.type === 'actionkey') { switch (e.action) { case 'expand' : target.removeClass('collapsed'); target.set('aria-expanded', true); break; case 'collapse' : target.addClass('collapsed'); target.set('aria-expanded', false); break; default : target.toggleClass('collapsed'); target.set('aria-expanded', !target.hasClass('collapsed')); } e.halt(); } else { target.toggleClass('collapsed'); target.set('aria-expanded', !target.hasClass('collapsed')); } } // If the accordian feature has been enabled collapse all siblings. if (this.get('accordian')) { target.siblings('li').each(function(){ if (this.get('id') !== target.get('id') && !this.hasClass('collapsed')) { this.addClass('collapsed'); this.set('aria-expanded', false); } }); } // If this block can dock tell the dock to resize if required and check // the width on the dock panel in case it is presently in use. if (this.get('candock') && M.core.dock.notifyBlockChange) { M.core.dock.notifyBlockChange(this.id); } return true; } }; // The tree extends the YUI base foundation. Y.extend(TREE, Y.Base, TREE.prototype, { NAME : 'navigation-tree', ATTRS : { /** * True if the block can dock. * @attribute candock * @type Boolean */ candock : { validator : Y.Lang.isBool, value : false }, /** * If set to true nodes will be opened/closed in an accordian fashion. * @attribute accordian * @type Boolean */ accordian : { validator : Y.Lang.isBool, value : false }, /** * The nodes that get shown. * @attribute expansionlimit * @type Number */ expansionlimit : { value : 0, setter : function(val) { val = parseInt(val, 10); if (val !== EXPANSIONLIMIT_EVERYTHING && val !== EXPANSIONLIMIT_COURSE && val !== EXPANSIONLIMIT_SECTION && val !== EXPANSIONLIMIT_ACTIVITY) { val = EXPANSIONLIMIT_EVERYTHING; } return val; } }, /** * The navigation tree block instance. * * @attribute instance * @default false * @type Number */ instance : { value : false, setter : function(val) { return parseInt(val, 10); } } } }); /** * The Branch class. * * This class is used to manage a tree branch, in particular its ability to load * its contents by AJAX. * * @namespace M.block_navigation * @class Branch * @constructor * @extends Base */ var BRANCH = function() { BRANCH.superclass.constructor.apply(this, arguments); }; BRANCH.prototype = { /** * The node for this branch (p) * @property node * @type Node * @protected */ node : null, /** * Initialises the branch when it is first created. * @method initializer * @param {Object} config */ initializer : function(config) { var i, children; if (config.branchobj !== null) { // Construct from the provided xml for (i in config.branchobj) { this.set(i, config.branchobj[i]); } children = this.get('children'); this.set('haschildren', (children.length > 0)); } if (config.overrides !== null) { // Construct from the provided xml for (i in config.overrides) { this.set(i, config.overrides[i]); } } // Get the node for this branch this.node = Y.one('#'+this.get('id')); var expansionlimit = this.get('tree').get('expansionlimit'); var type = this.get('type'); if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) { this.set('expandable', false); this.set('haschildren', false); } }, /** * Draws the branch within the tree. * * This function creates a DOM structure for the branch and then injects * it into the navigation tree at the correct point. * * It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces * the same thing as this but on the php side. * * @method draw * @chainable * @param {Node} element * @return Branch */ draw : function(element) { var isbranch = (this.get('expandable') || this.get('haschildren')); var branchli = Y.Node.create('
'); var link = this.get('link'); var branchp = Y.Node.create('').setAttribute('id', this.get('id')); var name; if (!link) { //add tab focus if not link (so still one focus per menu node). // it was suggested to have 2 foci. one for the node and one for the link in MDL-27428. branchp.setAttribute('tabindex', '0'); } if (isbranch) { branchli.addClass('collapsed').addClass('contains_branch'); branchli.set('aria-expanded', false); branchp.addClass('branch'); } // Prepare the icon, should be an object representing a pix_icon var branchicon = false; var icon = this.get('icon'); if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) { branchicon = Y.Node.create('