[ 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 doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0'); 77 78 79 Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this); 80 Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this); 81 Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); 82 }; 83 84 /** 85 * Toggle the animation of the clicked category node. 86 * 87 * @method toggle_category_expansion 88 * @private 89 * @param {EventFacade} e 90 */ 91 NS.toggle_category_expansion = function(e) { 92 // Load the actual dependencies now that we've been called. 93 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 94 // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that 95 // this function isn't called in the future, and call it for the first time. 96 NS.toggle_category_expansion = NS._toggle_category_expansion; 97 NS.toggle_category_expansion(e); 98 }); 99 }; 100 101 /** 102 * Toggle the animation of the clicked coursebox node. 103 * 104 * @method toggle_coursebox_expansion 105 * @private 106 * @param {EventFacade} e 107 */ 108 NS.toggle_coursebox_expansion = function(e) { 109 // Load the actual dependencies now that we've been called. 110 Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { 111 // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that 112 // this function isn't called in the future, and call it for the first time. 113 NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion; 114 NS.toggle_coursebox_expansion(e); 115 }); 116 117 e.preventDefault(); 118 }; 119 120 NS._toggle_coursebox_expansion = function(e) { 121 var courseboxnode; 122 123 // Grab the parent category container - this is where the new content will be added. 124 courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true); 125 e.preventDefault(); 126 127 if (courseboxnode.hasClass(CSS.LOADED)) { 128 // We've already loaded this content so we just need to toggle the view of it. 129 this.run_expansion(courseboxnode); 130 return; 131 } 132 133 this._toggle_generic_expansion({ 134 parentnode: courseboxnode, 135 childnode: courseboxnode.one(SELECTORS.CONTENTNODE), 136 spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION, 137 data: { 138 courseid: courseboxnode.getData('courseid'), 139 type: TYPE_COURSE 140 } 141 }); 142 }; 143 144 NS._toggle_category_expansion = function(e) { 145 var categorynode, 146 categoryid, 147 depth; 148 149 if (e.target.test('a') || e.target.test('img')) { 150 // Return early if either an anchor or an image were clicked. 151 return; 152 } 153 154 // Grab the parent category container - this is where the new content will be added. 155 categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true); 156 157 if (!categorynode.hasClass(CSS.HASCHILDREN)) { 158 // Nothing to do here - this category has no children. 159 return; 160 } 161 162 if (categorynode.hasClass(CSS.LOADED)) { 163 // We've already loaded this content so we just need to toggle the view of it. 164 this.run_expansion(categorynode); 165 return; 166 } 167 168 // We use Data attributes to store the category. 169 categoryid = categorynode.getData('categoryid'); 170 depth = categorynode.getData('depth'); 171 if (typeof categoryid === "undefined" || typeof depth === "undefined") { 172 return; 173 } 174 175 this._toggle_generic_expansion({ 176 parentnode: categorynode, 177 childnode: categorynode.one(SELECTORS.CONTENTNODE), 178 spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, 179 data: { 180 categoryid: categoryid, 181 depth: depth, 182 showcourses: categorynode.getData('showcourses'), 183 type: TYPE_CATEGORY 184 } 185 }); 186 }; 187 188 /** 189 * Wrapper function to handle toggling of generic types. 190 * 191 * @method _toggle_generic_expansion 192 * @private 193 * @param {Object} config 194 */ 195 NS._toggle_generic_expansion = function(config) { 196 var spinner; 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: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |