Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/* eslint-disable camelcase */
2
// Miscellaneous core Javascript functions for Moodle
3
// Global M object is initilised in inline javascript
4
 
5
/**
6
 * Add module to list of available modules that can be loaded from YUI.
7
 * @param {Array} modules
8
 */
9
M.yui.add_module = function(modules) {
10
    for (var modname in modules) {
11
        YUI_config.modules[modname] = modules[modname];
12
    }
13
    // Ensure thaat the YUI_config is applied to the main YUI instance.
14
    Y.applyConfig(YUI_config);
15
};
16
/**
17
 * The gallery version to use when loading YUI modules from the gallery.
18
 * Will be changed every time when using local galleries.
19
 */
20
M.yui.galleryversion = '2010.04.21-21-51';
21
 
22
/**
23
 * Various utility functions
24
 */
25
M.util = M.util || {};
26
 
27
/**
28
 * Language strings - initialised from page footer.
29
 */
30
M.str = M.str || {};
31
 
32
/**
33
 * Returns url for images.
34
 * @param {String} imagename
35
 * @param {String} component
36
 * @return {String}
37
 */
38
M.util.image_url = function(imagename, component) {
39
 
40
    if (!component || component == '' || component == 'moodle' || component == 'core') {
41
        component = 'core';
42
    }
43
 
44
    var url = M.cfg.wwwroot + '/theme/image.php';
45
    if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
46
        if (!M.cfg.svgicons) {
47
            url += '/_s';
48
        }
49
        url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
50
    } else {
51
        url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
52
        if (!M.cfg.svgicons) {
53
            url += '&svg=0';
54
        }
55
    }
56
 
57
    return url;
58
};
59
 
60
M.util.in_array = function(item, array) {
61
    return array.indexOf(item) !== -1;
62
};
63
 
64
/**
65
 * Init a collapsible region, see print_collapsible_region in weblib.php
66
 * @param {YUI} Y YUI3 instance with all libraries loaded
67
 * @param {String} id the HTML id for the div.
68
 * @param {String} userpref the user preference that records the state of this box. false if none.
69
 * @param {String} strtooltip
70
 */
71
M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
72
    Y.use('anim', function(Y) {
73
        new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
74
    });
75
};
76
 
77
/**
78
 * Object to handle a collapsible region : instantiate and forget styled object
79
 *
80
 * @class
81
 * @constructor
82
 * @param {YUI} Y YUI3 instance with all libraries loaded
83
 * @param {String} id The HTML id for the div.
84
 * @param {String} userpref The user preference that records the state of this box. false if none.
85
 * @param {String} strtooltip
86
 */
87
M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
88
    // Record the pref name
89
    this.userpref = userpref;
90
 
91
    // Find the divs in the document.
92
    this.div = Y.one('#'+id);
93
 
94
    // Get the caption for the collapsible region
95
    var caption = this.div.one('#'+id + '_caption');
96
 
97
    // Create a link
98
    var a = Y.Node.create('<a href="#"></a>');
99
    a.setAttribute('title', strtooltip);
100
 
101
    // Get all the nodes from caption, remove them and append them to <a>
102
    while (caption.hasChildNodes()) {
103
        child = caption.get('firstChild');
104
        child.remove();
105
        a.append(child);
106
    }
107
    caption.append(a);
108
 
109
    // Get the height of the div at this point before we shrink it if required
110
    var height = this.div.get('offsetHeight');
111
    var collapsedimage = 't/collapsed'; // ltr mode
112
    if (right_to_left()) {
113
        collapsedimage = 't/collapsed_rtl';
114
    } else {
115
        collapsedimage = 't/collapsed';
116
    }
117
    if (this.div.hasClass('collapsed')) {
118
        // Add the correct image and record the YUI node created in the process
119
        this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
120
        // Shrink the div as it is collapsed by default
121
        this.div.setStyle('height', caption.get('offsetHeight')+'px');
122
    } else {
123
        // Add the correct image and record the YUI node created in the process
124
        this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
125
    }
126
    a.append(this.icon);
127
 
128
    // Create the animation.
129
    var animation = new Y.Anim({
130
        node: this.div,
131
        duration: 0.3,
132
        easing: Y.Easing.easeBoth,
133
        to: {height:caption.get('offsetHeight')},
134
        from: {height:height}
135
    });
136
 
11 efrain 137
    animation.on('start', () => M.util.js_pending('CollapsibleRegion'));
138
    animation.on('resume', () => M.util.js_pending('CollapsibleRegion'));
139
    animation.on('pause', () => M.util.js_complete('CollapsibleRegion'));
140
 
1 efrain 141
    // Handler for the animation finishing.
142
    animation.on('end', function() {
143
        this.div.toggleClass('collapsed');
144
        var collapsedimage = 't/collapsed'; // ltr mode
145
        if (right_to_left()) {
146
            collapsedimage = 't/collapsed_rtl';
147
            } else {
148
            collapsedimage = 't/collapsed';
149
            }
150
        if (this.div.hasClass('collapsed')) {
151
            this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
152
        } else {
153
            this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
154
        }
11 efrain 155
 
156
        M.util.js_complete('CollapsibleRegion');
1 efrain 157
    }, this);
158
 
159
    // Hook up the event handler.
