Proyectos de Subversion Moodle

Rev

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