/*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) {
H5P.canHasFullScreen = (H5P.isFramed && H5P.externalEmbed !== false) ? ((document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled) ? true : false) : true;
}
// 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('
').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', '
' + H5P.t('contentChanged') + '
' + H5P.t('startingOver') + '
OK
', $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('
').prependTo($container).children().click(function () {
H5P.fullScreen($container, instance);
});
}
// Create action bar
var $actions = H5P.jQuery('
').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('' + H5P.getHeadTags(contentId) + '');
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 += '';
}
return tags;
};
var createScriptTags = function (scripts) {
var tags = '';
for (var i = 0; i < scripts.length; i++) {
tags += '';
}
return tags;
};
return createStyleTags(H5PIntegration.core.styles) +
createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
createScriptTags(H5PIntegration.core.scripts) +
createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
'';
};
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('').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('
\
\
' + title + '
\
' + content + '
\
\
\
')
.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'), '' + H5P.t('size') + ': × px
' + H5P.t('showAdvanced') + '
' + H5P.t('advancedHelp') + '
', $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 = '
' + label + '
' + html;
}
// Add wrapper
html = '
' + html + '
';
}
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 = '
' + html + '
';
}
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 '';
};
};
/**
* 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 += '
' + field.getLabel() + '
' + field.getValue() + '
';
}
return (html === '' ? html : '
' + html + '
');
};
};
/**
* 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('')
.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 = preloaded = {};
}
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);