160
    a.on('click', function(e, animation) {
161
        e.preventDefault();
162
        // Animate to the appropriate size.
163
        if (animation.get('running')) {
164
            animation.stop();
165
        }
166
        animation.set('reverse', this.div.hasClass('collapsed'));
167
        // Update the user preference.
168
        if (this.userpref) {
169
            require(['core_user/repository'], function(UserRepository) {
170
                UserRepository.setUserPreference(this.userpref, !this.div.hasClass('collapsed'));
171
            }.bind(this));
172
        }
173
        animation.run();
174
    }, this, animation);
175
};
176
 
177
/**
178
 * The user preference that stores the state of this box.
179
 * @property userpref
180
 * @type String
181
 */
182
M.util.CollapsibleRegion.prototype.userpref = null;
183
 
184
/**
185
 * The key divs that make up this
186
 * @property div
187
 * @type Y.Node
188
 */
189
M.util.CollapsibleRegion.prototype.div = null;
190
 
191
/**
192
 * The key divs that make up this
193
 * @property icon
194
 * @type Y.Node
195
 */
196
M.util.CollapsibleRegion.prototype.icon = null;
197
 
198
/**
199
 * Makes a best effort to connect back to Moodle to update a user preference,
200
 * however, there is no mechanism for finding out if the update succeeded.
201
 *
202
 * Before you can use this function in your JavsScript, you must have called
203
 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
204
 * the udpate is allowed, and how to safely clean and submitted values.
205
 *
206
 * @param {String} name the name of the setting to update.
207
 * @param {String} value the value to set it to.
208
 *
209
 * @deprecated since Moodle 4.3.
210
 */
211
M.util.set_user_preference = function(name, value) {
212
    Y.log('M.util.set_user_preference is deprecated. Please use the "core_user/repository" module instead.', 'warn');
213
 
214
    require(['core_user/repository'], function(UserRepository) {
215
        UserRepository.setUserPreference(name, value);
216
    });
217
};
218
 
219
/**
220
 * Prints a confirmation dialog in the style of DOM.confirm().
221
 *
222
 * @method show_confirm_dialog
223
 * @param {EventFacade} e
224
 * @param {Object} args
225
 * @param {String} args.message The question to ask the user
226
 * @param {Function} [args.callback] A callback to apply on confirmation.
227
 * @param {Object} [args.scope] The scope to use when calling the callback.
228
 * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
229
 * @param {String} [args.cancellabel] The label to use on the cancel button.
230
 * @param {String} [args.continuelabel] The label to use on the continue button.
231
 */
232
M.util.show_confirm_dialog = (e, {
233
    message,
234
    continuelabel,
235
    callback = null,
236
    scope = null,
237
    callbackargs = [],
238
} = {}) => {
239
    if (e.preventDefault) {
240
        e.preventDefault();
241
    }
242
 
243
    require(
244
        ['core/notification', 'core/str', 'core_form/changechecker', 'core/normalise'],
245
        function(Notification, Str, FormChangeChecker, Normalise) {
246
 
247
            if (scope === null && e.target) {
248
                // Fall back to the event target if no scope provided.
249
                scope = e.target;
250
            }
251
 
252
            Notification.saveCancelPromise(
253
                Str.get_string('confirmation', 'admin'),
254
                message,
255
                continuelabel || Str.get_string('yes', 'moodle'),
256
            )
257
            .then(() => {
258
                if (callback) {
259
                    callback.apply(scope, callbackargs);
260
                    return;
261
                }
262
 
263
                if (!e.target) {
264
                    window.console.error(
265
                        `M.util.show_confirm_dialog: No target found for event`,
266
                        e
267
                    );
268
                    return;
269
                }
270
 
271
                const target = Normalise.getElement(e.target);
272
 
273
                if (target.closest('a')) {
274
                    window.location = target.closest('a').getAttribute('href');
275
                    return;
276
                } else if (target.closest('input') || target.closest('button')) {
277
                    const form = target.closest('form');
278
                    const hiddenValue = document.createElement('input');
279
                    hiddenValue.setAttribute('type', 'hidden');
280
                    hiddenValue.setAttribute('name', target.getAttribute('name'));
281
                    hiddenValue.setAttribute('value', target.getAttribute('value'));
282
                    form.appendChild(hiddenValue);
283
                    FormChangeChecker.markFormAsDirty(form);
284
                    form.submit();
285
                    return;
286
                } else if (target.closest('form')) {
287
                    const form = target.closest('form');
288
                    FormChangeChecker.markFormAsDirty(form);
289
                    form.submit();
290
                    return;
291
                }
292
                window.console.error(
293
                    `Element of type ${target.tagName} is not supported by M.util.show_confirm_dialog.`
294
                );
295
 
296
                return;
297
            })
298
            .catch(() => {
299
                // User cancelled.
300
                return;
301
            });
302
        }
303
    );
304
};
305
 
