Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
        WITHCHILDRENTREES: '.with_children',
27
        LOADEDTREES: '.with_children.loaded',
28
        CONTENTNODE: '.content',
29
        CATEGORYLISTENLINK: '.category .info .categoryname',
30
        CATEGORYSPINNERLOCATION: '.categoryname',
31
        CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed',
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: '.info .moreinfo',
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
 * Expand all categories.
87
 *
88
 * @method expand_category
89
 * @private
90
 * @param {Node} categorynode The node to expand
91
 */
92
NS.expand_category = function(categorynode) {
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 expand_category with the _expand_category function to ensure that
96
        // this function isn't called in the future, and call it for the first time.
97
        NS.expand_category = NS._expand_category;
98
        NS.expand_category(categorynode);
99
    });
100
};
101
 
102
NS._expand_category = function(categorynode) {
103
    var categoryid,
104
        depth;
105
 
106
    if (!categorynode.hasClass(CSS.HASCHILDREN)) {
107
        // Nothing to do here - this category has no children.
108
        return;
109
    }
110
 
111
    if (categorynode.hasClass(CSS.LOADED)) {
112
        // We've already loaded this content so we just need to toggle the view of it.
113
        this.run_expansion(categorynode);
114
        return;
115
    }
116
 
117
    // We use Data attributes to store the category.
118
    categoryid = categorynode.getData('categoryid');
119
    depth = categorynode.getData('depth');
120
    if (typeof categoryid === "undefined" || typeof depth === "undefined") {
121
        return;
122
    }
123
 
124
    this._toggle_generic_expansion({
125
        parentnode: categorynode,
126
        childnode: categorynode.one(SELECTORS.CONTENTNODE),
127
        spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
128
        data: {
129
            categoryid: categoryid,
130
            depth: depth,
131
            showcourses: categorynode.getData('showcourses'),
132
            type: TYPE_CATEGORY
133
        }
134
    });
135
};
136
 
137
/**
138
 * Toggle the animation of the clicked category node.
139
 *
140
 * @method toggle_category_expansion
141
 * @private
142
 * @param {EventFacade} e
143
 */
144
NS.toggle_category_expansion = function(e) {
145
    // Load the actual dependencies now that we've been called.
146
    Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
147
        // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that
148
        // this function isn't called in the future, and call it for the first time.
149
        NS.toggle_category_expansion = NS._toggle_category_expansion;
150
        NS.toggle_category_expansion(e);
151
    });
152
};
153
 
154
/**
155
 * Toggle the animation of the clicked coursebox node.
156
 *
157
 * @method toggle_coursebox_expansion
158
 * @private
159
 * @param {EventFacade} e
160
 */
161
NS.toggle_coursebox_expansion = function(e) {
162
    // Load the actual dependencies now that we've been called.
163
    Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
164
        // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that
165
        // this function isn't called in the future, and call it for the first time.
166
        NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion;
167
        NS.toggle_coursebox_expansion(e);
168
    });
169
 
170
    e.preventDefault();
171
};
172
 
173
NS._toggle_coursebox_expansion = function(e) {
174
    var courseboxnode;
175
 
176
    // Grab the parent category container - this is where the new content will be added.
177
    courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true);
178
    e.preventDefault();
179
 
180
    if (courseboxnode.hasClass(CSS.LOADED)) {
181
        // We've already loaded this content so we just need to toggle the view of it.
182
        this.run_expansion(courseboxnode);
183
        return;
184
    }
185
 
186
    this._toggle_generic_expansion({
187
        parentnode: courseboxnode,
188
        childnode: courseboxnode.one(SELECTORS.CONTENTNODE),
189
        spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION,
190
        data: {
191
            courseid: courseboxnode.getData('courseid'),
192
            type: TYPE_COURSE
193
        }
194
    });
195
};
196
 
197
NS._toggle_category_expansion = function(e) {
198
    var categorynode,
199
        categoryid,
200
        depth;
201
 
202
    if (e.target.test('a') || e.target.test('img')) {
203
        // Return early if either an anchor or an image were clicked.
204
        return;
205
    }
206
 
207
    // Grab the parent category container - this is where the new content will be added.
208
    categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true);
209
 
210
    if (!categorynode.hasClass(CSS.HASCHILDREN)) {
211
        // Nothing to do here - this category has no children.
212
        return;
213
    }
214
 
215
    if (categorynode.hasClass(CSS.LOADED)) {
216
        // We've already loaded this content so we just need to toggle the view of it.
217
        this.run_expansion(categorynode);
218
        return;
219
    }
