h5p-php-library/js/h5p.js

2858 lines
85 KiB
JavaScript
Raw Normal View History

/*jshint multistr: true */
// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?
/** @namespace */
var H5P = window.H5P = window.H5P || {};
2013-01-17 09:01:43 +01:00
/**
* Tells us if we're inside of an iframe.
* @member {boolean}
*/
H5P.isFramed = (window.self !== window.parent);
2014-03-26 08:43:29 +01:00
/**
* jQuery instance of current window.
* @member {H5P.jQuery}
*/
2014-03-26 08:43:29 +01:00
H5P.$window = H5P.jQuery(window);
/**
* List over H5P instances on the current page.
* @member {Array}
*/
2014-10-13 22:19:59 +02:00
H5P.instances = [];
2014-03-26 08:43:29 +01:00
// Detect if we support fullscreen, and what prefix to use.
if (document.documentElement.requestFullScreen) {
/**
* Browser prefix to use when entering fullscreen mode.
* undefined means no fullscreen support.
* @member {string}
*/
2014-03-26 08:43:29 +01:00
H5P.fullScreenBrowserPrefix = '';
}
else if (document.documentElement.webkitRequestFullScreen) {
H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));
// Do not allow fullscreen for safari < 7.
if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
H5P.fullScreenBrowserPrefix = 'webkit';
}
2014-03-26 08:43:29 +01:00
}
else if (document.documentElement.mozRequestFullScreen) {
H5P.fullScreenBrowserPrefix = 'moz';
}
else if (document.documentElement.msRequestFullscreen) {
H5P.fullScreenBrowserPrefix = 'ms';
}
/**
* Keep track of when the H5Ps where started.
*
* @type {Object[]}
*/
H5P.opened = {};
/**
* Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found.
*
* @param {Object} target DOM Element
*/
2015-03-10 10:09:31 +01:00
H5P.init = function (target) {
2014-03-26 08:43:29 +01:00
// Useful jQuery object.
2015-03-10 10:09:31 +01:00
if (H5P.$body === undefined) {
H5P.$body = H5P.jQuery(document.body);
}
2013-04-26 17:27:35 +02:00
2015-03-13 12:51:31 +01:00
// Determine if we can use full screen
2016-11-11 11:57:54 +01:00
if (H5P.fullscreenSupported === undefined) {
/**
* Use this variable to check if fullscreen is supported. Fullscreen can be
* restricted when embedding since not all browsers support the native
* fullscreen, and the semi-fullscreen solution doesn't work when embedded.
* @type {boolean}
*/
H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled));
2019-01-14 15:26:27 +01:00
// -We should consider document.msFullscreenEnabled when they get their
// -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
// Update: Seems to be no need as they've moved on to Webkit
2015-03-13 12:51:31 +01:00
}
// Deprecated variable, kept to maintain backwards compatability
if (H5P.canHasFullScreen === undefined) {
/**
* @deprecated since version 1.11
* @type {boolean}
*/
2016-12-14 16:21:42 +01:00
H5P.canHasFullScreen = H5P.fullscreenSupported;
}
2014-03-26 08:43:29 +01:00
// H5Ps added in normal DIV.
2018-10-23 11:25:46 +02:00
H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
2015-03-10 10:09:31 +01:00
var $element = H5P.jQuery(this).addClass('h5p-initialized');
2014-03-26 08:43:29 +01:00
var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element);
var contentId = $element.data('content-id');
2015-02-27 13:59:42 +01:00
var contentData = H5PIntegration.contents['cid-' + contentId];
2014-04-14 10:50:37 +02:00
if (contentData === undefined) {
return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
}
2014-03-26 08:43:29 +01:00
var library = {
library: contentData.library,
2018-03-16 20:11:08 +01:00
params: JSON.parse(contentData.jsonContent),
metadata: contentData.metadata
2014-03-26 08:43:29 +01:00
};
2015-08-26 15:58:49 +02:00
H5P.getUserData(contentId, 'state', function (err, previousState) {
2015-04-07 19:32:44 +02:00
if (previousState) {
library.userDatas = {
state: previousState
};
}
else if (previousState === null && H5PIntegration.saveFreq) {
2015-04-07 19:32:44 +02:00
// Content has been reset. Display dialog.
delete contentData.contentUserData;
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);
H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
var closeDialog = function (event) {
if (event.type === 'click' || event.which === 32) {
2015-04-07 19:32:44 +02:00
dialog.close();
H5P.deleteUserData(contentId, 'state', 0);
2015-04-07 19:32:44 +02:00
}
};
$dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog);
H5P.trigger(instance, 'resize');
}).on('dialog-closed', function () {
H5P.trigger(instance, 'resize');
2015-04-07 19:32:44 +02:00
});
dialog.open();
}
2016-07-15 16:14:32 +02:00
// If previousState is false we don't have a previous state
2015-04-07 19:32:44 +02:00
});
2014-03-26 08:43:29 +01:00
// Create new instance.
var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true});
H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});
2014-03-26 08:43:29 +01:00
// Check if we should add and display a fullscreen button for this H5P.
2016-11-11 11:57:54 +01:00
if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {
H5P.jQuery(
'<div class="h5p-content-controls">' +
'<div role="button" ' +
'tabindex="0" ' +
'class="h5p-enable-fullscreen" ' +
2019-02-11 16:18:57 +01:00
'aria-label="' + H5P.t('fullscreen') + '" ' +
'title="' + H5P.t('fullscreen') + '">' +
'</div>' +
'</div>')
.prependTo($container)
.children()
.click(function () {
H5P.fullScreen($container, instance);
})
.keydown(function (e) {
if (e.which === 32 || e.which === 13) {
H5P.fullScreen($container, instance);
return false;
}
})
;
}
/**
* Create action bar
*/
var displayOptions = contentData.displayOptions;
var displayFrame = false;
if (displayOptions.frame) {
// Special handling of copyrights
if (displayOptions.copyright) {
var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);
if (!copyrights) {
displayOptions.copyright = false;
}
}
// Create action bar
var actionBar = new H5P.ActionBar(displayOptions);
var $actions = actionBar.getDOMElement();
2015-05-11 16:00:55 +02:00
actionBar.on('reuse', function () {
H5P.openReuseDialog($actions, contentData, library, instance, contentId);
instance.triggerXAPI('accessed-reuse');
});
actionBar.on('copyrights', function () {
var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container);
dialog.open(true);
instance.triggerXAPI('accessed-copyright');
});
actionBar.on('embed', function () {
2015-02-25 12:10:07 +01:00
H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
width: $element.width(),
height: $element.height()
}, instance);
instance.triggerXAPI('accessed-embed');
2015-02-12 11:52:55 +01:00
});
2015-04-13 18:41:53 +02:00
if (actionBar.hasActions()) {
displayFrame = true;
$actions.insertAfter($container);
}
2015-02-12 11:52:55 +01:00
}
$element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');
// Keep track of when we started
H5P.opened[contentId] = new Date();
// Handle events when the user finishes the content. Useful for logging exercise results.
H5P.on(instance, 'finish', function (event) {
if (event.data !== undefined) {
H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
}
});
2015-04-07 19:32:44 +02:00
// Auto save current state if supported
if (H5PIntegration.saveFreq !== false && (
instance.getCurrentState instanceof Function ||
2015-04-07 19:32:44 +02:00
typeof instance.getCurrentState === 'function')) {
2015-08-26 15:58:49 +02:00
var saveTimer, save = function () {
2015-04-07 19:32:44 +02:00
var state = instance.getCurrentState();
if (state !== undefined) {
H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
2015-04-07 19:32:44 +02:00
}
if (H5PIntegration.saveFreq) {
// Continue autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
2014-03-26 08:43:29 +01:00
};
2015-04-07 19:32:44 +02:00
if (H5PIntegration.saveFreq) {
// Start autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
// xAPI events will schedule a save in three seconds.
H5P.on(instance, 'xAPI', function (event) {
var verb = event.getVerb();
if (verb === 'completed' || verb === 'progressed') {
clearTimeout(saveTimer);
saveTimer = setTimeout(save, 3000);
}
});
2013-04-11 14:29:29 +02:00
}
2015-03-03 11:03:10 +01:00
if (H5P.isFramed) {
var resizeDelay;
if (H5P.externalEmbed === false) {
// Internal embed
// Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
var iframe = window.frameElement;
var resizeIframe = function () {
if (window.parent.H5P.isFullscreen) {
return; // Skip if full screen.
}
// Retain parent size to avoid jumping/scrolling
var parentHeight = iframe.parentElement.style.height;
iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
// Note: Force layout reflow
// This fixes a flickering bug for embedded content on iPads
// @see https://github.com/h5p/h5p-moodle-plugin/issues/237
iframe.getBoundingClientRect();
// Reset iframe height, in case content has shrinked.
iframe.style.height = '1px';
// Resize iframe so all content is visible.
iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
// Free parent
iframe.parentElement.style.height = parentHeight;
};
H5P.on(instance, 'resize', function () {
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
resizeIframe();
}, 1);
});
}
else if (H5P.communicator) {
// External embed
var parentIsFriendly = false;
// Handle that the resizer is loaded after the iframe
H5P.communicator.on('ready', function () {
H5P.communicator.send('hello');
});
// Handle hello message from our parent window
H5P.communicator.on('hello', function () {
// Initial setup/handshake is done
parentIsFriendly = true;
// Make iframe responsive
document.body.style.height = 'auto';
// Hide scrollbars for correct size
document.body.style.overflow = 'hidden';
// Content need to be resized to fit the new iframe size
H5P.trigger(instance, 'resize');
});
// When resize has been prepared tell parent window to resize
2018-10-23 11:25:46 +02:00
H5P.communicator.on('resizePrepared', function () {
H5P.communicator.send('resize', {
scrollHeight: document.body.scrollHeight
});
});
H5P.communicator.on('resize', function () {
H5P.trigger(instance, 'resize');
});
H5P.on(instance, 'resize', function () {
if (H5P.isFullscreen) {
return; // Skip iframe resize
}
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
// Only resize if the iframe can be resized
if (parentIsFriendly) {
H5P.communicator.send('prepareResize', {
scrollHeight: document.body.scrollHeight,
clientHeight: document.body.clientHeight
});
}
else {
H5P.communicator.send('hello');
}
}, 0);
});
2014-03-26 08:43:29 +01:00
}
}
if (!H5P.isFramed || H5P.externalEmbed === false) {
// Resize everything when window is resized.
H5P.jQuery(window.parent).resize(function () {
2014-06-24 15:31:02 +02:00
if (window.parent.H5P.isFullscreen) {
// Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event.
H5P.trigger(instance, 'resize');
}
else {
H5P.trigger(instance, 'resize');
}
});
2013-04-11 14:29:29 +02:00
}
2015-03-02 15:01:05 +01:00
H5P.instances.push(instance);
// Resize content.
H5P.trigger(instance, 'resize');
// Logic for hiding focus effects when using mouse
2019-09-26 12:58:15 +02:00
$element.addClass('using-mouse');
$element.on('mousedown keydown keyup', function (event) {
$element.toggleClass('using-mouse', event.type === 'mousedown');
});
if (H5P.externalDispatcher) {
H5P.externalDispatcher.trigger('initialized');
}
2013-01-17 09:01:43 +01:00
});
2014-03-26 08:43:29 +01:00
// Insert H5Ps that should be in iframes.
2015-03-10 10:09:31 +01:00
H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id');
2014-03-26 08:43:29 +01:00
this.contentDocument.open();
this.contentDocument.write('<!doctype html><html class="h5p-iframe"><head>' + H5P.getHeadTags(contentId) + '</head><body><div class="h5p-content" data-content-id="' + contentId + '"/></body></html>');
2014-03-26 08:43:29 +01:00
this.contentDocument.close();
});
// Listen for xAPI events
H5P.externalDispatcher.on('xAPI', H5P.xAPICompletedListener2);
2013-01-17 09:01:43 +01:00
};
2015-02-27 13:59:42 +01:00
/**
* Loop through assets for iframe content and create a set of tags for head.
*
* @private
* @param {number} contentId
* @returns {string} HTML
*/
H5P.getHeadTags = function (contentId) {
var createStyleTags = function (styles) {
var tags = '';
for (var i = 0; i < styles.length; i++) {
2015-03-23 09:45:02 +01:00
tags += '<link rel="stylesheet" href="' + styles[i] + '">';
2015-02-27 13:59:42 +01:00
}
return tags;
};
var createScriptTags = function (scripts) {
var tags = '';
for (var i = 0; i < scripts.length; i++) {
2015-03-23 09:45:02 +01:00
tags += '<script src="' + scripts[i] + '"></script>';
2015-02-27 13:59:42 +01:00
}
return tags;
};
return '<base target="_parent">' +
createStyleTags(H5PIntegration.core.styles) +
2015-02-27 13:59:42 +01:00
createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
createScriptTags(H5PIntegration.core.scripts) +
createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
'<script>H5PIntegration = window.parent.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
2015-02-27 13:59:42 +01:00
};
/**
* When embedded the communicator helps talk to the parent page.
*
* @type {Communicator}
*/
2015-08-26 15:58:49 +02:00
H5P.communicator = (function () {
/**
* @class
* @private
*/
function Communicator() {
var self = this;
// Maps actions to functions
var actionHandlers = {};
// Register message listener
window.addEventListener('message', function receiveMessage(event) {
if (window.parent !== event.source || event.data.context !== 'h5p') {
return; // Only handle messages from parent and in the correct context
}
if (actionHandlers[event.data.action] !== undefined) {
actionHandlers[event.data.action](event.data);
}
} , false);
/**
* Register action listener.
*
* @param {string} action What you are waiting for
* @param {function} handler What you want done
*/
self.on = function (action, handler) {
actionHandlers[action] = handler;
};
/**
* Send a message to the all mighty father.
*
* @param {string} action
* @param {Object} [data] payload
*/
2015-08-26 15:58:49 +02:00
self.send = function (action, data) {
if (data === undefined) {
data = {};
}
data.context = 'h5p';
data.action = action;
// Parent origin can be anything
window.parent.postMessage(data, '*');
};
}
return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
})();
2016-11-11 11:45:59 +01:00
/**
* Enter semi fullscreen for the given H5P instance
*
* @param {H5P.jQuery} $element Content container.
* @param {Object} instance
* @param {function} exitCallback Callback function called when user exits fullscreen.
* @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
*/
H5P.semiFullScreen = function ($element, instance, exitCallback, body) {
H5P.fullScreen($element, instance, exitCallback, body, true);
};
2013-04-11 14:29:29 +02:00
/**
* Enter fullscreen for the given H5P instance.
2013-04-26 17:27:35 +02:00
*
* @param {H5P.jQuery} $element Content container.
* @param {Object} instance
* @param {function} exitCallback Callback function called when user exits fullscreen.
* @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
* @param {Boolean} forceSemiFullScreen
2013-04-11 14:29:29 +02:00
*/
2016-11-11 11:45:59 +01:00
H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
if (H5P.exitFullScreen !== undefined) {
return; // Cannot enter new fullscreen until previous is over
}
if (H5P.isFramed && H5P.externalEmbed === false) {
2014-03-26 08:43:29 +01:00
// Trigger resize on wrapper in parent window.
2016-11-11 11:45:59 +01:00
window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen);
H5P.isFullscreen = true;
H5P.exitFullScreen = function () {
window.parent.H5P.exitFullScreen();
2015-02-24 16:02:14 +01:00
};
H5P.on(instance, 'exitFullScreen', function () {
H5P.isFullscreen = false;
H5P.exitFullScreen = undefined;
2015-02-24 16:02:14 +01:00
});
2014-03-26 08:43:29 +01:00
return;
}
2014-03-26 08:43:29 +01:00
var $container = $element;
2018-10-23 11:25:46 +02:00
var $classes, $iframe, $body;
2014-03-26 08:43:29 +01:00
if (body === undefined) {
$body = H5P.$body;
}
2014-03-26 08:43:29 +01:00
else {
// We're called from an iframe.
$body = H5P.jQuery(body);
$classes = $body.add($element.get());
var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id');
$iframe = H5P.jQuery(iframeSelector);
$element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container.
}
2014-03-26 08:43:29 +01:00
$classes = $element.add(H5P.$body).add($classes);
/**
* Prepare for resize by setting the correct styles.
*
* @private
* @param {string} classes CSS
*/
var before = function (classes) {
$classes.addClass(classes);
if ($iframe !== undefined) {
// Set iframe to its default size(100%).
$iframe.css('height', '');
2014-03-26 08:43:29 +01:00
}
};
/**
* Gets called when fullscreen mode has been entered.
* Resizes and sets focus on content.
*
* @private
*/
var entered = function () {
// Do not rely on window resize events.
H5P.trigger(instance, 'resize');
H5P.trigger(instance, 'focus');
H5P.trigger(instance, 'enterFullScreen');
};
/**
* Gets called when fullscreen mode has been exited.
* Resizes and sets focus on content.
*
* @private
* @param {string} classes CSS
*/
var done = function (classes) {
H5P.isFullscreen = false;
$classes.removeClass(classes);
// Do not rely on window resize events.
H5P.trigger(instance, 'resize');
H5P.trigger(instance, 'focus');
H5P.exitFullScreen = undefined;
2014-03-26 08:43:29 +01:00
if (exitCallback !== undefined) {
exitCallback();
}
2015-02-09 11:47:33 +01:00
H5P.trigger(instance, 'exitFullScreen');
2014-03-26 08:43:29 +01:00
};
H5P.isFullscreen = true;
2016-11-11 11:45:59 +01:00
if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
2013-04-11 14:29:29 +02:00
// Create semi fullscreen.
if (H5P.isFramed) {
2015-02-09 11:51:39 +01:00
return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
}
before('h5p-semi-fullscreen');
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'));
2015-10-27 16:57:28 +01:00
var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {
if (prevViewportContent) {
// Use content from the previous viewport tag
h5pViewport.content = prevViewportContent;
2015-10-27 16:57:28 +01:00
}
else {
// Remove viewport tag
head.removeChild(h5pViewport);
2015-10-27 16:57:28 +01:00
}
$disable.remove();
$body.unbind('keyup', keyup);
2014-03-26 08:43:29 +01:00
done('h5p-semi-fullscreen');
2013-04-11 14:29:29 +02:00
};
keyup = function (event) {
if (event.keyCode === 27) {
disableSemiFullscreen();
}
};
$disable.click(disableSemiFullscreen);
$body.keyup(keyup);
2015-10-27 16:57:28 +01:00
// Disable zoom
var prevViewportContent, h5pViewport;
2015-10-27 16:57:28 +01:00
var metaTags = document.getElementsByTagName('meta');
for (var i = 0; i < metaTags.length; i++) {
if (metaTags[i].name === 'viewport') {
// Use the existing viewport tag
h5pViewport = metaTags[i];
prevViewportContent = h5pViewport.content;
2015-10-27 16:57:28 +01:00
break;
}
}
if (!prevViewportContent) {
// Create a new viewport tag
h5pViewport = document.createElement('meta');
h5pViewport.name = 'viewport';
2015-10-27 16:57:28 +01:00
}
h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
if (!prevViewportContent) {
// Insert the new viewport tag
2015-10-27 16:57:28 +01:00
var head = document.getElementsByTagName('head')[0];
head.appendChild(h5pViewport);
2015-10-27 16:57:28 +01:00
}
entered();
}
2013-04-11 14:29:29 +02:00
else {
2014-03-26 08:43:29 +01:00
// Create real fullscreen.
before('h5p-fullscreen');
2014-01-29 16:03:51 +01:00
var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
2013-04-11 14:29:29 +02:00
document.addEventListener(eventName, function () {
if (first === undefined) {
// We are entering fullscreen mode
2013-04-11 14:29:29 +02:00
first = false;
entered();
2013-04-11 14:29:29 +02:00
return;
}
// We are exiting fullscreen
2014-03-26 08:43:29 +01:00
done('h5p-fullscreen');
2013-04-11 14:29:29 +02:00
document.removeEventListener(eventName, arguments.callee, false);
});
2013-04-26 17:27:35 +02:00
2013-04-11 14:29:29 +02:00
if (H5P.fullScreenBrowserPrefix === '') {
2014-03-26 08:43:29 +01:00
$element[0].requestFullScreen();
2013-04-11 14:29:29 +02:00
}
else {
2014-01-29 16:03:51 +01:00
var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
2014-03-26 08:43:29 +01:00
$element[0][method](params);
2013-04-11 14:29:29 +02:00
}
// Allows everone to exit
H5P.exitFullScreen = function () {
if (H5P.fullScreenBrowserPrefix === '') {
document.exitFullscreen();
}
else if (H5P.fullScreenBrowserPrefix === 'moz') {
document.mozCancelFullScreen();
}
else {
document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
}
};
}
};
(function () {
/**
* Helper for adding a query parameter to an existing path that may already
* contain one or a hash.
*
* @param {string} path
* @param {string} parameter
* @return {string}
*/
H5P.addQueryParameter = function (path, parameter) {
let newPath, secondSplit;
const firstSplit = path.split('?');
if (firstSplit[1]) {
// There is already an existing query
secondSplit = firstSplit[1].split('#');
newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
}
else {
// No existing query, just need to take care of the hash
secondSplit = firstSplit[0].split('#');
newPath = secondSplit[0] + '?';
}
newPath += parameter;
if (secondSplit[1]) {
// Add back the hash
newPath += '#' + secondSplit[1];
}
return newPath;
};
/**
* Helper for setting the crossOrigin attribute + the complete correct source.
* Note: This will start loading the resource.
*
* @param {Element} element DOM element, typically img, video or audio
* @param {Object} source File object from parameters/json_content (created by H5PEditor)
* @param {number} contentId Needed to determine the complete correct file path
*/
H5P.setSource = function (element, source, contentId) {
let path = source.path;
const crossOrigin = H5P.getCrossOrigin(source);
if (crossOrigin) {
element.crossOrigin = crossOrigin;
if (H5PIntegration.crossoriginCacheBuster) {
// Some sites may want to add a cache buster in case the same resource
// is used elsewhere without the crossOrigin attribute
path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
}
}
else {
// In case this element has been used before.
element.removeAttribute('crossorigin');
}
element.src = H5P.getPath(path, contentId);
};
/**
* Check if the given path has a protocol.
*
* @private
* @param {string} path
* @return {string}
*/
2014-06-24 14:41:35 +02:00
var hasProtocol = function (path) {
return path.match(/^[a-z0-9]+:\/\//i);
};
/**
* Get the crossOrigin policy to use for img, video and audio tags on the current site.
*
* @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)
* @returns {string|null} crossOrigin attribute value required by the source
*/
H5P.getCrossOrigin = function (source) {
if (typeof source !== 'object') {
// Deprecated usage.
return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
}
if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {
// This is a local file, use the local crossOrigin policy.
return H5PIntegration.crossorigin;
// Note: We cannot use this for all external sources since we do not know
// each server's individual policy. We could add support for a list of
// external sources and their policy later on.
}
};
/**
* Find the path to the content files based on the id of the content.
* Also identifies and returns absolute paths.
*
* @param {string} path
* Relative to content folder or absolute.
* @param {number} contentId
* ID of the content requesting the path.
* @returns {string}
* Complete URL to path.
*/
H5P.getPath = function (path, contentId) {
if (hasProtocol(path)) {
return path;
}
var prefix;
var isTmpFile = (path.substr(-4,4) === '#tmp');
if (contentId !== undefined && !isTmpFile) {
// Check for custom override URL
if (H5PIntegration.contents !== undefined &&
H5PIntegration.contents['cid-' + contentId]) {
prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;
}
if (!prefix) {
prefix = H5PIntegration.url + '/content/' + contentId;
}
}
else if (window.H5PEditor !== undefined) {
prefix = H5PEditor.filesPath;
}
else {
return;
}
if (!hasProtocol(prefix)) {
// Use absolute urls
prefix = window.location.protocol + "//" + window.location.host + prefix;
}
return prefix + '/' + path;
};
})();
/**
* THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
2015-03-23 10:53:21 +01:00
* Will be remove march 2016.
*
* Find the path to the content files folder based on the id of the content
*
* @deprecated
* Will be removed march 2016.
* @param contentId
* Id of the content requesting the path
* @returns {string}
* URL
*/
H5P.getContentPath = function (contentId) {
2015-03-23 10:53:21 +01:00
return H5PIntegration.url + '/content/' + contentId;
};
/**
* Get library class constructor from H5P by classname.
* Note that this class will only work for resolve "H5P.NameWithoutDot".
* Also check out {@link H5P.newRunnable}
*
* Used from libraries to construct instances of other libraries' objects by name.
*
* @param {string} name Name of library
* @returns {Object} Class constructor
*/
H5P.classFromName = function (name) {
2013-01-17 09:01:43 +01:00
var arr = name.split(".");
return this[arr[arr.length - 1]];
2013-01-17 09:01:43 +01:00
};
2014-03-26 08:43:29 +01:00
/**
* A safe way of creating a new instance of a runnable H5P.
*
* @param {Object} library
* Library/action object form params.
* @param {number} contentId
* Identifies the content.
* @param {H5P.jQuery} [$attachTo]
* Element to attach the instance to.
* @param {boolean} [skipResize]
* Skip triggering of the resize event after attaching.
* @param {Object} [extras]
* Extra parameters for the H5P content constructor
* @returns {Object}
* Instance.
2014-03-26 08:43:29 +01:00
*/
H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
2015-04-07 19:32:44 +02:00
var nameSplit, versionSplit, machineName;
2014-03-26 08:43:29 +01:00
try {
nameSplit = library.library.split(' ', 2);
2015-04-07 19:32:44 +02:00
machineName = nameSplit[0];
versionSplit = nameSplit[1].split('.', 2);
2014-03-26 08:43:29 +01:00
}
catch (err) {
return H5P.error('Invalid library string: ' + library.library);
}
2014-03-26 08:43:29 +01:00
if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
H5P.error('Invalid library params for: ' + library.library);
return H5P.error(library.params);
}
2014-03-26 08:43:29 +01:00
// Find constructor function
var constructor;
2014-03-26 08:43:29 +01:00
try {
nameSplit = nameSplit[0].split('.');
constructor = window;
for (var i = 0; i < nameSplit.length; i++) {
2014-03-26 08:43:29 +01:00
constructor = constructor[nameSplit[i]];
}
2014-03-26 08:43:29 +01:00
if (typeof constructor !== 'function') {
throw null;
}
}
catch (err) {
return H5P.error('Unable to find constructor for: ' + library.library);
}
if (extras === undefined) {
extras = {};
}
2015-04-07 19:32:44 +02:00
if (library.subContentId) {
extras.subContentId = library.subContentId;
2015-03-21 16:45:38 +01:00
}
2015-04-07 19:32:44 +02:00
if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
2015-04-07 19:32:44 +02:00
extras.previousState = library.userDatas.state;
}
2018-03-16 20:11:08 +01:00
if (library.metadata) {
extras.metadata = library.metadata;
}
// Makes all H5P libraries extend H5P.ContentType:
var standalone = extras.standalone || false;
// This order makes it possible for an H5P library to override H5P.ContentType functions!
constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype);
2015-04-07 19:32:44 +02:00
var instance;
// Some old library versions have their own custom third parameter.
// Make sure we don't send them the extras.
// (they will interpret it as something else)
if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) {
2015-04-07 19:32:44 +02:00
instance = new constructor(library.params, contentId);
}
else {
2015-04-07 19:32:44 +02:00
instance = new constructor(library.params, contentId, extras);
2015-03-21 16:45:38 +01:00
}
if (instance.$ === undefined) {
instance.$ = H5P.jQuery(instance);
}
if (instance.contentId === undefined) {
2015-02-18 15:20:20 +01:00
instance.contentId = contentId;
}
2015-04-07 19:32:44 +02:00
if (instance.subContentId === undefined && library.subContentId) {
instance.subContentId = library.subContentId;
2015-03-21 16:45:38 +01:00
}
if (instance.parent === undefined && extras && extras.parent) {
instance.parent = extras.parent;
2015-03-21 16:45:38 +01:00
}
if (instance.libraryInfo === undefined) {
instance.libraryInfo = {
versionedName: library.library,
versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1],
machineName: machineName,
majorVersion: versionSplit[0],
minorVersion: versionSplit[1]
};
}
2014-03-26 08:43:29 +01:00
if ($attachTo !== undefined) {
$attachTo.toggleClass('h5p-standalone', standalone);
2014-03-26 08:43:29 +01:00
instance.attach($attachTo);
2015-04-07 19:32:44 +02:00
H5P.trigger(instance, 'domChanged', {
'$target': $attachTo,
'library': machineName,
'key': 'newLibrary'
}, {'bubbles': true, 'external': true});
if (skipResize === undefined || !skipResize) {
2014-03-26 08:43:29 +01:00
// Resize content.
H5P.trigger(instance, 'resize');
2014-03-26 08:43:29 +01:00
}
}
return instance;
};
/**
* Used to print useful error messages. (to JavaScript error console)
2014-03-26 08:43:29 +01:00
*
* @param {*} err Error to print.
2014-03-26 08:43:29 +01:00
*/
H5P.error = function (err) {
2015-02-20 10:26:33 +01:00
if (window.console !== undefined && console.error !== undefined) {
console.error(err.stack ? err.stack : err);
2014-03-26 08:43:29 +01:00
}
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* Translate text strings.
*
* @param {string} key
* Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars.
* @param {Object} [vars]
* Data for placeholders.
* @param {string} [ns]
* Translation namespace. Defaults to H5P.
* @returns {string}
* Translated text
2014-03-26 08:43:29 +01:00
*/
H5P.t = function (key, vars, ns) {
if (ns === undefined) {
ns = 'H5P';
}
2015-02-27 13:59:42 +01:00
if (H5PIntegration.l10n[ns] === undefined) {
2014-03-26 08:43:29 +01:00
return '[Missing translation namespace "' + ns + '"]';
}
2015-02-27 13:59:42 +01:00
if (H5PIntegration.l10n[ns][key] === undefined) {
2014-03-26 08:43:29 +01:00
return '[Missing translation "' + key + '" in "' + ns + '"]';
}
2015-02-27 13:59:42 +01:00
var translation = H5PIntegration.l10n[ns][key];
2014-03-26 08:43:29 +01:00
if (vars !== undefined) {
// Replace placeholder with variables.
for (var placeholder in vars) {
translation = translation.replace(placeholder, vars[placeholder]);
}
2014-03-26 08:43:29 +01:00
}
return translation;
};
/**
* Creates a new popup dialog over the H5P content.
*
* @class
* @param {string} name
* Used for html class.
* @param {string} title
* Used for header.
* @param {string} content
* Displayed inside the dialog.
* @param {H5P.jQuery} $element
* Which DOM element the dialog should be inserted after.
*/
2014-03-26 08:43:29 +01:00
H5P.Dialog = function (name, title, content, $element) {
2017-02-07 13:48:42 +01:00
/** @alias H5P.Dialog# */
2014-03-26 08:43:29 +01:00
var self = this;
2019-10-17 15:01:22 +02:00
var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog" role="dialog" tabindex="-1">\
2014-03-26 08:43:29 +01:00
<div class="h5p-inner">\
<h2>' + title + '</h2>\
<div class="h5p-scroll-content">' + content + '</div>\
<div class="h5p-close" role="button" tabindex="0" aria-label="' + H5P.t('close') + '" title="' + H5P.t('close') + '"></div>\
2014-03-26 08:43:29 +01:00
</div>\
</div>')
.insertAfter($element)
.click(function (e) {
if (e && e.originalEvent && e.originalEvent.preventClosing) {
return;
}
2014-03-26 08:43:29 +01:00
self.close();
})
.children('.h5p-inner')
.click(function (e) {
e.originalEvent.preventClosing = true;
})
.find('.h5p-close')
.click(function () {
self.close();
})
2019-10-17 15:01:22 +02:00
.keypress(function (e) {
if (e.which === 13 || e.which === 32) {
self.close();
return false;
}
})
.end()
.find('a')
.click(function (e) {
e.stopPropagation();
})
.end()
.end();
2017-02-07 13:48:42 +01:00
/**
* Opens the dialog.
*/
self.open = function (scrollbar) {
if (scrollbar) {
$dialog.css('height', '100%');
}
2014-03-26 08:43:29 +01:00
setTimeout(function () {
$dialog.addClass('h5p-open'); // Fade in
// Triggering an event, in case something has to be done after dialog has been opened.
H5P.jQuery(self).trigger('dialog-opened', [$dialog]);
2019-10-17 15:01:22 +02:00
$dialog.focus();
2014-03-26 08:43:29 +01:00
}, 1);
};
2017-02-07 13:48:42 +01:00
/**
* Closes the dialog.
*/
self.close = function () {
2014-03-26 08:43:29 +01:00
$dialog.removeClass('h5p-open'); // Fade out
setTimeout(function () {
$dialog.remove();
H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
2019-10-17 15:01:22 +02:00
$element.attr('tabindex', '-1');
$element.focus();
2014-03-26 08:43:29 +01:00
}, 200);
};
};
/**
2015-05-11 16:00:55 +02:00
* Gather copyright information for the given content.
2014-03-26 08:43:29 +01:00
*
* @param {Object} instance
* H5P instance to get copyright information for.
* @param {Object} parameters
* Parameters of the content instance.
* @param {number} contentId
* Identifies the H5P content
* @param {Object} metadata
* Metadata of the content instance.
2015-05-11 16:00:55 +02:00
* @returns {string} Copyright information.
2014-03-26 08:43:29 +01:00
*/
H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
2015-05-12 10:04:21 +02:00
var copyrights;
if (instance.getCopyrights !== undefined) {
try {
// Use the instance's own copyright generator
copyrights = instance.getCopyrights();
}
catch (err) {
// Failed, prevent crashing page.
}
}
if (copyrights === undefined) {
// Create a generic flat copyright list
copyrights = new H5P.ContentCopyrights();
H5P.findCopyrights(copyrights, parameters, contentId);
}
var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
if (metadataCopyrights !== undefined) {
copyrights.addMediaInFront(metadataCopyrights);
}
2014-03-26 08:43:29 +01:00
if (copyrights !== undefined) {
// Convert to string
2014-03-26 08:43:29 +01:00
copyrights = copyrights.toString();
}
2015-05-11 16:00:55 +02:00
return copyrights;
2014-03-26 08:43:29 +01:00
};
/**
* Gather a flat list of copyright information from the given parameters.
*
* @param {H5P.ContentCopyrights} info
* Used to collect all information in.
* @param {(Object|Array)} parameters
* To search for file objects in.
* @param {number} contentId
* Used to insert thumbnails for images.
* @param {Object} extras - Extras.
* @param {object} extras.metadata - Metadata
* @param {object} extras.machineName - Library name of some kind.
* Metadata of the content instance.
*/
H5P.findCopyrights = function (info, parameters, contentId, extras) {
// If extras are
if (extras) {
extras.params = parameters;
buildFromMetadata(extras, extras.machineName, contentId);
}
var lastContentTypeName;
// Cycle through parameters
for (var field in parameters) {
if (!parameters.hasOwnProperty(field)) {
continue; // Do not check
}
/**
* @deprecated This hack should be removed after 2017-11-01
* The code that was using this was removed by HFP-574
* This note was seen on 2018-04-04, and consultation with
* higher authorities lead to keeping the code for now ;-)
*/
if (field === 'overrideSettings') {
console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
console.warn(parameters);
continue;
}
var value = parameters[field];
if (value && value.library && typeof value.library === 'string') {
lastContentTypeName = value.library.split(' ')[0];
}
else if (value && value.library && typeof value.library === 'object') {
lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;
}
if (value instanceof Array) {
// Cycle through array
H5P.findCopyrights(info, value, contentId);
}
else if (value instanceof Object) {
buildFromMetadata(value, lastContentTypeName, contentId);
// Check if object is a file with copyrights (old core)
if (value.copyright === undefined ||
value.copyright.license === undefined ||
value.path === undefined ||
value.mime === undefined) {
// Nope, cycle throught object
H5P.findCopyrights(info, value, contentId);
}
else {
// Found file, add copyrights
var copyrights = new H5P.MediaCopyright(value.copyright);
if (value.width !== undefined && value.height !== undefined) {
copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height));
}
info.addMedia(copyrights);
}
}
}
2018-10-23 11:25:46 +02:00
function buildFromMetadata(data, name, contentId) {
if (data.metadata) {
const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);
if (metadataCopyrights !== undefined) {
if (data.params && data.params.contentName === 'Image' && data.params.file) {
const path = data.params.file.path;
const width = data.params.file.width;
const height = data.params.file.height;
metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height));
}
info.addMedia(metadataCopyrights);
}
}
}
};
2018-10-23 11:25:46 +02:00
H5P.buildMetadataCopyrights = function (metadata) {
if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
var dataset = {
contentType: metadata.contentType,
title: metadata.title,
2018-10-23 11:25:46 +02:00
author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {
return (author.role) ? author.name + ' (' + author.role + ')' : author.name;
}).join(', ') : undefined,
source: metadata.source,
year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,
license: metadata.license,
version: metadata.licenseVersion,
licenseExtras: metadata.licenseExtras,
2018-10-23 11:25:46 +02:00
changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {
return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');
}).join(' / ') : undefined
};
return new H5P.MediaCopyright(dataset);
}
};
/**
* Display a dialog containing the download button and copy button.
*
* @param {H5P.jQuery} $element
* @param {Object} contentData
* @param {Object} library
* @param {Object} instance
* @param {number} contentId
*/
H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
let html = '';
if (contentData.displayOptions.export) {
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>';
}
if (contentData.displayOptions.export && contentData.displayOptions.copy) {
html += '<div class="h5p-horizontal-line-text"><span>or</span></div>';
}
if (contentData.displayOptions.copy) {
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>';
}
const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element);
// Selecting embed code when dialog is opened
H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) {
H5P.jQuery('<a href="https://h5p.org/node/442225" target="_blank">More Info</a>').click(function (e) {
e.stopPropagation();
}).appendTo($dialog.find('h2'));
$dialog.find('.h5p-download-button').click(function () {
window.location.href = contentData.exportUrl;
instance.triggerXAPI('downloaded');
dialog.close();
});
$dialog.find('.h5p-copy-button').click(function () {
const item = new H5P.ClipboardItem(library);
item.contentId = contentId;
H5P.setClipboard(item);
instance.triggerXAPI('copied');
dialog.close();
H5P.attachToastTo(
H5P.jQuery('.h5p-content:first')[0],
H5P.t('contentCopied'),
{
position: {
horizontal: 'centered',
vertical: 'centered',
noOverflowX: true
}
}
);
});
H5P.trigger(instance, 'resize');
}).on('dialog-closed', function () {
H5P.trigger(instance, 'resize');
});
dialog.open();
};
2014-03-26 08:43:29 +01:00
/**
* Display a dialog containing the embed code.
*
* @param {H5P.jQuery} $element
* Element to insert dialog after.
* @param {string} embedCode
* The embed code.
* @param {string} resizeCode
* The advanced resize code
* @param {Object} size
* The content's size.
* @param {number} size.width
* @param {number} size.height
2014-03-26 08:43:29 +01:00
*/
H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) {
2015-03-02 15:01:05 +01:00
var fullEmbedCode = embedCode + resizeCode;
2015-06-25 15:14:55 +02:00
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 type="text" value="' + Math.ceil(size.width) + '" class="h5p-embed-size"/> × <input 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);
2014-03-26 08:43:29 +01:00
// Selecting embed code when dialog is opened
H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
2015-02-25 13:05:45 +01:00
var $inner = $dialog.find('.h5p-inner');
2015-02-27 10:26:57 +01:00
var $scroll = $inner.find('.h5p-scroll-content');
var diff = $scroll.outerHeight() - $scroll.innerHeight();
2015-02-25 13:05:45 +01:00
var positionInner = function () {
H5P.trigger(instance, 'resize');
2015-02-25 13:05:45 +01:00
};
2015-02-25 12:10:07 +01:00
// Handle changing of width/height
var $w = $dialog.find('.h5p-embed-size:eq(0)');
var $h = $dialog.find('.h5p-embed-size:eq(1)');
var getNum = function ($e, d) {
var num = parseFloat($e.val());
if (isNaN(num)) {
return d;
}
return Math.ceil(num);
};
2015-08-26 15:58:49 +02:00
var updateEmbed = function () {
2015-03-02 15:01:05 +01:00
$dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
2015-02-25 12:10:07 +01:00
};
2015-03-02 15:01:05 +01:00
$w.change(updateEmbed);
2015-02-25 12:10:07 +01:00
$h.change(updateEmbed);
updateEmbed();
// Select text and expand textareas
2018-10-23 11:25:46 +02:00
$dialog.find('.h5p-embed-code-container').each(function () {
H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function () {
H5P.jQuery(this).select();
});
2015-03-02 15:01:05 +01:00
});
$dialog.find('.h5p-embed-code-container').eq(0).select();
positionInner();
2015-02-25 12:10:07 +01:00
// Expand advanced embed
2015-08-26 15:58:49 +02:00
var expand = function () {
2015-02-25 12:10:07 +01:00
var $expander = H5P.jQuery(this);
var $content = $expander.next();
if ($content.is(':visible')) {
2019-10-17 15:01:22 +02:00
$expander.removeClass('h5p-open').text(H5P.t('showAdvanced')).attr('aria-expanded', 'true');
2015-02-25 12:10:07 +01:00
$content.hide();
}
else {
2019-10-17 15:01:22 +02:00
$expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
2015-02-25 12:10:07 +01:00
$content.show();
}
2018-10-23 11:25:46 +02:00
$dialog.find('.h5p-embed-code-container').each(function () {
2015-03-02 15:01:05 +01:00
H5P.jQuery(this).css('height', this.scrollHeight + 'px');
});
2015-02-25 13:05:45 +01:00
positionInner();
2015-02-27 08:57:02 +01:00
};
$dialog.find('.h5p-expander').click(expand).keypress(function (event) {
if (event.keyCode === 32) {
2015-02-27 10:26:57 +01:00
expand.apply(this);
return false;
2015-02-27 08:57:02 +01:00
}
2015-02-25 12:10:07 +01:00
});
}).on('dialog-closed', function () {
H5P.trigger(instance, 'resize');
2014-03-26 08:43:29 +01:00
});
2014-03-26 08:43:29 +01:00
dialog.open();
};
/**
* Show a toast message.
*
* The reference element could be dom elements the toast should be attached to,
* or e.g. the document body for general toast messages.
*
* @param {DOM} element Reference element to show toast message for.
* @param {string} message Message to show.
* @param {object} [config] Configuration.
* @param {string} [config.style=h5p-toast] Style name for the tooltip.
* @param {number} [config.duration=3000] Toast message length in ms.
* @param {object} [config.position] Relative positioning of the toast.
* @param {string} [config.position.horizontal=centered] [before|left|centered|right|after].
* @param {string} [config.position.vertical=below] [above|top|centered|bottom|below].
* @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset.
* @param {number} [config.position.offsetVertical=0] Extra vetical offset.
* @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left.
* @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right.
* @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top.
* @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom.
* @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right.
* @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom.
* @param {object} [config.position.overflowReference=document.body] DOM reference for overflow.
*/
H5P.attachToastTo = function (element, message, config) {
if (element === undefined || message === undefined) {
return;
}
const eventPath = function (evt) {
var path = (evt.composedPath && evt.composedPath()) || evt.path;
var target = evt.target;
if (path != null) {
// Safari doesn't include Window, but it should.
return (path.indexOf(window) < 0) ? path.concat(window) : path;
}
if (target === window) {
return [window];
}
function getParents(node, memo) {
memo = memo || [];
var parentNode = node.parentNode;
if (!parentNode) {
return memo;
}
else {
return getParents(parentNode, memo.concat(parentNode));
}
}
return [target].concat(getParents(target), window);
};
/**
* Handle click while toast is showing.
*/
const clickHandler = function (event) {
/*
* A common use case will be to attach toasts to buttons that are clicked.
* The click would remove the toast message instantly without this check.
* Children of the clicked element are also ignored.
*/
var path = eventPath(event);
if (path.indexOf(element) !== -1) {
return;
}
clearTimeout(timer);
removeToast();
};
/**
* Remove the toast message.
*/
const removeToast = function () {
document.removeEventListener('click', clickHandler);
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
};
/**
* Get absolute coordinates for the toast.
*
* @param {DOM} element Reference element to show toast message for.
* @param {DOM} toast Toast element.
* @param {object} [position={}] Relative positioning of the toast message.
* @param {string} [position.horizontal=centered] [before|left|centered|right|after].
* @param {string} [position.vertical=below] [above|top|centered|bottom|below].
* @param {number} [position.offsetHorizontal=0] Extra horizontal offset.
* @param {number} [position.offsetVertical=0] Extra vetical offset.
* @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left.
* @param {boolean} [position.noOverflowRight=false] True to prevent overflow right.
* @param {boolean} [position.noOverflowTop=false] True to prevent overflow top.
* @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom.
* @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right.
* @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom.
* @return {object}
*/
const getToastCoordinates = function (element, toast, position) {
position = position || {};
position.offsetHorizontal = position.offsetHorizontal || 0;
position.offsetVertical = position.offsetVertical || 0;
const toastRect = toast.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
let left = 0;
let top = 0;
// Compute horizontal position
switch (position.horizontal) {
case 'before':
left = elementRect.left - toastRect.width - position.offsetHorizontal;
break;
case 'after':
left = elementRect.left + elementRect.width + position.offsetHorizontal;
break;
case 'left':
left = elementRect.left + position.offsetHorizontal;
break;
case 'right':
left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
break;
case 'centered':
left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
break;
default:
left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
}
// Compute vertical position
switch (position.vertical) {
case 'above':
top = elementRect.top - toastRect.height - position.offsetVertical;
break;
case 'below':
top = elementRect.top + elementRect.height + position.offsetVertical;
break;
case 'top':
top = elementRect.top + position.offsetVertical;
break;
case 'bottom':
top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
break;
case 'centered':
top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
break;
default:
top = elementRect.top + elementRect.height + position.offsetVertical;
}
// Prevent overflow
const overflowElement = document.body;
const bounds = overflowElement.getBoundingClientRect();
if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
left = bounds.x;
}
if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
left = bounds.x + bounds.width - toastRect.width;
}
if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
top = bounds.y;
}
if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
left = bounds.y + bounds.height - toastRect.height;
}
return {left: left, top: top};
};
// Sanitization
config = config || {};
config.style = config.style || 'h5p-toast';
config.duration = config.duration || 3000;
// Build toast
const toast = document.createElement('div');
toast.setAttribute('id', config.style);
toast.classList.add('h5p-toast-disabled');
toast.classList.add(config.style);
const msg = document.createElement('span');
msg.innerHTML = message;
toast.appendChild(msg);
document.body.appendChild(toast);
// The message has to be set before getting the coordinates
const coordinates = getToastCoordinates(element, toast, config.position);
toast.style.left = Math.round(coordinates.left) + 'px';
toast.style.top = Math.round(coordinates.top) + 'px';
toast.classList.remove('h5p-toast-disabled');
const timer = setTimeout(removeToast, config.duration);
// The toast can also be removed by clicking somewhere
document.addEventListener('click', clickHandler);
};
2014-03-26 08:43:29 +01:00
/**
* Copyrights for a H5P Content Library.
*
* @class
2014-03-26 08:43:29 +01:00
*/
H5P.ContentCopyrights = function () {
var label;
var media = [];
var content = [];
2014-03-26 08:43:29 +01:00
/**
* Set label.
2014-03-26 08:43:29 +01:00
*
* @param {string} newLabel
2014-03-26 08:43:29 +01:00
*/
this.setLabel = function (newLabel) {
label = newLabel;
};
2014-03-26 08:43:29 +01:00
/**
* Add sub content.
2014-03-26 08:43:29 +01:00
*
* @param {H5P.MediaCopyright} newMedia
*/
this.addMedia = function (newMedia) {
if (newMedia !== undefined) {
media.push(newMedia);
}
};
/**
* Add sub content in front.
*
* @param {H5P.MediaCopyright} newMedia
*/
this.addMediaInFront = function (newMedia) {
if (newMedia !== undefined) {
media.unshift(newMedia);
}
};
2014-03-26 08:43:29 +01:00
/**
* Add sub content.
2014-03-26 08:43:29 +01:00
*
* @param {H5P.ContentCopyrights} newContent
*/
this.addContent = function (newContent) {
if (newContent !== undefined) {
content.push(newContent);
}
};
2014-03-26 08:43:29 +01:00
/**
* Print content copyright.
2014-03-26 08:43:29 +01:00
*
* @returns {string} HTML.
2014-03-26 08:43:29 +01:00
*/
this.toString = function () {
var html = '';
2014-03-26 08:43:29 +01:00
// Add media rights
for (var i = 0; i < media.length; i++) {
html += media[i];
}
2014-03-26 08:43:29 +01:00
// Add sub content rights
for (i = 0; i < content.length; i++) {
2014-03-26 08:43:29 +01:00
html += content[i];
}
2014-03-26 08:43:29 +01:00
if (html !== '') {
// Add a label to this info
if (label !== undefined) {
html = '<h3>' + label + '</h3>' + html;
}
2014-03-26 08:43:29 +01:00
// Add wrapper
html = '<div class="h5p-content-copyrights">' + html + '</div>';
}
2014-03-26 08:43:29 +01:00
return html;
};
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* A ordered list of copyright fields for media.
*
* @class
* @param {Object} copyright
* Copyright information fields.
* @param {Object} [labels]
* Translation of labels.
* @param {Array} [order]
* Order of the fields.
* @param {Object} [extraFields]
* Add extra copyright fields.
2014-03-26 08:43:29 +01:00
*/
H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
var thumbnail;
var list = new H5P.DefinitionList();
2014-03-26 08:43:29 +01:00
/**
* Get translated label for field.
2014-03-26 08:43:29 +01:00
*
* @private
* @param {string} fieldName
* @returns {string}
2014-03-26 08:43:29 +01:00
*/
var getLabel = function (fieldName) {
if (labels === undefined || labels[fieldName] === undefined) {
return H5P.t(fieldName);
}
2014-03-26 08:43:29 +01:00
return labels[fieldName];
};
2014-03-26 08:43:29 +01:00
/**
2017-05-30 14:12:22 +02:00
* Get humanized value for the license field.
2014-03-26 08:43:29 +01:00
*
* @private
2017-05-30 14:12:22 +02:00
* @param {string} license
* @param {string} [version]
* @returns {string}
2014-03-26 08:43:29 +01:00
*/
2017-05-30 14:12:22 +02:00
var humanizeLicense = function (license, version) {
var copyrightLicense = H5P.copyrightLicenses[license];
// Build license string
var value = '';
if (!(license === 'PD' && version)) {
// Add license label
value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense);
2017-05-30 14:12:22 +02:00
}
// Check for version info
var versionInfo;
if (copyrightLicense.versions) {
2017-06-06 14:21:18 +02:00
if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) {
version = copyrightLicense.versions.default;
}
if (version && copyrightLicense.versions[version]) {
versionInfo = copyrightLicense.versions[version];
}
2017-05-30 14:12:22 +02:00
}
if (versionInfo) {
// Add license version
if (value) {
value += ' ';
}
value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo);
2017-05-30 14:12:22 +02:00
}
// Add link if specified
var link;
if (copyrightLicense.hasOwnProperty('link')) {
2017-05-30 14:12:22 +02:00
link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version);
}
else if (versionInfo && copyrightLicense.hasOwnProperty('link')) {
link = versionInfo.link;
2017-05-30 14:12:22 +02:00
}
if (link) {
value = '<a href="' + link + '" target="_blank">' + value + '</a>';
}
// Generate parenthesis
var parenthesis = '';
if (license !== 'PD' && license !== 'C') {
parenthesis += license;
}
if (version && version !== 'CC0 1.0') {
if (parenthesis && license !== 'GNU GPL') {
parenthesis += ' ';
}
parenthesis += version;
}
if (parenthesis) {
value += ' (' + parenthesis + ')';
}
if (license === 'C') {
value += ' &copy;';
2014-03-26 08:43:29 +01:00
}
2014-03-26 08:43:29 +01:00
return value;
};
2014-03-26 08:43:29 +01:00
if (copyright !== undefined) {
// Add the extra fields
for (var field in extraFields) {
if (extraFields.hasOwnProperty(field)) {
copyright[field] = extraFields[field];
}
}
2014-03-26 08:43:29 +01:00
if (order === undefined) {
// Set default order
order = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes'];
2014-03-26 08:43:29 +01:00
}
2014-03-26 08:43:29 +01:00
for (var i = 0; i < order.length; i++) {
var fieldName = order[i];
if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') {
2017-05-30 14:12:22 +02:00
var humanValue = copyright[fieldName];
if (fieldName === 'license') {
humanValue = humanizeLicense(copyright.license, copyright.version);
}
if (fieldName === 'source') {
humanValue = (humanValue) ? '<a href="' + humanValue + '" target="_blank">' + humanValue + '</a>' : undefined;
}
2017-05-30 14:12:22 +02:00
list.add(new H5P.Field(getLabel(fieldName), humanValue));
2014-03-26 08:43:29 +01:00
}
}
}
2014-03-26 08:43:29 +01:00
/**
* Set thumbnail.
2014-03-26 08:43:29 +01:00
*
* @param {H5P.Thumbnail} newThumbnail
*/
this.setThumbnail = function (newThumbnail) {
thumbnail = newThumbnail;
};
2014-03-26 08:43:29 +01:00
/**
* Checks if this copyright is undisclosed.
2014-03-26 08:43:29 +01:00
* I.e. only has the license attribute set, and it's undisclosed.
*
* @returns {boolean}
2014-03-26 08:43:29 +01:00
*/
this.undisclosed = function () {
if (list.size() === 1) {
var field = list.get(0);
2017-05-30 14:12:22 +02:00
if (field.getLabel() === getLabel('license') && field.getValue() === humanizeLicense('U')) {
2014-03-26 08:43:29 +01:00
return true;
}
}
return false;
};
2014-03-26 08:43:29 +01:00
/**
* Print media copyright.
2014-03-26 08:43:29 +01:00
*
* @returns {string} HTML.
2014-03-26 08:43:29 +01:00
*/
this.toString = function () {
var html = '';
2014-03-26 08:43:29 +01:00
if (this.undisclosed()) {
return html; // No need to print a copyright with a single undisclosed license.
}
2014-03-26 08:43:29 +01:00
if (thumbnail !== undefined) {
html += thumbnail;
}
html += list;
2014-03-26 08:43:29 +01:00
if (html !== '') {
html = '<div class="h5p-media-copyright">' + html + '</div>';
}
2014-03-26 08:43:29 +01:00
return html;
};
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* A simple and elegant class for creating thumbnails of images.
2014-03-26 08:43:29 +01:00
*
* @class
* @param {string} source
* @param {number} width
* @param {number} height
2014-03-26 08:43:29 +01:00
*/
H5P.Thumbnail = function (source, width, height) {
var thumbWidth, thumbHeight = 100;
if (width !== undefined) {
thumbWidth = Math.round(thumbHeight * (width / height));
}
/**
* Print thumbnail.
2014-03-26 08:43:29 +01:00
*
* @returns {string} HTML.
2014-03-26 08:43:29 +01:00
*/
this.toString = function () {
return '<img src="' + source + '" alt="' + H5P.t('thumbnail') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
};
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* Simple data structure class for storing a single field.
*
* @class
* @param {string} label
* @param {string} value
2014-03-26 08:43:29 +01:00
*/
H5P.Field = function (label, value) {
/**
* Public. Get field label.
*
* @returns {String}
*/
2014-03-26 08:43:29 +01:00
this.getLabel = function () {
return label;
};
2014-03-26 08:43:29 +01:00
/**
* Public. Get field value.
*
* @returns {String}
*/
2014-03-26 08:43:29 +01:00
this.getValue = function () {
return value;
};
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* Simple class for creating a definition list.
*
* @class
2014-03-26 08:43:29 +01:00
*/
H5P.DefinitionList = function () {
var fields = [];
2014-03-26 08:43:29 +01:00
/**
* Add field to list.
2014-03-26 08:43:29 +01:00
*
* @param {H5P.Field} field
*/
this.add = function (field) {
fields.push(field);
};
2014-03-26 08:43:29 +01:00
/**
* Get Number of fields.
2014-03-26 08:43:29 +01:00
*
* @returns {number}
2014-03-26 08:43:29 +01:00
*/
this.size = function () {
return fields.length;
};
2014-03-26 08:43:29 +01:00
/**
* Get field at given index.
2014-03-26 08:43:29 +01:00
*
* @param {number} index
* @returns {H5P.Field}
2014-03-26 08:43:29 +01:00
*/
this.get = function (index) {
return fields[index];
};
2014-03-26 08:43:29 +01:00
/**
* Print definition list.
2014-03-26 08:43:29 +01:00
*
* @returns {string} HTML.
2014-03-26 08:43:29 +01:00
*/
this.toString = function () {
var html = '';
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
html += '<dt>' + field.getLabel() + '</dt><dd>' + field.getValue() + '</dd>';
}
return (html === '' ? html : '<dl class="h5p-definition-list">' + html + '</dl>');
};
2014-05-02 15:45:45 +02:00
};
2014-03-26 08:43:29 +01:00
/**
* THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
*
* Helper object for keeping coordinates in the same format all over.
*
* @deprecated
* Will be removed march 2016.
* @class
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
2014-03-26 08:43:29 +01:00
*/
H5P.Coords = function (x, y, w, h) {
2013-01-17 09:01:43 +01:00
if ( !(this instanceof H5P.Coords) )
return new H5P.Coords(x, y, w, h);
/** @member {number} */
2013-01-17 09:01:43 +01:00
this.x = 0;
/** @member {number} */
2013-01-17 09:01:43 +01:00
this.y = 0;
/** @member {number} */
2013-01-17 09:01:43 +01:00
this.w = 1;
/** @member {number} */
2013-01-17 09:01:43 +01:00
this.h = 1;
if (typeof(x) === 'object') {
2013-01-17 09:01:43 +01:00
this.x = x.x;
this.y = x.y;
this.w = x.w;
this.h = x.h;
2018-10-23 11:25:46 +02:00
}
else {
2013-01-17 09:01:43 +01:00
if (x !== undefined) {
this.x = x;
}
if (y !== undefined) {
this.y = y;
}
if (w !== undefined) {
this.w = w;
}
if (h !== undefined) {
this.h = h;
}
2013-01-17 09:01:43 +01:00
}
return this;
};
2013-03-06 15:59:02 +01:00
/**
* Parse library string into values.
*
* @param {string} library
* library in the format "machineName majorVersion.minorVersion"
* @returns {Object}
* library as an object with machineName, majorVersion and minorVersion properties
* return false if the library parameter is invalid
2013-03-06 15:59:02 +01:00
*/
H5P.libraryFromString = function (library) {
var regExp = /(.+)\s(\d+)\.(\d+)$/g;
2013-03-06 15:59:02 +01:00
var res = regExp.exec(library);
if (res !== null) {
return {
'machineName': res[1],
'majorVersion': parseInt(res[2]),
'minorVersion': parseInt(res[3])
2013-03-06 15:59:02 +01:00
};
}
else {
return false;
}
};
2013-02-07 17:50:17 +01:00
/**
* Get the path to the library
*
* @param {string} library
2014-03-26 08:43:29 +01:00
* The library identifier in the format "machineName-majorVersion.minorVersion".
* @returns {string}
* The full path to the library.
*/
H5P.getLibraryPath = function (library) {
if (H5PIntegration.urlLibraries !== undefined) {
// This is an override for those implementations that has a different libraries URL, e.g. Moodle
return H5PIntegration.urlLibraries + '/' + library;
}
else {
return H5PIntegration.url + '/libraries/' + library;
}
};
/**
* Recursivly clone the given object.
2013-04-26 17:27:35 +02:00
*
* @param {Object|Array} object
* Object to clone.
* @param {boolean} [recursive]
* @returns {Object|Array}
* A clone of object.
*/
H5P.cloneObject = function (object, recursive) {
// TODO: Consider if this needs to be in core. Doesn't $.extend do the same?
var clone = object instanceof Array ? [] : {};
2013-04-26 17:27:35 +02:00
for (var i in object) {
if (object.hasOwnProperty(i)) {
if (recursive !== undefined && recursive && typeof object[i] === 'object') {
clone[i] = H5P.cloneObject(object[i], recursive);
}
else {
clone[i] = object[i];
}
}
}
2013-04-26 17:27:35 +02:00
return clone;
};
/**
* Remove all empty spaces before and after the value.
*
* @param {string} value
* @returns {string}
*/
H5P.trim = function (value) {
return value.replace(/^\s+|\s+$/g, '');
// TODO: Only include this or String.trim(). What is best?
// I'm leaning towards implementing the missing ones: http://kangax.github.io/compat-table/es5/
// So should we make this function deprecated?
};
/**
* Check if JavaScript path/key is loaded.
*
* @param {string} path
* @returns {boolean}
*/
H5P.jsLoaded = function (path) {
2015-02-27 13:59:42 +01:00
H5PIntegration.loadedJs = H5PIntegration.loadedJs || [];
return H5P.jQuery.inArray(path, H5PIntegration.loadedJs) !== -1;
};
/**
* Check if styles path/key is loaded.
*
* @param {string} path
* @returns {boolean}
*/
H5P.cssLoaded = function (path) {
2015-02-27 13:59:42 +01:00
H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
};
/**
* Shuffle an array in place.
*
* @param {Array} array
* Array to shuffle
* @returns {Array}
* The passed array is returned for chaining.
*/
H5P.shuffleArray = function (array) {
// TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it.
if (!(array instanceof Array)) {
return;
}
2013-07-25 01:50:16 +02:00
var i = array.length, j, tempi, tempj;
2013-01-17 09:01:43 +01:00
if ( i === 0 ) return false;
while ( --i ) {
j = Math.floor( Math.random() * ( i + 1 ) );
2013-07-25 01:50:16 +02:00
tempi = array[i];
tempj = array[j];
array[i] = tempj;
array[j] = tempi;
2013-01-17 09:01:43 +01:00
}
2013-07-25 01:50:16 +02:00
return array;
2013-01-17 09:01:43 +01:00
};
/**
* Post finished results for user.
*
* @deprecated
* Do not use this function directly, trigger the finish event instead.
* Will be removed march 2016
* @param {number} contentId
* Identifies the content
* @param {number} score
* Achieved score/points
* @param {number} maxScore
* The maximum score/points that can be achieved
* @param {number} [time]
* Reported time consumption/usage
*/
H5P.setFinished = function (contentId, score, maxScore, time) {
var validScore = typeof score === 'number' || score instanceof Number;
if (validScore && H5PIntegration.postUserStatistics === true) {
/**
* Return unix timestamp for the given JS Date.
*
* @private
* @param {Date} date
* @returns {Number}
*/
var toUnix = function (date) {
return Math.round(date.getTime() / 1000);
};
// Post the results
const data = {
contentId: contentId,
score: score,
maxScore: maxScore,
opened: toUnix(H5P.opened[contentId]),
finished: toUnix(new Date()),
2016-06-13 10:31:09 +02:00
time: time
};
H5P.jQuery.post(H5PIntegration.ajax.setFinished, data)
.fail(function () {
H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data);
});
}
};
2013-01-17 09:01:43 +01:00
// Add indexOf to browsers that lack them. (IEs)
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (needle) {
for (var i = 0; i < this.length; i++) {
if (this[i] === needle) {
return i;
}
}
return -1;
};
2013-01-17 09:01:43 +01:00
}
// Need to define trim() since this is not available on older IEs,
// and trim is used in several libs
if (String.prototype.trim === undefined) {
String.prototype.trim = function () {
return H5P.trim(this);
};
}
/**
* Trigger an event on an instance
*
* Helper function that triggers an event if the instance supports event handling
*
* @param {Object} instance
* Instance of H5P content
* @param {string} eventType
* Type of event to trigger
* @param {*} data
* @param {Object} extras
*/
H5P.trigger = function (instance, eventType, data, extras) {
// Try new event system first
if (instance.trigger !== undefined) {
2015-04-07 19:32:44 +02:00
instance.trigger(eventType, data, extras);
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
2015-02-20 10:26:33 +01:00
instance.$.trigger(eventType);
}
};
/**
* Register an event handler
*
* Helper function that registers an event handler for an event type if
* the instance supports event handling
*
* @param {Object} instance
* Instance of H5P content
* @param {string} eventType
* Type of event to listen for
* @param {H5P.EventCallback} handler
* Callback that gets triggered for events of the specified type
*/
H5P.on = function (instance, eventType, handler) {
// Try new event system first
if (instance.on !== undefined) {
instance.on(eventType, handler);
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.on !== undefined) {
2015-02-20 10:26:33 +01:00
instance.$.on(eventType, handler);
}
};
/**
* Generate random UUID
2015-04-07 19:32:44 +02:00
*
* @returns {string} UUID
*/
H5P.createUUID = function () {
2018-10-23 11:25:46 +02:00
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
var random = Math.random()*16|0, newChar = char === 'x' ? random : (random&0x3|0x8);
return newChar.toString(16);
});
};
/**
* Create title
*
* @param {string} rawTitle
* @param {number} maxLength
* @returns {string}
*/
H5P.createTitle = function (rawTitle, maxLength) {
if (!rawTitle) {
return '';
}
2015-03-22 12:45:03 +01:00
if (maxLength === undefined) {
maxLength = 60;
}
var title = H5P.jQuery('<div></div>')
.text(
// Strip tags
rawTitle.replace(/(<([^>]+)>)/ig,"")
// Escape
).text();
if (title.length > maxLength) {
title = title.substr(0, maxLength - 3) + '...';
}
return title;
2015-03-21 16:45:38 +01:00
};
2015-04-07 19:32:44 +02:00
// Wrap in privates
(function ($) {
/**
* Creates ajax requests for inserting, updateing and deleteing
* content user data.
*
* @private
* @param {number} contentId What content to store the data for.
* @param {string} dataType Identifies the set of data for this content.
* @param {string} subContentId Identifies sub content
* @param {function} [done] Callback when ajax is done.
* @param {object} [data] To be stored for future use.
* @param {boolean} [preload=false] Data is loaded when content is loaded.
* @param {boolean} [invalidate=false] Data is invalidated when content changes.
* @param {boolean} [async=true]
*/
function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
if (H5PIntegration.user === undefined) {
// Not logged in, no use in saving.
done('Not signed in.');
return;
}
2015-04-07 19:32:44 +02:00
var options = {
url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
2015-04-07 19:32:44 +02:00
dataType: 'json',
async: async === undefined ? true : async
};
if (data !== undefined) {
options.type = 'POST';
options.data = {
data: (data === null ? 0 : data),
preload: (preload ? 1 : 0),
2016-06-13 10:31:09 +02:00
invalidate: (invalidate ? 1 : 0)
2015-04-07 19:32:44 +02:00
};
}
else {
options.type = 'GET';
}
if (done !== undefined) {
2015-08-26 15:58:49 +02:00
options.error = function (xhr, error) {
2015-04-07 19:32:44 +02:00
done(error);
};
options.success = function (response) {
if (!response.success) {
2016-02-22 12:01:18 +01:00
done(response.message);
2015-04-07 19:32:44 +02:00
return;
}
if (response.data === false || response.data === undefined) {
done();
return;
}
done(undefined, response.data);
};
}
$.ajax(options);
}
2015-04-07 19:32:44 +02:00
/**
* Get user data for given content.
*
* @param {number} contentId
* What content to get data for.
* @param {string} dataId
* Identifies the set of data for this content.
* @param {function} done
* Callback with error and data parameters.
* @param {string} [subContentId]
* Identifies which data belongs to sub content.
2015-04-07 19:32:44 +02:00
*/
H5P.getUserData = function (contentId, dataId, done, subContentId) {
if (!subContentId) {
subContentId = 0; // Default
}
2015-11-04 17:30:58 +01:00
H5PIntegration.contents = H5PIntegration.contents || {};
var content = H5PIntegration.contents['cid-' + contentId] || {};
2015-04-07 19:32:44 +02:00
var preloadedData = content.contentUserData;
2016-07-15 16:14:32 +02:00
if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) {
2015-04-07 19:32:44 +02:00
if (preloadedData[subContentId][dataId] === 'RESET') {
done(undefined, null);
return;
}
try {
done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
}
catch (err) {
done(err);
}
}
else {
2015-08-26 15:58:49 +02:00
contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
2015-04-07 19:32:44 +02:00
if (err || data === undefined) {
done(err, data);
return; // Error or no data
}
// Cache in preloaded
if (content.contentUserData === undefined) {
2015-04-22 10:58:59 +02:00
content.contentUserData = preloadedData = {};
2015-04-07 19:32:44 +02:00
}
if (preloadedData[subContentId] === undefined) {
preloadedData[subContentId] = {};
}
preloadedData[subContentId][dataId] = data;
// Done. Try to decode JSON
try {
done(undefined, JSON.parse(data));
}
catch (e) {
done(e);
}
});
}
};
/**
* Async error handling.
*
* @callback H5P.ErrorCallback
* @param {*} error
*/
2015-04-07 19:32:44 +02:00
/**
* Set user data for given content.
*
* @param {number} contentId
* What content to get data for.
* @param {string} dataId
* Identifies the set of data for this content.
* @param {Object} data
* The data that is to be stored.
* @param {Object} [extras]
* Extra properties
* @param {string} [extras.subContentId]
* Identifies which data belongs to sub content.
* @param {boolean} [extras.preloaded=true]
* If the data should be loaded when content is loaded.
* @param {boolean} [extras.deleteOnChange=false]
* If the data should be invalidated when the content changes.
* @param {H5P.ErrorCallback} [extras.errorCallback]
* Callback with error as parameters.
* @param {boolean} [extras.async=true]
2015-04-07 19:32:44 +02:00
*/
H5P.setUserData = function (contentId, dataId, data, extras) {
var options = H5P.jQuery.extend(true, {}, {
subContentId: 0,
preloaded: true,
deleteOnChange: false,
async: true
}, extras);
2015-04-07 19:32:44 +02:00
try {
data = JSON.stringify(data);
}
catch (err) {
if (options.errorCallback) {
options.errorCallback(err);
}
2015-04-07 19:32:44 +02:00
return; // Failed to serialize.
}
var content = H5PIntegration.contents['cid-' + contentId];
2015-11-04 17:30:58 +01:00
if (content === undefined) {
content = H5PIntegration.contents['cid-' + contentId] = {};
}
2015-04-07 19:32:44 +02:00
if (!content.contentUserData) {
content.contentUserData = {};
}
var preloadedData = content.contentUserData;
if (preloadedData[options.subContentId] === undefined) {
preloadedData[options.subContentId] = {};
2015-04-07 19:32:44 +02:00
}
if (data === preloadedData[options.subContentId][dataId]) {
2015-04-07 19:32:44 +02:00
return; // No need to save this twice.
}
preloadedData[options.subContentId][dataId] = data;
2018-10-23 11:25:46 +02:00
contentUserDataAjax(contentId, dataId, options.subContentId, function (error) {
if (options.errorCallback && error) {
options.errorCallback(error);
2015-04-07 19:32:44 +02:00
}
}, data, options.preloaded, options.deleteOnChange, options.async);
2015-04-07 19:32:44 +02:00
};
/**
* Delete user data for given content.
*
* @param {number} contentId
* What content to remove data for.
* @param {string} dataId
* Identifies the set of data for this content.
* @param {string} [subContentId]
* Identifies which data belongs to sub content.
2015-04-07 19:32:44 +02:00
*/
H5P.deleteUserData = function (contentId, dataId, subContentId) {
if (!subContentId) {
subContentId = 0; // Default
}
// Remove from preloaded/cache
var preloadedData = H5PIntegration.contents['cid-' + contentId].contentUserData;
if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
delete preloadedData[subContentId][dataId];
}
contentUserDataAjax(contentId, dataId, subContentId, undefined, null);
};
/**
* Function for getting content for a certain ID
*
* @param {number} contentId
* @return {Object}
*/
H5P.getContentForInstance = function (contentId) {
var key = 'cid-' + contentId;
var exists = H5PIntegration && H5PIntegration.contents &&
H5PIntegration.contents[key];
return exists ? H5PIntegration.contents[key] : undefined;
};
/**
* Prepares the content parameters for storing in the clipboard.
*
* @class
* @param {Object} parameters The parameters for the content to store
* @param {string} [genericProperty] If only part of the parameters are generic, which part
* @param {string} [specificKey] If the parameters are specific, what content type does it fit
* @returns {Object} Ready for the clipboard
*/
H5P.ClipboardItem = function (parameters, genericProperty, specificKey) {
var self = this;
/**
* Set relative dimensions when params contains a file with a width and a height.
* Very useful to be compatible with wysiwyg editors.
*
* @private
*/
var setDimensionsFromFile = function () {
if (!self.generic) {
return;
}
var params = self.specific[self.generic];
if (!params.params.file || !params.params.file.width || !params.params.file.height) {
return;
}
self.width = 20; // %
self.height = (params.params.file.height / params.params.file.width) * self.width;
};
if (!genericProperty) {
genericProperty = 'action';
parameters = {
action: parameters
};
}
self.specific = parameters;
if (genericProperty && parameters[genericProperty]) {
self.generic = genericProperty;
}
if (specificKey) {
self.from = specificKey;
}
if (window.H5PEditor && H5PEditor.contentId) {
self.contentId = H5PEditor.contentId;
}
if (!self.specific.width && !self.specific.height) {
setDimensionsFromFile();
}
};
/**
* Store item in the H5P Clipboard.
*
* @param {H5P.ClipboardItem|*} clipboardItem
*/
H5P.clipboardify = function (clipboardItem) {
if (!(clipboardItem instanceof H5P.ClipboardItem)) {
clipboardItem = new H5P.ClipboardItem(clipboardItem);
}
H5P.setClipboard(clipboardItem);
};
/**
* Retrieve parsed clipboard data.
*
* @return {Object}
*/
H5P.getClipboard = function () {
return parseClipboard();
};
/**
* Set item in the H5P Clipboard.
*
* @param {H5P.ClipboardItem|object} clipboardItem - Data to be set.
*/
H5P.setClipboard = function (clipboardItem) {
localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem));
// Trigger an event so all 'Paste' buttons may be enabled.
H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
};
/**
* Get config for a library
*
* @param string machineName
* @return Object
*/
H5P.getLibraryConfig = function (machineName) {
var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName];
return hasConfig ? H5PIntegration.libraryConfig[machineName] : {};
};
/**
* Get item from the H5P Clipboard.
*
* @private
* @return {Object}
*/
var parseClipboard = function () {
var clipboardData = localStorage.getItem('h5pClipboard');
if (!clipboardData) {
return;
}
// Try to parse clipboard dat
try {
clipboardData = JSON.parse(clipboardData);
}
catch (err) {
console.error('Unable to parse JSON from clipboard.', err);
return;
}
// Update file URLs and reset content Ids
recursiveUpdate(clipboardData.specific, function (path) {
var isTmpFile = (path.substr(-4, 4) === '#tmp');
2019-05-08 14:16:22 +02:00
if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) {
// Comes from existing content
if (H5PEditor.contentId) {
// .. to existing content
return '../' + clipboardData.contentId + '/' + path;
}
else {
// .. to new content
return (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/' + path;
}
}
return path; // Will automatically be looked for in tmp folder
});
if (clipboardData.generic) {
// Use reference instead of key
clipboardData.generic = clipboardData.specific[clipboardData.generic];
}
return clipboardData;
};
/**
* Update file URLs and reset content IDs.
* Useful when copying content.
*
* @private
* @param {object} params Reference
* @param {function} handler Modifies the path to work when pasted
*/
var recursiveUpdate = function (params, handler) {
for (var prop in params) {
if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
var obj = params[prop];
if (obj.path !== undefined && obj.mime !== undefined) {
obj.path = handler(obj.path);
}
else {
if (obj.library !== undefined && obj.subContentId !== undefined) {
// Avoid multiple content with same ID
delete obj.subContentId;
}
recursiveUpdate(obj, handler);
}
}
}
};
2015-04-07 19:32:44 +02:00
// Init H5P when page is fully loadded
$(document).ready(function () {
window.addEventListener('storage', function (event) {
// Pick up clipboard changes from other tabs
if (event.key === 'h5pClipboard') {
// Trigger an event so all 'Paste' buttons may be enabled.
H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null});
}
});
var ccVersions = {
'default': '4.0',
'4.0': H5P.t('licenseCC40'),
'3.0': H5P.t('licenseCC30'),
'2.5': H5P.t('licenseCC25'),
'2.0': H5P.t('licenseCC20'),
'1.0': H5P.t('licenseCC10'),
};
/**
* Maps copyright license codes to their human readable counterpart.
*
* @type {Object}
*/
H5P.copyrightLicenses = {
'U': H5P.t('licenseU'),
'CC BY': {
label: H5P.t('licenseCCBY'),
link: 'http://creativecommons.org/licenses/by/:version',
versions: ccVersions
},
'CC BY-SA': {
label: H5P.t('licenseCCBYSA'),
link: 'http://creativecommons.org/licenses/by-sa/:version',
versions: ccVersions
},
'CC BY-ND': {
label: H5P.t('licenseCCBYND'),
link: 'http://creativecommons.org/licenses/by-nd/:version',
versions: ccVersions
},
'CC BY-NC': {
label: H5P.t('licenseCCBYNC'),
link: 'http://creativecommons.org/licenses/by-nc/:version',
versions: ccVersions
},
'CC BY-NC-SA': {
label: H5P.t('licenseCCBYNCSA'),
link: 'http://creativecommons.org/licenses/by-nc-sa/:version',
versions: ccVersions
},
'CC BY-NC-ND': {
label: H5P.t('licenseCCBYNCND'),
link: 'http://creativecommons.org/licenses/by-nc-nd/:version',
versions: ccVersions
},
'CC0 1.0': {
label: H5P.t('licenseCC010'),
link: 'https://creativecommons.org/publicdomain/zero/1.0/'
},
'GNU GPL': {
label: H5P.t('licenseGPL'),
link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html',
linkVersions: {
'v3': '3.0',
'v2': '2.0',
'v1': '1.0'
},
versions: {
'default': 'v3',
'v3': H5P.t('licenseV3'),
'v2': H5P.t('licenseV2'),
'v1': H5P.t('licenseV1')
}
},
'PD': {
label: H5P.t('licensePD'),
versions: {
'CC0 1.0': {
label: H5P.t('licenseCC010'),
link: 'https://creativecommons.org/publicdomain/zero/1.0/'
},
'CC PDM': {
label: H5P.t('licensePDM'),
link: 'https://creativecommons.org/publicdomain/mark/1.0/'
}
}
},
'ODC PDDL': '<a href="http://opendatacommons.org/licenses/pddl/1.0/" target="_blank">Public Domain Dedication and Licence</a>',
'CC PDM': {
label: H5P.t('licensePDM'),
link: 'https://creativecommons.org/publicdomain/mark/1.0/'
},
'C': H5P.t('licenseC'),
};
/**
* Indicates if H5P is embedded on an external page using iframe.
* @member {boolean} H5P.externalEmbed
*/
// Relay events to top window. This must be done before H5P.init
// since events may be fired on initialization.
if (H5P.isFramed && H5P.externalEmbed === false) {
H5P.externalDispatcher.on('*', function (event) {
window.parent.H5P.externalDispatcher.trigger.call(this, event);
});
}
/**
* Prevent H5P Core from initializing. Must be overriden before document ready.
* @member {boolean} H5P.preventInit
*/
2015-04-07 19:32:44 +02:00
if (!H5P.preventInit) {
// Note that this start script has to be an external resource for it to
// load in correct order in IE9.
H5P.init(document.body);
}
if (H5PIntegration.saveFreq !== false) {
// When was the last state stored
var lastStoredOn = 0;
2015-04-07 19:32:44 +02:00
// Store the current state of the H5P when leaving the page.
var storeCurrentState = function () {
// Make sure at least 250 ms has passed since last save
var currentTime = new Date().getTime();
if (currentTime - lastStoredOn > 250) {
lastStoredOn = currentTime;
for (var i = 0; i < H5P.instances.length; i++) {
var instance = H5P.instances[i];
if (instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function') {
var state = instance.getCurrentState();
if (state !== undefined) {
// Async is not used to prevent the request from being cancelled.
H5P.setUserData(instance.contentId, 'state', state, {deleteOnChange: true, async: false});
}
2015-04-07 19:32:44 +02:00
}
}
}
};
// iPad does not support beforeunload, therefore using unload
H5P.$window.one('beforeunload unload', function () {
// Only want to do this once
H5P.$window.off('pagehide beforeunload unload');
storeCurrentState();
2015-04-07 19:32:44 +02:00
});
// pagehide is used on iPad when tabs are switched
H5P.$window.on('pagehide', storeCurrentState);
2015-04-07 19:32:44 +02:00
}
});
})(H5P.jQuery);