h5p-php-library/js/h5p.js

1767 lines
51 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*jshint multistr: true */
// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?
var H5P = H5P || {};
// Determine if we're inside an iframe.
H5P.isFramed = (window.self !== window.top);
// Useful jQuery object.
H5P.$window = H5P.jQuery(window);
H5P.instances = [];
// Detect if we support fullscreen, and what prefix to use.
if (document.documentElement.requestFullScreen) {
H5P.fullScreenBrowserPrefix = '';
}
else if (document.documentElement.webkitRequestFullScreen) {
H5P.safariBrowser = navigator.userAgent.match(/Version\/(\d)/);
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';
}
}
else if (document.documentElement.mozRequestFullScreen) {
H5P.fullScreenBrowserPrefix = 'moz';
}
else if (document.documentElement.msRequestFullscreen) {
H5P.fullScreenBrowserPrefix = 'ms';
}
/** @const {Number} */
H5P.DISABLE_NONE = 0;
/** @const {Number} */
H5P.DISABLE_FRAME = 1;
/** @const {Number} */
H5P.DISABLE_DOWNLOAD = 2;
/** @const {Number} */
H5P.DISABLE_EMBED = 4;
/** @const {Number} */
H5P.DISABLE_COPYRIGHT = 8;
/** @const {Number} */
H5P.DISABLE_ABOUT = 16;
/**
* Keep track of when the H5Ps where started.
*
* @type {Array}
*/
H5P.opened = {};
/**
* Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found.
*/
H5P.init = function (target) {
// Useful jQuery object.
if (H5P.$body === undefined) {
H5P.$body = H5P.jQuery(document.body);
}
// Determine if we can use full screen
if (H5P.canHasFullScreen === undefined) {
// Restricts fullscreen when embedded.
// (embedded doesn't support semi-fullscreen solution)
H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled) ? true : false) : true;
// 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
}
// H5Ps added in normal DIV.
var $containers = H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
var $element = H5P.jQuery(this).addClass('h5p-initialized');
var $container = H5P.jQuery('<div class="h5p-container"></div>').appendTo($element);
var contentId = $element.data('content-id');
var contentData = H5PIntegration.contents['cid-' + contentId];
if (contentData === undefined) {
return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
}
var library = {
library: contentData.library,
params: JSON.parse(contentData.jsonContent)
};
H5P.getUserData(contentId, 'state', function (err, previousState) {
if (previousState) {
library.userDatas = {
state: previousState
};
}
else if (previousState === null && H5PIntegration.saveFreq) {
// 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) {
$dialog.find('.h5p-dialog-ok-button').click(function () {
dialog.close();
}).keypress(function (event) {
if (event.which === 32) {
dialog.close();
}
});
});
dialog.open();
}
});
// Create new instance.
var instance = H5P.newRunnable(library, contentId, $container, true);
// Check if we should add and display a fullscreen button for this H5P.
if (contentData.fullScreen == 1 && H5P.canHasFullScreen) {
H5P.jQuery('<div class="h5p-content-controls"><div role="button" tabindex="1" class="h5p-enable-fullscreen" title="' + H5P.t('fullscreen') + '"></div></div>').prependTo($container).children().click(function () {
H5P.fullScreen($container, instance);
});
}
// Create action bar
var $actions = H5P.jQuery('<ul class="h5p-actions"></ul>');
if (!(contentData.disable & H5P.DISABLE_DOWNLOAD)) {
// Add export button
H5P.jQuery('<li class="h5p-button h5p-export" role="button" tabindex="1" title="' + H5P.t('downloadDescription') + '">' + H5P.t('download') + '</li>').appendTo($actions).click(function () {
window.location.href = contentData.exportUrl;
});
}
if (!(contentData.disable & H5P.DISABLE_COPYRIGHT) && instance.getCopyrights !== undefined) {
// Add copyrights button
H5P.jQuery('<li class="h5p-button h5p-copyrights" role="button" tabindex="1" title="' + H5P.t('copyrightsDescription') + '">' + H5P.t('copyrights') + '</li>').appendTo($actions).click(function () {
H5P.openCopyrightsDialog($actions, instance, library.params, contentId);
});
}
if (!(contentData.disable & H5P.DISABLE_EMBED)) {
// Add embed button
H5P.jQuery('<li class="h5p-button h5p-embed" role="button" tabindex="1" title="' + H5P.t('embedDescription') + '">' + H5P.t('embed') + '</li>').appendTo($actions).click(function () {
H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
width: $container.width(),
height: $container.height()
});
});
}
if (!(contentData.disable & H5P.DISABLE_ABOUT)) {
// Add about H5P button icon
H5P.jQuery('<li><a class="h5p-link" href="http://h5p.org" target="_blank" title="' + H5P.t('h5pDescription') + '"></a></li>').appendTo($actions);
}
// Insert action bar if it has any content
if ($actions.children().length) {
$actions.insertAfter($container);
}
else {
$element.addClass('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);
}
});
// Listen for xAPI events.
H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
// Auto save current state if supported
if (H5PIntegration.saveFreq !== false && (
instance.getCurrentState instanceof Function ||
typeof instance.getCurrentState === 'function')) {
var saveTimer, save = function () {
var state = instance.getCurrentState();
if (state !== undefined) {
H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
}
if (H5PIntegration.saveFreq) {
// Continue autosave
saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
}
};
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);
}
});
}
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.parent.document.getElementById('h5p-iframe-' + contentId);
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';
// 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;
// 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
H5P.communicator.on('resizePrepared', function (data) {
H5P.communicator.send('resize', {
height: 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');
}
else {
H5P.communicator.send('hello');
}
}, 0);
});
}
}
if (!H5P.isFramed || H5P.externalEmbed === false) {
// Resize everything when window is resized.
H5P.jQuery(window.top).resize(function () {
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');
}
});
}
H5P.instances.push(instance);
// Resize content.
H5P.trigger(instance, 'resize');
});
// Insert H5Ps that should be in iframes.
H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id');
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>');
this.contentDocument.close();
});
};
/**
* 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++) {
tags += '<link rel="stylesheet" href="' + styles[i] + '">';
}
return tags;
};
var createScriptTags = function (scripts) {
var tags = '';
for (var i = 0; i < scripts.length; i++) {
tags += '<script src="' + scripts[i] + '"></script>';
}
return tags;
};
return createStyleTags(H5PIntegration.core.styles) +
createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
createScriptTags(H5PIntegration.core.scripts) +
createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
'<script>H5PIntegration = window.top.H5PIntegration; var H5P = H5P || {}; H5P.externalEmbed = false;</script>';
};
H5P.communicator = (function () {
/**
* @class
*/
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.
*
* @public
* @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.
*
* @public
* @param {String} action
* @param {Object} [data] payload
*/
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);
})();
/**
* Enable full screen for the given h5p.
*
* @param {jQuery} $element Content container.
* @param {object} instance
* @param {function} exitCallback Callback function called when user exits fullscreen.
* @param {jQuery} $body For internal use. Gives the body of the iframe.
* @returns {undefined}
*/
H5P.fullScreen = function ($element, instance, exitCallback, body) {
if (H5P.exitFullScreen !== undefined) {
return; // Cannot enter new fullscreen until previous is over
}
if (H5P.isFramed && H5P.externalEmbed === false) {
// Trigger resize on wrapper in parent window.
window.top.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get());
H5P.isFullscreen = true;
H5P.exitFullScreen = function () {
window.top.H5P.exitFullScreen();
};
H5P.on(instance, 'exitFullScreen', function () {
H5P.isFullscreen = false;
H5P.exitFullScreen = undefined;
});
return;
}
var $container = $element;
var $classes, $iframe;
if (body === undefined) {
$body = H5P.$body;
}
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.
}
$classes = $element.add(H5P.$body).add($classes);
/**
* Prepare for resize by setting the correct styles.
*
* @param {String} classes CSS
*/
var before = function (classes) {
$classes.addClass(classes);
if ($iframe !== undefined) {
// Set iframe to its default size(100%).
$iframe.css('height', '');
}
};
/**
* Gets called when fullscreen mode has been entered.
* Resizes and sets focus on content.
*/
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.
*
* @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;
if (exitCallback !== undefined) {
exitCallback();
}
H5P.trigger(instance, 'exitFullScreen');
};
H5P.isFullscreen = true;
if (H5P.fullScreenBrowserPrefix === undefined) {
// Create semi fullscreen.
if (H5P.isFramed) {
return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
}
before('h5p-semi-fullscreen');
var $disable = H5P.jQuery('<div role="button" tabindex="1" class="h5p-disable-fullscreen" title="' + H5P.t('disableFullscreen') + '"></div>').appendTo($container.find('.h5p-content-controls'));
var keyup, disableSemiFullscreen = function () {
$disable.remove();
$body.unbind('keyup', keyup);
done('h5p-semi-fullscreen');
};
keyup = function (event) {
if (event.keyCode === 27) {
disableSemiFullscreen();
}
};
$disable.click(disableSemiFullscreen);
$body.keyup(keyup);
entered();
}
else {
// Create real fullscreen.
before('h5p-fullscreen');
var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
document.addEventListener(eventName, function () {
if (first === undefined) {
// We are entering fullscreen mode
first = false;
entered();
return;
}
// We are exiting fullscreen
done('h5p-fullscreen');
document.removeEventListener(eventName, arguments.callee, false);
});
if (H5P.fullScreenBrowserPrefix === '') {
$element[0].requestFullScreen();
}
else {
var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
$element[0][method](params);
}
// Allows everone to exit
H5P.exitFullScreen = function () {
if (H5P.fullScreenBrowserPrefix === '') {
document.exitFullscreen();
}
else if (H5P.fullScreenBrowserPrefix === 'moz') {
document.mozCancelFullScreen();
}
else {
document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
}
};
}
};
/**
* Find the path to the content files based on the id of the content
*
* Also identifies and returns absolute paths
*
* @param string path
* Absolute path to a file, or relative path to a file in the content folder
* @param contentId
* Id of the content requesting a path
*/
H5P.getPath = function (path, contentId) {
var hasProtocol = function (path) {
return path.match(/^[a-z0-9]+:\/\//i);
};
if (hasProtocol(path)) {
return path;
}
if (contentId !== undefined) {
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
* Will be remove march 2016.
*
* Find the path to the content files folder based on the id of the content
*
* @param contentId
* Id of the content requesting a path
*/
H5P.getContentPath = function (contentId) {
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: H5P.newRunnable
*
* Used from libraries to construct instances of other libraries' objects by name.
*
* @param {string} name Name of library
* @returns Class constructor
*/
H5P.classFromName = function (name) {
var arr = name.split(".");
return this[arr[arr.length-1]];
};
/**
* A safe way of creating a new instance of a runnable H5P.
*
* TODO: Should we check if version matches the library?
* TODO: Dynamically try to load libraries currently not loaded? That will require a callback.
*
* @param {Object} library Library/action object form params.
* @param {Number} contentId
* @param {jQuery} $attachTo An optional element to attach the instance to.
* @param {Boolean} skipResize Optionally skip triggering of the resize event after attaching.
* @param {Object} extras - extra params for the H5P content constructor
* @return {Object} Instance.
*/
H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
var nameSplit, versionSplit, machineName;
try {
nameSplit = library.library.split(' ', 2);
machineName = nameSplit[0];
versionSplit = nameSplit[1].split('.', 2);
}
catch (err) {
return H5P.error('Invalid library string: ' + library.library);
}
if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
H5P.error('Invalid library params for: ' + library.library);
return H5P.error(library.params);
}
// Find constructor function
var constructor;
try {
nameSplit = nameSplit[0].split('.');
constructor = window;
for (var i = 0; i < nameSplit.length; i++) {
constructor = constructor[nameSplit[i]];
}
if (typeof constructor !== 'function') {
throw null;
}
}
catch (err) {
return H5P.error('Unable to find constructor for: ' + library.library);
}
if (extras === undefined) {
extras = {};
}
if (library.subContentId) {
extras.subContentId = library.subContentId;
}
if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
extras.previousState = library.userDatas.state;
}
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) {
instance = new constructor(library.params, contentId);
}
else {
instance = new constructor(library.params, contentId, extras);
}
if (instance.$ === undefined) {
instance.$ = H5P.jQuery(instance);
}
if (instance.contentId === undefined) {
instance.contentId = contentId;
}
if (instance.subContentId === undefined && library.subContentId) {
instance.subContentId = library.subContentId;
}
if (instance.parent === undefined && extras && extras.parent) {
instance.parent = extras.parent;
}
if ($attachTo !== undefined) {
instance.attach($attachTo);
H5P.trigger(instance, 'domChanged', {
'$target': $attachTo,
'library': machineName,
'key': 'newLibrary'
}, {'bubbles': true, 'external': true});
if (skipResize === undefined || !skipResize) {
// Resize content.
H5P.trigger(instance, 'resize');
}
}
return instance;
};
/**
* Used to print useful error messages.
*
* @param {mixed} err Error to print.
* @returns {undefined}
*/
H5P.error = function (err) {
if (window.console !== undefined && console.error !== undefined) {
console.error(err);
}
};
/**
* 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} Text
*/
H5P.t = function (key, vars, ns) {
if (ns === undefined) {
ns = 'H5P';
}
if (H5PIntegration.l10n[ns] === undefined) {
return '[Missing translation namespace "' + ns + '"]';
}
if (H5PIntegration.l10n[ns][key] === undefined) {
return '[Missing translation "' + key + '" in "' + ns + '"]';
}
var translation = H5PIntegration.l10n[ns][key];
if (vars !== undefined) {
// Replace placeholder with variables.
for (var placeholder in vars) {
translation = translation.replace(placeholder, vars[placeholder]);
}
}
return translation;
};
H5P.Dialog = function (name, title, content, $element) {
var self = this;
var $dialog = H5P.jQuery('<div class="h5p-popup-dialog h5p-' + name + '-dialog">\
<div class="h5p-inner">\
<h2>' + title + '</h2>\
<div class="h5p-scroll-content">' + content + '</div>\
<div class="h5p-close" role="button" tabindex="1" title="' + H5P.t('close') + '">\
</div>\
</div>')
.insertAfter($element)
.click(function () {
self.close();
})
.children('.h5p-inner')
.click(function () {
return false;
})
.find('.h5p-close')
.click(function () {
self.close();
})
.end()
.end();
this.open = function () {
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]);
}, 1);
};
this.close = function () {
$dialog.removeClass('h5p-open'); // Fade out
setTimeout(function () {
$dialog.remove();
}, 200);
};
};
/**
* Gather copyright information and display in a dialog over the content.
*
* @param {jQuery} $element to insert dialog after.
* @param {object} instance to get copyright information from.
* @returns {undefined}
*/
H5P.openCopyrightsDialog = function ($element, instance, parameters, contentId) {
var copyrights;
if (instance.getCopyrights !== undefined) {
// Use the instance's own copyright generator
copyrights = instance.getCopyrights();
}
else {
// Create a generic flat copyright list
copyrights = new H5P.ContentCopyrights();
H5P.findCopyrights(copyrights, parameters, contentId);
}
if (copyrights !== undefined) {
// Convert to string
copyrights = copyrights.toString();
}
if (copyrights === undefined || copyrights === '') {
// Use no copyrights default text
copyrights = H5P.t('noCopyrights');
}
// Open dialog with copyright information
var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $element);
dialog.open();
};
/**
* Gather a flat list of copyright information from the given parameters.
*
* @param {H5P.ContentCopyrights} info Used to collect all information in.
* @param {(Object|Arrray)} parameters To search for file objects in.
* @param {Number} contentId Used to insert thumbnails for images.
* @returns {undefined}
*/
H5P.findCopyrights = function (info, parameters, contentId) {
// Cycle through parameters
for (var field in parameters) {
if (!parameters.hasOwnProperty(field)) {
continue; // Do not check
}
var value = parameters[field];
if (value instanceof Array) {
// Cycle through array
H5P.findCopyrights(info, value, contentId);
}
else if (value instanceof Object) {
// Check if object is a file with copyrights
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);
}
}
else {
}
}
};
/**
* Display a dialog containing the embed code.
*
* @param {jQuery} $element to insert dialog after.
* @param {string} embed code.
* @returns {undefined}
*/
H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size) {
var fullEmbedCode = embedCode + resizeCode;
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="' + size.width + '" class="h5p-embed-size"/> × <input type="text" value="' + size.height + '" class="h5p-embed-size"/> px<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);
// Selecting embed code when dialog is opened
H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
var $inner = $dialog.find('.h5p-inner');
var $scroll = $inner.find('.h5p-scroll-content');
var diff = $scroll.outerHeight() - $scroll.innerHeight();
var positionInner = function () {
var height = $inner.height();
if ($scroll[0].scrollHeight + diff > height) {
$inner.css('height', ''); // 100%
}
else {
$inner.css('height', 'auto');
height = $inner.height();
}
$inner.css('marginTop', '-' + (height / 2) + 'px');
};
// 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);
};
var updateEmbed = function () {
$dialog.find('.h5p-embed-code-container:first').val(fullEmbedCode.replace(':w', getNum($w, size.width)).replace(':h', getNum($h, size.height)));
};
$w.change(updateEmbed);
$h.change(updateEmbed);
updateEmbed();
// Select text and expand textareas
$dialog.find('.h5p-embed-code-container').each(function(index, value) {
H5P.jQuery(this).css('height', this.scrollHeight + 'px').focus(function() {
H5P.jQuery(this).select();
});
});
$dialog.find('.h5p-embed-code-container').eq(0).select();
positionInner();
// Expand advanced embed
var expand = function () {
var $expander = H5P.jQuery(this);
var $content = $expander.next();
if ($content.is(':visible')) {
$expander.removeClass('h5p-open').text(H5P.t('showAdvanced'));
$content.hide();
}
else {
$expander.addClass('h5p-open').text(H5P.t('hideAdvanced'));
$content.show();
}
$dialog.find('.h5p-embed-code-container').each(function(index, value) {
H5P.jQuery(this).css('height', this.scrollHeight + 'px');
});
positionInner();
};
$dialog.find('.h5p-expander').click(expand).keypress(function (event) {
if (event.keyCode === 32) {
expand.apply(this);
}
});
});
dialog.open();
};
/**
* Copyrights for a H5P Content Library.
*/
H5P.ContentCopyrights = function () {
var label;
var media = [];
var content = [];
/**
* Public. Set label.
*
* @param {String} newLabel
*/
this.setLabel = function (newLabel) {
label = newLabel;
};
/**
* Public. Add sub content.
*
* @param {H5P.MediaCopyright} newMedia
*/
this.addMedia = function (newMedia) {
if (newMedia !== undefined) {
media.push(newMedia);
}
};
/**
* Public. Add sub content.
*
* @param {H5P.ContentCopyrights} newContent
*/
this.addContent = function (newContent) {
if (newContent !== undefined) {
content.push(newContent);
}
};
/**
* Public. Print content copyright.
*
* @returns {String} HTML.
*/
this.toString = function () {
var html = '';
// Add media rights
for (var i = 0; i < media.length; i++) {
html += media[i];
}
// Add sub content rights
for (i = 0; i < content.length; i++) {
html += content[i];
}
if (html !== '') {
// Add a label to this info
if (label !== undefined) {
html = '<h3>' + label + '</h3>' + html;
}
// Add wrapper
html = '<div class="h5p-content-copyrights">' + html + '</div>';
}
return html;
};
};
/**
* A ordered list of copyright fields for media.
*
* @param {Object} copyright information fields.
* @param {Object} labels translation. Optional.
* @param {Array} order of fields. Optional.
* @param {Object} extraFields for copyright. Optional.
*/
H5P.MediaCopyright = function (copyright, labels, order, extraFields) {
var thumbnail;
var list = new H5P.DefinitionList();
/**
* Private. Get translated label for field.
*
* @param {String} fieldName
* @return {String}
*/
var getLabel = function (fieldName) {
if (labels === undefined || labels[fieldName] === undefined) {
return H5P.t(fieldName);
}
return labels[fieldName];
};
/**
* Private. Get humanized value for field.
*
* @param {String} fieldName
* @return {String}
*/
var humanizeValue = function (fieldName, value) {
if (fieldName === 'license') {
return H5P.copyrightLicenses[value];
}
return value;
};
if (copyright !== undefined) {
// Add the extra fields
for (var field in extraFields) {
if (extraFields.hasOwnProperty(field)) {
copyright[field] = extraFields[field];
}
}
if (order === undefined) {
// Set default order
order = ['title', 'author', 'year', 'source', 'license'];
}
for (var i = 0; i < order.length; i++) {
var fieldName = order[i];
if (copyright[fieldName] !== undefined) {
list.add(new H5P.Field(getLabel(fieldName), humanizeValue(fieldName, copyright[fieldName])));
}
}
}
/**
* Public. Set thumbnail.
*
* @param {H5P.Thumbnail} newThumbnail
*/
this.setThumbnail = function (newThumbnail) {
thumbnail = newThumbnail;
};
/**
* Public. Checks if this copyright is undisclosed.
* I.e. only has the license attribute set, and it's undisclosed.
*
* @returns {Boolean}
*/
this.undisclosed = function () {
if (list.size() === 1) {
var field = list.get(0);
if (field.getLabel() === getLabel('license') && field.getValue() === humanizeValue('license', 'U')) {
return true;
}
}
return false;
};
/**
* Public. Print media copyright.
*
* @returns {String} HTML.
*/
this.toString = function () {
var html = '';
if (this.undisclosed()) {
return html; // No need to print a copyright with a single undisclosed license.
}
if (thumbnail !== undefined) {
html += thumbnail;
}
html += list;
if (html !== '') {
html = '<div class="h5p-media-copyright">' + html + '</div>';
}
return html;
};
};
// Translate table for copyright license codes.
H5P.copyrightLicenses = {
'U': 'Undisclosed',
'CC BY': 'Attribution',
'CC BY-SA': 'Attribution-ShareAlike',
'CC BY-ND': 'Attribution-NoDerivs',
'CC BY-NC': 'Attribution-NonCommercial',
'CC BY-NC-SA': 'Attribution-NonCommercial-ShareAlike',
'CC BY-NC-ND': 'Attribution-NonCommercial-NoDerivs',
'GNU GPL': 'General Public License',
'PD': 'Public Domain',
'ODC PDDL': 'Public Domain Dedication and Licence',
'CC PDM': 'Public Domain Mark',
'C': 'Copyright'
};
/**
* Simple class for creating thumbnails for images.
*
* @param {String} source
* @param {Number} width
* @param {Number} height
*/
H5P.Thumbnail = function (source, width, height) {
var thumbWidth, thumbHeight = 100;
if (width !== undefined) {
thumbWidth = Math.round(thumbHeight * (width / height));
}
/**
* Public. Print thumbnail.
*
* @returns {String} HTML.
*/
this.toString = function () {
return '<img src="' + source + '" alt="' + H5P.t('thumbnail') + '" class="h5p-thumbnail" height="' + thumbHeight + '"' + (thumbWidth === undefined ? '' : ' width="' + thumbWidth + '"') + '/>';
};
};
/**
* Simple data class for storing a single field.
*/
H5P.Field = function (label, value) {
/**
* Public. Get field label.
*
* @returns {String}
*/
this.getLabel = function () {
return label;
};
/**
* Public. Get field value.
*
* @returns {String}
*/
this.getValue = function () {
return value;
};
};
/**
* Simple class for creating a definition list.
*/
H5P.DefinitionList = function () {
var fields = [];
/**
* Public. Add field to list.
*
* @param {H5P.Field} field
*/
this.add = function (field) {
fields.push(field);
};
/**
* Public. Get Number of fields.
*
* @returns {Number}
*/
this.size = function () {
return fields.length;
};
/**
* Public. Get field at given index.
*
* @param {Number} index
* @returns {Object}
*/
this.get = function (index) {
return fields[index];
};
/**
* Public. Print definition list.
*
* @returns {String} HTML.
*/
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>');
};
};
/**
* THIS FUNCTION/CLASS IS DEPRECATED AND WILL BE REMOVED.
*
* Helper object for keeping coordinates in the same format all over.
*/
H5P.Coords = function (x, y, w, h) {
if ( !(this instanceof H5P.Coords) )
return new H5P.Coords(x, y, w, h);
this.x = 0;
this.y = 0;
this.w = 1;
this.h = 1;
if (typeof(x) === 'object') {
this.x = x.x;
this.y = x.y;
this.w = x.w;
this.h = x.h;
} else {
if (x !== undefined) {
this.x = x;
}
if (y !== undefined) {
this.y = y;
}
if (w !== undefined) {
this.w = w;
}
if (h !== undefined) {
this.h = h;
}
}
return this;
};
/**
* Parse library string into values.
*
* @param {string} library
* library in the format "machineName majorVersion.minorVersion"
* @returns
* library as an object with machineName, majorVersion and minorVersion properties
* return false if the library parameter is invalid
*/
H5P.libraryFromString = function (library) {
var regExp = /(.+)\s(\d)+\.(\d)$/g;
var res = regExp.exec(library);
if (res !== null) {
return {
'machineName': res[1],
'majorVersion': res[2],
'minorVersion': res[3]
};
}
else {
return false;
}
};
/**
* Get the path to the library
*
* @param {String} library
* The library identifier in the format "machineName-majorVersion.minorVersion".
* @returns {String} The full path to the library.
*/
H5P.getLibraryPath = function (library) {
return H5PIntegration.url + '/libraries/' + library;
};
/**
* Recursivly clone the given object.
* TODO: Consider if this needs to be in core. Doesn't $.extend do the same?
*
* @param {object} object Object to clone.
* @param {type} recursive
* @returns {object} A clone of object.
*/
H5P.cloneObject = function (object, recursive) {
var clone = object instanceof Array ? [] : {};
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];
}
}
}
return clone;
};
/**
* Remove all empty spaces before and after the value.
* 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?
*
* @param {String} value
* @returns {@exp;value@call;replace}
*/
H5P.trim = function (value) {
return value.replace(/^\s+|\s+$/g, '');
};
/**
* Check if javascript path/key is loaded.
*
* @param {String} path
* @returns {Boolean}
*/
H5P.jsLoaded = function (path) {
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) {
H5PIntegration.loadedCss = H5PIntegration.loadedCss || [];
return H5P.jQuery.inArray(path, H5PIntegration.loadedCss) !== -1;
};
/**
* Shuffle an array in place.
* TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it.
*
* @param {array} array Array to shuffle
* @returns {array} The passed array is returned for chaining.
*/
H5P.shuffleArray = function (array) {
if (!(array instanceof Array)) {
return;
}
var i = array.length, j, tempi, tempj;
if ( i === 0 ) return false;
while ( --i ) {
j = Math.floor( Math.random() * ( i + 1 ) );
tempi = array[i];
tempj = array[j];
array[i] = tempj;
array[j] = tempi;
}
return array;
};
/**
* DEPRECATED! Do not use this function directly, trigger the finish event
* instead.
*
* Post finished results for user.
*
* @param {Number} contentId
* @param {Number} score achieved
* @param {Number} maxScore that can be achieved
* @param {Number} time optional reported time usage
*/
H5P.setFinished = function (contentId, score, maxScore, time) {
if (H5PIntegration.postUserStatistics === true) {
/**
* Return unix timestamp for the given JS Date.
*
* @param {Date} date
* @returns {Number}
*/
var toUnix = function (date) {
return Math.round(date.getTime() / 1000);
};
// Post the results
// TODO: Should we use a variable with the complete path?
H5P.jQuery.post(H5PIntegration.ajaxPath + 'setFinished', {
contentId: contentId,
score: score,
maxScore: maxScore,
opened: toUnix(H5P.opened[contentId]),
finished: toUnix(new Date()),
time: time
});
}
};
// 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;
};
}
// 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 {function} instance
* An H5P instance
* @param {string} eventType
* The event type
*/
H5P.trigger = function(instance, eventType, data, extras) {
// Try new event system first
if (instance.trigger !== undefined) {
instance.trigger(eventType, data, extras);
}
// Try deprecated event system
else if (instance.$ !== undefined && instance.$.trigger !== undefined) {
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 {function} instance
* An h5p instance
* @param {string} eventType
* The event type
* @param {function} 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) {
instance.$.on(eventType, handler);
}
};
/**
* Create UUID
*
* @returns {String} UUID
*/
H5P.createUUID = function() {
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);
});
};
H5P.createTitle = function(rawTitle, maxLength) {
if (!rawTitle) {
return '';
}
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;
};
// 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) {
var options = {
url: H5PIntegration.ajax.contentUserData.replace(':contentId', contentId).replace(':dataType', dataType).replace(':subContentId', subContentId ? subContentId : 0),
dataType: 'json',
async: async === undefined ? true : async
};
if (data !== undefined) {
options.type = 'POST';
options.data = {
data: (data === null ? 0 : data),
preload: (preload ? 1 : 0),
invalidate: (invalidate ? 1 : 0)
};
}
else {
options.type = 'GET';
}
if (done !== undefined) {
options.error = function (xhr, error) {
done(error);
};
options.success = function (response) {
if (!response.success) {
done(response.error);
return;
}
if (response.data === false || response.data === undefined) {
done();
return;
}
done(undefined, response.data);
};
}
$.ajax(options);
}
/**
* Get user data for given content.
*
* @public
* @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.
*/
H5P.getUserData = function (contentId, dataId, done, subContentId) {
if (!subContentId) {
subContentId = 0; // Default
}
var content = H5PIntegration.contents['cid-' + contentId];
var preloadedData = content.contentUserData;
if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId]) {
if (preloadedData[subContentId][dataId] === 'RESET') {
done(undefined, null);
return;
}
try {
done(undefined, JSON.parse(preloadedData[subContentId][dataId]));
}
catch (err) {
done(err);
}
}
else {
contentUserDataAjax(contentId, dataId, subContentId, function (err, data) {
if (err || data === undefined) {
done(err, data);
return; // Error or no data
}
// Cache in preloaded
if (content.contentUserData === undefined) {
content.contentUserData = preloadedData = {};
}
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);
}
});
}
};
/**
* Set user data for given content.
*
* @public
* @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 - object holding the following properties:
* - {string} [subContentId] Identifies which data belongs to sub content.
* - {boolean} [preloaded=true] If the data should be loaded when content is loaded.
* - {boolean} [deleteOnChange=false] If the data should be invalidated when the content changes.
* - {function} [errorCallback] Callback with error as parameters.
* - {boolean} [async=true]
*/
H5P.setUserData = function (contentId, dataId, data, extras) {
var options = H5P.jQuery.extend(true, {}, {
subContentId: 0,
preloaded: true,
deleteOnChange: false,
async: true
}, extras);
try {
data = JSON.stringify(data);
}
catch (err) {
if (options.errorCallback) {
options.errorCallback(err);
}
return; // Failed to serialize.
}
var content = H5PIntegration.contents['cid-' + contentId];
if (!content.contentUserData) {
content.contentUserData = {};
}
var preloadedData = content.contentUserData;
if (preloadedData[options.subContentId] === undefined) {
preloadedData[options.subContentId] = {};
}
if (data === preloadedData[options.subContentId][dataId]) {
return; // No need to save this twice.
}
preloadedData[options.subContentId][dataId] = data;
contentUserDataAjax(contentId, dataId, options.subContentId, function (error, data) {
if (options.errorCallback && error) {
options.errorCallback(error);
}
}, data, options.preloaded, options.deleteOnChange, options.async);
};
/**
* Delete user data for given content.
*
* @public
* @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.
*/
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);
};
// Init H5P when page is fully loadded
$(document).ready(function () {
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) {
// Store the current state of the H5P when leaving the page.
H5P.$window.on('beforeunload', function () {
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});
}
}
}
});
}
// Relay events to top window.
if (H5P.isFramed && H5P.externalEmbed === false) {
H5P.externalDispatcher.on('*', window.top.H5P.externalDispatcher.trigger);
}
});
})(H5P.jQuery);