220
 
221
    // We use Data attributes to store the category.
222
    categoryid = categorynode.getData('categoryid');
223
    depth = categorynode.getData('depth');
224
    if (typeof categoryid === "undefined" || typeof depth === "undefined") {
225
        return;
226
    }
227
 
228
    this._toggle_generic_expansion({
229
        parentnode: categorynode,
230
        childnode: categorynode.one(SELECTORS.CONTENTNODE),
231
        spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
232
        data: {
233
            categoryid: categoryid,
234
            depth: depth,
235
            showcourses: categorynode.getData('showcourses'),
236
            type: TYPE_CATEGORY
237
        }
238
    });
239
};
240
 
241
/**
242
 * Wrapper function to handle toggling of generic types.
243
 *
244
 * @method _toggle_generic_expansion
245
 * @private
246
 * @param {Object} config
247
 */
248
NS._toggle_generic_expansion = function(config) {
249
    var spinner;
250
    if (config.spinnerhandle) {
251
      // Add a spinner to give some feedback to the user.
252
      spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
253
    }
254
 
255
    // Fetch the data.
256
    Y.io(URL, {
257
        method: 'POST',
258
        context: this,
259
        on: {
260
            complete: this.process_results
261
        },
262
        data: config.data,
263
        "arguments": {
264
            parentnode: config.parentnode,
265
            childnode: config.childnode,
266
            spinner: spinner
267
        }
268
    });
269
};
270
 
271
/**
272
 * Apply the animation on the supplied node.
273
 *
274
 * @method run_expansion
275
 * @private
276
 * @param {Node} categorynode The node to apply the animation to
277
 */
278
NS.run_expansion = function(categorynode) {
279
    var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
280
        self = this,
281
        ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
282
 
283
    // Add our animation to the categorychildren.
284
    this.add_animation(categorychildren);
285
 
286
 
287
    // If we already have the class, remove it before showing otherwise we perform the
288
    // animation whilst the node is hidden.
289
    if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
290
        // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
291
        categorychildren.setStyle('height', '0');
292
        categorynode.removeClass(CSS.SECTIONCOLLAPSED);
293
        categorynode.setAttribute('aria-expanded', 'true');
294
        categorychildren.fx.set('reverse', false);
295
    } else {
296
        categorychildren.fx.set('reverse', true);
297
        categorychildren.fx.once('end', function(e, categorynode) {
298
            categorynode.addClass(CSS.SECTIONCOLLAPSED);
299
            categorynode.setAttribute('aria-expanded', 'false');
300
        }, this, categorynode);
301
    }
302
 
303
    categorychildren.fx.once('end', function(e, categorychildren) {
304
        // Remove the styles that the animation has set.
305
        categorychildren.setStyles({
306
            height: '',
307
            opacity: ''
308
        });
309
 
310
        // To avoid memory gobbling, remove the animation. It will be added back if called again.
311
        this.destroy();
312
        self.update_collapsible_actions(ancestor);
313
    }, categorychildren.fx, categorychildren);
314
 
315
    // Now that everything has been set up, run the animation.
316
    categorychildren.fx.run();
317
};
318
 
319
/**
320
 * Toggle collapsing of all nodes.
321
 *
322
 * @method collapse_expand_all
323
 * @private
324
 * @param {EventFacade} e
325
 */
326
NS.collapse_expand_all = function(e) {
327
    // Load the actual dependencies now that we've been called.
328
    Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
329
        // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
330
        // this function isn't called in the future, and call it for the first time.
331
        NS.collapse_expand_all = NS._collapse_expand_all;
332
        NS.collapse_expand_all(e);
333
    });
334
 
335
    e.preventDefault();
336
};
337
 
338
NS._collapse_expand_all = function(e) {
339
    // The collapse/expand button has no actual target but we need to prevent it's default
340
    // action to ensure we don't make the page reload/jump.
341
    e.preventDefault();
342
 
343
    if (e.currentTarget.hasClass(CSS.DISABLED)) {
344
        // The collapse/expand is currently disabled.
345
        return;
346
    }
347
 
348
    var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
349
    if (!ancestor) {
350
        return;
351
    }
352
 
353
    var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
354
    if (collapseall.hasClass(CSS.COLLAPSEALL)) {
355
        this.collapse_all(ancestor);
356
    } else {
357
        this.expand_all(ancestor);
358
    }
359
    this.update_collapsible_actions(ancestor);
360
};
361
 
