Commit | Line | Data |
---|---|---|
53c1b936 ARN |
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 .name', | |
31 | CATEGORYSPINNERLOCATION: '.name', | |
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: '.name 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() { | |
1574e652 AN |
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); | |
53c1b936 ARN |
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', 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', 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 | 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); | |
4dab1c39 | 239 | categorynode.setAttribute('aria-expanded', 'true'); |
53c1b936 ARN |
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); | |
4dab1c39 | 245 | categorynode.setAttribute('aria-expanded', 'false'); |
53c1b936 ARN |
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 | NS.collapse_expand_all = function(e) { | |
266 | // The collapse/expand button has no actual target but we need to prevent it's default | |
267 | // action to ensure we don't make the page reload/jump. | |
268 | e.preventDefault(); | |
269 | ||
270 | if (e.currentTarget.hasClass(CSS.DISABLED)) { | |
271 | // The collapse/expand is currently disabled. | |
272 | return; | |
273 | } | |
274 | ||
275 | var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE); | |
276 | if (!ancestor) { | |
277 | return; | |
278 | } | |
279 | ||
280 | var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND); | |
281 | if (collapseall.hasClass(CSS.COLLAPSEALL)) { | |
282 | this.collapse_all(ancestor); | |
283 | } else { | |
284 | this.expand_all(ancestor); | |
285 | } | |
286 | this.update_collapsible_actions(ancestor); | |
287 | }; | |
288 | ||
289 | NS.expand_all = function(ancestor) { | |
290 | var finalexpansions = []; | |
291 | ||
292 | ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN) | |
293 | .each(function(c) { | |
294 | if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { | |
295 | // Expand the hidden children first without animation. | |
296 | c.removeClass(CSS.SECTIONCOLLAPSED); | |
297 | c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED); | |
298 | } else { | |
299 | finalexpansions.push(c); | |
300 | } | |
301 | }, this); | |
302 | ||
303 | // Run the final expansion with animation on the visible items. | |
304 | Y.all(finalexpansions).each(function(c) { | |
305 | this.run_expansion(c); | |
306 | }, this); | |
307 | ||
308 | }; | |
309 | ||
310 | NS.collapse_all = function(ancestor) { | |
311 | var finalcollapses = []; | |
312 | ||
313 | ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN) | |
314 | .each(function(c) { | |
315 | if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) { | |
316 | finalcollapses.push(c); | |
317 | } else { | |
318 | // Collapse the visible items first | |
319 | this.run_expansion(c); | |
320 | } | |
321 | }, this); | |
322 | ||
323 | // Run the final collapses now that the these are hidden hidden. | |
324 | Y.all(finalcollapses).each(function(c) { | |
325 | c.addClass(CSS.SECTIONCOLLAPSED); | |
326 | c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED); | |
327 | }, this); | |
328 | }; | |
329 | ||
330 | NS.update_collapsible_actions = function(ancestor) { | |
331 | var foundmaximisedchildren = false, | |
332 | // Grab the anchor for the collapseexpand all link. | |
333 | togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND); | |
334 | ||
335 | if (!togglelink) { | |
336 | // We should always have a togglelink but ensure. | |
337 | return; | |
338 | } | |
339 | ||
340 | // Search for any visibly expanded children. | |
341 | ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) { | |
342 | // If we can find any collapsed ancestors, skip. | |
343 | if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { | |
344 | return false; | |
345 | } | |
346 | foundmaximisedchildren = true; | |
347 | return true; | |
348 | }); | |
349 | ||
350 | if (foundmaximisedchildren) { | |
351 | // At least one maximised child found. Show the collapseall. | |
352 | togglelink.setHTML(M.util.get_string('collapseall', 'moodle')) | |
353 | .addClass(CSS.COLLAPSEALL) | |
354 | .removeClass(CSS.DISABLED); | |
355 | } else { | |
356 | // No maximised children found but there are collapsed children. Show the expandall. | |
357 | togglelink.setHTML(M.util.get_string('expandall', 'moodle')) | |
358 | .removeClass(CSS.COLLAPSEALL) | |
359 | .removeClass(CSS.DISABLED); | |
360 | } | |
361 | }; | |
362 | ||
363 | /** | |
364 | * Process the data returned by Y.io. | |
365 | * This includes appending it to the relevant part of the DOM, and applying our animations. | |
366 | * | |
367 | * @method process_results | |
368 | * @private | |
369 | * @param {String} tid The Transaction ID | |
370 | * @param {Object} response The Reponse returned by Y.IO | |
371 | * @param {Object} ioargs The additional arguments provided by Y.IO | |
372 | */ | |
373 | NS.process_results = function(tid, response, args) { | |
374 | var newnode, | |
375 | data; | |
376 | try { | |
377 | data = Y.JSON.parse(response.responseText); | |
378 | if (data.error) { | |
379 | return new M.core.ajaxException(data); | |
380 | } | |
381 | } catch (e) { | |
382 | return new M.core.exception(e); | |
383 | } | |
384 | ||
385 | // Insert the returned data into a new Node. | |
386 | newnode = Y.Node.create(data); | |
387 | ||
388 | // Append to the existing child location. | |
389 | args.childnode.appendChild(newnode); | |
390 | ||
391 | // Now that we have content, we can swap the classes on the toggled container. | |
392 | args.parentnode | |
393 | .addClass(CSS.LOADED) | |
394 | .removeClass(CSS.NOTLOADED); | |
395 | ||
396 | // Toggle the open/close status of the node now that it's content has been loaded. | |
397 | this.run_expansion(args.parentnode); | |
398 | ||
399 | // Remove the spinner now that we've started to show the content. | |
400 | if (args.spinner) { | |
401 | args.spinner.hide().destroy(); | |
402 | } | |
403 | }; | |
404 | ||
405 | /** | |
406 | * Add our animation to the Node. | |
407 | * | |
408 | * @method add_animation | |
409 | * @private | |
410 | * @param {Node} childnode | |
411 | */ | |
412 | NS.add_animation = function(childnode) { | |
413 | if (typeof childnode.fx !== "undefined") { | |
414 | // The animation has already been plugged to this node. | |
415 | return childnode; | |
416 | } | |
417 | ||
418 | childnode.plug(Y.Plugin.NodeFX, { | |
419 | from: { | |
420 | height: 0, | |
421 | opacity: 0 | |
422 | }, | |
423 | to: { | |
424 | // This sets a dynamic height in case the node content changes. | |
425 | height: function(node) { | |
426 | // Get expanded height (offsetHeight may be zero). | |
427 | return node.get('scrollHeight'); | |
428 | }, | |
429 | opacity: 1 | |
430 | }, | |
431 | duration: 0.2 | |
432 | }); | |
433 | ||
434 | return childnode; | |
435 | }; | |
436 | ||
437 | ||
1574e652 | 438 | }, '@VERSION@', {"requires": ["node", "event-key"]}); |