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.