/*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';
}
// 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();
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;
H5P.deprecatedOn(instance, 'resize', function () {
// Use a delay to make sure iframe is resized to the correct size.
clearTimeout(resizeDelay);
resizeDelay = setTimeout(function () {
resizeIframe();
}, 1);
});
H5P.instances.push(instance);
}
H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIListener);
H5P.deprecatedOn(instance, 'xAPI', H5P.xAPIEmitter);
// 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.
H5P.deprecatedTrigger(instance, 'resize');
}
else {
H5P.deprecatedTrigger(instance, 'resize');
}
});
// Resize content.
H5P.deprecatedTrigger(instance, '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();
});
};
/*
* TODO xAPI:
* 1. Create a xAPI.js file and move xAPI code there (public)
* 2. Be able to listen for events from both div and iframe embedded content
* via the same API (this is about adding communication between the iframe and
* it's parent and make sure that the parent distributes the events from the
* iframe) (public)
* 3. Create a separate Drupal module that is able to listen for events from
* both div and iframe embedded content and send them to analytics (custom for Zavango)
* 4. Move the event system code to a separate file (public)
* 5. Make sure the helper functions provides all the relevant data, example values
* and time spent (public)
* 6. Add documentation to the functions (public)
* 7. Add xAPI events to all the basic questiontype:
* 7.1 Multichoice
* 7.2 Fill in the blanks
* 7.3 Drag and drop
* 7.4 Drag the words
* 7.5 Mark the words
* 8. Add xAPI events to interactive video
*/
H5P.xAPIListener = function(event) {
if ('verb' in event.statement) {
if (event.statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed') {
var score = event.statement.result.score.raw;
var maxScore = event.statement.result.score.max;
var contentId = event.statement.object.contentId;
H5P.setFinished(contentId, score, maxScore);
}
}
};
H5P.xAPIEmitter = function (event) {
if (event.statement !== undefined) {
for (var i = 0; i < H5P.xAPIListeners.length; i++) {
H5P.xAPIListeners[i](event.statement);
}
}
};
H5P.xAPIListeners = [];
H5P.onXAPI = function(listener) {
H5P.xAPIListeners.push(listener);
};
H5P.onXAPI(function(statement) {
console.log(statement);
});
/**
* 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.
H5P.deprecatedTrigger(instance, 'resize');
H5P.deprecatedTrigger(instance, '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.
H5P.deprecatedTrigger(instance, 'resize');
H5P.deprecatedTrigger(instance, '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.
* @param {Object} The parent of this H5P
* @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);
}
// Make xAPI events bubble
// if (parent !== null && parent.trigger !== undefined) {
// instance.on('xAPI', parent.trigger);
// }
// Automatically call resize on resize event if defined
if (typeof instance.resize === 'function') {
H5P.deprecatedOn(instance, 'resize', instance.resize);
}
if ($attachTo !== undefined) {
instance.attach($attachTo);
if (skipResize === undefined || !skipResize) {
// Resize content.
H5P.deprecatedTrigger(instance, 'resize');
}
}
return instance;
};
H5P.EventEnabled = function() {
this.listeners = {};
};
H5P.EventEnabled.prototype.on = function(type, listener) {
if (typeof listener === 'function') {
if (this.listeners[type] === undefined) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
};
H5P.EventEnabled.prototype.off = function (type, listener) {
if (this.listeners[type] !== undefined) {
var removeIndex = listeners[type].indexOf(listener);
if (removeIndex) {
listeners[type].splice(removeIndex, 1);
}
}
};
H5P.EventEnabled.prototype.trigger = function (type, event) {
if (event === null) {
event = new H5P.Event();
}
if (this.listeners[type] !== undefined) {
for (var i = 0; i < this.listeners[type].length; i++) {
this.listeners[type][i](event);
}
}
};
H5P.Event = function() {
// We're going to add bubbling, propagation and other features here later
};
H5P.XAPIEvent = function() {
H5P.Event.call(this);
this.statement = {};
};
H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype);
H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent;
H5P.XAPIEvent.prototype.setScoredResult = function(score, maxScore) {
this.statement.result = {
'score': {
'min': 0,
'max': maxScore,
'raw': score
}
};
};
H5P.XAPIEvent.prototype.setVerb = function(verb) {
if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) {
this.statement.verb = {
'id': 'http://adlnet.gov/expapi/verbs/' + verb,
'display': {
'en-US': verb
}
};
}
else {
console.log('illegal verb');
}
// Else: Fail silently...
};
H5P.XAPIEvent.prototype.setObject = function(instance) {
this.statement.object = {
// TODO: Correct this. contentId might be vid
'id': window.location.origin + Drupal.settings.basePath + 'node/' + instance.contentId,
//'contentId': instance.contentId,
'objectType': 'Activity'
};
};
H5P.XAPIEvent.prototype.setActor = function() {
this.statement.actor = H5P.getActor();
};
H5P.EventEnabled.prototype.triggerXAPI = function(verb, extra) {
var event = this.createXAPIEventTemplate(verb, extra);
this.trigger('xAPI', event);
};
H5P.EventEnabled.prototype.createXAPIEventTemplate = function(verb, extra) {
var event = new H5P.XAPIEvent();
event.setActor();
event.setVerb(verb);
if (extra !== undefined) {
for (var i in extra) {
event.statement[i] = extra[i];
}
}
if (!('object' in event)) {
event.setObject(this);
}
return event;
};
H5P.getActor = function() {
var user = H5PIntegration.getUser();
return {
'name': user.name,
'mbox': 'mailto:' + user.mail,
'objectType': 'Agent'
};
};
H5P.XAPIEvent.allowedXAPIVerbs = [
'answered',
'asked',
'attempted',
'attended',
'commented',
'completed',
'exited',
'experienced',
'failed',
'imported',
'initialized',
'interacted',
'launched',
'mastered',
'passed',
'preferred',
'progressed',
'registered',
'responded',
'resumed',
'scored',
'shared',
'suspended',
'terminated',
'voided'
];
/**
* 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('