362
NS.expand_all = function(ancestor) {
363
    var finalexpansions = [];
364
 
365
    ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)
366
        .each(function(c) {
367
        if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) {
368
            // Expand the hidden children first without animation.
369
            c.removeClass(CSS.SECTIONCOLLAPSED);
370
            c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED);
371
        } else {
372
            finalexpansions.push(c);
373
        }
374
    }, this);
375
 
376
    // Run the final expansion with animation on the visible items.
377
    Y.all(finalexpansions).each(function(c) {
378
        this.expand_category(c);
379
    }, this);
380
 
381
};
382
 
383
NS.collapse_all = function(ancestor) {
384
    var finalcollapses = [];
385
 
386
    ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
387
        .each(function(c) {
388
        if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
389
            finalcollapses.push(c);
390
        } else {
391
            // Collapse the visible items first
392
            this.run_expansion(c);
393
        }
394
    }, this);
395
 
396
    // Run the final collapses now that the these are hidden hidden.
397
    Y.all(finalcollapses).each(function(c) {
398
        c.addClass(CSS.SECTIONCOLLAPSED);
399
        c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
400
    }, this);
401
};
402
 
403
NS.update_collapsible_actions = function(ancestor) {
404
    var foundmaximisedchildren = false,
405
        // Grab the anchor for the collapseexpand all link.
406
        togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
407
 
408
    if (!togglelink) {
409
        // We should always have a togglelink but ensure.
410
        return;
411
    }
412
 
413
    // Search for any visibly expanded children.
414
    ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
415
        // If we can find any collapsed ancestors, skip.
416
        if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
417
            return false;
418
        }
419
        foundmaximisedchildren = true;
420
        return true;
421
    });
422
 
423
    if (foundmaximisedchildren) {
424
        // At least one maximised child found. Show the collapseall.
425
        togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
426
            .addClass(CSS.COLLAPSEALL)
427
            .removeClass(CSS.DISABLED);
428
    } else {
429
        // No maximised children found but there are collapsed children. Show the expandall.
430
        togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
431
            .removeClass(CSS.COLLAPSEALL)
432
            .removeClass(CSS.DISABLED);
433
    }
434
};
435
 
436
/**
437
 * Process the data returned by Y.io.
438
 * This includes appending it to the relevant part of the DOM, and applying our animations.
439
 *
440
 * @method process_results
441
 * @private
442
 * @param {String} tid The Transaction ID
443
 * @param {Object} response The Reponse returned by Y.IO
444
 * @param {Object} ioargs The additional arguments provided by Y.IO
445
 */
446
NS.process_results = function(tid, response, args) {
447
    var newnode,
448
        data;
449
    try {
450
        data = Y.JSON.parse(response.responseText);
451
        if (data.error) {
452
            return new M.core.ajaxException(data);
453
        }
454
    } catch (e) {
455
        return new M.core.exception(e);
456
    }
457
 
458
    // Insert the returned data into a new Node.
459
    newnode = Y.Node.create(data);
460
 
461
    // Append to the existing child location.
462
    args.childnode.appendChild(newnode);
463
 
464
    // Now that we have content, we can swap the classes on the toggled container.
465
    args.parentnode
466
        .addClass(CSS.LOADED)
467
        .removeClass(CSS.NOTLOADED);
468
 
469
    // Toggle the open/close status of the node now that it's content has been loaded.
470
    this.run_expansion(args.parentnode);
471
 
472
    // Remove the spinner now that we've started to show the content.
473
    if (args.spinner) {
474
        args.spinner.hide().destroy();
475
    }
476
};
477
 
478
/**
479
 * Add our animation to the Node.
480
 *
481
 * @method add_animation
482
 * @private
483
 * @param {Node} childnode
484
 */
485
NS.add_animation = function(childnode) {
486
    if (typeof childnode.fx !== "undefined") {
487
        // The animation has already been plugged to this node.
488
        return childnode;
489
    }
490
 
491
    childnode.plug(Y.Plugin.NodeFX, {
492
        from: {
493
            height: 0,
494
            opacity: 0
495
        },
496
        to: {
497
            // This sets a dynamic height in case the node content changes.
498
            height: function(node) {
499
                // Get expanded height (offsetHeight may be zero).
500
                return node.get('scrollHeight');
501
            },
502
            opacity: 1
503
        },
504
        duration: 0.2
505
    });
506
 
507
    return childnode;
508
};