306
/** Useful for full embedding of various stuff */
307
M.util.init_maximised_embed = function(Y, id) {
308
    var obj = Y.one('#'+id);
309
    if (!obj) {
310
        return;
311
    }
312
 
313
    var get_htmlelement_size = function(el, prop) {
314
        if (Y.Lang.isString(el)) {
315
            el = Y.one('#' + el);
316
        }
317
        // Ensure element exists.
318
        if (el) {
319
            var val = el.getStyle(prop);
320
            if (val == 'auto') {
321
                val = el.getComputedStyle(prop);
322
            }
323
            val = parseInt(val);
324
            if (isNaN(val)) {
325
                return 0;
326
            }
327
            return val;
328
        } else {
329
            return 0;
330
        }
331
    };
332
 
333
    var resize_object = function() {
334
        obj.setStyle('display', 'none');
335
        var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
336
 
337
        if (newwidth > 500) {
338
            obj.setStyle('width', newwidth  + 'px');
339
        } else {
340
            obj.setStyle('width', '500px');
341
        }
342
 
343
        var headerheight = get_htmlelement_size('page-header', 'height');
344
        var footerheight = get_htmlelement_size('page-footer', 'height');
345
        var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
346
        if (newheight < 400) {
347
            newheight = 400;
348
        }
349
        obj.setStyle('height', newheight+'px');
350
        obj.setStyle('display', '');
351
    };
352
 
353
    resize_object();
354
    // fix layout if window resized too
355
    Y.use('event-resize', function (Y) {
356
        Y.on("windowresize", function() {
357
            resize_object();
358
        });
359
    });
360
};
361
 
362
/**
363
 * Breaks out all links to the top frame - used in frametop page layout.
364
 */
365
M.util.init_frametop = function(Y) {
366
    Y.all('a').each(function(node) {
367
        node.set('target', '_top');
368
    });
369
    Y.all('form').each(function(node) {
370
        node.set('target', '_top');
371
    });
372
};
373
 
374
/**
375
 * @deprecated since Moodle 3.3
376
 */
377
M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
378
    throw new Error('M.util.init_toggle_class_on_click can not be used any more. Please use jQuery instead.');
379
};
380
 
381
/**
382
 * Initialises a colour picker
383
 *
384
 * Designed to be used with admin_setting_configcolourpicker although could be used
385
 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
386
 * above or below the input (must have the same parent) and then call this with the
387
 * id.
388
 *
389
 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
390
 * contrib/blocks. For better docs refer to that.
391
 *
392
 * @param {YUI} Y
393
 * @param {int} id
394
 * @param {object} previewconf
395
 */
396
M.util.init_colour_picker = function(Y, id, previewconf) {
397
    /**
398
     * We need node and event-mouseenter
399
     */
400
    Y.use('node', 'event-mouseenter', function(){
401
        /**
402
         * The colour picker object
403
         */
404
        var colourpicker = {
405
            box : null,
406
            input : null,
407
            image : null,
408
            preview : null,
409
            current : null,
410
            eventClick : null,
411
            eventMouseEnter : null,
412
            eventMouseLeave : null,
413
            eventMouseMove : null,
414
            width : 300,
415
            height :  100,
416
            factor : 5,
417
            /**
418
             * Initalises the colour picker by putting everything together and wiring the events
419
             */
420
            init : function() {
421
                this.input = Y.one('#'+id);
422
                this.box = this.input.ancestor().one('.admin_colourpicker');
423
                this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
424
                this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
425
                this.preview = Y.Node.create('<div class="previewcolour"></div>');
426
                this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
427
                this.current = Y.Node.create('<div class="currentcolour"></div>');
428
                this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
429
                this.box.setContent('').append(this.image).append(this.preview).append(this.current);
430
 
431
                if (typeof(previewconf) === 'object' && previewconf !== null) {
432
                    Y.one('#'+id+'_preview').on('click', function(e){
433
                        if (Y.Lang.isString(previewconf.selector)) {
434
                            Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
435
                        } else {
436
                            for (var i in previewconf.selector) {
437
                                Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
438
                            }
439
                        }
440
                    }, this);
441
                }
442
 
443
                this.eventClick = this.image.on('click', this.pickColour, this);
444
                this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
445
            },
446
            /**
447
             * Starts to follow the mouse once it enter the image
448
             */
449
            startFollow : function(e) {
450
                this.eventMouseEnter.detach();
451
                this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
452
                this.eventMouseMove = this.image.on('mousemove', function(e){
453
                    this.preview.setStyle('backgroundColor', this.determineColour(e));
454
                }, this);
455
            },
456
            /**
457
             * Stops following the mouse
458
             */
459
            endFollow : function(e) {
460
                this.eventMouseMove.detach();
461
                this.eventMouseLeave.detach();
462
                this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
463
            },
464
            /**
465
             * Picks the colour the was clicked on
466
             */
467
            pickColour : function(e) {
468
                var colour = this.determineColour(e);
469
                this.input.set('value', colour);
470
                this.current.setStyle('backgroundColor', colour);
471
            },
472
            /**
473
             * Calculates the colour fromthe given co-ordinates
474
             */
475
            determineColour : function(e) {
476
                var eventx = Math.floor(e.pageX-e.target.getX());
477
                var eventy = Math.floor(e.pageY-e.target.getY());
478
 
479
                var imagewidth = this.width;
480
                var imageheight = this.height;
481
                var factor = this.factor;
482
                var colour = [255,0,0];
483
 
484
                var matrices = [
485
                    [  0,  1,  0],
486
                    [ -1,  0,  0],
487
                    [  0,  0,  1],
488
                    [  0, -1,  0],
489
                    [  1,  0,  0],
490
                    [  0,  0, -1]
491
                ];
492
 
493
                var matrixcount = matrices.length;
494
                var limit = Math.round(imagewidth/matrixcount);
495
                var heightbreak = Math.round(imageheight/2);
496
 
497
                for (var x = 0; x < imagewidth; x++) {
498
                    var divisor = Math.floor(x / limit);
499
                    var matrix = matrices[divisor];
500
 
501
                    colour[0] += matrix[0]*factor;
502
                    colour[1] += matrix[1]*factor;
503
                    colour[2] += matrix[2]*factor;
504
 
505
                    if (eventx==x) {
506
                        break;
507
                    }
508
                }
509
 
510
                var pixel = [colour[0], colour[1], colour[2]];
511
                if (eventy < heightbreak) {
512
                    pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
513
                    pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
514
                    pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
515
                } else if (eventy > heightbreak) {
516
                    pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
517
                    pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
518
                    pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
519
                }
520
 
521
                return this.convert_rgb_to_hex(pixel);
522
            },
523
            /**
524
             * Converts an RGB value to Hex
525
             */
526
            convert_rgb_to_hex : function(rgb) {
527
                var hex = '#';
528
                var hexchars = "0123456789ABCDEF";
529
                for (var i=0; i<3; i++) {
530
                    var number = Math.abs(rgb[i]);
531
                    if (number == 0 || isNaN(number)) {
532
                        hex += '00';
533
                    } else {
534
                        hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
535
                    }
536
                }
537
                return hex;
538
            }
539
        };
540
        /**
541
         * Initialise the colour picker :) Hoorah
542
         */
543
        colourpicker.init();
544
    });
545
};
546
 
