/*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);
// 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';
}
// Keep track of when the H5Ps where started
H5P.opened = {};
/**
* Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found.
*/
H5P.init = function () {
// Useful jQuery object.
H5P.$body = H5P.jQuery(document.body);
// Prepare internal resizer for content.
var $window = H5P.jQuery(window.parent);
// H5Ps added in normal DIV.
var $containers = H5P.jQuery(".h5p-content").each(function () {
var $element = H5P.jQuery(this);
var $container = H5P.jQuery('
').appendTo($element);
var contentId = $element.data('content-id');
var contentData = H5PIntegration.getContentData(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)
};
// 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.jQuery('').prependTo($container).children().click(function () {
H5P.fullScreen($container, instance);
});
}
var $actions = H5P.jQuery('');
if (contentData.exportUrl !== '') {
// Display export button
H5P.jQuery('' + H5P.t('download') + '').appendTo($actions).click(function () {
window.location.href = contentData.exportUrl;
});
}
// Display copyrights button
H5P.jQuery('' + H5P.t('copyrights') + '').appendTo($actions).click(function () {
H5P.openCopyrightsDialog($actions, instance, library.params, contentId);
});
if (contentData.embedCode !== undefined) {
// Display embed button
H5P.jQuery('' + H5P.t('embed') + '').appendTo($actions).click(function () {
H5P.openEmbedDialog($actions, contentData.embedCode);
});
}
if (H5PIntegration.showH5PIconInActionBar()) {
H5P.jQuery('').appendTo($actions);
}
$actions.insertAfter($container);
// Keep track of when we started
H5P.opened[contentId] = new Date();
// Handle events when the user finishes the content. Useful for logging exercise results.
instance.$.on('finish', function (event) {
if (event.data !== undefined) {
H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
}
});
if (H5P.isFramed) {
// 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;
};
var resizeDelay;
instance.$.on('resize', function () {
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
resizeIframe();
}, 1);
});
}
// Resize everything when window is resized.
$window.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.
instance.$.trigger('resize');
}
else {
instance.$.trigger('resize');
}
});
// Resize content.
instance.$.trigger('resize');
});
// Insert H5Ps that should be in iframes.
H5P.jQuery("iframe.h5p-iframe").each(function () {
var contentId = H5P.jQuery(this).data('content-id');
this.contentDocument.open();
this.contentDocument.write('' + H5PIntegration.getHeadTags(contentId) + '');
this.contentDocument.close();
});
};
/**
* 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.isFramed) {
// Trigger resize on wrapper in parent window.
window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get());
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.
instance.$.trigger('resize');
instance.$.trigger('focus');
};
/**
* 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.
instance.$.trigger('resize');
instance.$.trigger('focus');
if (exitCallback !== undefined) {
exitCallback();
}
};
H5P.isFullscreen = true;
if (H5P.fullScreenBrowserPrefix === undefined) {
// Create semi fullscreen.
before('h5p-semi-fullscreen');
var $disable = H5P.jQuery('').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);
}
}
};
/**
* 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.getContentPath(contentId);
}
else if (window.H5PEditor !== undefined) {
prefix = H5PEditor.filesPath;
}
else {
return;
}
if (!hasProtocol(prefix)) {
prefix = window.parent.location.protocol + "//" + window.parent.location.host + prefix;
}
return prefix + '/' + path;
};
/**
* THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
*
* 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.getContentPath(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.
* @return {Object} Instance.
*/
H5P.newRunnable = function (library, contentId, $attachTo, skipResize) {
var nameSplit, versionSplit;
try {
nameSplit = library.library.split(' ', 2);
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);
}
var instance = new constructor(library.params, contentId);
if (instance.$ === undefined) {
instance.$ = H5P.jQuery(instance);
}
if ($attachTo !== undefined) {
instance.attach($attachTo);
if (skipResize === undefined || !skipResize) {
// Resize content.
instance.$.trigger('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.i18n[ns] === undefined) {
return '[Missing translation namespace "' + ns + '"]';
}
if (H5PIntegration.i18n[ns][key] === undefined) {
return '[Missing translation "' + key + '" in "' + ns + '"]';
}
var translation = H5PIntegration.i18n[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('