[ Index ] |
PHP Cross Reference of Unnamed Project |
[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 var spinner; 198 if (config.spinnerhandle) { 199 // Add a spinner to give some feedback to the user. 200 spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show(); 201 } 202 203 // Fetch the data. 204 Y.io(URL, { 205 method: 'POST', 206 context: this, 207 on: { 208 complete: this.process_results 209 }, 210 data: config.data, 211 "arguments": { 212 parentnode: config.parentnode, 213 childnode: config.childnode, 214 spinner: spinner 215 } 216 }); 217 }; 218 219 /** 220 * Apply the animation on the supplied node. 221 * 222 * @method run_expansion 223 * @private 224 * @param {Node} categorynode The node to apply the animation to 225 */ 226 NS.run_expansion = function(categorynode) { 227 var categorychildren = categorynode.one(SELECTORS.CONTENTNODE), 228 self = this, 229 ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE); 230 231 // Add our animation to the categorychildren. 232 this.add_animation(categorychildren); 233 234 235 // If we already have the class, remove it before showing otherwise we perform the 236 // animation whilst the node is hidden. 237 if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) { 238 // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class. 239 categorychildren.setStyle('height', '0'); 240 categorynode.removeClass(CSS.SECTIONCOLLAPSED); 241 categorynode.setAttribute('aria-expanded', 'true'); 242 categorychildren.fx.set('reverse', false); 243 } else { 244 categorychildren.fx.set('reverse', true); 245 categorychildren.fx.once('end', function(e, categorynode) { 246 categorynode.addClass(CSS.SECTIONCOLLAPSED); 247 categorynode.setAttribute('aria-expanded', 'false'); 248 }, this, categorynode); 249 } 250 251 categorychildren.fx.once('end', function(e, categorychildren) { 252 // Remove the styles that the animation has set. 253 categorychildren.setStyles({ 254 height: '', 255 opacity: '' 256 }); 257 258 // To avoid memory gobbling, remove the animation. It will be added back if called again. 259 this.destroy(); 260 self.update_collapsible_actions(ancestor); 261 }, categorychildren.fx, categorychildren); 262 263 // Now that everything has been set up, run the animation. 264 categorychildren.fx.run(); 265 }; 266 267 /** 268 * Toggle collapsing of all nodes. 269 * 270 * @method collapse_expand_all 271 * @private 272 * @param {EventFacade} e 273 */ 274 NS.collapse_expand_all = function(e) { 275 // Load the actual dependencies now that we've been called. 276 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 277 // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that 278 // this function isn't called in the future, and call it for the first time. 279 NS.collapse_expand_all = NS._collapse_expand_all; 280 NS.collapse_expand_all(e); 281 }); 282 283 e.preventDefault(); 284 }; 285 286 NS._collapse_expand_all = function(e) { 287 // The collapse/expand button has no actual target but we need to prevent it's default 288 // action to ensure we don't make the page reload/jump. 289 e.preventDefault(); 290 291 if (e.currentTarget.hasClass(CSS.DISABLED)) { 292 // The collapse/expand is currently disabled. 293 return; 294 } 295 296 var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE); 297 if (!ancestor) { 298 return; 299 } 300 301 var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND); 302 if (collapseall.hasClass(CSS.COLLAPSEALL)) { 303 this.collapse_all(ancestor); 304 } else { 305 this.expand_all(ancestor); 306 } 307 this.update_collapsible_actions(ancestor); 308 }; 309 310 NS.expand_all = function(ancestor) { 311 var finalexpansions = []; 312 313 ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) 314 .each(function(c) { 315 if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { 316 // Expand the hidden children first without animation. 317 c.removeClass(CSS.SECTIONCOLLAPSED); 318 c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); 319 } else { 320 finalexpansions.push(c); 321 } 322 }, this); 323 324 // Run the final expansion with animation on the visible items. 325 Y.all(finalexpansions).each(function(c) { 326 this.run_expansion(c); 327 }, this); 328 329 }; 330 331 NS.collapse_all = function(ancestor) { 332 var finalcollapses = []; 333 334 ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN) 335 .each(function(c) { 336 if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) { 337 finalcollapses.push(c); 338 } else { 339 // Collapse the visible items first 340 this.run_expansion(c); 341 } 342 }, this); 343 344 // Run the final collapses now that the these are hidden hidden. 345 Y.all(finalcollapses).each(function(c) { 346 c.addClass(CSS.SECTIONCOLLAPSED); 347 c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED); 348 }, this); 349 }; 350 351 NS.update_collapsible_actions = function(ancestor) { 352 var foundmaximisedchildren = false, 353 // Grab the anchor for the collapseexpand all link. 354 togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND); 355 356 if (!togglelink) { 357 // We should always have a togglelink but ensure. 358 return; 359 } 360 361 // Search for any visibly expanded children. 362 ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) { 363 // If we can find any collapsed ancestors, skip. 364 if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { 365 return false; 366 } 367 foundmaximisedchildren = true; 368 return true; 369 }); 370 371 if (foundmaximisedchildren) { 372 // At least one maximised child found. Show the collapseall. 373 togglelink.setHTML(M.util.get_string('collapseall', 'moodle')) 374 .addClass(CSS.COLLAPSEALL) 375 .removeClass(CSS.DISABLED); 376 } else { 377 // No maximised children found but there are collapsed children. Show the expandall. 378 togglelink.setHTML(M.util.get_string('expandall', 'moodle')) 379 .removeClass(CSS.COLLAPSEALL) 380 .removeClass(CSS.DISABLED); 381 } 382 }; 383 384 /** 385 * Process the data returned by Y.io. 386 * This includes appending it to the relevant part of the DOM, and applying our animations. 387 * 388 * @method process_results 389 * @private 390 * @param {String} tid The Transaction ID 391 * @param {Object} response The Reponse returned by Y.IO 392 * @param {Object} ioargs The additional arguments provided by Y.IO 393 */ 394 NS.process_results = function(tid, response, args) { 395 var newnode, 396 data; 397 try { 398 data = Y.JSON.parse(response.responseText); 399 if (data.error) { 400 return new M.core.ajaxException(data); 401 } 402 } catch (e) { 403 return new M.core.exception(e); 404 } 405 406 // Insert the returned data into a new Node. 407 newnode = Y.Node.create(data); 408 409 // Append to the existing child location. 410 args.childnode.appendChild(newnode); 411 412 // Now that we have content, we can swap the classes on the toggled container. 413 args.parentnode 414 .addClass(CSS.LOADED) 415 .removeClass(CSS.NOTLOADED); 416 417 // Toggle the open/close status of the node now that it's content has been loaded. 418 this.run_expansion(args.parentnode); 419 420 // Remove the spinner now that we've started to show the content. 421 if (args.spinner) { 422 args.spinner.hide().destroy(); 423 } 424 }; 425 426 /** 427 * Add our animation to the Node. 428 * 429 * @method add_animation 430 * @private 431 * @param {Node} childnode 432 */ 433 NS.add_animation = function(childnode) { 434 if (typeof childnode.fx !== "undefined") { 435 // The animation has already been plugged to this node. 436 return childnode; 437 } 438 439 childnode.plug(Y.Plugin.NodeFX, { 440 from: { 441 height: 0, 442 opacity: 0 443 }, 444 to: { 445 // This sets a dynamic height in case the node content changes. 446 height: function(node) { 447 // Get expanded height (offsetHeight may be zero). 448 return node.get('scrollHeight'); 449 }, 450 opacity: 1 451 }, 452 duration: 0.2 453 }); 454 455 return childnode; 456 }; 457 458 459 }, '@VERSION@', {"requires": ["node", "event-key"]});
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |