PageSourceSearch

https://elviajedelili.netlify.app/libs/common.js

js elviajedelili.netlify.app collected 2026-10-03 10:41:44 UTC 93,065 bytes, 2,037 lines download raw bytes

1/*! ===========================================================================
2    eXe
3    Copyright 2004-2005, University of Auckland
4    Copyright 2004-2008 eXe Project, http://eXeLearning.org/
5
6    This program is free software; you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation; either version 2 of the License, or
9    (at your option) any later version.
10
11    This program is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
14    GNU General Public License for more details.
15
16    You should have received a copy of the GNU General Public License
17    along with this program; if not, write to the Free Software
18    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA    02111-1307    USA
19    ===========================================================================
20
21    ClozelangElement's functions by José Ramón Jiménez Reyes
22    More than one right answer in the Cloze iDevice by José Miguel Andonegi
23    2015. Refactored and completed by Ignacio Gros (http://www.gros.es) for http://exelearning.net/
24*/
25
26window.MathJax = window.MathJax || (function() {
27    var isWorkarea = typeof window.eXeLearning !== 'undefined' || document.querySelector('script[src*="app/common/exe_math"]');
28    var isIndex = document.documentElement.id === 'exe-index';
29    var basePath = isWorkarea ? '/app/common/exe_math' : (isIndex ? './libs/exe_math' : '../libs/exe_math');
30    
31    var externalExtensions = [
32        'amscd', 'bbox', 'boldsymbol', 'braket', 'bussproofs', 'cancel', 
33        'cases', 'centernot', 'color', 'colortbl', 'empheq', 'enclose', 
34        'extpfeil', 'gensymb', 'html', 'mathtools', 'mhchem', 'noerrors',
35        'physics', 'tagformat', 'textcomp', 'unicode', 'upgreek', 'verb', 
36        'setoptions',
37        'bbm', 'bboldx', 'begingroup', 'colorv2', 'dsfont', 'texhtml', 'units'
38    ];
39    
40    return {
41        tex: {
42            inlineMath: [["\\(", "\\)"]],
43            displayMath: [["$$", "$$"], ["\\[", "\\]"]],
44            processEscapes: true,
45            tags: 'ams',
46            packages: { '[+]': externalExtensions }
47        },
48        loader: {
49            paths: { mathjax: basePath },
50            load: externalExtensions.map(function(ext) { return '[tex]/' + ext; })
51        },
52        options: {
53            // MathJax Configuration Options
54        }
55    };
56})();
57
58var $exe = {
59
60    options: {
61        // Accessibility toolbar
62        atools: {
63            modeToggler: false,
64            translator: false,
65            i18n: {}
66        }
67    },
68
69    init: function () {
70        var bod = $('body');
71        this.hasMultimediaGalleries = false;
72        this.setMultimediaGalleries();
73        this.setModalWindowContentSize(); // To review
74        // No MediaElement in ePub
75        if (!bod.hasClass("exe-epub")) {
76            var n = document.body.innerHTML;
77            if (this.hasMultimediaGalleries || $(".mediaelement").length > 0) {
78                $exe.loadMediaPlayer.init();
79            }
80        } else {
81            // No inline SCRIPT tags in ePub (due to Chrome app Content Security Policy)
82            bod.addClass("js");
83        }
84        $exe.setIframesProperties();
85        $exe.hasTooltips();
86        $exe.math.init();
87        $exe.mermaid.init();
88        $exe.dl.init();
89        $exe.sfHover();
90        // Add a zoom icon to the images using CSS
91        $("a.exe-enlarge").each(function (i) {
92            var e = $(this);
93            var c = $(this).children();
94            if (c.length == 1 && c.eq(0).prop("tagName") == "IMG") {
95                e.prepend('<span class="exe-enlarge-icon"><b></b></span>');
96            }
97        });
98        // Disable autocomplete
99        $("INPUT.autocomplete-off").attr("autocomplete", "off");
100    },
101
102    // Math options (MathJax, etc.) - To review (some options might not be needed)
103    math: {
104        get engine() {
105            return $exeDevices.iDevice.gamification.math.engine;
106        },
107        get engineConfig() {
108            return $exeDevices.iDevice.gamification.math.engineConfig;
109        },
110        loadMathJax: function(callback) {
111            return $exeDevices.iDevice.gamification.math.loadMathJax(callback);
112        },
113        hasLatex: function(text) {
114            return $exeDevices.iDevice.gamification.math.hasLatex(text);
115        },
116        refresh: function(elements) {
117            return $exeDevices.iDevice.gamification.math.updateLatex(elements);
118        },
119        // Create links to the code and the image (different possibilities)
120        createLinks: function (math) {
121            var mathjax = false;
122            if (!math) {
123                var math = $(".exe-math");
124                mathjax = true;
125            }
126            math.each(function () {
127                var e = $(this);
128                if ($(".exe-math-links", e).length > 0) return;
129                var img = $(".exe-math-img img", e);
130                var txt = "LaTeX";
131                if (e.html().indexOf("<math") != -1) txt = "MathML";
132                var html = '';
133                if (img.length == 1) html += '<a href="' + img.attr("src") + '" target="_blank">GIF</a>';
134                if (!mathjax) {
135                    if (html != "") html += '<span> - </span>';
136                    html += '<a href="#" class="exe-math-code-lnk">' + txt + '</a>';
137                }
138                if (html != "") {
139                    html = '<p class="exe-math-links">' + html + '</p>';
140                    e.append(html);
141                }
142                $(".exe-math-code-lnk").click(function () {
143                    $exe.math.showCode(this);
144                    return false;
145                });
146            });
147        },
148        // Open a new window with the LaTeX or MathML code
149        showCode: function (e) {
150            var tit = e.innerHTML;
151            var block = $(e).parent().parent();
152            var code = $(".exe-math-code", block);
153            var a = window.open(tit);
154            a.document.open("text/html");
155            var html = '<!DOCTYPE html><html><head><title>' + tit + '</title>';
156            html += '<style type="text/css">body{font:10pt/1.5 Verdana,Arial,Helvetica,sans-serif;margin:10pt;padding:0}</style>';
157            html += '</head><body><pre><code>';
158            html += code.html();
159            html += '</code></pre></body></html>';
160            a.document.write(html);
161            a.document.close();
162        },
163        // Load MathJax or just create the links to the code and/or image
164        init: function () {
165            $("body").addClass("exe-auto-math"); // Always load it
166            var math = $(".exe-math");
167            var mathjax = false;
168            if (math.length > 0 || $("body").hasClass("exe-auto-math")) {
169                if ($("body").hasClass("exe-auto-math")) {
170                    var hasLatex = /(?:\\\(|\\\[|\\begin\{.*?})/.test($('body').html());
171                    if (hasLatex) mathjax = true;
172                }
173                math.each(function () {
174                    var e = $(this);
175                    if (e.hasClass("exe-math-engine")) {
176                        mathjax = true;
177                    }
178                });
179                if (mathjax) {
180                    math.each(function () {
181                        var isInline = false;
182                        var codeW = $(".exe-math-code", this);
183                        var code = codeW.html().trim();
184                        if (code.indexOf("\\(") == 0 || (code.indexOf("$") == 0 && code.indexOf("$$") != 0)) isInline = true;
185                        if (isInline) $(this).addClass("exe-math-inline");
186                        if (code.indexOf("<math") == -1) {
187                            if (isInline) {
188                                if (code.indexOf("$") == 0 && code.substr(code.length - 1) == "$") {
189                                    // $x$ is valid inline
190                                } else {
191                                    if (code.indexOf("\\(") != 0 && code.substr(code.length - 2) != "\\)") {
192                                        // Wrap the code: \( ... \)
193                                        codeW.html("\\(" + code + "\\)");
194                                    }
195                                }
196                            } else {
197                                if (code.indexOf("$$") == 0 && code.substr(code.length - 2) == "$$") {
198                                    // $$x$$ is valid block
199                                } else {
200                                    if (code.indexOf("\\[") != 0 && code.substr(code.length - 2) != "\\]") {
201                                        // Wrap the code: \[ ... \]
202                                        codeW.html("\\[" + code + "\\]");
203                                    }
204                                }
205                            }
206                        }
207                    });
208                    $exe.math.loadMathJax(function () {
209                        MathJax.typesetPromise();
210                        $exe.math.createLinks();
211                    });
212                } else {
213                    $exe.math.createLinks(math);
214                }
215            }
216        }
217    },
218    // Mermaid options
219    mermaid: {
220        // Mermaid script path
221        engine: $("html").prop("id") === "exe-index" ? "./libs/mermaid/mermaid.min.js" : "../app/common/mermaid/mermaid.min.js",
222        reload_pending: false,
223            //'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
224        loadMermaid: function () {
225            if (typeof window.mermaid === 'undefined') {
226                const script = document.createElement("script");
227                script.src = this.engine;
228                script.async = true;
229                script.onload = function () {
230                    mermaid = window.mermaid;
231                    mermaid.initialize({ startOnLoad: false });
232                    mermaid.run();
233                };
234                document.head.appendChild(script);
235                this.reload_pending = false;
236            } else {
237                // debounce reloading to avoid multiple calls
238                if (!this.reload_pending) {
239                    this.reload_pending = true;
240                    setTimeout(function () {
241                        $exe.mermaid.reload_pending = false;
242                        mermaid.run();
243                    }, 100);
244                }
245            }
246        },
247        init: function () {
248            var mermaidNodes = $(".mermaid");
249            if (mermaidNodes.length > 0) {
250                this.loadMermaid();
251            }
252        }
253    },
254    // Modal Window: Height problem in some browsers #328
255    setModalWindowContentSize: function () {
256        if (window.chrome) {
257            $(".exe-dialog-text img").each(
258                function () {
259                    var e = $(this);
260                    var h = e.attr("height");
261                    var w = e.attr("width");
262                    if (e.height() == 0 && e.css("height") == "0px" && h && w) {
263                        if (!isNaN(h) && h > 0 && !isNaN(w) && w > 0) {
264                            var maxW = 480;
265                            if (w < maxW) maxW = w;
266                            h = Math.round(maxW * h / w);
267                            e.css("height", h + "px");
268                        }
269                    }
270                }
271            );
272        }
273    },
274
275    // Transform links to audios or videos (with rel^='lightbox') in links to inline content
276    // (see prettyPhoto documentation)
277    setMultimediaGalleries: function () {
278        if (typeof ($.prettyPhoto) != 'undefined') {
279            var lightboxLinks = $("a[rel^='lightbox']");
280            lightboxLinks.each(function (i) {
281                var ref = $(this).attr("href");
282                var _ref = ref.toLowerCase();
283                var isAudio = _ref.indexOf(".mp3") != -1;
284                var isVideo = _ref.indexOf(".mp4") != -1 || _ref.indexOf(".flv") != -1 || _ref.indexOf(".ogg") != -1 || _ref.indexOf(".ogv") != -1;
285                if (isAudio || isVideo) {
286                    var id = "media-box-" + i;
287                    $(this).attr("href", "#" + id);
288                    var hiddenPlayer = $('<div class="exe-media-box js-hidden" id="' + id + '"></div>');
289                    if (isAudio) hiddenPlayer.html('<div class="exe-media-audio-box"><audio controls="controls" src="' + ref + '" class="exe-media-box-element exe-media-box-audio"><a href="' + ref + '">audio/mpeg</a></audio></div>');
290                    else hiddenPlayer.html('<div class="exe-media-video-box"><video width="480" height="385" controls="controls" class="exe-media-box-element"><source src="' + ref + '" /></video></div>');
291                    $("body").append(hiddenPlayer);
292                    $exe.hasMultimediaGalleries = true;
293                }
294                // Inline content title
295                var t = this.title;
296                if (ref.indexOf('#') == 0 && $(ref).length == 1 && t && t != "") $(ref).prepend('<h2 class="pp_title">' + t + '</h2>');
297            });
298            lightboxLinks.prettyPhoto({
299                social_tools: "",
300                deeplinking: false,
301                opacity: 0.85,
302                changepicturecallback: function () {
303                    var block = $("#pp_full_res")
304                    var media = $(".exe-media-box-element", block);
305                    if ($exe.loadMediaPlayer != undefined) {
306                        if ($exe.loadMediaPlayer.isReady) {
307                            if (media.length == 1) media.mediaelementplayer();
308                            $exe.loadMediaPlayer.isCalledInBox = true;
309                        }
310                    }
311                    // Add a download link and a CSS class to pp_content_container (see exe_lightbox.css)
312                    var cont = $(".pp_content_container");
313                    cont.attr("class", "pp_content_container");
314                    if (media.length == 1 && media[0].hasAttribute('src')) {
315                        if (media.hasClass("exe-media-box-audio")) cont.attr("class", "pp_content_container with-audio");
316                        var src = media.attr('src');
317                        var ext = src.split("/");
318                        ext = ext[ext.length - 1];
319                        ext = ext.split(".")[1];
320                        $(".pp_details .pp_description").append(' <span class="exe-media-download"><a href="' + src + '" title="' + $exe_i18n.download + '" download>' + ext + '</a></span>');
321                    } else {
322                        // Hide the title at the bottom (we use h2.pp_title instead)
323                        block = $(".pp_inline", block);
324                        if (block.length == 1) $(".pp_description").hide();
325                    }
326                }
327            });
328            // If there are galleries, but lightboxLinks.length==0, there's an error
329            // No links with the rel attribute were selected
330            // This might happen in some ePub readers
331            // See issue #258
332            var eXeGalleries = $('.GalleryIdevice');
333            if (lightboxLinks.length == 0 && eXeGalleries.length > 0 && typeof (exe_editor_mode) == "undefined") {
334                // We execute this code only outside eXe or the Image Gallery edition will fail (see issue #317)
335                $('.exeImageGallery a').each(function () {
336                    this.title += " ~ [" + this.href + "]";
337                    this.href = "#";
338                    this.onclick = function () {
339                        var ul = $(this).parent().parent();
340                        if (ul.length == 1 && ul.attr('id') != "") {
341                            if ($("#" + ul.attr('id') + "-warning").length == 0) {
342                                // Due to G. Chrome's Content Security Policy
343                                var txt = $exe_i18n.dataError;
344                                if ($('body').hasClass('exe-epub3')) txt += '<br /><br />' + $exe_i18n.epubJSerror;
345                                ul.prepend('<div id="' + ul.attr('id') + '-warning">' + txt + '</div>');
346                            }
347                        }
348                    }
349                });
350            }
351        }
352    },
353
354    // Load MediaElement if required - To review (Shall eXe still use MediaElement?)
355    loadMediaPlayer: {
356        isCalledInBox: false, // Box = prettyPhoto with video or audio
357        isReady: false,
358        // Start MediaElement
359        init: function () {
360            // Multimedia galleries
361            $exe.mediaelements = $(".mediaelement");
362            $exe.mediaelements.each(function () {
363                if (typeof this.localName != "undefined" && this.localName == "video") {
364                    var e = this.width;
365                    var t = $(window).width();
366                    if (e > t) {
367                        var n = t - 20;
368                        var r = parseInt(this.height * n / e);
369                        this.width = n;
370                        this.height = r
371                    }
372                }
373                $(this).mediaelementplayer();
374            });
375            $exe.loadMediaPlayer.isReady = true;
376            if (!$exe.loadMediaPlayer.isCalledInBox) $("#pp_full_res .exe-media-box-element").mediaelementplayer();
377        }
378    },
379
380    // Apply the 'sfhover' class to li elements when they are 'moused over'
381    // Very old browsers need this because they don't support li:h
381over
382    sfHover: function () {
383        var e = document.getElementById("siteNav");
384        if (e) {
385            var t = e.getElementsByTagName("LI");
386            for (var n = 0; n < t.length; n++) {
387                t[n].onmouseover = function () {
388                    this.className = this.className.replace(new RegExp("( ?|^)sfout\\b"), "");
389                    this.className += (this.className.length > 0 ? " " : "") + "sfhover";
390                };
391                t[n].onmouseout = function () {
392                    this.className = this.className.replace(new RegExp("( ?|^)sfhover\\b"), "");
393                    this.className += (this.className.length > 0 ? " " : "") + "sfout";
394                }
395            }
396            // Enable Keyboard:
397            var r = e.getElementsByTagName("A");
398            for (var n = 0; n < r.length; n++) {
399                r[n].onfocus = function () {
400                    this.className += (this.className.length > 0 ? " " : "") + "sffocus";
401                    this.parentNode.className += (this.parentNode.className.length > 0 ? " " : "") + "sfhover";
402                    if (this.parentNode.parentNode.parentNode.nodeName == "LI") {
403                        this.parentNode.parentNode.parentNode.className += (this.parentNode.parentNode.parentNode.className.length > 0 ? " " : "") + "sfhover";
404                        if (this.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "LI") {
405                            this.parentNode.parentNode.parentNode.parentNode.parentNode.className += (this.parentNode.parentNode.parentNode.parentNode.parentNode.className.length > 0 ? " " : "") + "sfhover"
406                        }
407                    }
408                };
409                r[n].onblur = function () {
410                    this.className = this.className.replace(new RegExp("( ?|^)sffocus\\b"), "");
411                    this.parentNode.className = this.parentNode.className.replace(new RegExp("( ?|^)sfhover\\b"), "");
412                    if (this.parentNode.parentNode.parentNode.nodeName == "LI") {
413                        this.parentNode.parentNode.parentNode.className = this.parentNode.parentNode.parentNode.className.replace(new RegExp("( ?|^)sfhover\\b"), "");
414                        if (this.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "LI") {
415                            this.parentNode.parentNode.parentNode.parentNode.parentNode.className = this.parentNode.parentNode.parentNode.parentNode.parentNode.className.replace(new RegExp("( ?|^)sfhover\\b"), "")
416                        }
417                    }
418                }
419            }
420        }
421    },
422
423    // RGB color to HEX
424    rgb2hex: function (a) {
425        if (/^#[0-9A-F]{6}$/i.test(a)) return a;
426        a = a.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
427
428        function hex(x) {
429            return ("0" + parseInt(x).toString(16)).slice(-2)
430        }
431        return "#" + hex(a[1]) + hex(a[2]) + hex(a[3])
432    },
433
434    // Use black or white text depending on the background color
435    useBlackOrWhite: function (h) {
436        var r = parseInt(h.substr(0, 2), 16);
437        var g = parseInt(h.substr(2, 2), 16);
438        var b = parseInt(h.substr(4, 2), 16);
439        var y = ((r * 299) + (g * 587) + (b * 114)) / 1000;
440        return (y >= 128) ? 'black' : 'white'
441    },
442
443    // Definition lists with improved presentation
444    dl: {
445        init: function () {
446            var l = $("dl.exe-dl");
447            if (l.length == 0) return false;
448            var h, e, t, bg, tc, s, id;
449            l.each(function (i) {
450                e = this;
451                bg = $exe.rgb2hex($(e).css("color"));
452                tc = $exe.useBlackOrWhite(bg.replace("#", ""));
453                s = " style='text-decoration:none;background:" + bg + ";color:" + tc + "'";
454                if (e.id == "") e.id = "exe-dl-" + i;
455                id = e.id;
456                $("dt", e).each(function () {
457                    t = this;
458                    h = $(t).html();
459                    $(t).html("<a href='#' class='exe-dd-toggler exe-dd-toggler-closed exe-dl-" + i + "-a'><span class='icon'" + s + ">+ </span>" + h + "</a>")
460                });
461            });
462            $('a.exe-dd-toggler').click(function () {
463                var e = $(this);
464                var s = $("span.icon", this);
465                var dd = $(this).parent().next("dd");
466                if (e.hasClass("exe-dd-toggler-closed")) {
467                    e.removeClass("exe-dd-toggler-closed");
468                    s.html("- ");
469                    dd.show();
470                } else {
471                    e.addClass("exe-dd-toggler-closed");
472                    s.html("+ ");
473                    dd.hide();
474                }
475                return false;
476            });
477        }
478    },
479
480    // If the page has tooltips we load the JS file
481    hasTooltips: function () {
482        if ($("A.exe-tooltip").length > 0) {
483            var p = "";
484            if (typeof (eXeLearning) !== 'undefined') {
485                p = eXeLearning.symfony.fullURL + "/app/common/exe_tooltips/";
486            } else {
487                var ref = window.location.href;
488                // Check if it's the home page
489                p = "libs/exe_tooltips/";
490                if (!document.getElementById("exe-index")) p = "../" + p;
491            }
492            $exe.loadScript(p + "exe_tooltips.js", "$exe.tooltips.init('" + p + "')")
493        }
494    },
495
496    // Internet Explorer? - To review (Export this function?)
497    isIE: function () {
498        var e = navigator.userAgent.toLowerCase();
499        return e.indexOf("msie") != -1 ? parseInt(e.split("msie")[1]) : false
500    },
501
502    // Add "http" to the IFRAMES without protocol in local pages and create a hidden link for the print version
503    setIframesProperties: function () {
504        var p = window.location.protocol;
505        var t = false;
506        if (p != "http" && p != "https") t = true;
507        $("iframe").each(function () {
508            var i = $(this);
509            var s = i.attr("src");
510            if (typeof (s) == "string") {
511                if (t && s.indexOf("//") == 0) $(this).attr("src", "http:" + s);
512                s = i.attr("src");
513                if (!i.hasClass("external-iframe") && s.indexOf("http") == 0) {
514                    i.addClass("external-iframe").before("<span class='external-iframe-src' style='display:none'><a href='" + s + "'>" + s + "</a></span>");
515                }
516            }
517        });
518    },
519
520    // Load a JavaScript or CSS file (in HEAD)
521    loadScript: function (url, callback) {
522        var s;
523        if (url.split(".").pop() == "css") {
524            s = document.createElement("link");
525            s.type = "text/css";
526            s.rel = "stylesheet";
527            s.href = url
528        } else {
529            s = document.createElement("script");
530            s.type = "text/javascript";
531            s.src = url
532        }
533        if (s.readyState) { // IE
534            s.onreadystatechange = function () {
535                if (s.readyState == "loaded" || s.readyState == "complete") {
536                    s.onreadystatechange = null;
537                    if (callback) eval(callback)
538                }
539            }
540        } else {
541            s.onload = function () {
542                if (callback) eval(callback)
543            }
544        }
545        document.getElementsByTagName("head")[0].appendChild(s)
546    },
547
548    // Check if you're in eXe
549    isInExe: function () {
550        return typeof eXeLearning !== "undefined";
551    },
552
553    // Check if you're in preview
554    isPreview: function () {
555        return $('body').hasClass('preview');
556    },
557
558    // Export idevice path - To review
559    getIdeviceInstalledExportPath: function (ideviceType) {
560        let ideviceNode;
561        if (this.isInExe()) {
562            ideviceNode = $(`article.idevice_node[idevice-type="${ideviceType}"]`);
563            return ideviceNode.attr('idevice-path');
564        } else {
565            ideviceNode = $(`article.idevice_node[data-idevice-type="${ideviceType}"]`);
566            return ideviceNode.attr('data-idevice-path');
567        }
568    }
569
570};
571
572// iDevices common code - To review (Part of this code should not be exported)
573var $exeDevices = {
574    iDevice: {
575        // Gamification
576        gamification: {
577            initGame($game, nameGame, gameClass, ideviceClass) {
578                if ($(".QuizTestIdevice .iDevice").length > 0) {
579                    $game.hasSCORMbutton = true;
580                }
581                let scormFlow = false;
582                $game.isInExe = eXe.app.isInExe();
583                $game.idevicePath = $game.isInExe
584                    ? eXe.app.getIdeviceInstalledExportPath(gameClass)
585                    : $(".idevice_node." + gameClass).eq(0).attr("data-idevice-path");
586
587                $game.activities = $(`.${ideviceClass}`);
588                if ($game.activities.length === 0) return;
589                if (!$exeDevices.iDevice.gamification.helpers.supportedBrowser(nameGame)) return;
590
591                if ($("#exe-submitButton").length > 0) {
592                    $game.activities.hide();
593                    if (typeof _ !== "undefined") {
594                        $game.activities.before(`<p>${_(nameGame)}</p>`);
595                    }
596                    return;
597                }
598                if (!($('html').is('#exe-index'))) {
599                    $game.scormAPIwrapper = '../libs/SCORM_API_wrapper.js';
600                    $game.scormFunctions = '../libs/SCOFunctions.js';
601                }
602
603                function initScorm() {
604                    if (typeof scorm !== "undefined" && scorm.init(
604)) {
605                        $game.mScorm = scorm;
606                        $game.userName = $exeDevices.iDevice.gamification.scorm.getUserName(scorm);
607                        $game.previousScore = $exeDevices.iDevice.gamification.scorm.getPreviousScore(scorm);
608                        if (typeof scorm.SetScoreMax === "function") {
609                            scorm.SetScoreMax(100);
610                            scorm.SetScoreMin(0);
611                        } else {
612                            scorm.set("cmi.core.score.max", "100");
613                            scorm.set("cmi.core.score.min", "0");
614                        }
615                    } else {
616                        console.warn("La inicialización SCORM devolvió false o scorm no está definido");
617                    }
618                }
619
620                function loadAndInitScorm() {
621                    if (typeof window.scorm === "undefined") {
622                        $.ajax({ url: $game.scormAPIwrapper, dataType: "script" })
623                            .then(() => $.ajax({ url: $game.scormFunctions, dataType: "script" }))
624                            .then(initScorm)
625                            .fail((_, textStatus) => {
626                                console.error("Error loading SCORM:", textStatus);
627                            })
628                            .always(() => {
629                                $game.enable();
630                            });
631                    } else {
632                        initScorm();
633                        $game.enable();
634                    }
635                }
636
637                try {
638                    if ($("body").hasClass("exe-scorm")) {
639                        scormFlow = true;
640                        loadAndInitScorm();
641                        return;
642                    }
643                } catch (err) {
644                    console.error("init() error:", err);
645                } finally {
646                    if (!scormFlow) {
647                        $game.enable();
648                    }
649                }
650            },
651
652            scorm: {
653                getUserName: function (scormgame) {
654                    if (scormgame && typeof scormgame.GetLearnerName === 'function') {
655                        return scormgame.GetLearnerName() || '';
656                    } else {
657                        return '';
658                    }
659                },
660
661                getPreviousScore: function (scormgame) {
662                    if (scormgame && typeof scormgame.GetScoreRaw === 'function') {
663                        return scormgame.GetScoreRaw() || '0';
664                    } else {
665                        return '0';
666                    }
667                },
668
669                // Nueva función: Obtener puntuación de una actividad específica desde suspend_data
670                getActivityScore: function (ideviceNumber) {
671                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM) {
672                        return 0;
673                    }
674
675                    const suspendData = pipwerks.SCORM.get("cmi.suspend_data") || "";
676                    const lmsData = $exeDevices.iDevice.gamification.scorm.parseSuspendData(suspendData);
677
678                    if (lmsData[ideviceNumber]) {
679                        // Score está guardado en escala 0-1000, convertir a 0-10
680                        return (lmsData[ideviceNumber].score / 10) || 0;
681                    }
682
683                    return 0;
684                },
685
686                // Nueva función: Obtener puntuación total del nodo desde cmi.core.score.raw
687                getTotalScore: function () {
688                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM) {
689                        return 0;
690                    }
691
692                    const rawScore = pipwerks.SCORM.get("cmi.core.score.raw");
693                    return parseFloat(rawScore) || 0;
694                },
695
696                endScorm: function (scormgame) {
697                    /*if (scormgame && typeof scormgame.quit == "function") scormgame.quit();*/
698                },
699
700                addButtonScoreNew: function (game, hasSCORMbutton, isInExe) {
701                    if (typeof game !== 'object' || game === null) return;
702
703                    let fB = '<div class="Games-BottonContainer d-flex align-items-center justify-content-end mx-auto p-0 w-100">';
704
705                    if (game.isScorm == 2) {
706                        const buttonText = game.textButtonScorm;
707                        if (buttonText != "") {
708                            fB += '<div class="Games-GetScore d-flex align-items-center justify-content-center w-100 mt-3">';
709                            fB += `<input type="button" value="${buttonText}" class="Games-SendScore btn btn-primary btn-sm mx-1 my-1" /> <span class="Games-RepeatActivity"></span>`;
710                            fB += '</div>';
711                        }
712                    } else if (game.isScorm == 1) {
713                        fB += `<div class="Games-GetScore d-flex align-items-center justify-content-center w-100 mt-3"><span class="Games-RepeatActivity"></span></div>`;
714                    }
715                    fB += '</div>';
716                    return fB;
717                },
718
719                parseJSONSafe: function (str) {
720                    try {
721                        return JSON.parse(str) || {};
722                    } catch (e) {
723                        console.error("parseJSONSafe: Could not parse JSON. Using empty object fallback.");
724                        return {};
725                    }
726                },
727
728                createScoreScormHtml: function (game) {
729                    let $exeScoreNode = $("#exeScoreNode");
730                    let initialScore = 0;
731                    
732                    if (typeof pipwerks !== 'undefined' && pipwerks.SCORM) {
733                        const rawScore = pipwerks.SCORM.get("cmi.core.score.raw");
734                        if (rawScore && rawScore !== "" && rawScore !== "0") {
735                            initialScore = parseFloat(rawScore) || 0;
736                        } else {
737                            const suspendData = pipwerks.SCORM.get("cmi.suspend_data") || "";
738                            if (suspendData && suspendData.trim() !== "") {
739                                const lmsData = $exeDevices.iDevice.gamification.scorm.parseSuspendData(suspendData);
740                                initialScore = $exeDevices.iDevice.gamification.scorm.getFinalScore(lmsData);
741                            }
742                        }
743                    }
744
745                    if ($exeScoreNode.length === 0) {
746                        const newScoreNodeHtml = `
747                                    <div id="exeScoreNode" class="text-end p-2">
748                                        <div id="eXeScoreNodeScore" class="bg-success text-white d-inline-block px-2 py-1">
749                                            ${game.msgs.msgYouScore}: ${initialScore}/100
750                                        </div>
751                                    </div>
752                                `;
753
754                        let $page = $(".page-content");
755                        if ($page.length === 0) {
756                            $page = $("#node-content");
757                        }
758                        $("#exeScoreNode").remove();
759
760                        if ($page.length > 0) {
761                            $page.prepend(newScoreNodeHtml);
762                        }
763                    } else {
764                        $("#eXeScoreNodeScore").text(`${game.msgs.msgYouScore}: ${initialScore}/100`);
765                    }
766                },
767
768                updateScormNew: function (game, lmsData) {
769                    let previouScore = '';
770
771                    if (lmsData && typeof pipwerks !== 'undefined' && pipwerks.SCORM) {
772                        const scoreVal = parseFloat(lmsData[game.ideviceNumber]?.score);
773                        previouScore = !Number.isNaN(scoreVal) ? (scoreVal / 10).toFixed(2) : '';
774                        $exeDevices.iDevice.gamification.scorm.showFinalScore(lmsData, game);
775                    }
776                    const $gmain = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
777
778                    const $sendScore = $gmain.closest('article').find(".Games-SendScore"),
779                        $repeatActivity = $gmain.closest('article').find(".Games-RepeatActivity");
780                    game.repeatActivity = true;
781                    let text = '';
782                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM) {
783                        text = game.msgs.msgScoreScorm;
784                    } else if (game.isScorm === 1) {
785                        if (game.repeatActivity && previouScore !== '') {
786                            text = game.msgs.msgYouLastScore + ': ' + previouScore;
787                        } else if (game.repeatActivity && previouScore === "") {
788                            text = game.msgs.msgSaveAuto + ' ' + game.msgs.msgPlaySeveralTimes;
789                        } else if (!game.repeatActivity && previouScore === "") {
790                            text = game.msgs.msgOnlySaveAuto;
791                        } else if (!game.repeatActivity && previouScore !== "") {
792                            text = game.msgs.msgActityComply + ' ' + game.msgs.msgYouLastScore + ': ' + previouScore;
793                        }
794                    } else if (game.isScorm === 2) {
795                        $sendScore.show();
796                        if (game.repeatActivity && previouScore !== '') {
797                            text = game.msgs.msgYouLastScore + ': ' + previouScore;
798                        } else if (game.repeatActivity && previouScore === '') {
799                            text = game.msgs.msgSeveralScore;
800                        } else if (!game.repeatActivity && previouScore === '') {
801                            text = game.msgs.msgOnlySaveScore;
802                        } else if (!game.repeatActivity && previouScore !== '') {
803                            $sendScore.hide();
804                            text = game.msgs.msgActityComply + ' ' + game.msgs.msgYouScore 
804+ ': ' + previouScore;
805                        }
806                    }
807                    $repeatActivity.text(text).fadeIn();
808                },
809
810                getFinalScore: function (lmsData) {
811                    if (!lmsData) {
812                        return 0;
813                    }
814
815                    const keys = Object.keys(lmsData);
816                    if (keys.length === 0) {
817                        return 0;
818                    }
819                    function clamp(num, min, max) {
820                        return Math.max(min, Math.min(num, max));
821                    }
822
823                    let interactionsData = keys.map(key => {
824                        const activity = lmsData[key] || {};
825                        const scoreVal = parseFloat(activity.score) || 0;
826                        const weightVal = parseFloat(activity.weighted) || 1;
827                        return {
828                            score: clamp(scoreVal, 0, 100),
829                            weighted: clamp(weightVal, 1, 100),
830                        };
831                    });
832
833                    let sumWeights = interactionsData.reduce((acc, item) => acc + item.weighted, 0);
834                    const factor = (sumWeights !== 0) ? 100 / sumWeights : 1;
835                    const tempWeights = interactionsData.map(item => {
836                        const scaled = item.weighted * factor;
837                        const floored = Math.floor(scaled);
838                        const fraction = scaled - floored;
839                        return {
840                            score: item.score,
841                            floored,
842                            fraction
843                        };
844                    });
845
846                    let sumFloors = tempWeights.reduce((acc, w) => acc + w.floored, 0);
847                    let diff = 100 - sumFloors;
848
849                    tempWeights.sort((a, b) => b.fraction - a.fraction);
850
851                    for (let i = 0; i < tempWeights.length && diff !== 0; i++) {
852                        if (diff > 0) {
853                            tempWeights[i].floored += 1;
854                            diff--;
855                        }
856                    }
857
858                    function round2(num) {
859                        return Math.round(num * 100) / 100;
860                    }
861
862                    let sumWeighted = 0;
863                    tempWeights.forEach(item => {
864                        sumWeighted += (item.score * item.floored);
865                    });
866                    const finalScore = round2(sumWeighted / 100);
867                    return finalScore;
868                },
869
870                registerActivity: function (game) {
871                    if (typeof game !== 'object' || game === null) return;
872
873                    let lmsData = {};
874                    if (typeof pipwerks !== 'undefined' && pipwerks.SCORM) {
875                        game.mainElement = game.main.charAt(0) === '.' ? game.mainElement = $(`${game.main}`).eq(0) : game.mainElement = $(`#${game.main}`).eq(0);
876                        $exeDevices.iDevice.gamification.scorm.createScoreScormHtml(game);
877                        game.title = game.mainElement.closest('article')
878                            .find('header .box-title').text() || '';
879                        game.title = game.title.replace(/"/g, ' ');
880                        let $idevices = $('.idevice_node');
881                        let deviceId = game.mainElement.closest('.idevice_node').attr('id');
882                        let index = $idevices.index($('#' + deviceId));
883
884                        game.ideviceNumber = index + 1;
885
886                        let suspendData = pipwerks.SCORM.get("cmi.suspend_data") || "";
887
888                        lmsData = $exeDevices.iDevice.gamification.scorm.parseSuspendData(suspendData);
889
890                        if (lmsData[game.ideviceNumber]) {
891                            game.previousScore = (lmsData[game.ideviceNumber].score / 10).toFixed(2);
892                            // Actualizar el score node con la puntuación recuperada
893                            const totalScore = $exeDevices.iDevice.gamification.scorm.getFinalScore(lmsData);
894                            if (totalScore > 0) {
895                                $("#eXeScoreNodeScore").text(`${game.msgs.msgYouScore}: ${totalScore}/100`);
896                            }
897                        } else {
898                            lmsData[game.ideviceNumber] = {
899                                title: game.title,
900                                score: 0,
901                                weighted: game.weighted
902                            };
903
904                            const newFormatData = $exeDevices.iDevice.gamification.scorm.convertToLineFormat(lmsData, game);
905                            pipwerks.SCORM.set("cmi.suspend_data", newFormatData);
906                        }
907                    }
908
909                    $exeDevices.iDevice.gamification.scorm.updateScormNew(game, lmsData);
910                },
911
912                convertToLineFormat: function (obj, game) {
913                    return Object.keys(obj).map(key => {
914                        const item = obj[key];
915                        const num = parseInt(key, 10);
916                        const title = item.title || "";
917                        const score = item.score != null ? item.score : 0;
918                        const weight = item.weighted ?? 0;
919                        const msgScore = game.msgs.msgScore ?? "Puntuación";
920                        const msgWeight = game.msgs.msgWeight ?? "Peso";
921
922                        return `${num}. "${title}"; ${msgScore}: ${score}%; ${msgWeight}: ${weight}%`;
923                    }).join('.\t');
924                },
925
926                parseActivity: function (line) {
927                    const regex = /^(\d+)\.\s"(.*?)";\s[^:]+:\s([\d.]+)%;\s[^:]+:\s([\d.]+)%\.?$/;
928
929                    const match = line.match(regex);
930
931                    if (match) {
932                        const [_, strIndex, title, score, weighted] = match;
933
934                        return {
935                            index: parseInt(strIndex, 10),
936                            title: title.trim(),
937                            score: parseFloat(score),
938                            weighted: parseFloat(weighted)
939                        }
940                    }
941
942                    return null;
943                },
944
945                parseSuspendData: function (data) {
946                    let obj = {};
947
948                    if (!data) return obj;
949
950                    const lines = data.split('.\t');
951                    lines.forEach(line => {
952                        line = line.trim();
953                        if (!line) return;
954
955                        const activityData = $exeDevices.iDevice.gamification.scorm.parseActivity(line);
956
957                        if (activityData) {
958                            const { index, title, score, weighted } = activityData;
959                            obj[index] = {
960                                title: title.trim(),
961                                score: parseFloat(score),
962                                weighted: parseFloat(weighted)
963                            };
964                        }
965                    });
966
967                    return obj;
968                },
969
970                sendScoreNew: function (auto, game) {
971                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM || typeof game !== 'object' || game === null) {
972                        return;
973                    }
974                    const $gmain = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
975                    const $article = $gmain.closest('.idevice_node').eq(0);
976                    const $sendScore = $article.find(".Games-SendScore");
977                    const $repeatActivity = $article.find(".Games-RepeatActivity");
978
979                    let message = '';
980                    if (game.gameStarted || game.gameOver) {
981                        game.repeatActivity = true;
982                        const suspendData = pipwerks.SCORM.get("cmi.suspend_data") || "";
983                        const lmsData = $exeDevices.iDevice.gamification.scorm.parseSuspendData(suspendData);
984                        const scoreVal = parseFloat(lmsData[game.ideviceNumber]?.score);
985                        const previousScore = !Number.isNaN(scoreVal) ? (scoreVal / 10).toFixed(2) : '';
986
987                        const scoreNumber = parseFloat(game.scorerp);
988                        const formattedScore = !isNaN(scoreNumber) ? scoreNumber.toFixed(2) : '0';
989                        game.scorerp = formattedScore;
990
991                        if (!auto) {
992                            $sendScore.show();
993                            if (!game.repeatActivity && previousScore !== '') {
994                                message = game.userName !== ''
995                                    ? (game.userName + ' ' + game.msgs.msgOnlySaveScore)
996                                    : game.msgs.msgOnlySaveScore;
997                            } else {
998                                game.previousScore = formattedScore;
999                                $exeDevices.iDevice.gamification.scorm.updateActivity(game, lmsData);
1000
1001                                message = game.userName !== ''
1002                                    ? (game.userName + '. ' + game.msgs.msgYouScore + ': ' + formattedScore)
1003                                    : (game.msgs.msgYouScore + ': ' + formattedScore);
1004
1005                                if (!game.repeatActivity) {
1006                                    $sendScore.hide();
1007                                }
1008
1009                                $repeatActivity.text(game.msgs.msgYouScore + ': ' + formattedScore).show();
1010                            }
1011                        } else {
1012                            game.previousScore = formattedScore;
1013                            $exeDevices.iDevice.gamification.scorm.updateActivity(game, lmsData);
1014                            message = game.msgs.msgYouScore + ': ' + formattedScore;
1015                            $repeatActivity.text(message).show();
1016                        }
1017
1018                    } else {
1019                        message = game.msgs.msgEndGameScore;
1020                    }
1021
1022                    $repeatActivity.text(message).show();
1023                    if (!auto && message) {
1024                        alert(message);
1025                    }
1026
1027                },
1028
1029                updateActivity: function (game, lmsData) {
1030                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM || typeof game !== 'object' || game === null) {
1031                        return;
1032                    }
1033
1034                    const updatedData = {
1035                        title: game.title,
1036                        score: game.scorerp * 10,
1037                        weighted: game.weighted
1038                    };
1039                    lmsData[game.ideviceNumber] = updatedData;
1040
1041                    const newFormatData = $exeDevices.iDevice.gamification.scorm.convertToLineFormat(lmsData, game);
1042
1043                    pipwerks.SCORM.set("cmi.suspend_data", newFormatData);
1044
1045                    $exeDevices.iDevice.gamification.scorm.showFinalScore(lmsData, game);
1046
1047                },
1048
1049                showFinalScore: function (lmsData, game) {
1050                    if (typeof pipwerks === 'undefined' || !pipwerks.SCORM || typeof game !== 'object' || game === null) {
1051                        return;
1052                    }
1053
1054                    if (!lmsData || typeof lmsData !== 'object') {
1055                        const suspendData = pipwerks.SCORM.get("cmi.suspend_data") || "";
1056                        lmsData = $exeDevices.iDevice.gamification.scorm.parseSuspendData(suspendData);
1057                    }
1058
1059                    const newFinalScore = $exeDevices.iDevice.gamification.scorm.getFinalScore(lmsData);
1060
1061                    pipwerks.SCORM.set("cmi.core.score.raw", newFinalScore);
1062
1063                    if (newFinalScore >= 50) {
1064                        pipwerks.SCORM.set("cmi.core.lesson_status", "passed");
1065                    } else {
1066                        pipwerks.SCORM.set("cmi.core.lesson_status", "failed");
1067                    }
1068
1069                    $("#eXeScoreNodeScore").text(`${game.msgs.msgYouScore}: ${newFinalScore}/100`);
1070
1071                }
1072            },
1073
1074            colors: {
1075                borderColors: {
1076                    black: "#1c1b1b",
1077                    blue: '#5877c6',
1078                    green: '#00a300',
1079                    red: '#b3092f',
1080                    white: '#f9f9f9',
1081                    yellow: '#f3d55a',
1082                    grey: '#777777',
1083                    incorrect: '#d9d9d9',
1084                    correct: '#00ff00'
1085                },
1086                backColor: {
1087                    black: "#1c1b1b",
1088                    blue: '#dfe3f1',
1089                    green: '#caede8',
1090                    red: '#fbd2d6',
1091                    white: '#f9f9f9',
1092                    yellow: '#fcf4d3',
1093                    correct: '#dcffdc',
1094                    blackl: '#333333'
1095                },
1096            },
1097
1098            report: {
1099                updateEvaluationIcon: function (game, isInExe) {
1100                    if (typeof game !== 'undefined' && game.main) {
1101                        const $gmain = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
1102                        game.id = $gmain.closest('.idevice_node').attr('id');
1103                    }
1104                    if (game && game.id && game.evaluation && game.evaluationID && game.evaluationID.length > 0) {
1105                        const data = $exeDevices.iDevice.gamification.report.getDataStorage(game.evaluationID);
1106                        const $gmain = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
1107
1108                        let score = '',
1109                            state = 0;
1110
1111                        if (!data) {
1112                            $exeDevices.iDevice.gamification.report.showEvaluationIcon(game, state, score);
1113                            return;
1114                        }
1115
1116                        const findObject = data.activities.find(
1117                            obj => obj.id === game.id
1118                        );
1119
1120                        if (findObject) {
1121                            state = findObject.state;
1122                            score = findObject.score;
1123                        }
1124
1125                        $exeDevices.iDevice.gamification.report.showEvaluationIcon(game, state, score);
1126
1127                        const anchorId = 'ac-' + game.id;
1128                        $(`#${anchorId}`).remove();
1129                        $gmain.closest(`.${game.idevice}`).prepend(`<div id="${anchorId}"></div>`);
1130
1131                        if ($exe.isInExe() || location.protocol === 'file:' || typeof window.API !== 'undefined' ||
1132                            typeof window.API_1484_11 !== 'undefined') {
1133                            return;
1134                        }
1135
1136                        if (document.readyState === 'complete') {
1137                            $exeDevices.iDevice.gamification.report.scrollToHash();
1138                        } else {
1139                            $(window).on('load', $exeDevices.iDevice.gamification.report.scrollToHash);
1140                        }
1141                    }
1142                },
1143
1144                showEvaluationIcon: function (game, state, score) {
1145                    if (typeof game !== 'object' || game === null) return;
1146
1147                    const $main = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0),
1148                        scoreNumber = parseFloat(score),
1149                        formattedScore = !isNaN(scoreNumber) ? scoreNumber.toFixed(2) : '0',
1150                        $header = $main.closest(`.${game.idevice}`);
1151
1152                    let icon = 'exequextsq.png',
1153                        text = game.msgs.msgUncompletedActivity;
1154
1155                    if (state === 1) {
1156                        icon = 'exequextrerrors.svg';
1157                        text = game.msgs.msgUnsuccessfulActivity.replace('%s', formattedScore);
1158                    } else if (state === 2) {
1159                        icon = 'exequexthits.svg';
1160                        text = game.msgs.msgSuccessfulActivity.replace('%s', formattedScore);
1161                    }
1162
1163                    $header.find('.Games-ReportIconDiv').remove()
1164
1165                    const sicon = `<div class="Games-ReportIconDiv d-flex justify-content-start align-items-center w-100 mb-1" style="gap: 0.1em;">
1166                            <img src="${game.idevicePath}${icon}" style="width:16px; height:16px; display:block;"><span style="font-size:0.9em;">${text}</span>
1167                        </div>`;
1168
1169                    $header.prepend(sicon);
1170                },
1171
1172                updateEvaluation: function (obj1, obj2, id1) {
1173                    if (!obj1) {
1174                        obj1 = {
1175                            id: id1,
1176                            activities: []
1177                        };
1178                    }
1179                    const findObject = obj1.activities.find(
1180                        obj => obj.id === obj2.id
1181                    );
1182                    if (findObject) {
1183                        findObject.state = obj2.state;
1184                        findObject.score = obj2.score;
1185                        findObject.name = obj2.name;
1186                        findObject.date = obj2.date;
1187                        findObject.page = obj2.page;
1188                    } else {
1189                        obj1.activities.push({
1190                            id: obj2.id,
1191                            type: obj2.type,
1192                            name: obj2.name,
1193                            score: obj2.score,
1194                            date: obj2.date,
1195                            state: obj2.state,
1196                            page: obj2.page,
1197                        });
1198                    }
1199                    return obj1;
1200                },
1201
1202                getDateString: function () {
1203                    const currentDate = new Date(),
1204                        day = String(currentDate.getDate()).padStart(2, '0'),
1205                        month = String(currentDate.getMonth() + 1).padStart(2, '0'),
1206                        year = String(currentDate.getFullYear()).padStart(4, '0'),
1207                        hours = String(currentDate.getHours()).padStart(2, '0'),
1208                        minutes = String(currentDate.getMinutes()).padStart(2, '0'),
1209                        seconds = String(currentDate.getSeconds()).padStart(2, '0');
1210                    return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
1211
1212                },
1213
1214                getNodeIdevice: function () {
1215                    let node = false;
1216                    if (!$exe.isInExe()) {
1217                        const pathSegments = window.location.pathname.split('/').filter(Boolean);
1218                        node = pathSegments.pop()
1219                    }
1220                    return node;
1221                },
1222
1223                getNameIdevice: function ($main) {
1224                    const selector = $exe.isInExe() ? 'header.box-head .box-title' : '.box-title';
1225                    return $main.closest('article').find(selector).eq(0).text();
1226                },
1227
1228                saveEvaluation: function (game,) {
1229                    if (typeof game !== 'undefined' && game.main) {
1230                        const $gmain = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
1231                        game.id = $gmain.closest('.idevice_node').attr('id');
1232                    }
1233                    if (game && game.id && game.evaluation && game.evaluationID.length > 0) {
1234                        const $main = game.main.charAt(0) === '.' ? $(`${game.main}`).eq(0) : $(`#${game.main}`).eq(0);
1235                        const name = $exeDevices.iDevice.gamification.report.getNameIdevice($main),
1236                            score = game.scorerp,
1237                            formattedDate = $exeDevices.iDevice.gamification.report.getDateString(),
1238                            scorm = {
1239                                'id': game.id,
1240                                'type': game.msgs.msgTypeGame,
1241                                'name': name,
1242                                'score': score,
1243                                'date': formattedDate,
1244                                'state': (parseFloat(score) >= 5 ? 2 : 1),
1245                                'page': $exeDevices.iDevice.gamification.report.getNodeIdevice()
1246                            },
1247                            data = $exeDevices.iDevice.gamification.report.updateEvaluation($exeDevices.iDevice.gamification.report.getDataStorage(game.evaluationID), scorm, game.id);
1248
1249                        localStorage.setItem('dataEvaluation-' + game.evaluationID, JSON.stringify(data));
1250                        $exeDevices.iDevice.gamification.report.showEvaluationIcon(game, scorm.state, scorm.score);
1251                        const event = new CustomEvent('gamification-evaluation-saved', { detail: { evaluationID: game.evaluationID, ideviceId: game.id, ideviceType: game.idevice, score: scorm.score, state: scorm.state } });
1252                        window.dispatchEvent(event);
1253                    }
1254                },
1255
1256                getDataStorage: function (id) {
1257                    return $exeDevices.iDevice.gamification.helpers.isJsonString(localStorage.getItem('dataEvaluation-' + id));
1258                },
1259
1260                scrollToHash: function () {
1261
1262                    if ($exe.isInExe() || location.protocol === 'file:') return;
1263
1264                    var hash = window.location.hash;
1265                    if (!hash) return;
1266
1267                    var id = hash.substring(1);
1268                    var pending = localStorage.getItem('hashScrolled');
1269                    if (pending !== id) return;
1270                    var $target = $('#' + id);
1271                    if ($target.length && $target.hasClass('idevice_node')) {
1272                        $('html, body').scrollTop($target.offset().top);
1273                    }
1274                },
1275
1276            },
1277
1278            math: {
1279                _loading: false,
1280                _callbacks: [],
1281
1282                engine: $("html").prop("id") == "exe-index" ? "./libs/exe_math/tex-mml-svg.js" : "../libs/exe_math/tex-mml-svg.js",
1283
1284                engineConfig: window.MathJax,
1285
1286                loadMathJax: function (callback) {
1287                    var self = $exeDevices.iDevice.gamification.math;
1288
1289                    if (typeof window.MathJax === 'object' && typeof MathJax.typesetPromise === 'function') {
1290                        if (callback) callback();
1291                        return;
1292                    }
1293
1294                    if (callback) {
1295                        self._callbacks.push(callback);
1296                    }
1297
1298                    if (self._loading) {
1299                        return;
1300                    }
1301
1302                    var existingScript = document.querySelector('script[src*="tex-mml-svg.js"]');
1303                    if (existingScript) {
1304                        self._loading = true;
1305                        var checkMathJax = function () {
1306                            if (typeof window.MathJax === 'object' && typeof MathJax.typesetPromise === 'function') {
1307                                self._loading = false;
1308                                while (self._callbacks.length > 0) {
1309                                    var cb = self._callbacks.shift();
1310                                    cb();
1311                                }
1312                            } else {
1313                                setTimeout(checkMathJax, 50);
1314                            }
1315                        };
1316                        checkMathJax();
1317                        return;
1318                    }
1319
1320                    self._loading = true;
1321                    var basePath = $("html").prop("id") == "exe-index" ? "./libs/exe_math" : "../libs/exe_math";
1322                    if (!window.MathJax) {
1323                        window.MathJax = self.engineConfig;
1324                    }
1325                    if (!window.MathJax.loader) window.MathJax.loader = {};
1326                    if (!window.MathJax.loader.paths) window.MathJax.loader.paths = {};
1327                    window.MathJax.loader.paths.mathjax = basePath;
1328                    var script = document.createElement('script');
1329                    script.src = self.engine;
1330                    script.async = true;
1331                    script.onload = function () {
1332                        var checkReady = function () {
1333                            if (typeof window.MathJax === 'object' && typeof MathJax.typesetPromise === 'function') {
1334                                self._loading = false;
1335                                while (self._callbacks.length > 0) {
1336                                    var cb = self._callbacks.shift();
1337                                    cb();
1338                                }
1339                            } else {
1340                                setTimeout(checkReady, 50);
1341                            }
1342                        };
1343                        checkReady();
1344                    };
1345                    document.head.appendChild(script);
1346                },
1347
1348                hasLatex: function (text) {
1349                    return /\\\(|\\\[|\\begin\{|\$\$/.test(text);
1350                },
1351
1352                updateLatex: function (target, opts) {
1353                    var self = $exeDevices.iDevice.gamification.math;
1354                    var options = opts || {};
1355
1356                    function nodesFrom(t) {
1357                        if (!t) return [];
1358                        if (typeof t === 'string') {
1359                            try { return Array.from(document.querySelectorAll(t)); }
1360                            catch (e) { console.warn('selector inválido:', t, e); return []; }
1361                        }
1362                        if (t.nodeType === 1) return [t];
1363                        if (typeof t.length === 'number') return Array.from(t);
1364                        return [];
1365                    }
1366
1367                    function runV3(nodes) {
1368                        if (!nodes.length) return;
1369                        var start = (MathJax.startup && MathJax.startup.promise) ? MathJax.startup.promise : Promise.resolve();
1370                        return start.then(function () {
1371                            if (typeof MathJax.typesetClear === 'function') MathJax.typesetClear(nodes);
1372                            return (MathJax.typesetPromise ? MathJax.typesetPromise(nodes) : MathJax.typeset(nodes));
1373                        }).catch(function (e) { console.error('MathJax v3 typeset error:', e); });
1374                    }
1375
1376                    function runV2(nodes) {
1377                        if (!nodes.length) return;
1378                        nodes.forEach(function (n) {
1379                            MathJax.Hub.Queue(['Typeset', MathJax.Hub, n]);
1380                        });
1381                    }
1382
1383                    function typesetNow() {
1384                        var nodes = nodesFrom(target);
1385                        if (!nodes.length) return;
1386
1387                        if (typeof MathJax === 'undefined') {
1388                            self.loadMathJax(function () {
1389                                typesetNow();
1390                            });
1391                            return;
1392                        }
1393
1394                        if (MathJax.typesetPromise || MathJax.startup) return runV3(nodes);
1395                        if (MathJax.Hub && typeof MathJax.Hub.Queue === 'function') return runV2(nodes);
1396                    }
1397
1398                    if (options.defer) {
1399                        // Espera a que termine la animación/visibilidad de la slide
1400                        requestAnimationFrame(function () { requestAnimationFrame(typesetNow); });
1401                    } else {
1402                        typesetNow();
1403                    }
1404                }
1405            },
1406
1407
1408            media: {
1409                extractURLGD: function (urlmedia) {
1410                    let sUrl = urlmedia;
1411
1412                    if (
1413                        typeof urlmedia !== 'undefined' &&
1414                        urlmedia.length > 0 &&
1415                        urlmedia.toLowerCase().startsWith('https://drive.google.com') &&
1416                        urlmedia.toLowerCase().includes('sharing')
1417                    ) {
1418                        sUrl = sUrl.replace(
1419                            /https:\/\/drive\.google\.com\/file\/d\/(.*?)\/.*?\?usp=sharing/g,
1420                            'https://docs.google.com/uc?export=open&id=$1'
1421                        );
1422                    } else if (
1423                        typeof urlmedia !== 'undefined' &&
1424                        urlmedia.length > 10 &&
1425                        $exeDevices.iDevice.gamification.media.getURLAudioMediaTeca(urlmedia)
1426                    ) {
1427                        sUrl = $exeDevices.iDevice.gamification.media.getURLAudioMediaTeca(urlmedia);
1428                    }
1429
1430                    return sUrl;
1431                },
1432
1433                getURLVideoMediaTeca: function (url) {
1434                    if (!url) return false;
1435
1436                    if (url.includes("https://mediateca.educa.madrid.org/video/")) {
1437                        const id = url.split("https://mediateca.educa.madrid.org/video/")[1].split("?")[0];
1438                        return `http://mediateca.educa.madrid.org/streaming.php?id=${id}`;
1439                    }
1440
1441                    return false;
1442                },
1443
1444                getURLAudioMediaTeca: function (url) {
1445                    if (!url) return false;
1446
1447                    let id = '';
1448                    if (url.includes("https://mediateca.educa.madrid.org/audio/")) {
1449                        id = url.split("https://mediateca.educa.madrid.org/audio/")[1].split("?")[0];
1450                    } else if (url.includes("https://mediateca.educa.madrid.org/video/")) {
1451                        id = url.split("https://mediateca.educa.madrid.org/video/")[1].split("?")[0];
1452                    } else {
1453                        return false;
1454                    }
1455                    return `https://mediateca.educa.madrid.org/streaming.php?id=${id}`;
1456                },
1457
1458                getIDYoutube: function (url) {
1459                    if (!url) return "";
1460                    const regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/,
1461                        match = url.match(regExp);
1462                    return (match && match[2].length === 11) ? match[2] : "";
1463                },
1464
1465                loadYoutubeApi: function (youTubeReady) {
1466                    onYouTubeIframeAPIReady = youTubeReady;
1467                    var tag = document.createElement('script');
1468                    tag.src = "https://www.youtube.com/iframe_api";
1469                    var firstScriptTag = document.getElementsByTagName('script')[0];
1470                    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
1471
1472                },
1473
1474                stopSound: function (game) {
1475                    if (typeof game !== 'object' || game === null) return;
1476                    if (game.playerAudio && typeof game.playerAudio.pause === "function") {
1477                        game.playerAudio.pause();
1478                        game.playerAudio.currentTime = 0;
1479                    }
1480                },
1481
1482                playSound: function (selectedFile, game) {
1483                    if (typeof game !== 'object' || game === null) return;
1484                    selectedFile = $exeDevices.iDevice.gamification.media.extractURLGD(selectedFile);
1485
1486                    if (game.playerAudio && !game.playerAudio.paused) {
1487                        game.playerAudio.pause();
1488                    }
1489
1490                    if (!game.playerAudio || game.playerAudio.src !== selectedFile) {
1491                        game.playerAudio = new Audio(selectedFile);
1492                        game.playerAudio.play().catch(error => console.error("Error playing audio:", error));
1493                    } else if (game.playerAudio.paused) {
1494                        game.playerAudio.play().catch(error => console.error("Error playing audio:", error));
1495                    }
1496                },
1497
1498                startVideo: function (id, start, end, game, type, instance, updateTimerDisplayLocal) {
1499                    if (typeof game !== 'object' || game === null) return;
1500
1501                    const mstart = start < 1 ? 0.1 : start;
1502
1503                    if (type === 1) {
1504                        if (game.localPlayer) {
1505                            game.pointEnd = end;
1506                            game.localPlayer.src = id
1507                            game.localPlayer.currentTime = parseFloat(start)
1508                            if (typeof game.localPlayer.play == "function") {
1509                                game.localPlayer.play();
1510                            }
1511                        }
1512                        clearInterval(game.timeUpdateInterval);
1513                        game.timeUpdateInterval = setInterval(function () {
1514                            updateTimerDisplayLocal(instance);
1515                        }, 1000);
1516                        return;
1517                    }
1518
1519                    if (game.player && typeof game.player.loadVideoById == "function") {
1520                        game.player.loadVideoById({
1521                            'videoId': id,
1522                            'startSeconds': mstart,
1523                            'endSeconds': end
1524                        });
1525                    }
1526                },
1527
1528                startVideoIntro: function (id, start, end, game, instance, type, updateTimerDis
1528playLocalIntro) {
1529                    if (typeof game !== 'object' || game === null) return;
1530
1531                    const mstart = start < 1 ? 0.1 : start;
1532
1533                    if (type === 1) {
1534                        if (game.localPlayerIntro) {
1535                            game.pointEndIntro = end;
1536                            game.localPlayerIntro.src = id
1537                            game.localPlayerIntro.currentTime = parseFloat(start)
1538                            if (typeof game.localPlayerIntro.play == "function") {
1539                                game.localPlayerIntro.play();
1540                            }
1541                        }
1542                        clearInterval(game.timeUpdateIntervalIntro);
1543                        game.timeUpdateIntervalIntro = setInterval(function () {
1544                            updateTimerDisplayLocalIntro(instance);
1545                        }, 1000);
1546                        return
1547                    }
1548
1549                    if (game.playerIntro) {
1550                        if (typeof game.playerIntro.loadVideoById == "function") {
1551                            game.playerIntro.loadVideoById({
1552                                'videoId': id,
1553                                'startSeconds': mstart,
1554                                'endSeconds': end
1555                            });
1556                        }
1557                    }
1558                },
1559
1560                stopVideo: function (game) {
1561                    if (typeof game !== 'object' || game === null) return;
1562
1563                    if (game.localPlayer && typeof game.localPlayer.pause === "function") {
1564                        game.localPlayer.pause();
1565                    }
1566                    if (game.player && typeof game.player.pauseVideo === "function") {
1567                        game.player.pauseVideo();
1568                    }
1569                },
1570
1571                playVideo: function (game) {
1572                    if (game && game.player && typeof game.player.playVideo == "function") {
1573                        game.player.playVideo();
1574                    }
1575                },
1576
1577                stopVideo: function (game) {
1578                    if (game && game.localPlayer && typeof game.localPlayer.pause === "function") {
1579                        game.localPlayer.pause();
1580                    }
1581                    if (game && game.player && typeof game.player.pauseVideo === "function") {
1582                        game.player.pauseVideo();
1583                    }
1584                },
1585
1586                stopVideoIntro: function (game) {
1587                    if (typeof game !== 'object' || game === null) return;
1588
1589                    if (game.localPlayerIntro && typeof game.localPlayerIntro.pause == "function") {
1590                        game.localPlayerIntro.pause();
1591                    }
1592
1593                    if (game.playerIntro && typeof game.playerIntro.pauseVideo == "function") {
1594                        game.playerIntro.pauseVideo();
1595                    }
1596                },
1597
1598                muteVideo: function (mute, game) {
1599                    if (game && game.localPlayer) {
1600                        if (mute) {
1601                            game.localPlayer.muted = true;
1602                        } else {
1603                            game.localPlayer.muted = false;;
1604                        }
1605                    }
1606                    if (game && game.player) {
1607                        if (mute) {
1608                            if (typeof game.player.mute == "function") {
1609                                game.player.mute();
1610                            }
1611                        } else {
1612                            if (typeof game.player.unMute == "function") {
1613                                game.player.unMute();
1614                            }
1615                        }
1616                    }
1617                },
1618
1619                YouTubeAPILoader: (function () {
1620                    let apiReadyPromise;
1621
1622                    function load() {
1623                        if (!apiReadyPromise) {
1624                            apiReadyPromise = new Promise((resolve, reject) => {
1625                                if (window.YT && window.YT.Player) {
1626                                    return resolve(window.YT);
1627                                }
1628                                window.onYouTubeIframeAPIReady = () => resolve(window.YT);
1629                                const tag = document.createElement('script');
1630                                tag.src = 'https://www.youtube.com/iframe_api';
1631                                tag.onerror = () => reject(new Error('No se pudo cargar la API de YouTube'));
1632                                document.head.appendChild(tag);
1633                            });
1634                        }
1635                        return apiReadyPromise;
1636                    }
1637
1638                    return { load };
1639                })(),
1640
1641            },
1642
1643            helpers: {
1644                sanitizeJSONString: function (jsonString) {
1645                    if (typeof jsonString !== 'string' || jsonString === '') return jsonString;
1646
1647                    let inString = false;
1648                    let result = '';
1649
1650                    for (let i = 0; i < jsonString.length; i++) {
1651                        const ch = jsonString[i];
1652
1653                        if (!inString) {
1654                            if (ch === '"') {
1655                                inString = true;
1656                            }
1657                            result += ch;
1658                            continue;
1659                        }
1660
1661                        if (ch === '\\') {
1662                            if (i + 1 < jsonString.length) {
1663                                result += ch + jsonString[i + 1];
1664                                i++;
1665                            } else {
1666                                result += ch;
1667                            }
1668                            continue;
1669                        }
1670
1671                        if (ch === '"') {
1672                            inString = false;
1673                            result += ch;
1674                            continue;
1675                        }
1676
1677                        const code = ch.charCodeAt(0);
1678
1679                        if (code === 0x08) {
1680                            result += '\\b';
1681                        } else if (code === 0x09) {
1682                            result += '\\t';
1683                        } else if (code === 0x0a) {
1684                            result += '\\n';
1685                        } else if (code === 0x0c) {
1686                            result += '\\f';
1687                        } else if (code === 0x0d) {
1688                            result += '\\r';
1689                        } else if (code === 0x2028 || code === 0x2029) {
1690                            const hex = code.toString(16).padStart(4, '0');
1691                            result += `\\u${hex}`;
1692                        } else if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
1693                            const hex = code.toString(16).padStart(4, '0');
1694                            result += `\\u${hex}`;
1695                        } else {
1696                            result += ch;
1697                        }
1698                    }
1699
1700                    return result;
1701                },
1702                isJsonString: function (str) {
1703                    if (typeof str !== 'string') return false;
1704                    str = str.trim();
1705                    if (str.startsWith('{') && str.endsWith('}')) {
1706                        try {
1707                            const o = JSON.parse(str);
1708                            if (o && typeof o === 'object' && !Array.isArray(o)) {
1709                                return o;
1710                            }
1711                        } catch (e) {
1712                            return false;
1713                        }
1714                    }
1715                    return false;
1716                },
1717
1718                shuffleAds: function (arr) {
1719                    if (Array.isArray(arr)) {
1720                        for (let i = arr.length - 1; i > 0; i--) {
1721                            const j = Math.floor(Math.random() * (i + 1));
1722                            [arr[i], arr[j]] = [arr[j], arr[i]];
1723                        }
1724                    }
1725                    return arr;
1726                },
1727
1728                decrypt: function (str) {
1729                    str = str || "";
1730                    str = (str === "undefined" || str === "null") ? "" : str;
1731                    str = unescape(str);
1732                    try {
1733                        const key = 146;
1734                        let pos = 0,
1735                            ostr = '';
1736                        while (pos < str.length) {
1737                            ostr += String.fromCharCode(key ^ str.charCodeAt(pos));
1738                            pos += 1;
1739                        }
1740                        return ostr;
1741                    } catch (ex) {
1742                        return '';
1743                    }
1744                },
1745
1746                encrypt: function (str = '') {
1747                    if (!str || str === 'undefined' || str === 'null') str = '';
1748                    try {
1749                        const key = 146;
1750                        return escape(str.split('').map(char => String.fromCharCode(char.charCodeAt(0) ^ key)).join(''));
1751                    } catch (ex) {
1752                        return '';
1753                    }
1754                },
1755
1756                exitFullscreen: function () {
1757                    if (document.exitFullscreen) {
1758                        document.exitFullscreen();
1759                    } else if (document.msExitFullscreen) {
1760                        document.msExitFullscreen();
1761                    } else if (document.mozCancelFullScreen) {
1762                        document.mozCancelFullScreen();
1763                    } else if (document.webkitExitFullscreen) {
1764                        document.webkitExitFullscreen();
1765                    }
1766                },
1767
1768                getFullscreen: function (element) {
1769                    if (element.requestFullscreen) {
1770                        element.requestFullscreen();
1771                    } else if (element.mozRequestFullScreen) {
1772                        element.mozRequestFullScreen();
1773                    } else if (element.webkitRequestFullscreen) {
1774                        element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
1775                    } else if (element.msRequestFullscreen) {
1776                        element.msRequestFullscreen();
1777                    }
1778                },
1779
1780                toggleFullscreen: function (element) {
1781                    const elem = element || document.documentElement;
1782                    if (!document.fullscreenElement && !document.mozFullScreenElement &&
1783                        !document.webkitFullscreenElement && !document.msFullscreenElement) {
1784                        $exeDevices.iDevice.gamification.helpers.getFullscreen(elem);
1785                    } else {
1786                        $exeDevices.iDevice.gamification.helpers.exitFullscreen();
1787                    }
1788                },
1789
1790                supportedBrowser: function (idevice) {
1791                    const isSupported = !(navigator.appName === 'Microsoft Internet Explorer' || navigator.userAgent.includes('MSIE '));
1792                    if (!isSupported) {
1793                        const bns = $(`.${idevice}-bns`).eq(0).text() || 'Your browser is not compatible with this tool.';
1794                        $(`.${idevice}-instructions`).text(bns);
1795                    }
1796                    return isSupported;
1797                },
1798
1799                getTimeSeconds: function (iT) {
1800                    const times = [15, 30, 60, 180, 300, 600]
1801                    if ((iT) < times.length) return times[iT];
1802                    return iT;
1803
1804                },
1805
1806                getTimeToString: function (iTime) {
1807                    const mMinutes = Math.floor(iTime / 60) % 60,
1808                        mSeconds = iTime % 60,
1809                        formattedMinutes = mMinutes < 10 ? `0${mMinutes}` : mMinutes,
1810                        formattedSeconds = mSeconds < 10 ? `0${mSeconds}` : mSeconds;
1811                    return `${formattedMinutes}:${formattedSeconds}`;
1812                },
1813
1814                getQuestions: function (questions, percentage) {
1815                    const totalQuestions = questions.length;
1816
1817                    if (percentage >= 100) return questions;
1818
1819                    const num = Math.max(1, Math.round((percentage * totalQuestions) / 100));
1820
1821                    if (num >= totalQuestions) return questions;
1822
1823                    const indices = Array.from({ length: totalQuestions }, (_, i) => i);
1824                    $exeDevices.iDevice.gamification.helpers.shuffleAds(indices);
1825
1826                    const selectedIndices = indices.slice(0, num).sort((a, b) => a - b),
1827                        selectedQuestions = selectedIndices.map(index => questions[index]);
1828
1829                    return selectedQuestions;
1830                },
1831                removeTags: (str) => {
1832                    const wrapper = $("<div></div>").html(str);
1833                    return wrapper.text();
1834                },
1835
1836                generarID: function () {
1837                    const fecha = new Date(),
1838                        a = fecha.getUTCFullYear(),
1839                        m = fecha.getUTCMonth() + 1,
1840                        d = fecha.getUTCDate(),
1841                        h = fecha.getUTCHours(),
1842                        min = fecha.getUTCMinutes(),
1843                        s = fecha.getUTCSeconds(),
1844                        o = fecha.getTimezoneOffset();
1845                    return `${a}${m}${d}${h}${min}${s}${o}`;
1846                },
1847
1848                hourToSeconds: function (str) {
1849                    let time = str.split(":");
1850                    if (time.length === 1) time = ["00", "00", time[0]];
1851                    if (time.length === 2) time = ["00", ...time];
1852                    return (+time[0] * 60 * 60) + (+time[1] * 60) + (+time[2]);
1853                },
1854                secondsToHour: function (totalSec) {
1855                    const time = Math.round(totalSec),
1856                        hours = String(Math.floor(time / 3600)).padStart(2, '0'),
1857                        minutes = String(Math.floor((time % 3600) / 60)).padStart(2, '0'),
1858                        seconds = String(time % 60).padStart(2, '0');
1859                    return `${hours}:${minutes}:${seconds}`;
1860                },
1861                arrayMove: function (arr, oldIndex, newIndex) {
1862                    if (newIndex >= arr.length) {
1863                        var k = newIndex - arr.length + 1;
1864                        while (k--) {
1865                            arr.push(undefined);
1866                        }
1867                    }
1868                    arr.splice(newIndex, 0, arr.splice(oldIndex, 1)[0]);
1869                },
1870
1871                showFullscreenImage: function (imageSrc, $container) {
1872                    const $overlay = $('<div>', {
1873                        class: 'Games-OverlayImage position-fixed top-0 start-0 end-0 bottom-0 d-flex align-items-center justify-content-center',
1874                        css: {
1875                            display: 'none',
1876                            'background-color': 'rgba(0, 0, 0, 0.8)',
1877                            'z-index': 2000
1878                        }
1879                    });
1880                    const $image = $('<img>', {
1881                        src: imageSrc,
1882                        class: 'Games-FullScreenImage mw-100 object-fit-contain',
1883                        css: {
1884                            'max-height': '100%',
1885                            'z-index': 2000
1886                        },
1887                        alt: 'Image'
1888                    });
1889
1890                    $overlay.append($image);
1891
1892                    const $ctn = $exeDevices.iDevice.gamification.helpers.isFullscreen()
1893                        ? $container
1894                        : $('body');
1895
1896                    $ctn.append($overlay);
1897
1898                    $overlay.fadeIn(300);
1899
1900                    $overlay.on('click', function () {
1901                        $overlay.fadeOut(300, function () {
1902                            $overlay.remove();
1903                        });
1904                    });
1905                },
1906
1907                isFullscreen: function () {
1908                    const fullscreenSupported = !!(
1909                        document.fullscreenEnabled ||
1910                        document.webkitFullscreenEnabled ||
1911                        document.mozFullScreenEnabled ||
1912                        document.msFullscreenEnabled
1913                    );
1914
1915                    if (!fullscreenSupported) return false;
1916                    const isFullscreenActive = !!(
1917                        document.fullscreenElement ||
1918                        document.webkitFullscreenElement ||
1919                        document.mozFullScreenElement ||
1920                        document.msFullscreenElement
1921                    );
1922
1923                    return isFullscreenActive;
1924                }
1925
1926            },
1927            observers: {
1928                debounce: function (func, wait) {
1929                    let timeout;
1930                    return function (...args) {
1931                        const later = () => {
1932                            clearTimeout(timeout);
1933                            func(...args);
1934                        };
1935                        clearTimeout(timeout);
1936                        timeout = setTimeout(later, wait);
1937                    };
1938                },
1939
1940                observeMutations: function ($idevice, element) {
1941                    if (!element) return;
1942
1943                    if (!$idevice.observers) $idevice.observers = new Map();
1944
1945                    if ($idevice.observers.has(element)) return $idevice.observers.get(element);
1946
1947                    const observer = new MutationObserver((mutations) => {
1948                        mutations.forEach((mutation) => {
1949                            const mode = element.getAttribute('mode');
1950                            if (
1951                                (mutation.attributeName === 'mode' && mode === 'edition') ||
1952                                (mutation.attributeName === 'node-selected' && mode === 'view')
1953                            ) {
1954                                $exeDevices.iDevice.gamification.observers.observersDisconnect($idevice);
1955                            }
1956                        });
1957                    });
1958
1959                    observer.observe(element, {
1960                        attributes: true,
1961                        attributeFilter: ['mode', 'node-selected'],
1962                    });
1963
1964                    $idevice.observers.set(element, observer);
1965
1966                    return observer;
1967                },
1968
1969                observeResize: function ($idevice, element) {
1970                    if (!element) return;
1971
1972                    if (!$idevice.observersresize) $idevice.observersresize = new Map();
1973                    if ($idevice.observersresize.has(element)) return $idevice.observersresize.get(element);
1974                    const resizeObserver = new ResizeObserver($exeDevices.iDevice.gamification.observers.debounce(resizeEntries => {
1975                        const isMap = $idevice.options instanceof Map;
1976                        const isArray = Array.isArray($idevice.options)
1977                        const optionEntries = isMap
1978                            ? $idevice.options.entries()
1979                            : isArray
1980                                ? $idevice.options.entries()
1981                                : [];
1982                        for (let [keyOrIndex, option] of optionEntries) {
1983                            if (typeof $idevice.refreshImageActive === "function") {
1984                                $idevice.refreshImageActive(keyOrIndex);
1985                            }
1986                            if (typeof $idevice.refreshGame === "function") {
1987                                $idevice.refreshGame(keyOrIndex);
1988                            }
1989                        }
1990                    }, 100));
1991
1992                    resizeObserver.observe(element);
1993                    $idevice.observersresize.set(element, resizeObserver);
1994                },
1995
1996                observersDisconnect: function ($idevice) {
1997                    if (!$idevice) return;
1998
1999                    const isMap = $idevice.options instanceof Map;
2000                    const isArray = Array.isArray($idevice.options);
2001                    const optionEntries = isMap
2002                        ? $idevice.options.entries()
2003                        : isArray
2004                            ? $idevice.options.entries()
2005                            : [];
2006
2007                    for (let [keyOrIndex, option] of optionEntries) {
2008                        if (option && option.gameStarted) {
2009                            if (typeof $idevice.stopSound === "function") {
2010                                $idevice.stopSound(option);
2011                            }
2012                        }
2013                        if (option && option.counterClock) {
2014                            clearInterval(option.counterClock);
2015                            option.counterClock = null;
2016                        }
2017                    }
2018
2019                    if ($idevice.observers) {
2020                        $idevice.observers.forEach((observer) => {
2021                            observer.disconnect();
2022                        });
2023                        $idevice.observers.clear();
2024                    }
2025
2026                    if ($idevice.observersresize) {
2027                        $idevice.observersresize.forEach((observer) => {
2028                            observer.disconnect();
2029                        });
2030                        $idevice.observersresize.clear();
2031                    }
2032                },
2033            },
2034        },
2035
2036    }
2037}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.