Proyectos de Subversion Moodle

Rev

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