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