547
M.util.init_block_hider = function(Y, config) {
548
    Y.use('base', 'node', function(Y) {
549
        M.util.block_hider = M.util.block_hider || (function(){
550
            var blockhider = function() {
551
                blockhider.superclass.constructor.apply(this, arguments);
552
            };
553
            blockhider.prototype = {
554
                initializer : function(config) {
555
                    this.set('block', '#'+this.get('id'));
556
                    var b = this.get('block'),
557
                        t = b.one('.title'),
558
                        a = null,
559
                        hide,
560
                        show;
561
                    if (t && (a = t.one('.block_action'))) {
562
                        hide = Y.Node.create('<img />')
563
                            .addClass('block-hider-hide')
564
                            .setAttrs({
565
                                alt:        config.tooltipVisible,
566
                                src:        this.get('iconVisible'),
567
                                tabIndex:   0,
568
                                'title':    config.tooltipVisible
569
                            });
570
                        hide.on('keypress', this.updateStateKey, this, true);
571
                        hide.on('click', this.updateState, this, true);
572
 
573
                        show = Y.Node.create('<img />')
574
                            .addClass('block-hider-show')
575
                            .setAttrs({
576
                                alt:        config.tooltipHidden,
577
                                src:        this.get('iconHidden'),
578
                                tabIndex:   0,
579
                                'title':    config.tooltipHidden
580
                            });
581
                        show.on('keypress', this.updateStateKey, this, false);
582
                        show.on('click', this.updateState, this, false);
583
 
584
                        a.insert(show, 0).insert(hide, 0);
585
                    }
586
                },
587
                updateState : function(e, hide) {
588
                    require(['core_user/repository'], function(UserRepository) {
589
                        UserRepository.setUserPreference(this.get('preference'), hide);
590
                    }.bind(this));
591
                    if (hide) {
592
                        this.get('block').addClass('hidden');
593
                        this.get('block').one('.block-hider-show').focus();
594
                    } else {
595
                        this.get('block').removeClass('hidden');
596
                        this.get('block').one('.block-hider-hide').focus();
597
                    }
598
                },
599
                updateStateKey : function(e, hide) {
600
                    if (e.keyCode == 13) { //allow hide/show via enter key
601
                        this.updateState(this, hide);
602
                    }
603
                }
604
            };
605
            Y.extend(blockhider, Y.Base, blockhider.prototype, {
606
                NAME : 'blockhider',
607
                ATTRS : {
608
                    id : {},
609
                    preference : {},
610
                    iconVisible : {
611
                        value : M.util.image_url('t/switch_minus', 'moodle')
612
                    },
613
                    iconHidden : {
614
                        value : M.util.image_url('t/switch_plus', 'moodle')
615
                    },
616
                    block : {
617
                        setter : function(node) {
618
                            return Y.one(node);
619
                        }
620
                    }
621
                }
622
            });
623
            return blockhider;
624
        })();
625
        new M.util.block_hider(config);
626
    });
627
};
628
 
629
/**
630
 * @var pending_js - The keys are the list of all pending js actions.
631
 * @type Object
632
 */
633
M.util.pending_js = [];
634
M.util.complete_js = [];
635
 
