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