1/*jshint multistr: true */ 2// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps? 3 4/** @namespace */ 5var H5P = window.H5P = window.H5P || {}; 6 7/** 8 * Tells us if we're inside of an iframe. 9 * @member {boolean} 10 */ 11H5P.isFramed = (window.self !== window.parent); 12 13/** 14 * jQuery instance of current window. 15 * @member {H5P.jQuery} 16 */ 17H5P.$window = H5P.jQuery(window); 18 19/** 20 * List over H5P instances on the current page. 21 * @member {Array} 22 */ 23H5P.instances = []; 24 25// Detect if we support fullscreen, and what prefix to use. 26if (document.documentElement.requestFullscreen) { 27 /** 28 * Browser prefix to use when entering fullscreen mode. 29 * undefined means no fullscreen support. 30 * @member {string} 31 */ 32 H5P.fullScreenBrowserPrefix = ''; 33} 34else if (document.documentElement.webkitRequestFullScreen) { 35 H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i); 36 H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1])); 37 38 // Do not allow fullscreen for safari < 7. 39 if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) { 40 H5P.fullScreenBrowserPrefix = 'webkit'; 41 } 42} 43else if (document.documentElement.mozRequestFullScreen) { 44 H5P.fullScreenBrowserPrefix = 'moz'; 45} 46else if (document.documentElement.msRequestFullscreen) { 47 H5P.fullScreenBrowserPrefix = 'ms'; 48} 49 50/** 51 * Keep track of when the H5Ps where started. 52 * 53 * @type {Object[]} 54 */ 55H5P.opened = {}; 56 57/** 58 * Initialize H5P content. 59 * Scans for ".h5p-content" in the document and initializes H5P instances where found. 60 * 61 * @param {Object} target DOM Element 62 */ 63H5P.init = function (target) { 64 // Useful jQuery object. 65 if (H5P.$body === undefined) { 66 H5P.$body = H5P.jQuery(document.body); 67 } 68 69 // Determine if we can use full screen 70 if (H5P.fullscreenSupported === undefined) { 71 /** 72 * Use this variable to check if fullscreen is supported. Fullscreen can be 73 * restricted when embedding since not all browsers support the native 74 * fullscreen, and the semi-fullscreen solution doesn't work when embedded. 75 * @type {boolean} 76 */ 77 H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled)); 78 // -We should consider document.msFullscreenEnabled when they get their 79 // -element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe 80 // Update: Seems to be no need as they've moved on to Webkit 81 } 82 83 // Deprecated variable, kept to maintain backwards compatability 84 if (H5P.canHasFullScreen === undefined) { 85 /** 86 * @deprecated since version 1.11 87 * @type {boolean} 88 */ 89 H5P.canHasFullScreen = H5P.fullscreenSupported; 90 } 91 92 // H5Ps added in normal DIV. 93 H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () { 94 var $element = H5P.jQuery(this).addClass('h5p-initialized'); 95 var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element); 96 var contentId = $element.data('content-id'); 97 var contentData = H5PIntegration.contents['cid-' + contentId]; 98 if (contentData === undefined) { 99 return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?'); 100 } 101 var library = { 102 library: contentData.library, 103 params: JSON.parse(contentData.jsonContent), 104 metadata: contentData.metadata 105 }; 106 107 // Apply theme density 108 let density = H5PIntegration.theme?.density ?? 'large'; 109 $element.addClass('h5p-' + density); 110 111 H5P.getUserData(contentId, 'state', function (err, previousState) { 112 if (previousState) { 113 library.userDatas = { 114 state: previousState 115 }; 116 } 117 else if (previousState === null && H5PIntegration.saveFreq) { 118 // Content has been reset. Display dialog. 119 delete contentData.contentUserData; 120 var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', '<p>' + H5P.t('contentChanged') + '</p><p>' + H5P.t('startingOver') + '</p><div class="h5p-dialog-ok-button" tabIndex="0" role="button">OK</div>', $container); 121 H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { 122 123 var closeDialog = function (event) { 124 if (event.type === 'click' || event.which === 32) { 125 dialog.close(); 126 H5P.deleteUserData(contentId, 'state', 0); 127 } 128 }; 129 130 $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog); 131 H5P.trigger(instance, 'resize'); 132 }).on('dialog-closed', function () { 133 H5P.trigger(instance, 'resize'); 134 }); 135 dialog.open(); 136 } 137 // If previousState is false we don't have a previous state 138 }); 139 140 // Create new instance. 141 var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true}); 142 143 H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance}); 144 145 // Check if we should add and display a fullscreen button for this H5P. 146 if (contentData.fullScreen == 1 && H5P.fullscreenSupported) { 147 H5P.jQuery(
148 '<div class="h5p-content-controls">' + 149 '<div role="button" ' + 150 'tabindex="0" ' + 151 'class="h5p-enable-fullscreen" ' + 152 'aria-label="' + H5P.t('fullscreen') + '" ' + 153 'title="' + H5P.t('fullscreen') + '">' + 154 '</div>' + 155 '</div>') 156 .prependTo($container) 157 .children() 158 .click(function () { 159 H5P.fullScreen($container, instance); 160 }) 161 .keydown(function (e) { 162 if (e.which === 32 || e.which === 13) { 163 H5P.fullScreen($container, instance); 164 return false; 165 } 166 }) 167 ; 168 } 169 170 /** 171 * Create action bar 172 */ 173 var displayOptions = contentData.displayOptions; 174 var displayFrame = false; 175 if (displayOptions.frame) { 176 // Special handling of copyrights 177 if (displayOptions.copyright) { 178 var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata); 179 if (!copyrights) { 180 displayOptions.copyright = false; 181 } 182 } 183 184 // Create action bar 185 var actionBar = new H5P.ActionBar(displayOptions); 186 var $actions = actionBar.getDOMElement(); 187 188 actionBar.on('reuse', function () { 189 H5P.openReuseDialog($actions, contentData, { 190 library: contentData.library, 191 params: JSON.parse(contentData.jsonContent), 192 metadata: contentData.metadata 193 }, instance, contentId); 194 instance.triggerXAPI('accessed-reuse'); 195 }); 196 actionBar.on('copyrights', function () { 197 var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container, $actions.find('.h5p-copyrights')[0]); 198 dialog.open(true); 199 instance.triggerXAPI('accessed-copyright'); 200 }); 201 actionBar.on('embed', function () { 202 H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, { 203 width: $element.width(), 204 height: $element.height() 205 }, instance); 206 instance.triggerXAPI('accessed-embed'); 207 }); 208 209 if (actionBar.hasActions()) { 210 displayFrame = true; 211 $actions.insertAfter($container); 212 } 213 } 214 215 $element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame'); 216 217 // Keep track of when we started 218 H5P.opened[contentId] = new Date(); 219 220 // Handle events when the user finishes the content. Useful for logging exercise results. 221 H5P.on(instance, 'finish', function (event) { 222 if (event.data !== undefined) { 223 H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time); 224 } 225 }); 226 227 // Listen for xAPI events. 228 H5P.on(instance, 'xAPI', H5P.xAPICompletedListener); 229 230 // Auto save current state if supported 231 if (H5PIntegration.saveFreq !== false && ( 232 instance.getCurrentState instanceof Function || 233 typeof instance.getCurrentState === 'function')) { 234 235 var saveTimer, save = function () { 236 var state = instance.getCurrentState(); 237 if (state !== undefined) { 238 H5P.setUserData(contentId, 'state', state, {deleteOnChange: true}); 239 } 240 if (H5PIntegration.saveFreq) { 241 // Continue autosave 242 saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); 243 } 244 }; 245 246 if (H5PIntegration.saveFreq) { 247 // Start autosave 248 saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000); 249 } 250 251 // xAPI events will schedule a save in three seconds. 252 H5P.on(instance, 'xAPI', function (event) { 253 var verb = event.getVerb(); 254 if (verb === 'completed' || verb === 'progressed') { 255 clearTimeout(saveTimer); 256 saveTimer = setTimeout(save, 3000); 257 } 258 }); 259 } 260 261 if (H5P.isFramed) { 262 var resizeDelay; 263 if (H5P.externalEmbed === false) { 264 // Internal embed 265 // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars. 266 var iframe = window.frameElement; 267 var resizeIframe = function () { 268 if (window.parent.H5P.isFullscreen) { 269 return; // Skip if full screen. 270 } 271 272 // Retain parent size to avoid jumping/scrolling 273 var parentHeight = iframe.parentElement.style.height; 274 iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px'; 275 276 // Note: Force layout reflow 277 // This fixes a flickering bug for embedded content on iPads 278 // @see https://github.com/h5p/h5p-moodle-plugin/issues/237 279 iframe.getBoundingClientRect(); 280 281 // Reset iframe height, in case content has shrinked. 282 iframe.style.height = '1px'; 283 284 // Resize iframe so all content is visible. 285 iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px'; 286 287 // Free parent 288 iframe.parentElement.style.height = parentHeight; 289 }; 290 291 H5P.on(instance, 'resize', function () { 292 // Use a delay to make sure iframe is resized to the correct size. 293 clearTimeout(resizeDelay); 294 resizeDelay = setTimeout(function () { 295 resizeIframe(); 296 }, 1); 297 }); 298 } 299 else if (H5P.communicator) { 300 // External embed 301 var parentIsFriendly = false;
302 303 // Handle that the resizer is loaded after the iframe 304 H5P.communicator.on('ready', function () { 305 H5P.communicator.send('hello'); 306 }); 307 308 // Handle hello message from our parent window 309 H5P.communicator.on('hello', function () { 310 // Initial setup/handshake is done 311 parentIsFriendly = true; 312 313 // Make iframe responsive 314 document.body.style.height = 'auto'; 315 316 // Hide scrollbars for correct size 317 document.body.style.overflow = 'hidden'; 318 319 // Content need to be resized to fit the new iframe size 320 H5P.trigger(instance, 'resize'); 321 }); 322 323 // When resize has been prepared tell parent window to resize 324 H5P.communicator.on('resizePrepared', function () { 325 H5P.communicator.send('resize', { 326 scrollHeight: document.body.scrollHeight 327 }); 328 }); 329 330 H5P.communicator.on('resize', function () { 331 H5P.trigger(instance, 'resize'); 332 }); 333 334 H5P.on(instance, 'resize', function () { 335 if (H5P.isFullscreen) { 336 return; // Skip iframe resize 337 } 338 339 // Use a delay to make sure iframe is resized to the correct size. 340 clearTimeout(resizeDelay); 341 resizeDelay = setTimeout(function () { 342 // Only resize if the iframe can be resized 343 if (parentIsFriendly) { 344 H5P.communicator.send('prepareResize', { 345 scrollHeight: document.body.scrollHeight, 346 clientHeight: document.body.clientHeight 347 }); 348 } 349 else { 350 H5P.communicator.send('hello'); 351 } 352 }, 0); 353 }); 354 } 355 } 356 357 if (!H5P.isFramed || H5P.externalEmbed === false) { 358 // Resize everything when window is resized. 359 H5P.jQuery(window.parent).resize(function () { 360 H5P.trigger(instance, 'resize'); 361 }); 362 } 363 364 H5P.instances.push(instance); 365 366 // Resize content. 367 H5P.trigger(instance, 'resize'); 368 369 // Logic for hiding focus effects when using mouse 370 $element.addClass('using-mouse'); 371 $element.on('mousedown keydown keyup', function (event) { 372 $element.toggleClass('using-mouse', event.type === 'mousedown'); 373 }); 374 375 if (H5P.externalDispatcher) { 376 H5P.externalDispatcher.trigger('initialized'); 377 } 378 }); 379 380 // Insert H5Ps that should be in iframes. 381 H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () { 382 const iframe = this; 383 const $iframe = H5P.jQuery(iframe); 384 385 const contentId = $iframe.data('content-id'); 386 const contentData = H5PIntegration.contents['cid-' + contentId]; 387 const contentLanguage = contentData && contentData.metadata && contentData.metadata.defaultLanguage 388 ? contentData.metadata.defaultLanguage : 'en'; 389 390 const writeDocument = function () { 391 iframe.contentDocument.open();
392 iframe.contentDocument.write('<!doctype html><html class="h5p-iframe" lang="' + contentLanguage + '"><head>' + H5P.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>'); 393 iframe.contentDocument.close(); 394 }; 395 396 $iframe.addClass('h5p-initialized') 397 if (iframe.contentDocument === null) { 398 // In some Edge cases the iframe isn't always loaded when the page is ready. 399 $iframe.on('load', writeDocument); 400 $iframe.attr('src', 'about:blank'); 401 } 402 else { 403 writeDocument(); 404 } 405 }); 406}; 407 408/** 409 * Loop through assets for iframe content and create a set of tags for head. 410 * 411 * @private 412 * @param {number} contentId 413 * @returns {string} HTML 414 */ 415H5P.getHeadTags = function (contentId) { 416 var createStyleTags = function (styles) { 417 var tags = ''; 418 for (var i = 0; i < styles.length; i++) { 419 tags += '<link rel="stylesheet" href="' + styles[i] + '">'; 420 } 421 return tags; 422 }; 423 424 var createScriptTags = function (scripts) { 425 var tags = ''; 426 for (var i = 0; i < scripts.length; i++) { 427 tags += '<script src="' + scripts[i] + '"></script>'; 428 } 429 return tags; 430 }; 431 432 return '<base target="_parent">' + 433 createStyleTags(H5PIntegration.core.styles) + 434 createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) + 435 createScriptTags(H5PIntegration.core.scripts) + 436 createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) + 437 '<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>'; 438}; 439 440/** 441 * When embedded the communicator helps talk to the parent page. 442 * 443 * @type {Communicator} 444 */ 445H5P.communicator = (function () { 446 /** 447 * @class 448 * @private 449 */ 450 function Communicator() { 451 var self = this; 452 453 // Maps actions to functions 454 var actionHandlers = {}; 455 456 // Register message listener 457 window.addEventListener('message', function receiveMessage(event) { 458 if (window.parent !== event.source || event.data.context !== 'h5p') { 459 return; // Only handle messages from parent and in the correct context 460 } 461 462 if (actionHandlers[event.data.action] !== undefined) { 463 actionHandlers[event.data.action](event.data); 464 } 465 } , false); 466 467 468 /** 469 * Register action listener. 470 * 471 * @param {string} action What you are waiting for 472 * @param {function} handler What you want done 473 */ 474 self.on = function (action, handler) { 475 actionHandlers[action] = handler; 476 }; 477 478 /** 479 * Send a message to the all mighty father. 480 * 481 * @param {string} action 482 * @param {Object} [data] payload 483 */ 484 self.send = function (action, data) { 485 if (data === undefined) { 486 data = {}; 487 } 488 data.context = 'h5p'; 489 data.action = action; 490 491 // Parent origin can be anything 492 window.parent.postMessage(data, '*'); 493 }; 494 } 495 496 return (window.postMessage && window.addEventListener ? new Communicator() : undefined); 497})(); 498 499/** 500 * Enter semi fullscreen for the given H5P instance 501 * 502 * @param {H5P.jQuery} $element Content container. 503 * @param {Object} instance 504 * @param {function} exitCallback Callback function called when user exits fullscreen. 505 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe. 506 */ 507H5P.semiFullScreen = function ($element, instance, exitCallback, body) { 508 H5P.fullScreen($element, instance, exitCallback, body, true); 509}; 510 511/** 512 * Enter fullscreen for the given H5P instance. 513 * 514 * @param {H5P.jQuery} $element Content container. 515 * @param {Object} instance 516 * @param {function} exitCallback Callback function called when user exits fullscreen. 517 * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe. 518 * @param {Boolean} forceSemiFullScreen 519 */ 520H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) { 521 if (H5P.exitFullScreen !== undefined) { 522 return; // Cannot enter new fullscreen until previous is over 523 } 524 525 if (H5P.isFramed && H5P.externalEmbed === false) { 526 // Trigger resize on wrapper in parent window. 527 window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen); 528 H5P.isFullscreen = true; 529 H5P.exitFullScreen = function () { 530 window.parent.H5P.exitFullScreen(); 531 }; 532 H5P.on(instance, 'exitFullScreen', function () { 533 H5P.isFullscreen = false;
534 H5P.exitFullScreen = undefined; 535 }); 536 return; 537 } 538 539 var $container = $element; 540 var $classes, $iframe, $body; 541 if (body === undefined) { 542 $body = H5P.$body; 543 } 544 else { 545 // We're called from an iframe. 546 $body = H5P.jQuery(body); 547 $classes = $body.add($element.get()); 548 var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id'); 549 $iframe = H5P.jQuery(iframeSelector); 550 $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container. 551 } 552 553 $classes = $element.add(H5P.$body).add($classes); 554 555 /** 556 * Prepare for resize by setting the correct styles. 557 * 558 * @private 559 * @param {string} classes CSS 560 */ 561 var before = function (classes) { 562 $classes.addClass(classes); 563 564 if ($iframe !== undefined) { 565 // Set iframe to its default size(100%). 566 $iframe.css('height', ''); 567 } 568 }; 569 570 /** 571 * Gets called when fullscreen mode has been entered. 572 * Resizes and sets focus on content. 573 * 574 * @private 575 */ 576 var entered = function () { 577 // Do not rely on window resize events. 578 H5P.trigger(instance, 'resize'); 579 H5P.trigger(instance, 'focus'); 580 H5P.trigger(instance, 'enterFullScreen'); 581 }; 582 583 /** 584 * Gets called when fullscreen mode has been exited. 585 * Resizes and sets focus on content. 586 * 587 * @private 588 * @param {string} classes CSS 589 */ 590 var done = function (classes) { 591 H5P.isFullscreen = false; 592 $classes.removeClass(classes); 593 594 // Do not rely on window resize events. 595 H5P.trigger(instance, 'resize'); 596 H5P.trigger(instance, 'focus'); 597 598 H5P.exitFullScreen = undefined; 599 if (exitCallback !== undefined) { 600 exitCallback(); 601 } 602 603 H5P.trigger(instance, 'exitFullScreen'); 604 }; 605 606 H5P.isFullscreen = true; 607 if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) { 608 // Create semi fullscreen. 609 610 if (H5P.isFramed) { 611 return; // TODO: Should we support semi-fullscreen for IE9 & 10 ? 612 } 613 614 before('h5p-semi-fullscreen'); 615 var $disable = H5P.jQuery('<div role="button" tabindex="0" class="h5p-disable-fullscreen" title="' + H5P.t('disableFullscreen') + '" aria-label="' + H5P.t('disableFullscreen') + '"></div>').appendTo($container.find('.h5p-content-controls')); 616 var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () { 617 if (prevViewportContent) { 618 // Use content from the previous viewport tag 619 h5pViewport.content = prevViewportContent; 620 } 621 else { 622 // Remove viewport tag 623 head.removeChild(h5pViewport); 624 } 625 $disable.remove(); 626 $body.unbind('keyup', keyup); 627 done('h5p-semi-fullscreen'); 628 }; 629 keyup = function (event) { 630 if (event.keyCode === 27) { 631 disableSemiFullscreen(); 632 } 633 }; 634 $disable.click(disableSemiFullscreen); 635 $body.keyup(keyup); 636 637 // Disable zoom 638 var prevViewportContent, h5pViewport; 639 var metaTags = document.getElementsByTagName('meta'); 640 for (var i = 0; i < metaTags.length; i++) { 641 if (metaTags[i].name === 'viewport') { 642 // Use the existing viewport tag 643 h5pViewport = metaTags[i]; 644 prevViewportContent = h5pViewport.content; 645 break; 646 } 647 } 648 if (!prevViewportContent) { 649 // Create a new viewport tag 650 h5pViewport = document.createElement('meta'); 651 h5pViewport.name = 'viewport'; 652 } 653 h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'; 654 if (!prevViewportContent) { 655 // Insert the new viewport tag 656 var head = document.getElementsByTagName('head')[0]; 657 head.appendChild(h5pViewport); 658 } 659 660 entered(); 661 } 662 else { 663 // Create real fullscreen. 664 665 before('h5p-fullscreen'); 666 var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange'); 667 document.addEventListener(eventName, function fullscreenCallback() { 668 if (first === undefined) { 669 // We are entering fullscreen mode 670 first = false; 671 entered(); 672 return; 673 } 674 675 // We are exiting fullscreen 676 done('h5p-fullscreen'); 677 document.removeEventListener(eventName, fullscreenCallback, false); 678 }); 679 680 if (H5P.fullScreenBrowserPrefix === '') { 681 $element[0].requestFullscreen(); 682 } 683 else { 684 var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen'); 685 var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined); 686 $element[0][method](params); 687 } 688 689 // Allows everone to exit 690 H5P.exitFullScreen = function () { 691 if (H5P.fullScreenBrowserPrefix === '') { 692 document.exitFullscreen(); 693 } 694 else if (H5P.fullScreenBrowserPrefix === 'moz') { 695 document.mozCancelFullScreen(); 696 } 697 else { 698 document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen'](); 699 } 700 }; 701 } 702}; 703 704(function () { 705 /** 706 * Helper for adding a query parameter to an existing path that may already 707 * contain one or a hash. 708 * 709 * @param {string} path 710 * @param {string} parameter 711 * @return {string} 712 */ 713 H5P.addQueryParameter = function (path, parameter) { 714 let newPath, secondSplit; 715 const firstSplit = path.split('?'); 716 if (firstSplit[1]) { 717 // There is already an existing query 718 secondSplit = firstSplit[1].split('#'); 719 newPath = firstSplit[0] + '?' + secondSplit[0] + '&'; 720 } 721 else { 722 // No existing query, just need to take care of the hash 723 secondSplit = firstSplit[0].split('#'); 724 newPath = secondSplit[0] + '?'; 725 } 726 newPath += parameter; 727 if (secondSplit[1]) { 728 // Add back the hash 729 newPath += '#' + secondSplit[1]; 730 } 731 return newPath; 732 }; 733 734 /** 735 * Helper for setting the crossOrigin attribute + the complete correct source. 736 * Note: This will start loading the resource. 737 * 738 * @param {Element} element DOM element, typically img, video or audio 739 * @param {Object} source File object from parameters/json_content (created by H5PEditor) 740 * @param {number} contentId Needed to determine the complete correct file path 741 */ 742 H5P.setSource = function (element, source, contentId) { 743 let path = source.path; 744 745 const crossOrigin = H5P.getCrossOrigin(source); 746 if (crossOrigin) { 747 element.crossOrigin = crossOrigin; 748 749 if (H5PIntegration.crossoriginCacheBuster) { 750 // Some sites may want to add a cache buster in case the same resource 751 // is used elsewhere without the crossOrigin attribute
752 path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster); 753 } 754 } 755 else { 756 // In case this element has been used before. 757 element.removeAttribute('crossorigin'); 758 } 759 760 element.src = H5P.getPath(path, contentId); 761 }; 762 763 /** 764 * Check if the given path has a protocol. 765 * 766 * @private 767 * @param {string} path 768 * @return {string} 769 */ 770 var hasProtocol = function (path) { 771 return path.match(/^[a-z0-9]+:\/\//i); 772 }; 773 774 /** 775 * Get the crossOrigin policy to use for img, video and audio tags on the current site. 776 * 777 * @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage) 778 * @returns {string|null} crossOrigin attribute value required by the source 779 */ 780 H5P.getCrossOrigin = function (source) { 781 if (typeof source !== 'object') { 782 // Deprecated usage. 783 return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null; 784 } 785 786 if (H5PIntegration.crossorigin && !hasProtocol(source.path)) { 787 // This is a local file, use the local crossOrigin policy. 788 return H5PIntegration.crossorigin; 789 // Note: We cannot use this for all external sources since we do not know 790 // each server's individual policy. We could add support for a list of 791 // external sources and their policy later on. 792 } 793 }; 794 795 /** 796 * Find the path to the content files based on the id of the content. 797 * Also identifies and returns absolute paths. 798 * 799 * @param {string} path 800 * Relative to content folder or absolute. 801 * @param {number} contentId 802 * ID of the content requesting the path. 803 * @returns {string} 804 * Complete URL to path. 805 */ 806 H5P.getPath = function (path, contentId) { 807 if (hasProtocol(path)) { 808 return path; 809 } 810 811 var prefix; 812 var isTmpFile = (path.substr(-4,4) === '#tmp'); 813 if (contentId !== undefined && !isTmpFile) { 814 // Check for custom override URL 815 if (H5PIntegration.contents !== undefined && 816 H5PIntegration.contents['cid-' + contentId]) { 817 prefix = H5PIntegration.contents['cid-' + contentId].contentUrl; 818 } 819 if (!prefix) { 820 prefix = H5PIntegration.url + '/content/' + contentId; 821 } 822 } 823 else if (window.H5PEditor !== undefined) { 824 prefix = H5PEditor.filesPath; 825 } 826 else { 827 return; 828 } 829 830 if (!hasProtocol(prefix)) { 831 // Use absolute urls 832 prefix = window.location.protocol + "//" + window.location.host + prefix; 833 } 834 835 return prefix + '/' + path; 836 }; 837})(); 838 839/** 840 * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD 841 * Will be remove march 2016. 842 * 843 * Find the path to the content files folder based on the id of the content 844 * 845 * @deprecated 846 * Will be removed march 2016. 847 * @param contentId 848 * Id of the content requesting the path 849 * @returns {string} 850 * URL 851 */ 852H5P.getContentPath = function (contentId) { 853 return H5PIntegration.url + '/content/' + contentId; 854}; 855 856/** 857 * Get library class constructor from H5P by classname. 858 * Note that this class will only work for resolve "H5P.NameWithoutDot". 859 * Also check out {@link H5P.newRunnable} 860 * 861 * Used from libraries to construct instances of other libraries' objects by name. 862 * 863 * @param {string} name Name of library 864 * @returns {Object} Class constructor 865 */ 866H5P.classFromName = function (name) { 867 var arr = name.split("."); 868 return this[arr[arr.length - 1]]; 869}; 870 871/** 872 * A safe way of creating a new instance of a runnable H5P. 873 * 874 * @param {Object} library 875 * Library/action object form params. 876 * @param {number} contentId 877 * Identifies the content. 878 * @param {H5P.jQuery} [$attachTo] 879 * Element to attach the instance to. 880 * @param {boolean} [skipResize] 881 * Skip triggering of the resize event after attaching. 882 * @param {Object} [extras] 883 * Extra parameters for the H5P content constructor 884 * @returns {Object} 885 * Instance. 886 */ 887H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) { 888 var nameSplit, versionSplit, machineName; 889 try { 890 nameSplit = library.library.split(' ', 2); 891 machineName = nameSplit[0]; 892 versionSplit = nameSplit[1].split('.', 2); 893 }
894 catch (err) { 895 return H5P.error('Invalid library string: ' + library.library); 896 } 897 898 if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) { 899 H5P.error('Invalid library params for: ' + library.library); 900 return H5P.error(library.params); 901 } 902 903 // Find constructor function 904 var constructor; 905 try { 906 nameSplit = nameSplit[0].split('.'); 907 constructor = window; 908 for (var i = 0; i < nameSplit.length; i++) { 909 constructor = constructor[nameSplit[i]]; 910 } 911 if (typeof constructor !== 'function') { 912 throw null; 913 } 914 } 915 catch (err) { 916 return H5P.error('Unable to find constructor for: ' + library.library); 917 } 918 919 if (extras === undefined) { 920 extras = {}; 921 } 922 if (library.subContentId) { 923 extras.subContentId = library.subContentId; 924 } 925 926 if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) { 927 extras.previousState = library.userDatas.state; 928 } 929 930 if (library.metadata) { 931 extras.metadata = library.metadata; 932 } 933 934 // Makes all H5P libraries extend H5P.ContentType: 935 var standalone = extras.standalone || false; 936 // This order makes it possible for an H5P library to override H5P.ContentType functions! 937 constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype); 938 939 var instance; 940 // Some old library versions have their own custom third parameter. 941 // Make sure we don't send them the extras. 942 // (they will interpret it as something else) 943 if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) { 944 instance = new constructor(library.params, contentId); 945 } 946 else { 947 instance = new constructor(library.params, contentId, extras); 948 } 949 950 if (instance.$ === undefined) { 951 instance.$ = H5P.jQuery(instance); 952 } 953 954 if (instance.contentId === undefined) { 955 instance.contentId = contentId; 956 } 957 if (instance.subContentId === undefined && library.subContentId) { 958 instance.subContentId = library.subContentId; 959 } 960 if (instance.parent === undefined && extras && extras.parent) { 961 instance.parent = extras.parent; 962 } 963 if (instance.libraryInfo === undefined) { 964 instance.libraryInfo = { 965 versionedName: library.library, 966 versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1], 967 machineName: machineName, 968 majorVersion: versionSplit[0], 969 minorVersion: versionSplit[1] 970 }; 971 } 972 973 if ($attachTo !== undefined) { 974 $attachTo.toggleClass('h5p-standalone', standalone); 975 instance.attach($attachTo); 976 H5P.trigger(instance, 'domChanged', { 977 '$target': $attachTo, 978 'library': machineName, 979 'key': 'newLibrary' 980 }, {'bubbles': true, 'external': true}); 981 982 if (skipResize === undefined || !skipResize) { 983 // Resize content. 984 H5P.trigger(instance, 'resize'); 985 } 986 } 987 return instance; 988}; 989 990/** 991 * Used to print useful error messages. (to JavaScript error console) 992 * 993 * @param {*} err Error to print. 994 */ 995H5P.error = function (err) { 996 if (window.console !== undefined && console.error !== undefined) { 997 console.error(err.stack ? err.stack : err); 998 } 999}; 1000 1001/** 1002 * Translate text strings. 1003 * 1004 * @param {string} key 1005 * Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars. 1006 * @param {Object} [vars] 1007 * Data for placeholders. 1008 * @param {string} [ns] 1009 * Translation namespace. Defaults to H5P. 1010 * @returns {string} 1011 * Translated text 1012 */ 1013H5P.t = function (key, vars, ns) { 1014 if (ns === undefined) { 1015 ns = 'H5P'; 1016 } 1017 1018 if (H5PIntegration.l10n[ns] === undefined) { 1019 return '[Missing translation namespace "' + ns + '"]'; 1020 } 1021 1022 if (H5PIntegration.l10n[ns][key] === undefined) { 1023 return '[Missing translation "' + key + '" in "' + ns + '"]'; 1024 } 1025 1026 var translation = H5PIntegration.l10n[ns][key]; 1027 1028 if (vars !== undefined) { 1029 // Replace placeholder with variables. 1030 for (var placeholder in vars) { 1031 translation = translation.replace(placeholder, vars[placeholder]); 1032 } 1033 } 1034 1035 return translation; 1036}; 1037 1038/** 1039 * Creates a new popup dialog over the H5P content. 1040 * 1041 * @class 1042 * @param {string} name 1043 * Used for html class. 1044 * @param {string} title 1045 * Used for header. 1046 * @param {string} content 1047 * Displayed inside the dialog. 1048 * @param {H5P.jQuery} $element 1049 * Which DOM element the dialog should be inserted after. 1050 * @param {H5P.jQuery} $returnElement 1051 * Which DOM element the focus should be moved to on close 1052 */ 1053H5P.Dialog = function (name, title, content, $element, $returnElement) { 1054 /** @alias H5P.Dialog# */ 1055 var self = this; 1056 this.activeElement = document.activeElement; 1057 var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" aria-labelledby="' + name + '-dialog-header" aria-modal="true" role="dialog" tabindex="-1">\ 1058 <div class="h5p-inner">\ 1059 <h2 id="' + name + '-dialog-header">' + title + '</h2>\ 1060 <div class="h5p-scroll-content">' + content + '</div>\ 1061 <div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\ 1062 </div>\ 1063 </div>') 1064 .insertAfter($element) 1065 .click(function (e) { 1066 if (e && e.originalEvent && e.originalEvent.preventClosing) { 1067 return; 1068 } 1069 1070 self.close(); 1071 }) 1072 .children('.h5p-inner') 1073 .click(function (e) { 1074 e.originalEvent.preventClosing = true; 1075 }) 1076 .find('.h5p-close') 1077 .click(function () { 1078 self.close(); 1079 }) 1080 .keypress(function (e) { 1081 if (e.which === 13 || e.which === 32) { 1082 self.close(); 1083 return false;
1084 } 1085 }) 1086 .end() 1087 .find('a') 1088 .click(function (e) { 1089 e.stopPropagation(); 1090 }) 1091 .end() 1092 .end(); 1093 1094 /** 1095 * Opens the dialog. 1096 */ 1097 self.open = function (scrollbar) { 1098 if (scrollbar) { 1099 $dialog.css('height', '100%'); 1100 } 1101 setTimeout(function () { 1102 $dialog.addClass('h5p-open'); // Fade in 1103 // Triggering an event, in case something has to be done after dialog has been opened. 1104 H5P.jQuery(self).trigger('dialog-opened', [$dialog]); 1105 $dialog.focus(); 1106 }, 1); 1107 }; 1108 1109 /** 1110 * Closes the dialog. 1111 */ 1112 self.close = function () { 1113 $dialog.removeClass('h5p-open'); // Fade out 1114 setTimeout(function () { 1115 $dialog.remove(); 1116 H5P.jQuery(self).trigger('dialog-closed', [$dialog]); 1117 $element.attr('tabindex', '-1'); 1118 if ($returnElement) { 1119 $returnElement.focus(); 1120 } 1121 else if(self.activeElement) { 1122 self.activeElement.focus(); 1123 } 1124 else { 1125 $element.focus(); 1126 } 1127 }, 200); 1128 }; 1129}; 1130 1131/** 1132 * Gather copyright information for the given content. 1133 * 1134 * @param {Object} instance 1135 * H5P instance to get copyright information for. 1136 * @param {Object} parameters 1137 * Parameters of the content instance. 1138 * @param {number} contentId 1139 * Identifies the H5P content 1140 * @param {Object} metadata 1141 * Metadata of the content instance. 1142 * @returns {string} Copyright information. 1143 */ 1144H5P.getCopyrights = function (instance, parameters, contentId, metadata) { 1145 var copyrights; 1146 1147 if (instance.getCopyrights !== undefined) { 1148 try { 1149 // Use the instance's own copyright generator 1150 copyrights = instance.getCopyrights(); 1151 } 1152 catch (err) { 1153 // Failed, prevent crashing page. 1154 } 1155 } 1156 1157 if (copyrights === undefined) { 1158 // Create a generic flat copyright list 1159 copyrights = new H5P.ContentCopyrights(); 1160 H5P.findCopyrights(copyrights, parameters, contentId); 1161 } 1162 1163 var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName); 1164 if (metadataCopyrights !== undefined) { 1165 copyrights.addMediaInFront(metadataCopyrights); 1166 } 1167 1168 if (copyrights !== undefined) { 1169 // Convert to string 1170 copyrights = copyrights.toString(); 1171 } 1172 return copyrights; 1173}; 1174 1175/** 1176 * Gather a flat list of copyright information from the given parameters. 1177 * 1178 * @param {H5P.ContentCopyrights} info 1179 * Used to collect all information in. 1180 * @param {(Object|Array)} parameters 1181 * To search for file objects in. 1182 * @param {number} contentId 1183 * Used to insert thumbnails for images. 1184 * @param {Object} extras - Extras. 1185 * @param {object} extras.metadata - Metadata 1186 * @param {object} extras.machineName - Library name of some kind. 1187 * Metadata of the content instance. 1188 */ 1189H5P.findCopyrights = function (info, parameters, contentId, extras) { 1190 // If extras are 1191 if (extras) { 1192 extras.params = parameters; 1193 buildFromMetadata(extras, extras.machineName, contentId); 1194 } 1195 1196 var lastContentTypeName; 1197 // Cycle through parameters 1198 for (var field in parameters) { 1199 if (!parameters.hasOwnProperty(field)) { 1200 continue; // Do not check 1201 } 1202 1203 /** 1204 * @deprecated This hack should be removed after 2017-11-01 1205 * The code that was using this was removed by HFP-574 1206 * This note was seen on 2018-04-04, and consultation with 1207 * higher authorities lead to keeping the code for now ;-) 1208 */ 1209 if (field === 'overrideSettings') { 1210 console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used."); 1211 console.warn(parameters); 1212 continue; 1213 } 1214 1215 var value = parameters[field]; 1216 1217 if (value && value.library && typeof value.library === 'string') { 1218 lastContentTypeName = value.library.split(' ')[0]; 1219 } 1220 else if (value && value.library && typeof value.library === 'object') { 1221 lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName; 1222 } 1223 1224 if (value instanceof Array) { 1225 // Cycle through array 1226 H5P.findCopyrights(info, value, contentId); 1227 } 1228 else if (value instanceof Object) { 1229 buildFromMetadata(value, lastContentTypeName, contentId); 1230 1231 // Check if object is a file with copyrights (old core) 1232 if (value.copyright === undefined || 1233 value.copyright.license === undefined || 1234 value.path === undefined || 1235 value.mime === undefined) { 1236 1237 // Nope, cycle throught object 1238 H5P.findCopyrights(info, value, contentId); 1239 } 1240 else { 1241 // Found file, add copyrights 1242 var copyrights = new H5P.MediaCopyright(value.copyright); 1243 if (value.width !== undefined && value.height !== undefined) { 1244 copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height)); 1245 } 1246 info.addMedia(copyrights); 1247 } 1248 } 1249 } 1250 1251 function buildFromMetadata(data, name, contentId) { 1252 if (data.metadata) { 1253 const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name); 1254 if (metadataCopyrights !== undefined) { 1255 if (data.params && data.params.contentName === 'Image' && data.params.file) { 1256 const path = data.params.file.path; 1257 const width = data.params.file.width; 1258 const height = data.params.file.height; 1259 metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height, data.params.alt)); 1260 } 1261 info.addMedia(metadataCopyrights); 1262 } 1263 } 1264 } 1265}; 1266 1267H5P.buildMetadataCopyrights = function (metadata) { 1268 if (metadata && metadata.license !== undefined && metadata.license !== 'U') { 1269 var dataset = { 1270 contentType: metadata.contentType, 1271 title: metadata.title, 1272 author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) { 1273 return (author.role) ? author.name + ' (' + author.role + ')' : author.name; 1274 }).join(', ') : undefined, 1275 source: metadata.source, 1276 year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined, 1277 license: metadata.license, 1278 version: metadata.licenseVersion, 1279 licenseExtras: metadata.licenseExtras, 1280 changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) { 1281 return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : ''); 1282 }).join(' / ') : undefined 1283 }; 1284 1285 return new H5P.MediaCopyright(dataset); 1286 } 1287}; 1288 1289/** 1290 * Display a dialog containing the download button and copy button. 1291 * 1292 * @param {H5P.jQuery} $element 1293 * @param {Object} contentData 1294 * @param {Object} library 1295 * @param {Object} instance 1296 * @param {number} contentId 1297 */ 1298H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) { 1299 let html = ''; 1300 let buttonCount = 0; 1301 if (contentData.displayOptions.export) { 1302 html += '<button type="button" class="h5p-big-button h5p-download-button"><div class="h5p-button-title">Download as an .h5p file</div><div class="h5p-button-description">.h5p files may be uploaded to any web-site where H5P content may be created.</div></button>'; 1303 buttonCount += 1; 1304 } 1305 if (contentData.displayOptions.export && contentData.displayOptions.copy) { 1306 html += '<div class="h5p-horizontal-line-text"><span>or</span></div>'; 1307 } 1308 if (contentData.displayOptions.copy) { 1309 html += '<button type="button" class="h5p-big-button h5p-copy-button"><div class="h5p-button-title">Copy content</div><div class="h5p-button-description">Copied content may be pasted anywhere this content type is supported on this website.</div></button>'; 1310 buttonCount += 1; 1311 } 1312 1313 const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element); 1314 1315 // Selecting embed code when dialog is opened 1316 H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) { 1317 const scrollContent = $dialog[0].querySelector('.h5p-scroll-content'); 1318 scrollContent.style.setProperty('--button-count', buttonCount); 1319 H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').click(function (e) { 1320 e.stopPropagation(); 1321 }).appendTo($dialog.find('h2')); 1322 $dialog.find('.h5p-download-button').click(function () { 1323 window.location.href = contentData.exportUrl; 1324 instance.triggerXAPI('downloaded'); 1325 dialog.close(); 1326 }); 1327 $dialog.find('.h5p-copy-button').click(function () { 1328 const item = new H5P.ClipboardItem(library); 1329 item.contentId = contentId; 1330 H5P.setClipboard(item); 1331 instance.triggerXAPI('copied'); 1332 dialog.close(); 1333 H5P.attachToastTo( 1334 H5P.jQuery('.h5p-content:first')[0], 1335 H5P.t('contentCopied'), 1336 { 1337 position: { 1338 horizontal: 'centered', 1339 vertical: 'centered', 1340 noOverflowX: true 1341 } 1342 } 1343 ); 1344 }); 1345 H5P.trigger(instance, 'resize'); 1346 }).on('dialog-closed', function () { 1347 H5P.trigger(instance, 'resize'); 1348 }); 1349 1350 dialog.open(); 1351}; 1352 1353/** 1354 * Display a dialog containing the embed code. 1355 * 1356 * @param {H5P.jQuery} $element 1357 * Element to insert dialog after. 1358 * @param {string} embedCode 1359 * The embed code. 1360 * @param {string} resizeCode 1361 * The advanced resize code 1362 * @param {Object} size 1363 * The content's size. 1364 * @param {number} size.width 1365 * @param {number} size.height 1366 */ 1367H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) { 1368 var fullEmbedCode = embedCode + resizeCode;
1369 var dialog = new H5P.Dialog('embed', H5P.t('embed'), '<textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>' + H5P.t('size') + ': <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.width) + '" class="h5p-embed-size"/> × <input aria-label="'+ H5P.t('width') +'" type="text" value="' + Math.ceil(size.height) + '" class="h5p-embed-size"/> px<br/><div role="button" tabindex="0" class="h5p-expander">' + H5P.t('showAdvanced') + '</div><div class="h5p-expander-content"><p>' + H5P.t('advancedHelp') + '</p><textarea class="h5p-embed-code-container" autocorrect="off" autocapitalize="off" spellcheck="false">' + resizeCode + '</textarea></div>', $element); 1370 1371 // Selecting embed code when dialog is opened 1372 H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) { 1373 var $inner = $dialog.find('.h5p-inner'); 1374 var $scroll = $inner.find('.h5p-scroll-content'); 1375 var diff = $scroll.outerHeight() - $scroll.innerHeight(); 1376 var positionInner = function () { 1377 H5P.trigger(instance, 'resize'); 1378 }; 1379 1380 // Handle changing of width/height 1381 var $w = $dialog.find('.h5p-embed-size:eq(0)'); 1382 var $h = $dialog.find('.h5p-embed-size:eq(1)'); 1383 var getNum = function ($e, d) { 1384 var num = parseFloat($e.val()); 1385 if (isNaN(num)) { 1386 return d; 1387 } 1388 return Math.ceil(num); 1389 }; 1390 var updateEmbed = function () { 1391 $dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height))); 1392 }; 1393 1394 $w.change(updateEmbed); 1395 $h.change(updateEmbed); 1396 updateEmbed(); 1397 1398 // Select text and expand textareas 1399 $dialog.find('.h5p-embed-code-container').each(function () { 1400 H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () { 1401 H5P.jQuery(this).select(); 1402 }); 1403 }); 1404 $dialog.find('.h5p-embed-code-container').eq(0).select(); 1405 positionInner(); 1406 1407 // Expand advanced embed 1408 var expand = function () { 1409 var $expander = H5P.jQuery(this); 1410 var $content = $expander.next(); 1411 if ($content.is(':visible')) { 1412 $expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true'); 1413 $content.hide(); 1414 } 1415 else { 1416 $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false'); 1417 $content.show(); 1418 } 1419 $dialog.find('.h5p-embed-code-container').each(function () { 1420 H5P.jQuery(this).css('height', this.scrollHeight + 'px'); 1421 }); 1422 positionInner(); 1423 }; 1424 $dialog.find('.h5p-expander').click(expand).keypress(function (event) { 1425 if (event.keyCode === 32) { 1426 expand.apply(this); 1427 return false; 1428 } 1429 }); 1430 }).on('dialog-closed', function () { 1431 H5P.trigger(instance, 'resize'); 1432 }); 1433 1434 dialog.open(); 1435}; 1436 1437/** 1438 * Show a toast message. 1439 * 1440 * The reference element could be dom elements the toast should be attached to, 1441 * or e.g. the document body for general toast messages. 1442 * 1443 * @param {DOM} element Reference element to show toast message for. 1444 * @param {string} message Message to show. 1445 * @param {object} [config] Configuration. 1446 * @param {string} [config.style=h5p-toast] Style name for the tooltip. 1447 * @param {number} [config.duration=3000] Toast message length in ms. 1448 * @param {object} [config.position] Relative positioning of the toast. 1449 * @param {string} [config.position.horizontal=centered] [before|left|centered|right|after]. 1450 * @param {string} [config.position.vertical=below] [above|top|centered|bottom|below]. 1451 * @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset. 1452 * @param {number} [config.position.offsetVertical=0] Extra vetical offset. 1453 * @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left. 1454 * @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right. 1455 * @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top. 1456 * @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom. 1457 * @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right. 1458 * @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom. 1459 * @param {object} [config.position.overflowReference=document.body] DOM reference for overflow. 1460 */ 1461H5P.attachToastTo = function (element, message, config) { 1462 if (element === undefined || message === undefined) { 1463 return; 1464 } 1465 1466 const eventPath = function (evt) { 1467 var path = (evt.composedPath && evt.composedPath()) || evt.path; 1468 var target = evt.target; 1469 1470 if (path != null) { 1471 // Safari doesn't include Window, but it should. 1472 return (path.indexOf(window) < 0) ? path.concat(window) : path; 1473 } 1474 1475 if (target === window) { 1476 return [window]; 1477 } 1478 1479 function getParents(node, memo) { 1480 memo = memo || []; 1481 var parentNode = node.parentNode; 1482 1483 if (!parentNode) { 1484 return memo; 1485 } 1486 else { 1487 return getParents(parentNode, memo.concat(parentNode)); 1488 } 1489 } 1490 1491 return [target].concat(getParents(target), window); 1492 }; 1493 1494 /** 1495 * Handle click while toast is showing. 1496 */ 1497 const clickHandler = function (event) { 1498 /* 1499 * A common use case will be to attach toasts to buttons that are clicked. 1500 * The click would remove the toast message instantly without this check. 1501 * Children of the clicked element are also ignored. 1502 */ 1503 var path = eventPath(event); 1504 if (path.indexOf(element) !== -1) { 1505 return; 1506 } 1507 clearTimeout(timer); 1508 removeToast(); 1509 }; 1510 1511 1512 1513 /** 1514 * Remove the toast message. 1515 */ 1516 const removeToast = function () { 1517 document.removeEventListener('click', clickHandler); 1518 if (toast.parentNode) { 1519 toast.parentNode.removeChild(toast); 1520 } 1521 }; 1522 1523 /** 1524 * Get absolute coordinates for the toast. 1525 * 1526 * @param {DOM} element Reference element to show toast message for. 1527 * @param {DOM} toast Toast element. 1528 * @param {object} [position={}] Relative positioning of the toast message. 1529 * @param {string} [position.horizontal=centered] [before|left|centered|right|after]. 1530 * @param {string} [position.vertical=below] [above|top|centered|bottom|below]. 1531 * @param {number} [position.offsetHorizontal=0] Extra horizontal offset. 1532 * @param {number} [position.offsetVertical=0] Extra vetical offset. 1533 * @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left. 1534 * @param {boolean} [position.noOverflowRight=false] True to prevent overflow right. 1535 * @param {boolean}
1535 [position.noOverflowTop=false] True to prevent overflow top. 1536 * @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom. 1537 * @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right. 1538 * @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom. 1539 * @return {object} 1540 */ 1541 const getToastCoordinates = function (element, toast, position) { 1542 position = position || {}; 1543 position.offsetHorizontal = position.offsetHorizontal || 0; 1544 position.offsetVertical = position.offsetVertical || 0; 1545 1546 const toastRect = toast.getBoundingClientRect(); 1547 const elementRect = element.getBoundingClientRect(); 1548 1549 let left = 0; 1550 let top = 0; 1551 1552 // Compute horizontal position 1553 switch (position.horizontal) { 1554 case 'before': 1555 left = elementRect.left - toastRect.width - position.offsetHorizontal; 1556 break; 1557 case 'after': 1558 left = elementRect.left + elementRect.width + position.offsetHorizontal; 1559 break; 1560 case 'left': 1561 left = elementRect.left + position.offsetHorizontal; 1562 break; 1563 case 'right': 1564 left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal; 1565 break; 1566 case 'centered': 1567 left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal; 1568 break; 1569 default: 1570 left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal; 1571 } 1572 1573 // Compute vertical position 1574 switch (position.vertical) { 1575 case 'above': 1576 top = elementRect.top - toastRect.height - position.offsetVertical; 1577 break; 1578 case 'below': 1579 top = elementRect.top + elementRect.height + position.offsetVertical; 1580 break; 1581 case 'top': 1582 top = elementRect.top + position.offsetVertical; 1583 break; 1584 case 'bottom': 1585 top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical; 1586 break; 1587 case 'centered': 1588 top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical; 1589 break; 1590 default: 1591 top = elementRect.top + elementRect.height + position.offsetVertical; 1592 } 1593 1594 // Prevent overflow 1595 const overflowElement = document.body; 1596 const bounds = overflowElement.getBoundingClientRect(); 1597 if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) { 1598 left = bounds.x; 1599 } 1600 if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) { 1601 left = bounds.x + bounds.width - toastRect.width; 1602 } 1603 if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) { 1604 top = bounds.y; 1605 } 1606 if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) { 1607 left = bounds.y + bounds.height - toastRect.height; 1608 } 1609 1610 return {left: left, top: top}; 1611 }; 1612 1613 // Sanitization 1614 config = config || {}; 1615 config.style = config.style || 'h5p-toast'; 1616 config.duration = config.duration || 3000; 1617 1618 // Build toast 1619 const toast = document.createElement('div'); 1620 toast.setAttribute('id', config.style); 1621 toast.classList.add('h5p-toast-disabled'); 1622 toast.classList.add(config.style); 1623 1624 const msg = document.createElement('span'); 1625 msg.innerHTML = message; 1626 toast.appendChild(msg); 1627 1628 document.body.appendChild(toast); 1629 1630 // The message has to be set before getting the coordinates 1631 const coordinates = getToastCoordinates(element, toast, config.position); 1632 toast.style.left = Math.round(coordinates.left) + 'px'; 1633 toast.style.top = Math.round(coordinates.top) + 'px'; 1634 1635 toast.classList.remove('h5p-toast-disabled'); 1636 const timer = setTimeout(removeToast, config.duration); 1637 1638 // The toast can also be removed by clicking somewhere 1639 document.addEventListener('click', clickHandler); 1640}; 1641 1642/** 1643 * Copyrights for a H5P Content Library. 1644 * 1645 * @class 1646 */ 1647H5P.ContentCopyrights = function () { 1648 var label; 1649 var media = []; 1650 var content = []; 1651 1652 /** 1653 * Set label. 1654 * 1655 * @param {string} newLabel 1656 */ 1657 this.setLabel = function (newLabel) { 1658 label = newLabel; 1659 }; 1660 1661 /** 1662 * Add sub content. 1663 * 1664 * @param {H5P.MediaCopyright} newMedia 1665 */ 1666 this.addMedia = function (newMedia) { 1667 if (newMedia !== undefined) { 1668 media.push(newMedia); 1669 } 1670 }; 1671 1672 /** 1673 * Add sub content in front. 1674 * 1675 * @param {H5P.MediaCopyright} newMedia 1676 */ 1677 this.addMediaInFront = function (newMedia) { 1678 if (newMedia !== undefined) { 1679 media.unshift(newMedia); 1680 } 1681 }; 1682 1683 /** 1684 * Add sub content. 1685 * 1686 * @param {H5P.ContentCopyrights} newContent 1687 */ 1688 this.addContent = function (newContent) { 1689 if (newContent !== undefined) { 1690 content.push(newContent); 1691 } 1692 }; 1693 1694 /** 1695 * Print content copyright. 1696 * 1697 * @returns {string} HTML. 1698 */ 1699 this.toString = function () { 1700 var html = ''; 1701 1702 // Add media rights 1703 for (var i = 0; i < media.length; i++) { 1704 html += media[i]; 1705 } 1706 1707 // Add sub content rights 1708 for (i = 0; i < content.length; i++) { 1709 html += content[i]; 1710 } 1711 1712 1713 if (html !== '') { 1714 // Add a label to this info 1715 if (label !== undefined) { 1716 html = '<h3>' + label + '</h3>' + html; 1717 } 1718 1719 // Add wrapper 1720 html = '<div class="h5p-content-copyrights">' + html + '</div>'; 1721 } 1722 1723 return html; 1724 }; 1725}; 1726 1727/** 1728 * A ordered list of copyright fields for media. 1729 * 1730 * @class 1731 * @param {Object} copyright 1732 * Copyright information fields. 1733 * @param {Object} [labels] 1734 * Translation of labels. 1735 * @param {Array} [order] 1736 * Order of the fields. 1737 * @param {Object} [extraFields] 1738 * Add extra copyright fields. 1739 */ 1740H5P.MediaCopyright = function (copyright, labels, order, extraFields) { 1741 var thumbnail; 1742 var list = new H5P.DefinitionList(); 1743 1744 /** 1745 * Get translated label for field. 1746 * 1747 * @private 1748 * @param {string} fieldName 1749 * @returns {string} 1750 */ 1751 var getLabel = function (fieldName) { 1752 if (labels === undefined || labels[fieldName] === undefined) { 1753 return H5P.t(fieldName); 1754 } 1755 1756 return labels[fieldName]; 1757 }; 1758 1759 /** 1760 * Get humanized value for the license field. 1761 * 1762 * @private 1763 * @param {string} license 1764 * @param {string}
1764 [version] 1765 * @returns {string} 1766 */ 1767 var humanizeLicense = function (license, version) { 1768 var copyrightLicense = H5P.copyrightLicenses[license]; 1769 1770 // Build license string 1771 var value = ''; 1772 if (!(license === 'PD' && version)) { 1773 // Add license label 1774 value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense); 1775 } 1776 1777 // Check for version info 1778 var versionInfo; 1779 if (copyrightLicense.versions) { 1780 if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) { 1781 version = copyrightLicense.versions.default; 1782 } 1783 if (version && copyrightLicense.versions[version]) { 1784 versionInfo = copyrightLicense.versions[version]; 1785 } 1786 } 1787 1788 if (versionInfo) { 1789 // Add license version 1790 if (value) { 1791 value += ' '; 1792 } 1793 value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo); 1794 } 1795 1796 // Add link if specified 1797 var link; 1798 if (copyrightLicense.hasOwnProperty('link')) { 1799 link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version); 1800 } 1801 else if (versionInfo && copyrightLicense.hasOwnProperty('link')) { 1802 link = versionInfo.link; 1803 } 1804 if (link) { 1805 value = '<a href="' + link + '" target="_blank">' + value + '</a>'; 1806 } 1807 1808 // Generate parenthesis 1809 var parenthesis = ''; 1810 if (license !== 'PD' && license !== 'C') { 1811 parenthesis += license; 1812 } 1813 if (version && version !== 'CC0 1.0') { 1814 if (parenthesis && license !== 'GNU GPL') { 1815 parenthesis += ' '; 1816 } 1817 parenthesis += version; 1818 } 1819 if (parenthesis) { 1820 value += ' (' + parenthesis + ')'; 1821 } 1822 if (license === 'C') { 1823 value += ' ©'; 1824 } 1825 1826 return value; 1827 }; 1828 1829 if (copyright !== undefined) { 1830 // Add the extra fields 1831 for (var field in extraFields) { 1832 if (extraFields.hasOwnProperty(field)) { 1833 copyright[field] = extraFields[field]; 1834 } 1835 } 1836 1837 if (order === undefined) { 1838 // Set default order 1839 order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes']; 1840 } 1841 1842 for (var i = 0; i < order.length; i++) { 1843 var fieldName = order[i]; 1844 if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') { 1845 var humanValue = copyright[fieldName]; 1846 if (fieldName === 'license') { 1847 humanValue = humanizeLicense(copyright.license, copyright.version); 1848 } 1849 if (fieldName === 'source') { 1850 humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined; 1851 } 1852 list.add(new H5P.Field(getLabel(fieldName), humanValue)); 1853 } 1854 } 1855 } 1856 1857 /** 1858 * Set thumbnail. 1859 * 1860 * @param {H5P.Thumbnail} newThumbnail 1861 */ 1862 this.setThumbnail = function (newThumbnail) { 1863 thumbnail = newThumbnail; 1864 }; 1865 1866 /** 1867 * Checks if this copyright is undisclosed. 1868 * I.e. only has the license attribute set, and it's undisclosed. 1869 * 1870 * @returns {boolean} 1871 */ 1872 this.undisclosed = function () { 1873 if (list.size() === 1) { 1874 var field = list.get(0); 1875 if (field.getLabel() === getLabel('license') && field.getValue() === humanizeLicense('U')) { 1876 return true; 1877 } 1878 } 1879 return false; 1880 }; 1881 1882 /** 1883 * Print media copyright. 1884 * 1885 * @returns {string} HTML. 1886 */ 1887 this.toString = function () { 1888 var html = ''; 1889 1890 if (this.undisclosed()) { 1891 return html; // No need to print a copyright with a single undisclosed license. 1892 } 1893 1894 if (thumbnail !== undefined) { 1895 html += thumbnail; 1896 } 1897 html += list; 1898 1899 if (html !== '') { 1900 html = '<div class="h5p-media-copyright">' + html + '</div>'; 1901 } 1902 1903 return html; 1904 }; 1905}; 1906 1907/** 1908 * A simple and elegant class for creating thumbnails of images. 1909 * 1910 * @class 1911 * @param {string} source 1912 * @param {number} width 1913 * @param {number} height 1914 * @param {string} alt 1915 * alternative text for the thumbnail 1916 */ 1917H5P.Thumbnail = function (source, width, height, alt) {
1918 var thumbWidth, thumbHeight = 100; 1919 if (width !== undefined) { 1920 thumbWidth = Math.round(thumbHeight * (width / height)); 1921 } 1922 1923 /** 1924 * Print thumbnail. 1925 * 1926 * @returns {string} HTML. 1927 */ 1928 this.toString = function () { 1929 return '<img src="' + source + '" alt="' + (alt ? alt : '') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>'; 1930 }; 1931}; 1932 1933/** 1934 * Simple data structure class for storing a single field. 1935 * 1936 * @class 1937 * @param {string} label 1938 * @param {string} value 1939 */ 1940H5P.Field = function (label, value) { 1941 /** 1942 * Public. Get field label. 1943 * 1944 * @returns {String} 1945 */ 1946 this.getLabel = function () { 1947 return label; 1948 }; 1949 1950 /** 1951 * Public. Get field value. 1952 * 1953 * @returns {String} 1954 */ 1955 this.getValue = function () { 1956 return value; 1957 }; 1958}; 1959 1960/** 1961 * Simple class for creating a definition list. 1962 * 1963 * @class 1964 */ 1965H5P.DefinitionList = function () { 1966 var fields = []; 1967 1968 /** 1969 * Add field to list. 1970 * 1971 * @param {H5P.Field} field 1972 */ 1973 this.add = function (field) { 1974 fields.push(field); 1975 }; 1976 1977 /** 1978 * Get Number of fields. 1979 * 1980 * @returns {number} 1981 */ 1982 this.size = function () { 1983 return fields.length; 1984 }; 1985 1986 /** 1987 * Get field at given index. 1988 * 1989 * @param {number} index 1990 * @returns {H5P.Field} 1991 */ 1992 this.get = function (index) { 1993 return fields[index]; 1994 }; 1995 1996 /** 1997 * Print definition list. 1998 * 1999 * @returns {string} HTML. 2000 */ 2001 this.toString = function () { 2002 var html = ''; 2003 for (var i = 0; i < fields.length; i++) { 2004 var field = fields[i]; 2005 html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>'; 2006 } 2007 return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>'); 2008 }; 2009}; 2010 2011/** 2012 * THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED. 2013 * 2014 * Helper object for keeping coordinates in the same format all over. 2015 * 2016 * @deprecated 2017 * Will be removed march 2016. 2018 * @class 2019 * @param {number} x 2020 * @param {number} y 2021 * @param {number} w 2022 * @param {number} h 2023 */ 2024H5P.Coords = function (x, y, w, h) { 2025 if ( !(this instanceof H5P.Coords) ) 2026 return new H5P.Coords(x, y, w, h); 2027 2028 /** @member {number} */ 2029 this.x = 0; 2030 /** @member {number} */ 2031 this.y = 0; 2032 /** @member {number} */ 2033 this.w = 1; 2034 /** @member {number} */ 2035 this.h = 1; 2036 2037 if (typeof(x) === 'object') { 2038 this.x = x.x; 2039 this.y = x.y; 2040 this.w = x.w; 2041 this.h = x.h; 2042 } 2043 else { 2044 if (x !== undefined) { 2045 this.x = x; 2046 } 2047 if (y !== undefined) { 2048 this.y = y; 2049 } 2050 if (w !== undefined) { 2051 this.w = w; 2052 } 2053 if (h !== undefined) { 2054 this.h = h; 2055 } 2056 } 2057 return this; 2058}; 2059 2060/** 2061 * Parse library string into values. 2062 * 2063 * @param {string} library 2064 * library in the format "machineName majorVersion.minorVersion" 2065 * @returns {Object} 2066 * library as an object with machineName, majorVersion and minorVersion properties 2067 * return false if the library parameter is invalid 2068 */ 2069H5P.libraryFromString = function (library) { 2070 var regExp = /(.+)\s(\d+)\.(\d+)$/g; 2071 var res = regExp.exec(library); 2072 if (res !== null) { 2073 return { 2074 'machineName': res[1], 2075 'majorVersion': parseInt(res[2]), 2076 'minorVersion': parseInt(res[3]) 2077 }; 2078 } 2079 else { 2080 return false; 2081 } 2082}; 2083 2084/** 2085 * Get the path to the library 2086 * 2087 * @param {string} library 2088 * The library identifier in the format "machineName-majorVersion.minorVersion". 2089 * @returns {string} 2090 * The full path to the library. 2091 */ 2092H5P.getLibraryPath = function (library) { 2093 if (H5PIntegration && 2094 H5PIntegration.libraryDirectories && 2095 library in H5PIntegration.libraryDirectories) { 2096 // Use H5PIntegration.libraryDirectories if it exists for this library 2097 library = H5PIntegration.libraryDirectories[library]; 2098 } 2099 2100 if (H5PIntegration.urlLibraries !== undefined) { 2101 // This is an override for those implementations that has a different libraries URL, e.g. Moodle 2102 return H5PIntegration.urlLibraries + '/' + library; 2103 } 2104 else { 2105 return H5PIntegration.url + '/libraries/' + library; 2106 } 2107}; 2108 2109/** 2110 * Recursivly clone the given object. 2111 * 2112 * @param {Object|Array} object 2113 * Object to clone. 2114 * @param {boolean} [recursive] 2115 * @returns {Object|Array} 2116 * A clone of object. 2117 */ 2118H5P.cloneObject = function (object, recursive) { 2119 // TODO: Consider if this needs to be in core. Doesn't $.extend do the same? 2120 var clone = object instanceof Array ? [] : {}; 2121 2122 for (var i in object) { 2123 if (object.hasOwnProperty(i)) { 2124 if (recursive !== undefined && recursive && typeof object[i] === 'object') { 2125 clone[i] = H5P.cloneObject(object[i], recursive); 2126 } 2127 else { 2128 clone[i] = object[i]; 2129 } 2130 } 2131 } 2132 2133 return clone; 2134}; 2135 2136/** 2137 * Remove all empty spaces before and after the value. 2138 * 2139 * @param {string} value 2140 * @returns {string} 2141 */ 2142H5P.trim = function (value) { 2143 return value.replace(/^\s+|\s+$/g, ''); 2144 2145 // TODO: Only include this or String.trim(). What is best? 2146 // I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/ 2147 // So should we make this function deprecated? 2148}; 2149 2150/** 2151 * Recursive function that detects deep empty structures. 2152 * 2153 * @param {*} value 2154 * @returns {bool} 2155 */ 2156H5P.isEmpty = value => { 2157 if (!value && value !== 0 && value !== false) { 2158 return true; // undefined, null, NaN and empty strings. 2159 } 2160 else if (Array.isArray(value)) { 2161 for (let i = 0; i < value.length; i++) { 2162 if (!H5P.isEmpty(value[i])) { 2163 return false; // Array contains a non-empty value 2164 } 2165 } 2166 return true; // Empty array 2167 } 2168 else if (typeof value === 'object') { 2169 for (let prop in value) { 2170 if (value.hasOwnProperty(prop) && !H5P.isEmpty(value[prop])) { 2171 return false; // Object contains a non-empty value 2172 } 2173 } 2174 return true; // Empty object 2175 } 2176 return false;
2177}; 2178 2179/** 2180 * Check if JavaScript path/key is loaded. 2181 * 2182 * @param {string} path 2183 * @returns {boolean} 2184 */ 2185H5P.jsLoaded = function (path) { 2186 H5PIntegration.loadedJs = H5PIntegration.loadedJs || []; 2187 return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1; 2188}; 2189 2190/** 2191 * Check if styles path/key is loaded. 2192 * 2193 * @param {string} path 2194 * @returns {boolean} 2195 */ 2196H5P.cssLoaded = function (path) { 2197 H5PIntegration.loadedCss = H5PIntegration.loadedCss || []; 2198 return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1; 2199}; 2200 2201/** 2202 * Shuffle an array in place. 2203 * 2204 * @param {Array} array 2205 * Array to shuffle 2206 * @returns {Array} 2207 * The passed array is returned for chaining. 2208 */ 2209H5P.shuffleArray = function (array) { 2210 // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it. 2211 if (!(array instanceof Array)) { 2212 return; 2213 } 2214 2215 var i = array.length, j, tempi, tempj; 2216 if ( i === 0 ) return false; 2217 while ( --i ) { 2218 j = Math.floor( Math.random() * ( i + 1 ) ); 2219 tempi = array[i]; 2220 tempj = array[j]; 2221 array[i] = tempj; 2222 array[j] = tempi; 2223 } 2224 return array; 2225}; 2226 2227/** 2228 * Post finished results for user. 2229 * 2230 * @deprecated 2231 * Do not use this function directly, trigger the finish event instead. 2232 * Will be removed march 2016 2233 * @param {number} contentId 2234 * Identifies the content 2235 * @param {number} score 2236 * Achieved score/points 2237 * @param {number} maxScore 2238 * The maximum score/points that can be achieved 2239 * @param {number} [time] 2240 * Reported time consumption/usage 2241 */ 2242H5P.setFinished = function (contentId, score, maxScore, time) { 2243 var validScore = typeof score === 'number' || score instanceof Number; 2244 if (validScore && H5PIntegration.postUserStatistics === true) { 2245 /** 2246 * Return unix timestamp for the given JS Date. 2247 * 2248 * @private 2249 * @param {Date} date 2250 * @returns {Number} 2251 */ 2252 var toUnix = function (date) { 2253 return Math.round(date.getTime() / 1000); 2254 }; 2255 2256 // Post the results 2257 const data = { 2258 contentId: contentId, 2259 score: score, 2260 maxScore: maxScore, 2261 opened: toUnix(H5P.opened[contentId]), 2262 finished: toUnix(new Date()), 2263 time: time 2264 }; 2265 H5P.jQuery.post(H5PIntegration.ajax.setFinished, data) 2266 .fail(function () { 2267 H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data); 2268 }); 2269 } 2270}; 2271 2272// Add indexOf to browsers that lack them. (IEs) 2273if (!Array.prototype.indexOf) { 2274 Array.prototype.indexOf = function (needle) { 2275 for (var i = 0; i < this.length; i++) { 2276 if (this[i] === needle) { 2277 return i; 2278 } 2279 } 2280 return -1; 2281 }; 2282} 2283 2284// Need to define trim() since this is not available on older IEs, 2285// and trim is used in several libs 2286if (String.prototype.trim === undefined) { 2287 String.prototype.trim = function () { 2288 return H5P.trim(this); 2289 }; 2290} 2291 2292/** 2293 * Trigger an event on an instance 2294 * 2295 * Helper function that triggers an event if the instance supports event handling 2296 * 2297 * @param {Object} instance 2298 * Instance of H5P content 2299 * @param {string} eventType 2300 * Type of event to trigger 2301 * @param {*} data 2302 * @param {Object} extras 2303 */ 2304H5P.trigger = function (instance, eventType, data, extras) { 2305 // Try new event system first 2306 if (instance.trigger !== undefined) { 2307 instance.trigger(eventType, data, extras); 2308 } 2309 // Try deprecated event system 2310 else if (instance.$ !== undefined && instance.$.trigger !== undefined) { 2311 instance.$.trigger(eventType); 2312 } 2313}; 2314 2315/** 2316 * Register an event handler 2317 * 2318 * Helper function that registers an event handler for an event type if 2319 * the instance supports event handling 2320 * 2321 * @param {Object} instance 2322 * Instance of H5P content 2323 * @param {string} eventType 2324 * Type of event to listen for 2325 * @param {H5P.EventCallback} handler 2326 * Callback that gets triggered for events of the specified type 2327 */ 2328H5P.on = function (instance, eventType, handler) { 2329 // Try new event system first 2330 if (instance.on !== undefined) { 2331 instance.on(eventType, handler); 2332 } 2333 // Try deprecated event system 2334 else if (instance.$ !== undefined && instance.$.on !== undefined) { 2335 instance.$.on(eventType, handler); 2336 } 2337}; 2338 2339/** 2340 * Generate random UUID 2341 * 2342 * @returns {string} UUID 2343 */ 2344H5P.createUUID = function () { 2345 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) { 2346 var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8); 2347 return newChar.toString(16); 2348 }); 2349}; 2350 2351/** 2352 * Create title 2353 * 2354 * @param {string} rawTitle 2355 * @param {number} maxLength 2356 * @returns {string} 2357 */ 2358H5P.createTitle = function (rawTitle, maxLength) { 2359 if (!rawTitle) { 2360 return ''; 2361 } 2362 if (maxLength === undefined) { 2363 maxLength = 60; 2364 } 2365 var title = H5P.jQuery('<div></div>') 2366 .text( 2367 // Strip tags 2368 rawTitle.replace(/(<([^>]+)>)/ig,"") 2369 // Escape 2370 ).text(); 2371 if (title.length > maxLength) { 2372 title = title.substr(0, maxLength - 3) + '...'; 2373 } 2374 return title; 2375}; 2376 2377// Wrap in privates 2378(function ($) { 2379 2380 /** 2381 * Creates ajax requests for inserting, updateing and deleteing 2382 * content user data. 2383 * 2384 * @private 2385 * @param {number} contentId What content to store the data for. 2386 * @param {string} dataType Identifies the set of data for this content. 2387 * @param {string} subContentId Identifies sub content 2388 * @param {function} [done] Callback when ajax is done. 2389 * @param {object} [data] To be stored for future use. 2390 * @param {boolean} [preload=false] Data is loaded when content is loaded. 2391 * @param {boolean}
2391 [invalidate=false] Data is invalidated when content changes. 2392 * @param {boolean} [async=true] 2393 */ 2394 function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) { 2395 if (H5PIntegration.user === undefined) { 2396 // Not logged in, no use in saving. 2397 done('Not signed in.'); 2398 return; 2399 } 2400 2401 var options = { 2402 url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0), 2403 dataType: 'json', 2404 async: async === undefined ? true : async 2405 }; 2406 if (data !== undefined) { 2407 options.type = 'POST'; 2408 options.data = { 2409 data: (data === null ? 0 : data), 2410 preload: (preload ? 1 : 0), 2411 invalidate: (invalidate ? 1 : 0) 2412 }; 2413 } 2414 else { 2415 options.type = 'GET'; 2416 } 2417 if (done !== undefined) { 2418 options.error = function (xhr, error) { 2419 done(error); 2420 }; 2421 options.success = function (response) { 2422 if (!response.success) { 2423 done(response.message); 2424 return; 2425 } 2426 2427 if (response.data === false || response.data === undefined) { 2428 done(); 2429 return; 2430 } 2431 2432 done(undefined, response.data); 2433 }; 2434 } 2435 2436 $.ajax(options); 2437 } 2438 2439 /** 2440 * Get user data for given content. 2441 * 2442 * @param {number} contentId 2443 * What content to get data for. 2444 * @param {string} dataId 2445 * Identifies the set of data for this content. 2446 * @param {function} done 2447 * Callback with error and data parameters. 2448 * @param {string} [subContentId] 2449 * Identifies which data belongs to sub content. 2450 */ 2451 H5P.getUserData = function (contentId, dataId, done, subContentId) { 2452 if (!subContentId) { 2453 subContentId = 0; // Default 2454 } 2455 2456 H5PIntegration.contents = H5PIntegration.contents || {}; 2457 var content = H5PIntegration.contents['cid-' + contentId] || {}; 2458 var preloadedData = content.contentUserData; 2459 if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) { 2460 if (preloadedData[subContentId][dataId] === 'RESET') { 2461 done(undefined, null); 2462 return; 2463 } 2464 try { 2465 done(undefined, JSON.parse(preloadedData[subContentId][dataId])); 2466 } 2467 catch (err) { 2468 done(err); 2469 } 2470 } 2471 else { 2472 contentUserDataAjax(contentId, dataId, subContentId, function (err, data) { 2473 if (err || data === undefined) { 2474 done(err, data); 2475 return; // Error or no data 2476 } 2477 2478 // Cache in preloaded 2479 if (content.contentUserData === undefined) { 2480 content.contentUserData = preloadedData = {}; 2481 } 2482 if (preloadedData[subContentId] === undefined) { 2483 preloadedData[subContentId] = {}; 2484 } 2485 preloadedData[subContentId][dataId] = data; 2486 2487 // Done. Try to decode JSON 2488 try { 2489 done(undefined, JSON.parse(data)); 2490 } 2491 catch (e) { 2492 done(e); 2493 } 2494 }); 2495 } 2496 }; 2497 2498 /** 2499 * Async error handling. 2500 * 2501 * @callback H5P.ErrorCallback 2502 * @param {*} error 2503 */ 2504 2505 /** 2506 * Set user data for given content. 2507 * 2508 * @param {number} contentId 2509 * What content to get data for. 2510 * @param {string} dataId 2511 * Identifies the set of data for this content. 2512 * @param {Object} data 2513 * The data that is to be stored. 2514 * @param {Object} [extras] 2515 * Extra properties 2516 * @param {string} [extras.subContentId] 2517 * Identifies which data belongs to sub content. 2518 * @param {boolean} [extras.preloaded=true] 2519 * If the data should be loaded when content is loaded. 2520 * @param {boolean} [extras.deleteOnChange=false] 2521 * If the data should be invalidated when the content changes. 2522 * @param {H5P.ErrorCallback} [extras.errorCallback] 2523 * Callback with error as parameters. 2524 * @param {boolean} [extras.async=true] 2525 */ 2526 H5P.setUserData = function (contentId, dataId, data, extras) { 2527 var options = H5P.jQuery.extend(true, {}, { 2528 subContentId: 0, 2529 preloaded: true, 2530 deleteOnChange: false, 2531 async: true 2532 }, extras); 2533 2534 try { 2535 data = JSON.stringify(data); 2536 } 2537 catch (err) { 2538 if (options.errorCallback) { 2539 options.errorCallback(err); 2540 } 2541 return; // Failed to serialize. 2542 } 2543 2544 var content = H5PIntegration.contents['cid-' + contentId]; 2545 if (content === undefined) { 2546 content = H5PIntegration.contents['cid-' + contentId] = {}; 2547 } 2548 if (!content.contentUserData) { 2549 content.contentUserData = {}; 2550 } 2551 var preloadedData = content.contentUserData; 2552 if (preloadedData[options.subContentId] === undefined) { 2553 preloadedData[options.subContentId] = {}; 2554 } 2555 if (data === preloadedData[options.subContentId][dataId]) { 2556 return; // No need to save this twice. 2557 } 2558 2559 preloadedData[options.subContentId][dataId] = data; 2560 contentUserDataAjax(contentId, dataId, options.subContentId, function (error) { 2561 if (options.errorCallback && error) { 2562 options.errorCallback(error); 2563 } 2564 }, data, options.preloaded, options.deleteOnChange, options.async); 2565 }; 2566 2567 /** 2568 * Delete user data for given content. 2569 * 2570 * @param {number} contentId
2571 * What content to remove data for. 2572 * @param {string} dataId 2573 * Identifies the set of data for this content. 2574 * @param {string} [subContentId] 2575 * Identifies which data belongs to sub content. 2576 */ 2577 H5P.deleteUserData = function (contentId, dataId, subContentId) { 2578 if (!subContentId) { 2579 subContentId = 0; // Default 2580 } 2581 2582 // Remove from preloaded/cache 2583 var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData; 2584 if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) { 2585 delete preloadedData[subContentId][dataId]; 2586 } 2587 2588 contentUserDataAjax(contentId, dataId, subContentId, undefined, null); 2589 }; 2590 2591 /** 2592 * Function for getting content for a certain ID 2593 * 2594 * @param {number} contentId 2595 * @return {Object} 2596 */ 2597 H5P.getContentForInstance = function (contentId) { 2598 var key = 'cid-' + contentId; 2599 var exists = H5PIntegration && H5PIntegration.contents && 2600 H5PIntegration.contents[key]; 2601 2602 return exists ? H5PIntegration.contents[key] : undefined; 2603 }; 2604 2605 /** 2606 * Prepares the content parameters for storing in the clipboard. 2607 * 2608 * @class 2609 * @param {Object} parameters The parameters for the content to store 2610 * @param {string} [genericProperty] If only part of the parameters are generic, which part 2611 * @param {string} [specificKey] If the parameters are specific, what content type does it fit 2612 * @returns {Object} Ready for the clipboard 2613 */ 2614 H5P.ClipboardItem = function (parameters, genericProperty, specificKey) { 2615 var self = this; 2616 2617 /** 2618 * Set relative dimensions when params contains a file with a width and a height. 2619 * Very useful to be compatible with wysiwyg editors. 2620 * 2621 * @private 2622 */ 2623 var setDimensionsFromFile = function () { 2624 if (!self.generic) { 2625 return; 2626 } 2627 var params = self.specific[self.generic]; 2628 if (!params.params.file || !params.params.file.width || !params.params.file.height) { 2629 return; 2630 } 2631 2632 self.width = 20; // % 2633 self.height = (params.params.file.height / params.params.file.width) * self.width; 2634 }; 2635 2636 if (!genericProperty) { 2637 genericProperty = 'action'; 2638 parameters = { 2639 action: parameters 2640 }; 2641 } 2642 2643 self.specific = parameters; 2644 2645 if (genericProperty && parameters[genericProperty]) { 2646 self.generic = genericProperty; 2647 } 2648 if (specificKey) { 2649 self.from = specificKey; 2650 } 2651 2652 if (window.H5PEditor && H5PEditor.contentId) { 2653 self.contentId = H5PEditor.contentId; 2654 } 2655 2656 if (!self.specific.width && !self.specific.height) { 2657 setDimensionsFromFile(); 2658 } 2659 }; 2660 2661 /** 2662 * Store item in the H5P Clipboard. 2663 * 2664 * @param {H5P.ClipboardItem|*} clipboardItem 2665 */ 2666 H5P.clipboardify = function (clipboardItem) { 2667 if (!(clipboardItem instanceof H5P.ClipboardItem)) { 2668 clipboardItem = new H5P.ClipboardItem(clipboardItem); 2669 } 2670 H5P.setClipboard(clipboardItem); 2671 }; 2672 2673 /** 2674 * Retrieve parsed clipboard data. 2675 * 2676 * @return {Object} 2677 */ 2678 H5P.getClipboard = function () { 2679 return parseClipboard(); 2680 }; 2681 2682 /** 2683 * Set item in the H5P Clipboard. 2684 * 2685 * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set. 2686 */ 2687 H5P.setClipboard = function (clipboardItem) { 2688 localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem)); 2689 2690 // Trigger an event so all 'Paste' buttons may be enabled. 2691 H5P.externalDispatcher.trigger('datainclipboard', {reset: false}); 2692 }; 2693 2694 /** 2695 * Get config for a library 2696 * 2697 * @param string machineName 2698 * @return Object 2699 */ 2700 H5P.getLibraryConfig = function (machineName) { 2701 var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName]; 2702 return hasConfig ? H5PIntegration.libraryConfig[machineName] : {}; 2703 }; 2704 2705 /** 2706 * Get item from the H5P Clipboard. 2707 * 2708 * @private 2709 * @return {Object} 2710 */ 2711 var parseClipboard = function () { 2712 var clipboardData = localStorage.getItem('h5pClipboard'); 2713 if (!clipboardData) { 2714 return; 2715 } 2716 2717 // Try to parse clipboard dat 2718 try { 2719 clipboardData = JSON.parse(clipboardData); 2720 } 2721 catch (err) { 2722 console.error('Unable to parse JSON from clipboard.', err); 2723 return; 2724 } 2725 2726 // Update file URLs and reset content Ids 2727 recursiveUpdate(clipboardData.specific, function (path) { 2728 var isTmpFile = (path.substr(-4, 4) === '#tmp'); 2729 if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) { 2730 // Comes from existing content 2731 2732 let prefix; 2733 if (H5PEditor.contentId) { 2734 // .. to existing content 2735 prefix = '../' + clipboardData.contentId + '/'; 2736 } 2737 else { 2738 // .. to new content 2739 prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/'; 2740 } 2741 return path.substr(0, prefix.length) === prefix ? path : prefix + path; 2742 } 2743 2744 return path; // Will automatically be looked for in tmp folder 2745 }); 2746 2747 2748 if (clipboardData.generic) { 2749 // Use reference instead of key 2750 clipboardData.generic = clipboardData.specific[clipboardData.generic]; 2751 } 2752 2753 return clipboardData; 2754 }; 2755 2756 /** 2757 * Update file URLs and reset content IDs. 2758 * Useful when copying content. 2759 * 2760 * @private 2761 * @param {object} params Reference 2762 * @param {function} handler Modifies the path to work when pasted 2763 */ 2764 var recursiveUpdate = function (params, handler) { 2765 for (var prop in params) { 2766 if (params.hasOwnProperty(prop) && params[prop] instanceof Object) { 2767 var obj = params[prop]; 2768 if (obj.path !== undefined && obj.mime !== undefined) { 2769 obj.path = handler(obj.path); 2770 } 2771 else { 2772 if (obj.library !== undefined && obj.subContentId !== undefined) { 2773 // Avoid multiple content with same ID 2774 delete obj.subContentId; 2775 } 2776 recursiveUpdate(obj, handler); 2777 } 2778 } 2779 } 2780 }; 2781 2782 // Init H5P when page is fully loadded 2783 $(document).ready(function () { 2784 2785 window.addEventListener('storage', function (event) { 2786 // Pick up clipboard changes from other tabs 2787 if (event.key === 'h5pClipboard') { 2788 // Trigger an event so all 'Paste' buttons may be enabled. 2789 H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null}); 2790 } 2791 }); 2792 2793 var ccVersions = { 2794 'default': '4.0', 2795 '4.0': H5P.t('licenseCC40'), 2796 '3.0': H5P.t('licenseCC30'), 2797 '2.5': H5P.t('licenseCC25'), 2798 '2.0': H5P.t('licenseCC20'), 2799 '1.0': H5P.t('licenseCC10'), 2800 }; 2801 2802 /** 2803 * Maps copyright license codes to their human readable counterpart. 2804 * 2805 * @type {Object} 2806 */ 2807 H5P.copyrightLicenses = { 2808 'U': H5P.t('licenseU'), 2809 'CC BY': { 2810 label: H5P.t('licenseCCBY'), 2811 link: 'http://creativecommons.org/licenses/by/:version', 2812 versions: ccVersions 2813 }, 2814 'CC BY-SA': { 2815 label: H5P.t('licenseCCBYSA'), 2816 link: 'http://creativecommons.org/licenses/by-sa/:version', 2817 versions: ccVersions 2818 }, 2819 'CC BY-ND': { 2820 label: H5P.t('licenseCCBYND'), 2821 link: 'http://creativecommons.org/licenses/by-nd/:version', 2822 versions: ccVersions 2823 }, 2824 'CC BY-NC': { 2825 label: H5P.t('licenseCCBYNC'), 2826 link: 'http://creativecommons.org/licenses/by-nc/:version', 2827 versions: ccVersions 2828 }, 2829 'CC BY-NC-SA': { 2830 label: H5P.t('licenseCCBYNCSA'), 2831 link: 'http://creativecommons.org/licenses/by-nc-sa/:version', 2832 versions: ccVersions 2833 }, 2834 'CC BY-NC-ND': { 2835 label: H5P.t('licenseCCBYNCND'), 2836 link: 'http://creativecommons.org/licenses/by-nc-nd/:version', 2837 versions: ccVersions 2838 }, 2839 'CC0 1.0': { 2840 label: H5P.t('licenseCC010'), 2841 link: 'https://creativecommons.org/publicdomain/zero/1.0/' 2842 }, 2843 'GNU GPL': { 2844 label: H5P.t('licenseGPL'), 2845 link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html', 2846 linkVersions: { 2847 'v3': '3.0', 2848 'v2': '2.0', 2849 'v1': '1.0' 2850 }, 2851 versions: { 2852 'default': 'v3', 2853 'v3': H5P.t('licenseV3'),
2854 'v2': H5P.t('licenseV2'), 2855 'v1': H5P.t('licenseV1') 2856 } 2857 }, 2858 'PD': { 2859 label: H5P.t('licensePD'), 2860 versions: { 2861 'CC0 1.0': { 2862 label: H5P.t('licenseCC010'), 2863 link: 'https://creativecommons.org/publicdomain/zero/1.0/' 2864 }, 2865 'CC PDM': { 2866 label: H5P.t('licensePDM'), 2867 link: 'https://creativecommons.org/publicdomain/mark/1.0/' 2868 } 2869 } 2870 }, 2871 'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>', 2872 'CC PDM': { 2873 label: H5P.t('licensePDM'), 2874 link: 'https://creativecommons.org/publicdomain/mark/1.0/' 2875 }, 2876 'C': H5P.t('licenseC'), 2877 }; 2878 2879 /** 2880 * Indicates if H5P is embedded on an external page using iframe. 2881 * @member {boolean} H5P.externalEmbed 2882 */ 2883 2884 // Relay events to top window. This must be done before H5P.init 2885 // since events may be fired on initialization. 2886 if (H5P.isFramed && H5P.externalEmbed === false) { 2887 H5P.externalDispatcher.on('*', function (event) { 2888 window.parent.H5P.externalDispatcher.trigger.call(this, event); 2889 }); 2890 } 2891 2892 /** 2893 * Prevent H5P Core from initializing. Must be overriden before document ready. 2894 * @member {boolean} H5P.preventInit 2895 */ 2896 if (!H5P.preventInit) { 2897 // Note that this start script has to be an external resource for it to 2898 // load in correct order in IE9. 2899 H5P.init(document.body); 2900 } 2901 2902 if (H5PIntegration.saveFreq !== false) { 2903 // When was the last state stored 2904 var lastStoredOn = 0; 2905 // Store the current state of the H5P when leaving the page. 2906 var storeCurrentState = function () { 2907 // Make sure at least 250 ms has passed since last save 2908 var currentTime = new Date().getTime(); 2909 if (currentTime - lastStoredOn > 250) { 2910 lastStoredOn = currentTime; 2911 for (var i = 0; i < H5P.instances.length; i++) { 2912 var instance = H5P.instances[i]; 2913 if (instance.getCurrentState instanceof Function || 2914 typeof instance.getCurrentState === 'function') { 2915 var state = instance.getCurrentState(); 2916 if (state !== undefined) { 2917 // Async is not used to prevent the request from being cancelled. 2918 H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false}); 2919 } 2920 } 2921 } 2922 } 2923 }; 2924 // iPad does not support beforeunload, therefore using unload 2925 H5P.$window.one('beforeunload unload', function () { 2926 // Only want to do this once 2927 H5P.$window.off('pagehide beforeunload unload'); 2928 storeCurrentState(); 2929 }); 2930 // pagehide is used on iPad when tabs are switched 2931 H5P.$window.on('pagehide', storeCurrentState); 2932 } 2933 }); 2934 2935})(H5P.jQuery);
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.