636
/**
637
 * Register any long running javascript code with a unique identifier.
638
 * This is used to ensure that Behat steps do not continue with interactions until the page finishes loading.
639
 *
640
 * All calls to M.util.js_pending _must_ be followed by a subsequent call to M.util.js_complete with the same exact
641
 * uniqid.
642
 *
643
 * This function may also be called with no arguments to test if there is any js calls pending.
644
 *
645
 * The uniqid specified may be any Object, including Number, String, or actual Object; however please note that the
646
 * paired js_complete function performs a strict search for the key specified. As such, if using an Object, the exact
647
 * Object must be passed into both functions.
648
 *
649
 * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
650
 * @return  {Number}    Number of pending items
651
 */
652
M.util.js_pending = function(uniqid) {
653
    if (typeof uniqid !== 'undefined') {
654
        M.util.pending_js.push(uniqid);
655
    }
656
 
657
    return M.util.pending_js.length;
658
};
659
 
660
// Start this asap.
661
M.util.js_pending('init');
662
 
663
/**
664
 * Register listeners for Y.io start/end so we can wait for them in behat.
665
 */
666
YUI.add('moodle-core-io', function(Y) {
667
    Y.on('io:start', function(id) {
668
        M.util.js_pending('io:' + id);
669
    });
670
    Y.on('io:end', function(id) {
671
        M.util.js_complete('io:' + id);
672
    });
673
}, '@VERSION@', {
674
    condition: {
675
        trigger: 'io-base',
676
        when: 'after'
677
    }
678
});
679
 
680
/**
681
 * Unregister some long running javascript code using the unique identifier specified in M.util.js_pending.
682
 *
683
 * This function must be matched with an identical call to M.util.js_pending.
684
 *
685
 * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
686
 * @return  {Number}    Number of pending items remaining after removing this item
687
 */
688
M.util.js_complete = function(uniqid) {
689
    const index = M.util.pending_js.indexOf(uniqid);
690
    if (index >= 0) {
691
        M.util.complete_js.push(M.util.pending_js.splice(index, 1)[0]);
692
    } else {
693
        window.console.log("Unable to locate key for js_complete call", uniqid);
694
    }
695
 
696
    return M.util.pending_js.length;
697
};
698
 
699
/**
700
 * Returns a string registered in advance for usage in JavaScript
701
 *
702
 * If you do not pass the third parameter, the function will just return
703
 * the corresponding value from the M.str object. If the third parameter is
704
 * provided, the function performs {$a} placeholder substitution in the
705
 * same way as PHP get_string() in Moodle does.
706
 *
707
 * @param {String} identifier string identifier
708
 * @param {String} component the component providing the string
709
 * @param {Object|String} [a] optional variable to populate placeholder with
710
 */
711
M.util.get_string = function(identifier, component, a) {
712
    var stringvalue;
713
 
714
    if (M.cfg.developerdebug) {
715
        // creating new instance if YUI is not optimal but it seems to be better way then
716
        // require the instance via the function API - note that it is used in rare cases
717
        // for debugging only anyway
718
        // To ensure we don't kill browser performance if hundreds of get_string requests
719
        // are made we cache the instance we generate within the M.util namespace.
720
        // We don't publicly define the variable so that it doesn't get abused.
721
        if (typeof M.util.get_string_yui_instance === 'undefined') {
722
            M.util.get_string_yui_instance = new YUI({ debug : true });
723
        }
724
        var Y = M.util.get_string_yui_instance;
725
    }
726
 
727
    if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
728
        stringvalue = '[[' + identifier + ',' + component + ']]';
729
        if (M.cfg.developerdebug) {
730
            Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
731
        }
732
        return stringvalue;
733
    }
734
 
735
    stringvalue = M.str[component][identifier];
736
 
737
    if (typeof a == 'undefined') {
738
        // no placeholder substitution requested
739
        return stringvalue;
740
    }
741
 
742
    if (typeof a == 'number' || typeof a == 'string') {
743
        // replace all occurrences of {$a} with the placeholder value
744
        stringvalue = stringvalue.replace(/\{\$a\}/g, a);
745
        return stringvalue;
746
    }
747
 
748
    if (typeof a == 'object') {
749
        // replace {$a->key} placeholders
750
        for (var key in a) {
751
            if (typeof a[key] != 'number' && typeof a[key] != 'string') {
752
                if (M.cfg.developerdebug) {
753
                    Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
754
                }
755
                continue;
756
            }
757
            var search = '{$a->' + key + '}';
758
            search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
759
            search = new RegExp(search, 'g');
760
            stringvalue = stringvalue.replace(search, a[key]);
761
        }
762
        return stringvalue;
763
    }
764
 
765
    if (M.cfg.developerdebug) {
766
        Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
767
    }
768
    return stringvalue;
769
};
770
 
771
/**
772
 * Set focus on username or password field of the login form.
773
 * @deprecated since Moodle 3.3.
774
 */
775
M.util.focus_login_form = function(Y) {
776
    Y.log('M.util.focus_login_form no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
777
};
778
 
779
/**
780
 * Set focus on login error message.
781
 * @deprecated since Moodle 3.3.
782
 */
783
M.util.focus_login_error = function(Y) {
784
    Y.log('M.util.focus_login_error no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
785
};
786
 
787
/**
788
 * Adds lightbox hidden element that covers the whole node.
789
 *
790
 * @param {YUI} Y
791
 * @param {Node} the node lightbox should be added to
792
 * @retun {Node} created lightbox node
793
 */
794
M.util.add_lightbox = function(Y, node) {
795
    var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
796
 
797
    // Check if lightbox is already there
798
    if (node.one('.lightbox')) {
799
        return node.one('.lightbox');
800
    }
801
 
802
    node.setStyle('position', 'relative');
803
    var waiticon = Y.Node.create('<img />')
804
    .setAttrs({
805
        'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
806
    })
807
    .setStyles({
808
        'position' : 'relative',
809
        'top' : '50%'
810
    });
811
 
812
    var lightbox = Y.Node.create('<div></div>')
813
    .setStyles({
814
        'opacity' : '.75',
815
        'position' : 'absolute',
816
        'width' : '100%',
817
        'height' : '100%',
818
        'top' : 0,
819
        'left' : 0,
820
        'backgroundColor' : 'white',
821
        'textAlign' : 'center'
822
    })
823
    .setAttribute('class', 'lightbox')
824
    .hide();
825
 
826
    lightbox.appendChild(waiticon);
827
    node.append(lightbox);
828
    return lightbox;
829
}
830
 
831
/**
832
 * Appends a hidden spinner element to the specified node.
833
 *
834
 * @param {YUI} Y
835
 * @param {Node} the node the spinner should be added to
836
 * @return {Node} created spinner node
837
 */
838
M.util.add_spinner = function(Y, node) {
839
    var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
840
 
841
    // Check if spinner is already there
842
    if (node.one('.spinner')) {
843
        return node.one('.spinner');
844
    }
845
 
846
    var spinner = Y.Node.create('<img />')
847
        .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
848
        .addClass('spinner')
849
        .addClass('iconsmall')
850
        .hide();
851
 
852
    node.append(spinner);
853
    return spinner;
854
}
855
 
856
/**
857
 * @deprecated since Moodle 3.3.
858
 */
859
function checkall() {
860
    throw new Error('checkall can not be used any more. Please use jQuery instead.');
861
}
862
 
863
/**
864
 * @deprecated since Moodle 3.3.
865
 */
866
function checknone() {
867
    throw new Error('checknone can not be used any more. Please use jQuery instead.');
868
}
869
 
870
/**
871
 * @deprecated since Moodle 3.3.
872
 */
873
function select_all_in_element_with_id(id, checked) {
874
    throw new Error('select_all_in_element_with_id can not be used any more. Please use jQuery instead.');
875
}
876
 
877
/**
878
 * @deprecated since Moodle 3.3.
879
 */
880
function select_all_in(elTagName, elClass, elId) {
881
    throw new Error('select_all_in can not be used any more. Please use jQuery instead.');
882
}
883
 
884
/**
885
 * @deprecated since Moodle 3.3.
886
 */
887
function deselect_all_in(elTagName, elClass, elId) {
888
    throw new Error('deselect_all_in can not be used any more. Please use jQuery instead.');
889
}
890
 
891
/**
892
 * @deprecated since Moodle 3.3.
893
 */
894
function confirm_if(expr, message) {
895
    throw new Error('confirm_if can not be used any more.');
896
}
897
 
898
/**
899
 * @deprecated since Moodle 3.3.
900
 */
901
function findParentNode(el, elName, elClass, elId) {
902
    throw new Error('findParentNode can not be used any more. Please use jQuery instead.');
903
}
904
 
905
function unmaskPassword(id) {
906
    var pw = document.getElementById(id);
907
    var chb = document.getElementById(id+'unmask');
908
 
909
    // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
910
    // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
911
    // functionality won't work in IE8 or lower.
912
    // This is a temporary fixed to allow other browsers to function properly.
913
    if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
914
        if (chb.checked) {
915
            pw.type = "text";
916
        } else {
917
            pw.type = "password";
918
        }
919
    } else {  //IE Browser version 8 or lower
920
        try {
921
            // first try IE way - it can not set name attribute later
922
            if (chb.checked) {
923
              var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
924
            } else {
925
              var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
926
            }
927
            newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
928
        } catch (e) {
929
            var newpw = document.createElement('input');
930
            newpw.setAttribute('autocomplete', 'off');
931
            newpw.setAttribute('name', pw.name);
932
            if (chb.checked) {
933
              newpw.setAttribute('type', 'text');
934
            } else {
935
              newpw.setAttribute('type', 'password');
936
            }
937
            newpw.setAttribute('class', pw.getAttribute('class'));
938
        }
939
        newpw.id = pw.id;
940
        newpw.size = pw.size;
941
        newpw.onblur = pw.onblur;
942
        newpw.onchange = pw.onchange;
943
        newpw.value = pw.value;
944
        pw.parentNode.replaceChild(newpw, pw);
945
    }
946
}
947
 
948
/**
949
 * @deprecated since Moodle 3.3.
950
 */
951
function filterByParent(elCollection, parentFinder) {
952
    throw new Error('filterByParent can not be used any more. Please use jQuery instead.');
953
}
954
 
955
/**
956
 * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
957
 */
958
function fix_column_widths() {
959
    Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
960
}
961
 
962
/**
963
 * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
964
 */
965
function fix_column_width(colName) {
966
    Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
967
}
968
 
969
 
970
/*
971
   Insert myValue at current cursor position
972
 */
973
function insertAtCursor(myField, myValue) {
974
    // IE support
975
    if (document.selection) {
976
        myField.focus();
977
        sel = document.selection.createRange();
978
        sel.text = myValue;
979
    }
980
    // Mozilla/Netscape support
981
    else if (myField.selectionStart || myField.selectionStart == '0') {
982
        var startPos = myField.selectionStart;
983
        var endPos = myField.selectionEnd;
984
        myField.value = myField.value.substring(0, startPos)
985
            + myValue + myField.value.substring(endPos, myField.value.length);
986
    } else {
987
        myField.value += myValue;
988
    }
989
}
990
 
991
/**
992
 * Increment a file name.
993
 *
994
 * @param string file name.
995
 * @param boolean ignoreextension do not extract the extension prior to appending the
996
 *                                suffix. Useful when incrementing folder names.
997
 * @return string the incremented file name.
998
 */
999
function increment_filename(filename, ignoreextension) {
1000
    var extension = '';
1001
    var basename = filename;
1002
 
1003
    // Split the file name into the basename + extension.
1004
    if (!ignoreextension) {
1005
        var dotpos = filename.lastIndexOf('.');
1006
        if (dotpos !== -1) {
1007
            basename = filename.substr(0, dotpos);
1008
            extension = filename.substr(dotpos, filename.length);
1009
        }
1010
    }
1011
 
1012
    // Look to see if the name already has (NN) at the end of it.
1013
    var number = 0;
1014
    var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1015
    if (hasnumber !== null) {
1016
        // Note the current number & remove it from the basename.
1017
        number = parseInt(hasnumber[2], 10);
1018
        basename = hasnumber[1];
1019
    }
1020
 
1021
    number++;
1022
    var newname = basename + ' (' + number + ')' + extension;
1023
    return newname;
1024
}
1025
 
1026
/**
1027
 * Return whether we are in right to left mode or not.
1028
 *
1029
 * @return boolean
1030
 */
1031
function right_to_left() {
1032
    var body = Y.one('body');
1033
    var rtl = false;
1034
    if (body && body.hasClass('dir-rtl')) {
1035
        rtl = true;
1036
    }
1037
    return rtl;
1038
}
1039
 
1040
function openpopup(event, args) {
1041
 
1042
    if (event) {
1043
        if (event.preventDefault) {
1044
            event.preventDefault();
1045
        } else {
1046
            event.returnValue = false;
1047
        }
1048
    }
1049
 
1050
    // Make sure the name argument is set and valid.
1051
    var nameregex = /[^a-z0-9_]/i;
1052
    if (typeof args.name !== 'string') {
1053
        args.name = '_blank';
1054
    } else if (args.name.match(nameregex)) {
1055
        // Cleans window name because IE does not support funky ones.
1056
        if (M.cfg.developerdebug) {
1057
            alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1058
        }
1059
        args.name = args.name.replace(nameregex, '_');
1060
    }
1061
 
1062
    var fullurl = args.url;
1063
    if (!args.url.match(/https?:\/\//)) {
1064
        fullurl = M.cfg.wwwroot + args.url;
1065
    }
1066
    if (args.fullscreen) {
1067
        args.options = args.options.
1068
                replace(/top=\d+/, 'top=0').
1069
                replace(/left=\d+/, 'left=0').
1070
                replace(/width=\d+/, 'width=' + screen.availWidth).
1071
                replace(/height=\d+/, 'height=' + screen.availHeight);
1072
    }
1073
    var windowobj = window.open(fullurl,args.name,args.options);
1074
    if (!windowobj) {
1075
        return true;
1076
    }
1077
 
1078
    if (args.fullscreen) {
1079
        // In some browser / OS combinations (E.g. Chrome on Windows), the
1080
        // window initially opens slighly too big. The width and heigh options
1081
        // seem to control the area inside the browser window, so what with
1082
        // scroll-bars, etc. the actual window is bigger than the screen.
1083
        // Therefore, we need to fix things up after the window is open.
1084
        var hackcount = 100;
1085
        var get_size_exactly_right = function() {
1086
            windowobj.moveTo(0, 0);
1087
            windowobj.resizeTo(screen.availWidth, screen.availHeight);
1088
 
1089
            // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1090
            // something like windowobj.resizeTo(1280, 1024) too soon (up to
1091
            // about 50ms) after the window is open, then it actually behaves
1092
            // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1093
            // check that the resize actually worked, and if not, repeatedly try
1094
            // again after a short delay until it works (but with a limit of
1095
            // hackcount repeats.
1096
            if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1097
                hackcount -= 1;
1098
                setTimeout(get_size_exactly_right, 10);
1099
            }
1100
        }
1101
        setTimeout(get_size_exactly_right, 0);
1102
    }
1103
    windowobj.focus();
1104
 
1105
    return false;
1106
}
1107
 
1108
/** Close the current browser window. */
1109
function close_window(e) {
1110
    if (e.preventDefault) {
1111
        e.preventDefault();
1112
    } else {
1113
        e.returnValue = false;
1114
    }
1115
    window.close();
1116
}
1117
 
1118
/**
1119
 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1120
 * @param controlid the control id.
1121
 */
1122
function focuscontrol(controlid) {
1123
    var control = document.getElementById(controlid);
1124
    if (control) {
1125
        control.focus();
1126
    }
1127
}
1128
 
1129
/**
1130
 * Transfers keyboard focus to an HTML element based on the old style style of focus
1131
 * This function should be removed as soon as it is no longer used
1132
 */
1133
function old_onload_focus(formid, controlname) {
1134
    if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1135
        document.forms[formid].elements[controlname].focus();
1136
    }
1137
}
1138
 
