[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 YUI.add('moodle-course-categoryexpander', function (Y, NAME) { 2 3 /** 4 * Adds toggling of subcategory with automatic loading using AJAX. 5 * 6 * This also includes application of an animation to improve user experience. 7 * 8 * @module moodle-course-categoryexpander 9 */ 10 11 /** 12 * The course category expander. 13 * 14 * @constructor 15 * @class Y.Moodle.course.categoryexpander 16 */ 17 18 var CSS = { 19 CONTENTNODE: 'content', 20 COLLAPSEALL: 'collapse-all', 21 DISABLED: 'disabled', 22 LOADED: 'loaded', 23 NOTLOADED: 'notloaded', 24 SECTIONCOLLAPSED: 'collapsed', 25 HASCHILDREN: 'with_children' 26 }, 27 SELECTORS = { 28 LOADEDTREES: '.with_children.loaded', 29 CONTENTNODE: '.content', 30 CATEGORYLISTENLINK: '.category .info .categoryname', 31 CATEGORYSPINNERLOCATION: '.categoryname', 32 CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', 33 CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', 34 COLLAPSEEXPAND: '.collapseexpand', 35 COURSEBOX: '.coursebox', 36 COURSEBOXLISTENLINK: '.coursebox .moreinfo', 37 COURSEBOXSPINNERLOCATION: '.coursename a', 38 COURSECATEGORYTREE: '.course_category_tree', 39 PARENTWITHCHILDREN: '.category' 40 }, 41 NS = Y.namespace('Moodle.course.categoryexpander'), 42 TYPE_CATEGORY = 0, 43 TYPE_COURSE = 1, 44 URL = M.cfg.wwwroot + '/course/category.ajax.php'; 45 46 /** 47 * Set up the category expander. 48 * 49 * No arguments are required. 50 * 51 * @method init 52 */ 53 NS.init = function() { 54 var doc = Y.one(Y.config.doc); 55 doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this); 56 doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this); 57 doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this); 58 59 // Only set up they keybaord listeners when tab is first pressed - it 60 // may never happen and modifying the DOM on a large number of nodes 61 // can be very expensive. 62 doc.once('key', this.setup_keyboard_listeners, 'tab', this); 63 }; 64 65 /** 66 * Set up keyboard expansion for course content. 67 * 68 * This includes setting up the delegation but also adding the nodes to the 69 * tabflow. 70 * 71 * @method setup_keyboard_listeners 72 */ 73 NS.setup_keyboard_listeners = function() { 74 var doc = Y.one(Y.config.doc); 75 76 Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander'); 77 doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0'); 78 79 80 Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this); 81 Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this); 82 Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); 83 }; 84 85 /** 86 * Toggle the animation of the clicked category node. 87 * 88 * @method toggle_category_expansion 89 * @private 90 * @param {EventFacade} e 91 */ 92 NS.toggle_category_expansion = function(e) { 93 // Load the actual dependencies now that we've been called. 94 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 95 // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that 96 // this function isn't called in the future, and call it for the first time. 97 NS.toggle_category_expansion = NS._toggle_category_expansion; 98 NS.toggle_category_expansion(e); 99 }); 100 }; 101 102 /** 103 * Toggle the animation of the clicked coursebox node. 104 * 105 * @method toggle_coursebox_expansion 106 * @private 107 * @param {EventFacade} e 108 */ 109 NS.toggle_coursebox_expansion = function(e) { 110 // Load the actual dependencies now that we've been called. 111 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 112 // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that 113 // this function isn't called in the future, and call it for the first time. 114 NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion; 115 NS.toggle_coursebox_expansion(e); 116 }); 117 118 e.preventDefault(); 119 }; 120 121 NS._toggle_coursebox_expansion = function(e) { 122 var courseboxnode; 123 124 // Grab the parent category container - this is where the new content will be added. 125 courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true); 126 e.preventDefault(); 127 128 if (courseboxnode.hasClass(CSS.LOADED)) { 129 // We've already loaded this content so we just need to toggle the view of it. 130 this.run_expansion(courseboxnode); 131 return; 132 } 133 134 this._toggle_generic_expansion({ 135 parentnode: courseboxnode, 136 childnode: courseboxnode.one(SELECTORS.CONTENTNODE), 137 spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION, 138 data: { 139 courseid: courseboxnode.getData('courseid'), 140 type: TYPE_COURSE 141 } 142 }); 143 }; 144 145 NS._toggle_category_expansion = function(e) { 146 var categorynode, 147 categoryid, 148 depth; 149 150 if (e.target.test('a') || e.target.test('img')) { 151 // Return early if either an anchor or an image were clicked. 152 return; 153 } 154 155 // Grab the parent category container - this is where the new content will be added. 156 categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true); 157 158 if (!categorynode.hasClass(CSS.HASCHILDREN)) { 159 // Nothing to do here - this category has no children. 160 return; 161 } 162 163 if (categorynode.hasClass(CSS.LOADED)) { 164 // We've already loaded this content so we just need to toggle the view of it. 165 this.run_expansion(categorynode); 166 return; 167 } 168 169 // We use Data attributes to store the category. 170 categoryid = categorynode.getData('categoryid'); 171 depth = categorynode.getData('depth'); 172 if (typeof categoryid === "undefined" || typeof depth === "undefined") { 173 return; 174 } 175 176 this._toggle_generic_expansion({ 177 parentnode: categorynode, 178 childnode: categorynode.one(SELECTORS.CONTENTNODE), 179 spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, 180 data: { 181 categoryid: categoryid, 182 depth: depth, 183 showcourses: categorynode.getData('showcourses'), 184 type: TYPE_CATEGORY 185 } 186 }); 187 }; 188 189 /** 190 * Wrapper function to handle toggling of generic types. 191 * 192 * @method _toggle_generic_expansion 193 * @private 194 * @param {Object} config 195 */ 196 NS._toggle_generic_expansion = function(config) { 197 if (config.spinnerhandle) { 198 // Add a spinner to give some feedback to the user. 199 spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show(); 200 } 201 202 // Fetch the data. 203 Y.io(URL, { 204 method: 'POST', 205 context: this, 206 on: { 207 complete: this.process_results 208 }, 209 data: config.data, 210 "arguments": { 211 parentnode: config.parentnode, 212 childnode: config.childnode, 213 spinner: spinner 214 } 215 }); 216 }; 217 218 /** 219 * Apply the animation on the supplied node. 220 * 221 * @method run_expansion 222 * @private 223 * @param {Node} categorynode The node to apply the animation to 224 */ 225 NS.run_expansion = function(categorynode) { 226 var categorychildren = categorynode.one(SELECTORS.CONTENTNODE), 227 self = this, 228 ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE); 229 230 // Add our animation to the categorychildren. 231 this.add_animation(categorychildren); 232 233 234 // If we already have the class, remove it before showing otherwise we perform the 235 // animation whilst the node is hidden. 236 if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) { 237 // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class. 238 categorychildren.setStyle('height', '0'); 239 categorynode.removeClass(CSS.SECTIONCOLLAPSED); 240 categorynode.setAttribute('aria-expanded', 'true'); 241 categorychildren.fx.set('reverse', false); 242 } else { 243 categorychildren.fx.set('reverse', true); 244 categorychildren.fx.once('end', function(e, categorynode) { 245 categorynode.addClass(CSS.SECTIONCOLLAPSED); 246 categorynode.setAttribute('aria-expanded', 'false'); 247 }, this, categorynode); 248 } 249 250 categorychildren.fx.once('end', function(e, categorychildren) { 251 // Remove the styles that the animation has set. 252 categorychildren.setStyles({ 253 height: '', 254 opacity: '' 255 }); 256 257 // To avoid memory gobbling, remove the animation. It will be added back if called again. 258 this.destroy(); 259 self.update_collapsible_actions(ancestor); 260 }, categorychildren.fx, categorychildren); 261 262 // Now that everything has been set up, run the animation. 263 categorychildren.fx.run(); 264 }; 265 266 /** 267 * Toggle collapsing of all nodes. 268 * 269 * @method collapse_expand_all 270 * @private 271 * @param {EventFacade} e 272 */ 273 NS.collapse_expand_all = function(e) { 274 // Load the actual dependencies now that we've been called. 275 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 276 // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that 277 // this function isn't called in the future, and call it for the first time. 278 NS.collapse_expand_all = NS._collapse_expand_all; 279 NS.collapse_expand_all(e); 280 }); 281 282 e.preventDefault(); 283 }; 284 285 NS._collapse_expand_all = function(e) { 286 // The collapse/expand button has no actual target but we need to prevent it's default 287 // action to ensure we don't make the page reload/jump. 288 e.preventDefault(); 289 290 if (e.currentTarget.hasClass(CSS.DISABLED)) { 291 // The collapse/expand is currently disabled. 292 return; 293 } 294 295 var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE); 296 if (!ancestor) { 297 return; 298 } 299 300 var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND); 301 if (collapseall.hasClass(CSS.COLLAPSEALL)) { 302 this.collapse_all(ancestor); 303 } else { 304 this.expand_all(ancestor); 305 } 306 this.update_collapsible_actions(ancestor); 307 }; 308 309 NS.expand_all = function(ancestor) { 310 var finalexpansions = []; 311 312 ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) 313 .each(function(c) { 314 if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { 315 // Expand the hidden children first without animation. 316 c.removeClass(CSS.SECTIONCOLLAPSED); 317 c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); 318 } else { 319 finalexpansions.push(c); 320 } 321 }, this); 322 323 // Run the final expansion with animation on the visible items. 324 Y.all(finalexpansions).each(function(c) { 325 this.run_expansion(c); 326 }, this); 327 328 }; 329 330 NS.collapse_all = function(ancestor) { 331 var finalcollapses = []; 332 333 ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN) 334 .each(function(c) { 335 if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) { 336 finalcollapses.push(c); 337 } else { 338 // Collapse the visible items first 339 this.run_expansion(c); 340 } 341 }, this); 342 343 // Run the final collapses now that the these are hidden hidden. 344 Y.all(finalcollapses).each(function(c) { 345 c.addClass(CSS.SECTIONCOLLAPSED); 346 c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED); 347 }, this); 348 }; 349 350 NS.update_collapsible_actions = function(ancestor) { 351 var foundmaximisedchildren = false, 352 // Grab the anchor for the collapseexpand all link. 353 togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND); 354 355 if (!togglelink) { 356 // We should always have a togglelink but ensure. 357 return; 358 } 359 360 // Search for any visibly expanded children. 361 ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) { 362 // If we can find any collapsed ancestors, skip. 363 if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { 364 return false; 365 } 366 foundmaximisedchildren = true; 367 return true; 368 }); 369 370 if (foundmaximisedchildren) { 371 // At least one maximised child found. Show the collapseall. 372 togglelink.setHTML(M.util.get_string('collapseall', 'moodle')) 373 .addClass(CSS.COLLAPSEALL) 374 .removeClass(CSS.DISABLED); 375 } else { 376 // No maximised children found but there are collapsed children. Show the expandall. 377 togglelink.setHTML(M.util.get_string('expandall', 'moodle')) 378 .removeClass(CSS.COLLAPSEALL) 379 .removeClass(CSS.DISABLED); 380 } 381 }; 382 383 /** 384 * Process the data returned by Y.io. 385 * This includes appending it to the relevant part of the DOM, and applying our animations. 386 * 387 * @method process_results 388 * @private 389 * @param {String} tid The Transaction ID 390 * @param {Object} response The Reponse returned by Y.IO 391 * @param {Object} ioargs The additional arguments provided by Y.IO 392 */ 393 NS.process_results = function(tid, response, args) { 394 var newnode, 395 data; 396 try { 397 data = Y.JSON.parse(response.responseText); 398 if (data.error) { 399 return new M.core.ajaxException(data); 400 } 401 } catch (e) { 402 return new M.core.exception(e); 403 } 404 405 // Insert the returned data into a new Node. 406 newnode = Y.Node.create(data); 407 408 // Append to the existing child location. 409 args.childnode.appendChild(newnode); 410 411 // Now that we have content, we can swap the classes on the toggled container. 412 args.parentnode 413 .addClass(CSS.LOADED) 414 .removeClass(CSS.NOTLOADED); 415 416 // Toggle the open/close status of the node now that it's content has been loaded. 417 this.run_expansion(args.parentnode); 418 419 // Remove the spinner now that we've started to show the content. 420 if (args.spinner) { 421 args.spinner.hide().destroy(); 422 } 423 }; 424 425 /** 426 * Add our animation to the Node. 427 * 428 * @method add_animation 429 * @private 430 * @param {Node} childnode 431 */ 432 NS.add_animation = function(childnode) { 433 if (typeof childnode.fx !== "undefined") { 434 // The animation has already been plugged to this node. 435 return childnode; 436 } 437 438 childnode.plug(Y.Plugin.NodeFX, { 439 from: { 440 height: 0, 441 opacity: 0 442 }, 443 to: { 444 // This sets a dynamic height in case the node content changes. 445 height: function(node) { 446 // Get expanded height (offsetHeight may be zero). 447 return node.get('scrollHeight'); 448 }, 449 opacity: 1 450 }, 451 duration: 0.2 452 }); 453 454 return childnode; 455 }; 456 457 458 }, '@VERSION@', {"requires": ["node", "event-key"]});
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |