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