1139
function build_querystring(obj) {
1140
    return convert_object_to_string(obj, '&');
1141
}
1142
 
1143
function build_windowoptionsstring(obj) {
1144
    return convert_object_to_string(obj, ',');
1145
}
1146
 
1147
function convert_object_to_string(obj, separator) {
1148
    if (typeof obj !== 'object') {
1149
        return null;
1150
    }
1151
    var list = [];
1152
    for(var k in obj) {
1153
        k = encodeURIComponent(k);
1154
        var value = obj[k];
1155
        if(obj[k] instanceof Array) {
1156
            for(var i in value) {
1157
                list.push(k+'[]='+encodeURIComponent(value[i]));
1158
            }
1159
        } else {
1160
            list.push(k+'='+encodeURIComponent(value));
1161
        }
1162
    }
1163
    return list.join(separator);
1164
}
1165
 
1166
/**
1167
 * @deprecated since Moodle 3.3.
1168
 */
1169
function stripHTML(str) {
1170
    throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
1171
}
1172
 
1173
function updateProgressBar(id, percent, msg, estimate) {
1174
    var event,
1175
        el = document.getElementById(id),
1176
        eventData = {};
1177
 
1178
    if (!el) {
1179
        return;
1180
    }
1181
 
1182
    eventData.message = msg;
1183
    eventData.percent = percent;
1184
    eventData.estimate = estimate;
1185
 
1186
    try {
1187
        event = new CustomEvent('update', {
1188
            bubbles: false,
1189
            cancelable: true,
1190
            detail: eventData
1191
        });
1192
    } catch (exception) {
1193
        if (!(exception instanceof TypeError)) {
1194
            throw exception;
1195
        }
1196
        event = document.createEvent('CustomEvent');
1197
        event.initCustomEvent('update', false, true, eventData);
1198
        event.prototype = window.Event.prototype;
1199
    }
1200
 
1201
    el.dispatchEvent(event);
1202
}
1203
 
