/*jshint multistr: true */
// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?
/** @namespace */
var H5P = window.H5P = window.H5P || {};
/**
* Tells us if we're inside of an iframe.
* @member {boolean}
*/
H5P.isFramed = (window.self !== window.parent);
/**
* jQuery instance of current window.
* @member {H5P.jQuery}
*/
H5P.$window = H5P.jQuery(window);
/**
* List over H5P instances on the current page.
* @member {Array}
*/
H5P.instances = [];
// Detect if we support fullscreen, and what prefix to use.
if (document.documentElement.requestFullscreen) {
/**
* Browser prefix to use when entering fullscreen mode.
* undefined means no fullscreen support.
* @member {string}
*/
H5P.fullScreenBrowserPrefix = '';
}
else if (document.documentElement.webkitRequestFullScreen) {
H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));
// Do not allow fullscreen for safari < 7.
if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
H5P.fullScreenBrowserPrefix = 'webkit';
}
}
else if (document.documentElement.mozRequestFullScreen) {
H5P.fullScreenBrowserPrefix = 'moz';
}
else if (document.documentElement.msRequestFullscreen) {
H5P.fullScreenBrowserPrefix = 'ms';
}
/**
* Keep track of when the H5Ps where started.
*
* @type {Object[]}
*/
H5P.opened = {};
/**
* Initialize H5P content.
* Scans for ".h5p-content" in the document and initializes H5P instances where found.
*
* @param {Object} target DOM Element
*/
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.fullscreenSupported === undefined) {
/**
* Use this variable to check if fullscreen is supported. Fullscreen can be
* restricted when embedding since not all browsers support the native
* fullscreen, and the semi-fullscreen solution doesn't work when embedded.
* @type {boolean}
*/
H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled));
// -We should consider document.msFullscreenEnabled when they get their
// -element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe
// Update: Seems to be no need as they've moved on to Webkit
}
// Determine if we can use local storage
if (H5P.localStorageSupported === undefined) {
try {
H5P.localStorageSupported = (window.localStorage) ? true : false;
}
catch (error) {
H5P.localStorageSupported = false;
}
}
// Deprecated variable, kept to maintain backwards compatability
if (H5P.canHasFullScreen === undefined) {
/**
* @deprecated since version 1.11
* @type {boolean}
*/
H5P.canHasFullScreen = H5P.fullscreenSupported;
}
// H5Ps added in normal DIV.
H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
var $element = H5P.jQuery(this).addClass('h5p-initialized');
var $container = H5P.jQuery('
' + 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 () {
H5P.trigger(instance, 'resize');
};
// 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 () {
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')).attr('aria-expanded', 'true');
$content.hide();
}
else {
$expander.addClass('h5p-open').text(H5P.t('hideAdvanced')).attr('aria-expanded', 'false');
$content.show();
}
$dialog.find('.h5p-embed-code-container').each(function () {
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);
return false;
}
});
}).on('dialog-closed', function () {
H5P.trigger(instance, 'resize');
});
dialog.open();
};
/**
* Show a toast message.
*
* The reference element could be dom elements the toast should be attached to,
* or e.g. the document body for general toast messages.
*
* @param {DOM} element Reference element to show toast message for.
* @param {string} message Message to show.
* @param {object} [config] Configuration.
* @param {string} [config.style=h5p-toast] Style name for the tooltip.
* @param {number} [config.duration=3000] Toast message length in ms.
* @param {object} [config.position] Relative positioning of the toast.
* @param {string} [config.position.horizontal=centered] [before|left|centered|right|after].
* @param {string} [config.position.vertical=below] [above|top|centered|bottom|below].
* @param {number} [config.position.offsetHorizontal=0] Extra horizontal offset.
* @param {number} [config.position.offsetVertical=0] Extra vetical offset.
* @param {boolean} [config.position.noOverflowLeft=false] True to prevent overflow left.
* @param {boolean} [config.position.noOverflowRight=false] True to prevent overflow right.
* @param {boolean} [config.position.noOverflowTop=false] True to prevent overflow top.
* @param {boolean} [config.position.noOverflowBottom=false] True to prevent overflow bottom.
* @param {boolean} [config.position.noOverflowX=false] True to prevent overflow left and right.
* @param {boolean} [config.position.noOverflowY=false] True to prevent overflow top and bottom.
* @param {object} [config.position.overflowReference=document.body] DOM reference for overflow.
*/
H5P.attachToastTo = function (element, message, config) {
if (element === undefined || message === undefined) {
return;
}
const eventPath = function (evt) {
var path = (evt.composedPath && evt.composedPath()) || evt.path;
var target = evt.target;
if (path != null) {
// Safari doesn't include Window, but it should.
return (path.indexOf(window) < 0) ? path.concat(window) : path;
}
if (target === window) {
return [window];
}
function getParents(node, memo) {
memo = memo || [];
var parentNode = node.parentNode;
if (!parentNode) {
return memo;
}
else {
return getParents(parentNode, memo.concat(parentNode));
}
}
return [target].concat(getParents(target), window);
};
/**
* Handle click while toast is showing.
*/
const clickHandler = function (event) {
/*
* A common use case will be to attach toasts to buttons that are clicked.
* The click would remove the toast message instantly without this check.
* Children of the clicked element are also ignored.
*/
var path = eventPath(event);
if (path.indexOf(element) !== -1) {
return;
}
clearTimeout(timer);
removeToast();
};
/**
* Remove the toast message.
*/
const removeToast = function () {
document.removeEventListener('click', clickHandler);
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
};
/**
* Get absolute coordinates for the toast.
*
* @param {DOM} element Reference element to show toast message for.
* @param {DOM} toast Toast element.
* @param {object} [position={}] Relative positioning of the toast message.
* @param {string} [position.horizontal=centered] [before|left|centered|right|after].
* @param {string} [position.vertical=below] [above|top|centered|bottom|below].
* @param {number} [position.offsetHorizontal=0] Extra horizontal offset.
* @param {number} [position.offsetVertical=0] Extra vetical offset.
* @param {boolean} [position.noOverflowLeft=false] True to prevent overflow left.
* @param {boolean} [position.noOverflowRight=false] True to prevent overflow right.
* @param {boolean} [position.noOverflowTop=false] True to prevent overflow top.
* @param {boolean} [position.noOverflowBottom=false] True to prevent overflow bottom.
* @param {boolean} [position.noOverflowX=false] True to prevent overflow left and right.
* @param {boolean} [position.noOverflowY=false] True to prevent overflow top and bottom.
* @return {object}
*/
const getToastCoordinates = function (element, toast, position) {
position = position || {};
position.offsetHorizontal = position.offsetHorizontal || 0;
position.offsetVertical = position.offsetVertical || 0;
const toastRect = toast.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
let left = 0;
let top = 0;
// Compute horizontal position
switch (position.horizontal) {
case 'before':
left = elementRect.left - toastRect.width - position.offsetHorizontal;
break;
case 'after':
left = elementRect.left + elementRect.width + position.offsetHorizontal;
break;
case 'left':
left = elementRect.left + position.offsetHorizontal;
break;
case 'right':
left = elementRect.left + elementRect.width - toastRect.width - position.offsetHorizontal;
break;
case 'centered':
left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
break;
default:
left = elementRect.left + elementRect.width / 2 - toastRect.width / 2 + position.offsetHorizontal;
}
// Compute vertical position
switch (position.vertical) {
case 'above':
top = elementRect.top - toastRect.height - position.offsetVertical;
break;
case 'below':
top = elementRect.top + elementRect.height + position.offsetVertical;
break;
case 'top':
top = elementRect.top + position.offsetVertical;
break;
case 'bottom':
top = elementRect.top + elementRect.height - toastRect.height - position.offsetVertical;
break;
case 'centered':
top = elementRect.top + elementRect.height / 2 - toastRect.height / 2 + position.offsetVertical;
break;
default:
top = elementRect.top + elementRect.height + position.offsetVertical;
}
// Prevent overflow
const overflowElement = document.body;
const bounds = overflowElement.getBoundingClientRect();
if ((position.noOverflowLeft || position.noOverflowX) && (left < bounds.x)) {
left = bounds.x;
}
if ((position.noOverflowRight || position.noOverflowX) && ((left + toastRect.width) > (bounds.x + bounds.width))) {
left = bounds.x + bounds.width - toastRect.width;
}
if ((position.noOverflowTop || position.noOverflowY) && (top < bounds.y)) {
top = bounds.y;
}
if ((position.noOverflowBottom || position.noOverflowY) && ((top + toastRect.height) > (bounds.y + bounds.height))) {
left = bounds.y + bounds.height - toastRect.height;
}
return {left: left, top: top};
};
// Sanitization
config = config || {};
config.style = config.style || 'h5p-toast';
config.duration = config.duration || 3000;
// Build toast
const toast = document.createElement('div');
toast.setAttribute('id', config.style);
toast.classList.add('h5p-toast-disabled');
toast.classList.add(config.style);
const msg = document.createElement('span');
msg.innerHTML = message;
toast.appendChild(msg);
document.body.appendChild(toast);
// The message has to be set before getting the coordinates
const coordinates = getToastCoordinates(element, toast, config.position);
toast.style.left = Math.round(coordinates.left) + 'px';
toast.style.top = Math.round(coordinates.top) + 'px';
toast.classList.remove('h5p-toast-disabled');
const timer = setTimeout(removeToast, config.duration);
// The toast can also be removed by clicking somewhere
document.addEventListener('click', clickHandler);
};
/**
* Copyrights for a H5P Content Library.
*
* @class
*/
H5P.ContentCopyrights = function () {
var label;
var media = [];
var content = [];
/**
* Set label.
*
* @param {string} newLabel
*/
this.setLabel = function (newLabel) {
label = newLabel;
};
/**
* Add sub content.
*
* @param {H5P.MediaCopyright} newMedia
*/
this.addMedia = function (newMedia) {
if (newMedia !== undefined) {
media.push(newMedia);
}
};
/**
* Add sub content in front.
*
* @param {H5P.MediaCopyright} newMedia
*/
this.addMediaInFront = function (newMedia) {
if (newMedia !== undefined) {
media.unshift(newMedia);
}
};
/**
* Add sub content.
*
* @param {H5P.ContentCopyrights} newContent
*/
this.addContent = function (newContent) {
if (newContent !== undefined) {
content.push(newContent);
}
};
/**
* 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 = '