Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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