1204
M.util.help_popups = {
1205
    setup : function(Y) {
1206
        Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1207
    },
1208
    open_popup : function(e) {
1209
        // Prevent the default page action
1210
        e.preventDefault();
1211
 
1212
        // Grab the anchor that was clicked
1213
        var anchor = e.target.ancestor('a', true);
1214
        var args = {
1215
            'name'          : 'popup',
1216
            'url'           : anchor.getAttribute('href'),
1217
            'options'       : ''
1218
        };
1219
        var options = [
1220
            'height=600',
1221
            'width=800',
1222
            'top=0',
1223
            'left=0',
1224
            'menubar=0',
1225
            'location=0',
1226
            'scrollbars',
1227
            'resizable',
1228
            'toolbar',
1229
            'status',
1230
            'directories=0',
1231
            'fullscreen=0',
1232
            'dependent'
1233
        ]
1234
        args.options = options.join(',');
1235
 
1236
        openpopup(e, args);
1237
    }
1238
}
1239
 
1240
/**
1241
 * Custom menu namespace
1242
 */
1243
M.core_custom_menu = {
1244
    /**
1245
     * This method is used to initialise a custom menu given the id that belongs
1246
     * to the custom menu's root node.
1247
     *
1248
     * @param {YUI} Y
1249
     * @param {string} nodeid
1250
     */
1251
    init : function(Y, nodeid) {
1252
        var node = Y.one('#'+nodeid);
1253
        if (node) {
1254
            Y.use('node-menunav', function(Y) {
1255
                // Get the node
1256
                // Remove the javascript-disabled class.... obviously javascript is enabled.
1257
                node.removeClass('javascript-disabled');
1258
                // Initialise the menunav plugin
1259
                node.plug(Y.Plugin.NodeMenuNav);
1260
            });
1261
        }
1262
    }
1263
};
1264
 
1265
/**
1266
 * Used to store form manipulation methods and enhancments
1267
 */
1268
M.form = M.form || {};
1269
 
1270
/**
1271
 * Converts a nbsp indented select box into a multi drop down custom control much
1272
 * like the custom menu. Can no longer be used.
1273
 * @deprecated since Moodle 3.3
1274
 */
1275
M.form.init_smartselect = function() {
1276
    throw new Error('M.form.init_smartselect can not be used any more.');
1277
};
1278
 
1279
/**
1280
 * Initiates the listeners for skiplink interaction
1281
 *
1282
 * @param {YUI} Y
1283
 */
1284
M.util.init_skiplink = function(Y) {
1285
    Y.one(Y.config.doc.body).delegate('click', function(e) {
1286
        e.preventDefault();
1287
        e.stopPropagation();
1288
        var node = Y.one(this.getAttribute('href'));
1289
        node.setAttribute('tabindex', '-1');
1290
        node.focus();
1291
        return true;
1292
    }, 'a.skip');
1293
};