diff --git a/src/core/features/h5p/assets/fonts/h5p-core-23.eot b/src/core/features/h5p/assets/fonts/h5p-core-23.eot new file mode 100644 index 000000000..f86828cff Binary files /dev/null and b/src/core/features/h5p/assets/fonts/h5p-core-23.eot differ diff --git a/src/core/features/h5p/assets/fonts/h5p-core-23.svg b/src/core/features/h5p/assets/fonts/h5p-core-23.svg new file mode 100644 index 000000000..dd65f1809 --- /dev/null +++ b/src/core/features/h5p/assets/fonts/h5p-core-23.svg @@ -0,0 +1,62 @@ + + + + + + +{ + "fontFamily": "h5p-core-21", + "description": "Font generated by IcoMoon.", + "majorVersion": 1, + "minorVersion": 1, + "version": "Version 1.1", + "fontId": "h5p-core-21", + "psName": "h5p-core-21", + "subFamily": "Regular", + "fullName": "h5p-core-21" +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/features/h5p/assets/fonts/h5p-core-23.ttf b/src/core/features/h5p/assets/fonts/h5p-core-23.ttf new file mode 100644 index 000000000..3a3348adf Binary files /dev/null and b/src/core/features/h5p/assets/fonts/h5p-core-23.ttf differ diff --git a/src/core/features/h5p/assets/fonts/h5p-core-23.woff b/src/core/features/h5p/assets/fonts/h5p-core-23.woff new file mode 100644 index 000000000..ff03f4248 Binary files /dev/null and b/src/core/features/h5p/assets/fonts/h5p-core-23.woff differ diff --git a/src/core/features/h5p/assets/js/h5p-action-bar.js b/src/core/features/h5p/assets/js/h5p-action-bar.js new file mode 100644 index 000000000..608a848b3 --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-action-bar.js @@ -0,0 +1,100 @@ +/** + * @class + * @augments H5P.EventDispatcher + * @param {Object} displayOptions + * @param {boolean} displayOptions.export Triggers the display of the 'Download' button + * @param {boolean} displayOptions.copyright Triggers the display of the 'Copyright' button + * @param {boolean} displayOptions.embed Triggers the display of the 'Embed' button + * @param {boolean} displayOptions.icon Triggers the display of the 'H5P icon' link + */ +H5P.ActionBar = (function ($, EventDispatcher) { + "use strict"; + + function ActionBar(displayOptions) { + EventDispatcher.call(this); + + /** @alias H5P.ActionBar# */ + var self = this; + + var hasActions = false; + + // Create action bar + var $actions = H5P.jQuery(''); + + /** + * Helper for creating action bar buttons. + * + * @private + * @param {string} type + * @param {string} customClass Instead of type class + */ + var addActionButton = function (type, customClass) { + /** + * Handles selection of action + */ + var handler = function () { + self.trigger(type); + }; + H5P.jQuery('
  • ', { + 'class': 'h5p-button h5p-noselect h5p-' + (customClass ? customClass : type), + role: 'button', + tabindex: 0, + title: H5P.t(type + 'Description'), + html: H5P.t(type), + on: { + click: handler, + keypress: function (e) { + if (e.which === 32) { + handler(); + e.preventDefault(); // (since return false will block other inputs) + } + } + }, + appendTo: $actions + }); + + hasActions = true; + }; + + // Register action bar buttons + if (displayOptions.export || displayOptions.copy) { + // Add export button + addActionButton('reuse', 'export'); + } + if (displayOptions.copyright) { + addActionButton('copyrights'); + } + if (displayOptions.embed) { + addActionButton('embed'); + } + if (displayOptions.icon) { + // Add about H5P button icon + H5P.jQuery('
  • ').appendTo($actions); + hasActions = true; + } + + /** + * Returns a reference to the dom element + * + * @return {H5P.jQuery} + */ + self.getDOMElement = function () { + return $actions; + }; + + /** + * Does the actionbar contain actions? + * + * @return {Boolean} + */ + self.hasActions = function () { + return hasActions; + }; + } + + ActionBar.prototype = Object.create(EventDispatcher.prototype); + ActionBar.prototype.constructor = ActionBar; + + return ActionBar; + +})(H5P.jQuery, H5P.EventDispatcher); diff --git a/src/core/features/h5p/assets/js/h5p-confirmation-dialog.js b/src/core/features/h5p/assets/js/h5p-confirmation-dialog.js new file mode 100644 index 000000000..cd3536e7a --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-confirmation-dialog.js @@ -0,0 +1,410 @@ +/*global H5P*/ +H5P.ConfirmationDialog = (function (EventDispatcher) { + "use strict"; + + /** + * Create a confirmation dialog + * + * @param [options] Options for confirmation dialog + * @param [options.instance] Instance that uses confirmation dialog + * @param [options.headerText] Header text + * @param [options.dialogText] Dialog text + * @param [options.cancelText] Cancel dialog button text + * @param [options.confirmText] Confirm dialog button text + * @param [options.hideCancel] Hide cancel button + * @param [options.hideExit] Hide exit button + * @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog + * @param [options.classes] Extra classes for popup + * @constructor + */ + function ConfirmationDialog(options) { + EventDispatcher.call(this); + var self = this; + + // Make sure confirmation dialogs have unique id + H5P.ConfirmationDialog.uniqueId += 1; + var uniqueId = H5P.ConfirmationDialog.uniqueId; + + // Default options + options = options || {}; + options.headerText = options.headerText || H5P.t('confirmDialogHeader'); + options.dialogText = options.dialogText || H5P.t('confirmDialogBody'); + options.cancelText = options.cancelText || H5P.t('cancelLabel'); + options.confirmText = options.confirmText || H5P.t('confirmLabel'); + + /** + * Handle confirming event + * @param {Event} e + */ + function dialogConfirmed(e) { + self.hide(); + self.trigger('confirmed'); + e.preventDefault(); + } + + /** + * Handle dialog canceled + * @param {Event} e + */ + function dialogCanceled(e) { + self.hide(); + self.trigger('canceled'); + e.preventDefault(); + } + + /** + * Flow focus to element + * @param {HTMLElement} element Next element to be focused + * @param {Event} e Original tab event + */ + function flowTo(element, e) { + element.focus(); + e.preventDefault(); + } + + // Offset of exit button + var exitButtonOffset = 2 * 16; + var shadowOffset = 8; + + // Determine if we are too large for our container and must resize + var resizeIFrame = false; + + // Create background + var popupBackground = document.createElement('div'); + popupBackground.classList + .add('h5p-confirmation-dialog-background', 'hidden', 'hiding'); + + // Create outer popup + var popup = document.createElement('div'); + popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); + if (options.classes) { + options.classes.forEach(function (popupClass) { + popup.classList.add(popupClass); + }); + } + + popup.setAttribute('role', 'dialog'); + popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId); + popupBackground.appendChild(popup); + popup.addEventListener('keydown', function (e) { + if (e.which === 27) {// Esc key + // Exit dialog + dialogCanceled(e); + } + }); + + // Popup header + var header = document.createElement('div'); + header.classList.add('h5p-confirmation-dialog-header'); + popup.appendChild(header); + + // Header text + var headerText = document.createElement('div'); + headerText.classList.add('h5p-confirmation-dialog-header-text'); + headerText.innerHTML = options.headerText; + header.appendChild(headerText); + + // Popup body + var body = document.createElement('div'); + body.classList.add('h5p-confirmation-dialog-body'); + popup.appendChild(body); + + // Popup text + var text = document.createElement('div'); + text.classList.add('h5p-confirmation-dialog-text'); + text.innerHTML = options.dialogText; + text.id = 'h5p-confirmation-dialog-dialog-text-' + uniqueId; + body.appendChild(text); + + // Popup buttons + var buttons = document.createElement('div'); + buttons.classList.add('h5p-confirmation-dialog-buttons'); + body.appendChild(buttons); + + // Cancel button + var cancelButton = document.createElement('button'); + cancelButton.classList.add('h5p-core-cancel-button'); + cancelButton.textContent = options.cancelText; + + // Confirm button + var confirmButton = document.createElement('button'); + confirmButton.classList.add('h5p-core-button'); + confirmButton.classList.add('h5p-confirmation-dialog-confirm-button'); + confirmButton.textContent = options.confirmText; + + // Exit button + var exitButton = document.createElement('button'); + exitButton.classList.add('h5p-confirmation-dialog-exit'); + exitButton.setAttribute('aria-hidden', 'true'); + exitButton.tabIndex = -1; + exitButton.title = options.cancelText; + + // Cancel handler + cancelButton.addEventListener('click', dialogCanceled); + cancelButton.addEventListener('keydown', function (e) { + if (e.which === 32) { // Space + dialogCanceled(e); + } + else if (e.which === 9 && e.shiftKey) { // Shift-tab + flowTo(confirmButton, e); + } + }); + + if (!options.hideCancel) { + buttons.appendChild(cancelButton); + } + else { + // Center buttons + buttons.classList.add('center'); + } + + // Confirm handler + confirmButton.addEventListener('click', dialogConfirmed); + confirmButton.addEventListener('keydown', function (e) { + if (e.which === 32) { // Space + dialogConfirmed(e); + } + else if (e.which === 9 && !e.shiftKey) { // Tab + const nextButton = !options.hideCancel ? cancelButton : confirmButton; + flowTo(nextButton, e); + } + }); + buttons.appendChild(confirmButton); + + // Exit handler + exitButton.addEventListener('click', dialogCanceled); + exitButton.addEventListener('keydown', function (e) { + if (e.which === 32) { // Space + dialogCanceled(e); + } + }); + if (!options.hideExit) { + popup.appendChild(exitButton); + } + + // Wrapper element + var wrapperElement; + + // Focus capturing + var focusPredator; + + // Maintains hidden state of elements + var wrapperSiblingsHidden = []; + var popupSiblingsHidden = []; + + // Element with focus before dialog + var previouslyFocused; + + /** + * Set parent of confirmation dialog + * @param {HTMLElement} wrapper + * @returns {H5P.ConfirmationDialog} + */ + this.appendTo = function (wrapper) { + wrapperElement = wrapper; + return this; + }; + + /** + * Capture the focus element, send it to confirmation button + * @param {Event} e Original focus event + */ + var captureFocus = function (e) { + if (!popupBackground.contains(e.target)) { + e.preventDefault(); + confirmButton.focus(); + } + }; + + /** + * Hide siblings of element from assistive technology + * + * @param {HTMLElement} element + * @returns {Array} The previous hidden state of all siblings + */ + var hideSiblings = function (element) { + var hiddenSiblings = []; + var siblings = element.parentNode.children; + var i; + for (i = 0; i < siblings.length; i += 1) { + // Preserve hidden state + hiddenSiblings[i] = siblings[i].getAttribute('aria-hidden') ? + true : false; + + if (siblings[i] !== element) { + siblings[i].setAttribute('aria-hidden', true); + } + } + return hiddenSiblings; + }; + + /** + * Restores assistive technology state of element's siblings + * + * @param {HTMLElement} element + * @param {Array} hiddenSiblings Hidden state of all siblings + */ + var restoreSiblings = function (element, hiddenSiblings) { + var siblings = element.parentNode.children; + var i; + for (i = 0; i < siblings.length; i += 1) { + if (siblings[i] !== element && !hiddenSiblings[i]) { + siblings[i].removeAttribute('aria-hidden'); + } + } + }; + + /** + * Start capturing focus of parent and send it to dialog + */ + var startCapturingFocus = function () { + focusPredator = wrapperElement.parentNode || wrapperElement; + focusPredator.addEventListener('focus', captureFocus, true); + }; + + /** + * Clean up event listener for capturing focus + */ + var stopCapturingFocus = function () { + focusPredator.removeAttribute('aria-hidden'); + focusPredator.removeEventListener('focus', captureFocus, true); + }; + + /** + * Hide siblings in underlay from assistive technologies + */ + var disableUnderlay = function () { + wrapperSiblingsHidden = hideSiblings(wrapperElement); + popupSiblingsHidden = hideSiblings(popupBackground); + }; + + /** + * Restore state of underlay for assistive technologies + */ + var restoreUnderlay = function () { + restoreSiblings(wrapperElement, wrapperSiblingsHidden); + restoreSiblings(popupBackground, popupSiblingsHidden); + }; + + /** + * Fit popup to container. Makes sure it doesn't overflow. + * @params {number} [offsetTop] Offset of popup + */ + var fitToContainer = function (offsetTop) { + var popupOffsetTop = parseInt(popup.style.top, 10); + if (offsetTop !== undefined) { + popupOffsetTop = offsetTop; + } + + if (!popupOffsetTop) { + popupOffsetTop = 0; + } + + // Overflows height + if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) { + popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset; + } + + if (popupOffsetTop - exitButtonOffset <= 0) { + popupOffsetTop = exitButtonOffset + shadowOffset; + + // We are too big and must resize + resizeIFrame = true; + } + popup.style.top = popupOffsetTop + 'px'; + }; + + /** + * Show confirmation dialog + * @params {number} offsetTop Offset top + * @returns {H5P.ConfirmationDialog} + */ + this.show = function (offsetTop) { + // Capture focused item + previouslyFocused = document.activeElement; + wrapperElement.appendChild(popupBackground); + startCapturingFocus(); + disableUnderlay(); + popupBackground.classList.remove('hidden'); + fitToContainer(offsetTop); + setTimeout(function () { + popup.classList.remove('hidden'); + popupBackground.classList.remove('hiding'); + + setTimeout(function () { + // Focus confirm button + confirmButton.focus(); + + // Resize iFrame if necessary + if (resizeIFrame && options.instance) { + var minHeight = parseInt(popup.offsetHeight, 10) + + exitButtonOffset + (2 * shadowOffset); + self.setViewPortMinimumHeight(minHeight); + options.instance.trigger('resize'); + resizeIFrame = false; + } + }, 100); + }, 0); + + return this; + }; + + /** + * Hide confirmation dialog + * @returns {H5P.ConfirmationDialog} + */ + this.hide = function () { + popupBackground.classList.add('hiding'); + popup.classList.add('hidden'); + + // Restore focus + stopCapturingFocus(); + if (!options.skipRestoreFocus) { + previouslyFocused.focus(); + } + restoreUnderlay(); + setTimeout(function () { + popupBackground.classList.add('hidden'); + wrapperElement.removeChild(popupBackground); + self.setViewPortMinimumHeight(null); + }, 100); + + return this; + }; + + /** + * Retrieve element + * + * @return {HTMLElement} + */ + this.getElement = function () { + return popup; + }; + + /** + * Get previously focused element + * @return {HTMLElement} + */ + this.getPreviouslyFocused = function () { + return previouslyFocused; + }; + + /** + * Sets the minimum height of the view port + * + * @param {number|null} minHeight + */ + this.setViewPortMinimumHeight = function (minHeight) { + var container = document.querySelector('.h5p-container') || document.body; + container.style.minHeight = (typeof minHeight === 'number') ? (minHeight + 'px') : minHeight; + }; + } + + ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype); + ConfirmationDialog.prototype.constructor = ConfirmationDialog; + + return ConfirmationDialog; + +}(H5P.EventDispatcher)); + +H5P.ConfirmationDialog.uniqueId = -1; diff --git a/src/core/features/h5p/assets/js/h5p-content-type.js b/src/core/features/h5p/assets/js/h5p-content-type.js new file mode 100644 index 000000000..47c4d21bf --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-content-type.js @@ -0,0 +1,41 @@ +/** + * H5P.ContentType is a base class for all content types. Used by newRunnable() + * + * Functions here may be overridable by the libraries. In special cases, + * it is also possible to override H5P.ContentType on a global level. + * + * NOTE that this doesn't actually 'extend' the event dispatcher but instead + * it creates a single instance which all content types shares as their base + * prototype. (in some cases this may be the root of strange event behavior) + * + * @class + * @augments H5P.EventDispatcher + */ +H5P.ContentType = function (isRootLibrary) { + + function ContentType() {} + + // Inherit from EventDispatcher. + ContentType.prototype = new H5P.EventDispatcher(); + + /** + * Is library standalone or not? Not beeing standalone, means it is + * included in another library + * + * @return {Boolean} + */ + ContentType.prototype.isRoot = function () { + return isRootLibrary; + }; + + /** + * Returns the file path of a file in the current library + * @param {string} filePath The path to the file relative to the library folder + * @return {string} The full path to the file + */ + ContentType.prototype.getLibraryFilePath = function (filePath) { + return H5P.getLibraryPath(this.libraryInfo.versionedNameNoSpaces) + '/' + filePath; + }; + + return ContentType; +}; diff --git a/src/core/features/h5p/assets/js/h5p-content-upgrade-process.js b/src/core/features/h5p/assets/js/h5p-content-upgrade-process.js new file mode 100644 index 000000000..fbaa4f2bf --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-content-upgrade-process.js @@ -0,0 +1,313 @@ +/*jshint -W083 */ +var H5PUpgrades = H5PUpgrades || {}; + +H5P.ContentUpgradeProcess = (function (Version) { + + /** + * @class + * @namespace H5P + */ + function ContentUpgradeProcess(name, oldVersion, newVersion, params, id, loadLibrary, done) { + var self = this; + + // Make params possible to work with + try { + params = JSON.parse(params); + if (!(params instanceof Object)) { + throw true; + } + } + catch (event) { + return done({ + type: 'errorParamsBroken', + id: id + }); + } + + self.loadLibrary = loadLibrary; + self.upgrade(name, oldVersion, newVersion, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { + if (err) { + err.id = id; + return done(err); + } + + done(null, JSON.stringify({params: upgradedParams, metadata: upgradedMetadata})); + }); + } + + /** + * Run content upgrade. + * + * @public + * @param {string} name + * @param {Version} oldVersion + * @param {Version} newVersion + * @param {Object} params + * @param {Object} metadata + * @param {Function} done + */ + ContentUpgradeProcess.prototype.upgrade = function (name, oldVersion, newVersion, params, metadata, done) { + var self = this; + + // Load library details and upgrade routines + self.loadLibrary(name, newVersion, function (err, library) { + if (err) { + return done(err); + } + if (library.semantics === null) { + return done({ + type: 'libraryMissing', + library: library.name + ' ' + library.version.major + '.' + library.version.minor + }); + } + + // Run upgrade routines on params + self.processParams(library, oldVersion, newVersion, params, metadata, function (err, params, metadata) { + if (err) { + return done(err); + } + + // Check if any of the sub-libraries need upgrading + asyncSerial(library.semantics, function (index, field, next) { + self.processField(field, params[field.name], function (err, upgradedParams) { + if (upgradedParams) { + params[field.name] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params, metadata); + }); + }); + }); + }; + + /** + * Run upgrade hooks on params. + * + * @public + * @param {Object} library + * @param {Version} oldVersion + * @param {Version} newVersion + * @param {Object} params + * @param {Function} next + */ + ContentUpgradeProcess.prototype.processParams = function (library, oldVersion, newVersion, params, metadata, next) { + if (H5PUpgrades[library.name] === undefined) { + if (library.upgradesScript) { + // Upgrades script should be loaded so the upgrades should be here. + return next({ + type: 'scriptMissing', + library: library.name + ' ' + newVersion + }); + } + + // No upgrades script. Move on + return next(null, params, metadata); + } + + // Run upgrade hooks. Start by going through major versions + asyncSerial(H5PUpgrades[library.name], function (major, minors, nextMajor) { + if (major < oldVersion.major || major > newVersion.major) { + // Older than the current version or newer than the selected + nextMajor(); + } + else { + // Go through the minor versions for this major version + asyncSerial(minors, function (minor, upgrade, nextMinor) { + minor =+ minor; + if (minor <= oldVersion.minor || minor > newVersion.minor) { + // Older than or equal to the current version or newer than the selected + nextMinor(); + } + else { + // We found an upgrade hook, run it + var unnecessaryWrapper = (upgrade.contentUpgrade !== undefined ? upgrade.contentUpgrade : upgrade); + + try { + unnecessaryWrapper(params, function (err, upgradedParams, upgradedExtras) { + params = upgradedParams; + if (upgradedExtras && upgradedExtras.metadata) { // Optional + metadata = upgradedExtras.metadata; + } + nextMinor(err); + }, {metadata: metadata}); + } + catch (err) { + if (console && console.error) { + console.error("Error", err.stack); + console.error("Error", err.name); + console.error("Error", err.message); + } + next(err); + } + } + }, nextMajor); + } + }, function (err) { + next(err, params, metadata); + }); + }; + + /** + * Process parameter fields to find and upgrade sub-libraries. + * + * @public + * @param {Object} field + * @param {Object} params + * @param {Function} done + */ + ContentUpgradeProcess.prototype.processField = function (field, params, done) { + var self = this; + + if (params === undefined) { + return done(); + } + + switch (field.type) { + case 'library': + if (params.library === undefined || params.params === undefined) { + return done(); + } + + // Look for available upgrades + var usedLib = params.library.split(' ', 2); + for (var i = 0; i < field.options.length; i++) { + var availableLib = (typeof field.options[i] === 'string') ? field.options[i].split(' ', 2) : field.options[i].name.split(' ', 2); + if (availableLib[0] === usedLib[0]) { + if (availableLib[1] === usedLib[1]) { + return done(); // Same version + } + + // We have different versions + var usedVer = new Version(usedLib[1]); + var availableVer = new Version(availableLib[1]); + if (usedVer.major > availableVer.major || (usedVer.major === availableVer.major && usedVer.minor >= availableVer.minor)) { + return done({ + type: 'errorTooHighVersion', + used: usedLib[0] + ' ' + usedVer, + supported: availableLib[0] + ' ' + availableVer + }); // Larger or same version that's available + } + + // A newer version is available, upgrade params + return self.upgrade(availableLib[0], usedVer, availableVer, params.params, params.metadata, function (err, upgradedParams, upgradedMetadata) { + if (!err) { + params.library = availableLib[0] + ' ' + availableVer.major + '.' + availableVer.minor; + params.params = upgradedParams; + if (upgradedMetadata) { + params.metadata = upgradedMetadata; + } + } + done(err, params); + }); + } + } + + // Content type was not supporte by the higher version + done({ + type: 'errorNotSupported', + used: usedLib[0] + ' ' + usedVer + }); + break; + + case 'group': + if (field.fields.length === 1 && field.isSubContent !== true) { + // Single field to process, wrapper will be skipped + self.processField(field.fields[0], params, function (err, upgradedParams) { + if (upgradedParams) { + params = upgradedParams; + } + done(err, params); + }); + } + else { + // Go through all fields in the group + asyncSerial(field.fields, function (index, subField, next) { + var paramsToProcess = params ? params[subField.name] : null; + self.processField(subField, paramsToProcess, function (err, upgradedParams) { + if (upgradedParams) { + params[subField.name] = upgradedParams; + } + next(err); + }); + + }, function (err) { + done(err, params); + }); + } + break; + + case 'list': + // Go trough all params in the list + asyncSerial(params, function (index, subParams, next) { + self.processField(field.field, subParams, function (err, upgradedParams) { + if (upgradedParams) { + params[index] = upgradedParams; + } + next(err); + }); + }, function (err) { + done(err, params); + }); + break; + + default: + done(); + } + }; + + /** + * Helps process each property on the given object asynchronously in serial order. + * + * @private + * @param {Object} obj + * @param {Function} process + * @param {Function} finished + */ + var asyncSerial = function (obj, process, finished) { + var id, isArray = obj instanceof Array; + + // Keep track of each property that belongs to this object. + if (!isArray) { + var ids = []; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + ids.push(id); + } + } + } + + var i = -1; // Keeps track of the current property + + /** + * Private. Process the next property + */ + var next = function () { + id = isArray ? i : ids[i]; + process(id, obj[id], check); + }; + + /** + * Private. Check if we're done or have an error. + * + * @param {String} err + */ + var check = function (err) { + // We need to use a real async function in order for the stack to clear. + setTimeout(function () { + i++; + if (i === (isArray ? obj.length : ids.length) || (err !== undefined && err !== null)) { + finished(err); + } + else { + next(); + } + }, 0); + }; + + check(); // Start + }; + + return ContentUpgradeProcess; +})(H5P.Version); diff --git a/src/core/features/h5p/assets/js/h5p-content-upgrade-worker.js b/src/core/features/h5p/assets/js/h5p-content-upgrade-worker.js new file mode 100644 index 000000000..3507a358a --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-content-upgrade-worker.js @@ -0,0 +1,63 @@ +/* global importScripts */ +var H5P = H5P || {}; +importScripts('h5p-version.js', 'h5p-content-upgrade-process.js'); + +var libraryLoadedCallback; + +/** + * Register message handlers + */ +var messageHandlers = { + newJob: function (job) { + // Start new job + new H5P.ContentUpgradeProcess(job.name, new H5P.Version(job.oldVersion), new H5P.Version(job.newVersion), job.params, job.id, function loadLibrary(name, version, next) { + // TODO: Cache? + postMessage({ + action: 'loadLibrary', + name: name, + version: version.toString() + }); + libraryLoadedCallback = next; + }, function done(err, result) { + if (err) { + // Return error + postMessage({ + action: 'error', + id: job.id, + err: err.message ? err.message : err + }); + + return; + } + + // Return upgraded content + postMessage({ + action: 'done', + id: job.id, + params: result + }); + }); + }, + libraryLoaded: function (data) { + var library = data.library; + if (library.upgradesScript) { + try { + importScripts(library.upgradesScript); + } + catch (err) { + libraryLoadedCallback(err); + return; + } + } + libraryLoadedCallback(null, data.library); + } +}; + +/** + * Handle messages from our master + */ +onmessage = function (event) { + if (event.data.action !== undefined && messageHandlers[event.data.action]) { + messageHandlers[event.data.action].call(this, event.data); + } +}; diff --git a/src/core/features/h5p/assets/js/h5p-content-upgrade.js b/src/core/features/h5p/assets/js/h5p-content-upgrade.js new file mode 100644 index 000000000..9dc066c5c --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-content-upgrade.js @@ -0,0 +1,445 @@ +/* global H5PAdminIntegration H5PUtils */ + +(function ($, Version) { + var info, $log, $container, librariesCache = {}, scriptsCache = {}; + + // Initialize + $(document).ready(function () { + // Get library info + info = H5PAdminIntegration.libraryInfo; + + // Get and reset container + const $wrapper = $('#h5p-admin-container').html(''); + $log = $('').appendTo($wrapper); + $container = $('

    ' + info.message + '

    ').appendTo($wrapper); + + // Make it possible to select version + var $version = $(getVersionSelect(info.versions)).appendTo($container); + + // Add "go" button + $(''); + H5PLibraryDetails.$next = $(''); + + H5PLibraryDetails.$previous.on('click', function () { + if (H5PLibraryDetails.$previous.hasClass('disabled')) { + return; + } + + H5PLibraryDetails.currentPage--; + H5PLibraryDetails.updatePager(); + H5PLibraryDetails.createContentTable(); + }); + + H5PLibraryDetails.$next.on('click', function () { + if (H5PLibraryDetails.$next.hasClass('disabled')) { + return; + } + + H5PLibraryDetails.currentPage++; + H5PLibraryDetails.updatePager(); + H5PLibraryDetails.createContentTable(); + }); + + // This is the Page x of y widget: + H5PLibraryDetails.$pagerInfo = $(''); + + H5PLibraryDetails.$pager = $('
    ').append(H5PLibraryDetails.$previous, H5PLibraryDetails.$pagerInfo, H5PLibraryDetails.$next); + H5PLibraryDetails.$content.append(H5PLibraryDetails.$pager); + + H5PLibraryDetails.$pagerInfo.on('click', function () { + var width = H5PLibraryDetails.$pagerInfo.innerWidth(); + H5PLibraryDetails.$pagerInfo.hide(); + + // User has updated the pageNumber + var pageNumerUpdated = function () { + var newPageNum = $gotoInput.val()-1; + var intRegex = /^\d+$/; + + $goto.remove(); + H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); + + // Check if input value is valid, and that it has actually changed + if (!(intRegex.test(newPageNum) && newPageNum >= 0 && newPageNum < H5PLibraryDetails.getNumPages() && newPageNum != H5PLibraryDetails.currentPage)) { + return; + } + + H5PLibraryDetails.currentPage = newPageNum; + H5PLibraryDetails.updatePager(); + H5PLibraryDetails.createContentTable(); + }; + + // We create an input box where the user may type in the page number + // he wants to be displayed. + // Reson for doing this is when user has ten-thousands of elements in list, + // this is the easiest way of getting to a specified page + var $gotoInput = $('', { + type: 'number', + min : 1, + max: H5PLibraryDetails.getNumPages(), + on: { + // Listen to blur, and the enter-key: + 'blur': pageNumerUpdated, + 'keyup': function (event) { + if (event.keyCode === 13) { + pageNumerUpdated(); + } + } + } + }).css({width: width}); + var $goto = $('', { + 'class': 'h5p-pager-goto' + }).css({width: width}).append($gotoInput).insertAfter(H5PLibraryDetails.$pagerInfo); + + $gotoInput.focus(); + }); + + H5PLibraryDetails.updatePager(); + }; + + /** + * Calculates number of pages + */ + H5PLibraryDetails.getNumPages = function () { + return Math.ceil(H5PLibraryDetails.currentContent.length / H5PLibraryDetails.PAGER_SIZE); + }; + + /** + * Update the pager text, and enables/disables the next and previous buttons as needed + */ + H5PLibraryDetails.updatePager = function () { + H5PLibraryDetails.$pagerInfo.css({display: 'inline-block'}); + + if (H5PLibraryDetails.getNumPages() > 0) { + var message = H5PUtils.translateReplace(H5PLibraryDetails.library.translations.pageXOfY, { + '$x': (H5PLibraryDetails.currentPage+1), + '$y': H5PLibraryDetails.getNumPages() + }); + H5PLibraryDetails.$pagerInfo.html(message); + } + else { + H5PLibraryDetails.$pagerInfo.html(''); + } + + H5PLibraryDetails.$previous.toggleClass('disabled', H5PLibraryDetails.currentPage <= 0); + H5PLibraryDetails.$next.toggleClass('disabled', H5PLibraryDetails.currentContent.length < (H5PLibraryDetails.currentPage+1)*H5PLibraryDetails.PAGER_SIZE); + }; + + /** + * Creates the search element + */ + H5PLibraryDetails.createSearchElement = function () { + + H5PLibraryDetails.$search = $(''); + + var performSeach = function () { + var searchString = $('.h5p-content-search > input').val(); + + // If search string same as previous, just do nothing + if (H5PLibraryDetails.currentFilter === searchString) { + return; + } + + if (searchString.trim().length === 0) { + // If empty search, use the complete list + H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content; + } + else if (H5PLibraryDetails.filterCache[searchString]) { + // If search is cached, no need to filter + H5PLibraryDetails.currentContent = H5PLibraryDetails.filterCache[searchString]; + } + else { + var listToFilter = H5PLibraryDetails.library.content; + + // Check if we can filter the already filtered results (for performance) + if (searchString.length > 1 && H5PLibraryDetails.currentFilter === searchString.substr(0, H5PLibraryDetails.currentFilter.length)) { + listToFilter = H5PLibraryDetails.currentContent; + } + H5PLibraryDetails.currentContent = $.grep(listToFilter, function (content) { + return content.title && content.title.match(new RegExp(searchString, 'i')); + }); + } + + H5PLibraryDetails.currentFilter = searchString; + // Cache the current result + H5PLibraryDetails.filterCache[searchString] = H5PLibraryDetails.currentContent; + H5PLibraryDetails.currentPage = 0; + H5PLibraryDetails.createContentTable(); + + // Display search results: + if (H5PLibraryDetails.$searchResults) { + H5PLibraryDetails.$searchResults.remove(); + } + if (searchString.trim().length > 0) { + H5PLibraryDetails.$searchResults = $('' + H5PLibraryDetails.currentContent.length + ' hits on ' + H5PLibraryDetails.currentFilter + ''); + H5PLibraryDetails.$search.append(H5PLibraryDetails.$searchResults); + } + H5PLibraryDetails.updatePager(); + }; + + var inputTimer; + $('input', H5PLibraryDetails.$search).on('change keypress paste input', function () { + // Here we start the filtering + // We wait at least 500 ms after last input to perform search + if (inputTimer) { + clearTimeout(inputTimer); + } + + inputTimer = setTimeout( function () { + performSeach(); + }, 500); + }); + + H5PLibraryDetails.$content.append(H5PLibraryDetails.$search); + }; + + /** + * Creates the page size selector + */ + H5PLibraryDetails.createPageSizeSelector = function () { + H5PLibraryDetails.$search.append('
    ' + H5PLibraryDetails.library.translations.pageSizeSelectorLabel + ':102050100200
    '); + + // Listen to clicks on the page size selector: + $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).on('click', function () { + H5PLibraryDetails.PAGER_SIZE = $(this).data('page-size'); + $('.h5p-admin-pager-size-selector > span', H5PLibraryDetails.$search).removeClass('selected'); + $(this).addClass('selected'); + H5PLibraryDetails.currentPage = 0; + H5PLibraryDetails.createContentTable(); + H5PLibraryDetails.updatePager(); + }); + }; + + // Initialize me: + $(document).ready(function () { + if (!H5PLibraryDetails.initialized) { + H5PLibraryDetails.initialized = true; + H5PLibraryDetails.init(); + } + }); + +})(H5P.jQuery); diff --git a/src/core/features/h5p/assets/js/h5p-library-list.js b/src/core/features/h5p/assets/js/h5p-library-list.js new file mode 100644 index 000000000..344b73672 --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-library-list.js @@ -0,0 +1,140 @@ +/* global H5PAdminIntegration H5PUtils */ +var H5PLibraryList = H5PLibraryList || {}; + +(function ($) { + + /** + * Initializing + */ + H5PLibraryList.init = function () { + var $adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector).html(''); + + var libraryList = H5PAdminIntegration.libraryList; + if (libraryList.notCached) { + $adminContainer.append(H5PUtils.getRebuildCache(libraryList.notCached)); + } + + // Create library list + $adminContainer.append(H5PLibraryList.createLibraryList(H5PAdminIntegration.libraryList)); + }; + + /** + * Create the library list + * + * @param {object} libraries List of libraries and headers + */ + H5PLibraryList.createLibraryList = function (libraries) { + var t = H5PAdminIntegration.l10n; + if (libraries.listData === undefined || libraries.listData.length === 0) { + return $('
    ' + t.NA + '
    '); + } + + // Create table + var $table = H5PUtils.createTable(libraries.listHeaders); + $table.addClass('libraries'); + + // Add libraries + $.each (libraries.listData, function (index, library) { + var $libraryRow = H5PUtils.createTableRow([ + library.title, + '', + { + text: library.numContent, + class: 'h5p-admin-center' + }, + { + text: library.numContentDependencies, + class: 'h5p-admin-center' + }, + { + text: library.numLibraryDependencies, + class: 'h5p-admin-center' + }, + '
    ' + + '' + + (library.detailsUrl ? '' : '') + + (library.deleteUrl ? '' : '') + + '
    ' + ]); + + H5PLibraryList.addRestricted($('.h5p-admin-restricted', $libraryRow), library.restrictedUrl, library.restricted); + + var hasContent = !(library.numContent === '' || library.numContent === 0); + if (library.upgradeUrl === null) { + $('.h5p-admin-upgrade-library', $libraryRow).remove(); + } + else if (library.upgradeUrl === false || !hasContent) { + $('.h5p-admin-upgrade-library', $libraryRow).attr('disabled', true); + } + else { + $('.h5p-admin-upgrade-library', $libraryRow).attr('title', t.upgradeLibrary).click(function () { + window.location.href = library.upgradeUrl; + }); + } + + // Open details view when clicked + $('.h5p-admin-view-library', $libraryRow).on('click', function () { + window.location.href = library.detailsUrl; + }); + + var $deleteButton = $('.h5p-admin-delete-library', $libraryRow); + if (libraries.notCached !== undefined || + hasContent || + (library.numContentDependencies !== '' && + library.numContentDependencies !== 0) || + (library.numLibraryDependencies !== '' && + library.numLibraryDependencies !== 0)) { + // Disabled delete if content. + $deleteButton.attr('disabled', true); + } + else { + // Go to delete page om click. + $deleteButton.attr('title', t.deleteLibrary).on('click', function () { + window.location.href = library.deleteUrl; + }); + } + + $table.append($libraryRow); + }); + + return $table; + }; + + H5PLibraryList.addRestricted = function ($checkbox, url, selected) { + if (selected === null) { + $checkbox.remove(); + } + else { + $checkbox.change(function () { + $checkbox.attr('disabled', true); + + $.ajax({ + dataType: 'json', + url: url, + cache: false + }).fail(function () { + $checkbox.attr('disabled', false); + + // Reset + $checkbox.attr('checked', !$checkbox.is(':checked')); + }).done(function (result) { + url = result.url; + $checkbox.attr('disabled', false); + }); + }); + + if (selected) { + $checkbox.attr('checked', true); + } + } + }; + + // Initialize me: + $(document).ready(function () { + if (!H5PLibraryList.initialized) { + H5PLibraryList.initialized = true; + H5PLibraryList.init(); + } + }); + +})(H5P.jQuery); diff --git a/src/core/features/h5p/assets/js/h5p-resizer.js b/src/core/features/h5p/assets/js/h5p-resizer.js new file mode 100644 index 000000000..ed78724ec --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-resizer.js @@ -0,0 +1,131 @@ +// H5P iframe Resizer +(function () { + if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) { + return; // Not supported + } + window.h5pResizerInitialized = true; + + // Map actions to handlers + var actionHandlers = {}; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.hello = function (iframe, data, respond) { + // Make iframe responsive + iframe.style.width = '100%'; + + // Bugfix for Chrome: Force update of iframe width. If this is not done the + // document size may not be updated before the content resizes. + iframe.getBoundingClientRect(); + + // Tell iframe that it needs to resize when our window resizes + var resize = function () { + if (iframe.contentWindow) { + // Limit resize calls to avoid flickering + respond('resize'); + } + else { + // Frame is gone, unregister. + window.removeEventListener('resize', resize); + } + }; + window.addEventListener('resize', resize, false); + + // Respond to let the iframe know we can resize it + respond('hello'); + }; + + /** + * Prepare iframe resize. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.prepareResize = function (iframe, data, respond) { + // Do not resize unless page and scrolling differs + if (iframe.clientHeight !== data.scrollHeight || + data.scrollHeight !== data.clientHeight) { + + // Reset iframe height, in case content has shrinked. + iframe.style.height = data.clientHeight + 'px'; + respond('resizePrepared'); + } + }; + + /** + * Resize parent and iframe to desired height. + * + * @private + * @param {Object} iframe Element + * @param {Object} data Payload + * @param {Function} respond Send a response to the iframe + */ + actionHandlers.resize = function (iframe, data) { + // Resize iframe so all content is visible. Use scrollHeight to make sure we get everything + iframe.style.height = data.scrollHeight + 'px'; + }; + + /** + * Keyup event handler. Exits full screen on escape. + * + * @param {Event} event + */ + var escape = function (event) { + if (event.keyCode === 27) { + exitFullScreen(); + } + }; + + // Listen for messages from iframes + window.addEventListener('message', function receiveMessage(event) { + if (event.data.context !== 'h5p') { + return; // Only handle h5p requests. + } + + // Find out who sent the message + var iframe, iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + iframe = iframes[i]; + break; + } + } + + if (!iframe) { + return; // Cannot find sender + } + + // Find action handler handler + if (actionHandlers[event.data.action]) { + actionHandlers[event.data.action](iframe, event.data, function respond(action, data) { + if (data === undefined) { + data = {}; + } + data.action = action; + data.context = 'h5p'; + event.source.postMessage(data, event.origin); + }); + } + }, false); + + // Let h5p iframes know we're ready! + var iframes = document.getElementsByTagName('iframe'); + var ready = { + context: 'h5p', + action: 'ready' + }; + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].src.indexOf('h5p') !== -1) { + iframes[i].contentWindow.postMessage(ready, '*'); + } + } + +})(); diff --git a/src/core/features/h5p/assets/js/h5p-utils.js b/src/core/features/h5p/assets/js/h5p-utils.js new file mode 100644 index 000000000..b5aa3334e --- /dev/null +++ b/src/core/features/h5p/assets/js/h5p-utils.js @@ -0,0 +1,506 @@ +/* global H5PAdminIntegration*/ +var H5PUtils = H5PUtils || {}; + +(function ($) { + /** + * Generic function for creating a table including the headers + * + * @param {array} headers List of headers + */ + H5PUtils.createTable = function (headers) { + var $table = $('
    '); + + if (headers) { + var $thead = $(''); + var $tr = $(''); + + $.each(headers, function (index, value) { + if (!(value instanceof Object)) { + value = { + html: value + }; + } + + $('', value).appendTo($tr); + }); + + $table.append($thead.append($tr)); + } + + return $table; + }; + + /** + * Generic function for creating a table row + * + * @param {array} rows Value list. Object name is used as class name in + */ + H5PUtils.createTableRow = function (rows) { + var $tr = $(''); + + $.each(rows, function (index, value) { + if (!(value instanceof Object)) { + value = { + html: value + }; + } + + $('', value).appendTo($tr); + }); + + return $tr; + }; + + /** + * Generic function for creating a field containing label and value + * + * @param {string} label The label displayed in front of the value + * @param {string} value The value + */ + H5PUtils.createLabeledField = function (label, value) { + var $field = $('
    '); + + $field.append('
    ' + label + '
    '); + $field.append('
    ' + value + '
    '); + + return $field; + }; + + /** + * Replaces placeholder fields in translation strings + * + * @param {string} template The translation template string in the following format: "$name is a $sex" + * @param {array} replacors An js object with key and values. Eg: {'$name': 'Frode', '$sex': 'male'} + */ + H5PUtils.translateReplace = function (template, replacors) { + $.each(replacors, function (key, value) { + template = template.replace(new RegExp('\\'+key, 'g'), value); + }); + return template; + }; + + /** + * Get throbber with given text. + * + * @param {String} text + * @returns {$} + */ + H5PUtils.throbber = function (text) { + return $('
    ', { + class: 'h5p-throbber', + text: text + }); + }; + + /** + * Makes it possbile to rebuild all content caches from admin UI. + * @param {Object} notCached + * @returns {$} + */ + H5PUtils.getRebuildCache = function (notCached) { + var $container = $('

    ' + notCached.message + '

    ' + notCached.progress + '

    '); + var $button = $('').appendTo($container).click(function () { + var $spinner = $('
    ', {class: 'h5p-spinner'}).replaceAll($button); + var parts = ['|', '/', '-', '\\']; + var current = 0; + var spinning = setInterval(function () { + $spinner.text(parts[current]); + current++; + if (current === parts.length) current = 0; + }, 100); + + var $counter = $container.find('.progress'); + var build = function () { + $.post(notCached.url, function (left) { + if (left === '0') { + clearInterval(spinning); + $container.remove(); + location.reload(); + } + else { + var counter = $counter.text().split(' '); + counter[0] = left; + $counter.text(counter.join(' ')); + build(); + } + }); + }; + build(); + }); + + return $container; + }; + + /** + * Generic table class with useful helpers. + * + * @class + * @param {Object} classes + * Custom html classes to use on elements. + * e.g. {tableClass: 'fixed'}. + */ + H5PUtils.Table = function (classes) { + var numCols; + var sortByCol; + var $sortCol; + var sortCol; + var sortDir; + + // Create basic table + var tableOptions = {}; + if (classes.table !== undefined) { + tableOptions['class'] = classes.table; + } + var $table = $('', tableOptions); + var $thead = $('').appendTo($table); + var $tfoot = $('').appendTo($table); + var $tbody = $('').appendTo($table); + + /** + * Add columns to given table row. + * + * @private + * @param {jQuery} $tr Table row + * @param {(String|Object)} col Column properties + * @param {Number} id Used to seperate the columns + */ + var addCol = function ($tr, col, id) { + var options = { + on: {} + }; + + if (!(col instanceof Object)) { + options.text = col; + } + else { + if (col.text !== undefined) { + options.text = col.text; + } + if (col.class !== undefined) { + options.class = col.class; + } + + if (sortByCol !== undefined && col.sortable === true) { + // Make sortable + options.role = 'button'; + options.tabIndex = 0; + + // This is the first sortable column, use as default sort + if (sortCol === undefined) { + sortCol = id; + sortDir = 0; + } + + // This is the sort column + if (sortCol === id) { + options['class'] = 'h5p-sort'; + if (sortDir === 1) { + options['class'] += ' h5p-reverse'; + } + } + + options.on.click = function () { + sort($th, id); + }; + options.on.keypress = function (event) { + if ((event.charCode || event.keyCode) === 32) { // Space + sort($th, id); + } + }; + } + } + + // Append + var $th = $(''); + var $tr = $('').appendTo($newThead); + for (var i = 0; i < cols.length; i++) { + addCol($tr, cols[i], i); + } + + // Update DOM + $thead.replaceWith($newThead); + $thead = $newThead; + }; + + /** + * Set table rows. + * + * @public + * @param {Array} rows Table rows with cols: [[1,'hello',3],[2,'asd',6]] + */ + this.setRows = function (rows) { + var $newTbody = $(''); + + for (var i = 0; i < rows.length; i++) { + var $tr = $('').appendTo($newTbody); + + for (var j = 0; j < rows[i].length; j++) { + $(''); + var $tr = $('').appendTo($newTbody); + $(''); + var $tr = $('').appendTo($newTfoot); + $('"!==p[1]||ue.test(a)?0:u:u.firstChild)&&a.childNodes.length;o--;)d.nodeName(c=a.childNodes[o],"tbody")&&!c.childNodes.length&&a.removeChild(c);for(d.merge(m,u.childNodes),u.textContent="";u.firstChild;)u.removeChild(u.firstChild);u=g.lastChild}else m.push(t.createTextNode(a));for(u&&g.removeChild(u),f.appendChecked||d.grep(oe(m,"input"),le),v=0;a=m[v++];)if(r&&d.inArray(a,r)>-1)i&&i.push(a);else if(s=d.contains(a.ownerDocument,a),u=oe(g.appendChild(a),"script"),s&&ae(u),n)for(o=0;a=u[o++];)ee.test(a.type||"")&&n.push(a);return u=null,g}!function(){var t,n,i=r.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(f[t]=n in e)||(i.setAttribute(n,"t"),f[t]=!1===i.attributes[n].expando);i=null}();var fe=/^(?:input|select|textarea)$/i,de=/^key/,pe=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,he=/^(?:focusinfocus|focusoutblur)$/,ge=/^([^.]*)(?:\.(.+)|)/;function me(){return!0}function ve(){return!1}function ye(){try{return r.activeElement}catch(e){}}function xe(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)xe(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ve;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return d().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=d.guid++)),e.each(function(){d.event.add(this,t,i,r,n)})}d.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d._data(e);if(v){for(n.handler&&(n=(u=n).handler,i=u.selector),n.guid||(n.guid=d.guid++),(a=v.events)||(a=v.events={}),(c=v.handle)||((c=v.handle=function(e){return void 0===d||e&&d.event.triggered===e.type?void 0:d.event.dispatch.apply(c.elem,arguments)}).elem=e),s=(t=(t||"").match(H)||[""]).length;s--;)h=m=(o=ge.exec(t[s])||[])[1],g=(o[2]||"").split(".").sort(),h&&(l=d.event.special[h]||{},h=(i?l.delegateType:l.bindType)||h,l=d.event.special[h]||{},f=d.extend({type:h,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&d.expr.match.needsContext.test(i),namespace:g.join(".")},u),(p=a[h])||((p=a[h]=[]).delegateCount=0,l.setup&&!1!==l.setup.call(e,r,g,c)||(e.addEventListener?e.addEventListener(h,c,!1):e.attachEvent&&e.attachEvent("on"+h,c))),l.add&&(l.add.call(e,f),f.handler.guid||(f.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,f):p.push(f),d.event.global[h]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,h,g,m,v=d.hasData(e)&&d._data(e);if(v&&(c=v.events)){for(l=(t=(t||"").match(H)||[""]).length;l--;)if(h=m=(s=ge.exec(t[l])||[])[1],g=(s[2]||"").split(".").sort(),h){for(f=d.event.special[h]||{},p=c[h=(r?f.delegateType:f.bindType)||h]||[],s=s[2]&&new RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=p.length;o--;)a=p[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(p.splice(o,1),a.selector&&p.delegateCount--,f.remove&&f.remove.call(e,a));u&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,g,v.handle)||d.removeEvent(e,h,v.handle),delete c[h])}else for(h in c)d.event.remove(e,h+t[l],n,r,!0);d.isEmptyObject(c)&&(delete v.handle,d._removeData(e,"events"))}},trigger:function(t,n,i,o){var a,s,u,l,f,p,h,g=[i||r],m=c.call(t,"type")?t.type:t,v=c.call(t,"namespace")?t.namespace.split("."):[];if(u=p=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!he.test(m+d.event.triggered)&&(m.indexOf(".")>-1&&(v=m.split("."),m=v.shift(),v.sort()),s=m.indexOf(":")<0&&"on"+m,(t=t[d.expando]?t:new d.Event(m,"object"==typeof t&&t)).isTrigger=o?2:3,t.namespace=v.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:d.makeArray(n,[t]),f=d.event.special[m]||{},o||!f.trigger||!1!==f.trigger.apply(i,n))){if(!o&&!f.noBubble&&!d.isWindow(i)){for(l=f.delegateType||m,he.test(l+m)||(u=u.parentNode);u;u=u.parentNode)g.push(u),p=u;p===(i.ownerDocument||r)&&g.push(p.defaultView||p.parentWindow||e)}for(h=0;(u=g[h++])&&!t.isPropagationStopped();)t.type=h>1?l:f.bindType||m,(a=(d._data(u,"events")||{})[t.type]&&d._data(u,"handle"))&&a.apply(u,n),(a=s&&u[s])&&a.apply&&M(u)&&(t.result=a.apply(u,n),!1===t.result&&t.preventDefault());if(t.type=m,!o&&!t.isDefaultPrevented()&&(!f._default||!1===f._default.apply(g.pop(),n))&&M(i)&&s&&i[m]&&!d.isWindow(i)){(p=i[s])&&(i[s]=null),d.event.triggered=m;try{i[m]()}catch(e){}d.event.triggered=void 0,p&&(i[s]=p)}return t.result}},dispatch:function(e){e=d.event.fix(e);var t,n,r,o,a,s,u=i.call(arguments),l=(d._data(this,"events")||{})[e.type]||[],c=d.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,e)){for(s=d.event.handlers.call(this,e,l),t=0;(o=s[t++])&&!e.isPropagationStopped();)for(e.currentTarget=o.elem,n=0;(a=o.handlers[n++])&&!e.isImmediatePropagationStopped();)e.rnamespace&&!e.rnamespace.test(a.namespace)||(e.handleObj=a,e.data=a.data,void 0!==(r=((d.event.special[a.origType]||{}).handle||a.handler).apply(o.elem,u))&&!1===(e.result=r)&&(e.preventDefault(),e.stopPropagation()));return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,u=e.target;if(s&&u.nodeType&&("click"!==e.type||isNaN(e.button)||e.button<1))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(!0!==u.disabled||"click"!==e.type)){for(r=[],n=0;n-1:d.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&a.push({elem:u,handlers:r})}return s]","i"),Te=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,Ce=/\s*$/g,Se=re(r).appendChild(r.createElement("div"));function Ae(e,t){return d.nodeName(e,"table")&&d.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function De(e){return e.type=(null!==d.find.attr(e,"type"))+"/"+e.type,e}function je(e){var t=Ne.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Le(e,t){if(1===t.nodeType&&d.hasData(e)){var n,r,i,o=d._data(e),a=d._data(t,o),s=o.events;if(s)for(n in delete a.handle,a.events={},s)for(r=0,i=s[n].length;r1&&"string"==typeof m&&!f.checkClone&&Ee.test(m))return e.each(function(i){var o=e.eq(i);v&&(t[0]=m.call(this,i,o.html())),qe(o,t,n,r)});if(h&&(i=(c=ce(t,e[0].ownerDocument,!1,e,r)).firstChild,1===c.childNodes.length&&(c=i),i||r)){for(s=(u=d.map(oe(c,"script"),De)).length;p")},clone:function(e,t,n){var r,i,o,a,s,u=d.contains(e.ownerDocument,e);if(f.html5Clone||d.isXMLDoc(e)||!we.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Se.innerHTML=e.outerHTML,Se.removeChild(o=Se.firstChild)),!(f.noCloneEvent&&f.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||d.isXMLDoc(e)))for(r=oe(o),s=oe(e),a=0;null!=(i=s[a]);++a)r[a]&&He(i,r[a]);if(t)if(n)for(s=s||oe(e),r=r||oe(o),a=0;null!=(i=s[a]);a++)Le(i,r[a]);else Le(e,o);return(r=oe(o,"script")).length>0&&ae(r,!u&&oe(e,"script")),r=s=i=null,o},cleanData:function(e,t){for(var r,i,o,a,s=0,u=d.expando,l=d.cache,c=f.attributes,p=d.event.special;null!=(r=e[s]);s++)if((t||M(r))&&(a=(o=r[u])&&l[o])){if(a.events)for(i in a.events)p[i]?d.event.remove(r,i):d.removeEvent(r,i,a.handle);l[o]&&(delete l[o],c||void 0===r.removeAttribute?r[u]=void 0:r.removeAttribute(u),n.push(o))}}}),d.fn.extend({domManip:qe,detach:function(e){return _e(this,e,!0)},remove:function(e){return _e(this,e)},text:function(e){return Q(this,function(e){return void 0===e?d.text(this):this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(e))},null,e,arguments.length)},append:function(){return qe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ae(this,e).appendChild(e)})},prepend:function(){return qe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Ae(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&d.cleanData(oe(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&d.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return d.clone(this,e,t)})},html:function(e){return Q(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(be,""):void 0;if("string"==typeof e&&!Ce.test(e)&&(f.htmlSerialize||!we.test(e))&&(f.leadingWhitespace||!te.test(e))&&!ie[(Z.exec(e)||["",""])[1].toLowerCase()]){e=d.htmlPrefilter(e);try{for(;n")).appendTo(t.documentElement))[0].contentWindow||Fe[0].contentDocument).document).write(),t.close(),n=Oe(e,t),Fe.detach()),Me[e]=n),n}var Pe=/^margin/,Be=new RegExp("^("+$+")(?!px)[a-z%]+$","i"),We=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];for(o in i=n.apply(e,r||[]),t)e.style[o]=a[o];return i},Ie=r.documentElement;!function(){var t,n,i,o,a,s,u=r.createElement("div"),l=r.createElement("div");function c(){var c,f,d=r.documentElement;d.appendChild(u),l.style.cssText="-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",t=i=s=!1,n=a=!0,e.getComputedStyle&&(f=e.getComputedStyle(l),t="1%"!==(f||{}).top,s="2px"===(f||{}).marginLeft,i="4px"===(f||{width:"4px"}).width,l.style.marginRight="50%",n="4px"===(f||{marginRight:"4px"}).marginRight,(c=l.appendChild(r.createElement("div"))).style.cssText=l.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",l.style.width="1px",a=!parseFloat((e.getComputedStyle(c)||{}).marginRight),l.removeChild(c)),l.style.display="none",(o=0===l.getClientRects().length)&&(l.style.display="",l.innerHTML="
    ', options).appendTo($tr); + if (sortCol === id) { + $sortCol = $th; // Default sort column + } + }; + + /** + * Updates the UI when a column header has been clicked. + * Triggers sorting callback. + * + * @private + * @param {jQuery} $th Table header + * @param {Number} id Used to seperate the columns + */ + var sort = function ($th, id) { + if (id === sortCol) { + // Change sorting direction + if (sortDir === 0) { + sortDir = 1; + $th.addClass('h5p-reverse'); + } + else { + sortDir = 0; + $th.removeClass('h5p-reverse'); + } + } + else { + // Change sorting column + $sortCol.removeClass('h5p-sort').removeClass('h5p-reverse'); + $sortCol = $th.addClass('h5p-sort'); + sortCol = id; + sortDir = 0; + } + + sortByCol({ + by: sortCol, + dir: sortDir + }); + }; + + /** + * Set table headers. + * + * @public + * @param {Array} cols + * Table header data. Can be strings or objects with options like + * "text" and "sortable". E.g. + * [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] + * @param {Function} sort Callback which is runned when sorting changes + * @param {Object} [order] + */ + this.setHeaders = function (cols, sort, order) { + numCols = cols.length; + sortByCol = sort; + + if (order) { + sortCol = order.by; + sortDir = order.dir; + } + + // Create new head + var $newThead = $('
    ', { + html: rows[i][j] + }).appendTo($tr); + } + } + + $tbody.replaceWith($newTbody); + $tbody = $newTbody; + + return $tbody; + }; + + /** + * Set custom table body content. This can be a message or a throbber. + * Will cover all table columns. + * + * @public + * @param {jQuery} $content Custom content + */ + this.setBody = function ($content) { + var $newTbody = $('
    ', { + colspan: numCols + }).append($content).appendTo($tr); + $tbody.replaceWith($newTbody); + $tbody = $newTbody; + }; + + /** + * Set custom table foot content. This can be a pagination widget. + * Will cover all table columns. + * + * @public + * @param {jQuery} $content Custom content + */ + this.setFoot = function ($content) { + var $newTfoot = $('
    ', { + colspan: numCols + }).append($content).appendTo($tr); + $tfoot.replaceWith($newTfoot); + }; + + + /** + * Appends the table to the given container. + * + * @public + * @param {jQuery} $container + */ + this.appendTo = function ($container) { + $table.appendTo($container); + }; + }; + + /** + * Generic pagination class. Creates a useful pagination widget. + * + * @class + * @param {Number} num Total number of items to pagiate. + * @param {Number} limit Number of items to dispaly per page. + * @param {Function} goneTo + * Callback which is fired when the user wants to go to another page. + * @param {Object} l10n + * Localization / translations. e.g. + * { + * currentPage: 'Page $current of $total', + * nextPage: 'Next page', + * previousPage: 'Previous page' + * } + */ + H5PUtils.Pagination = function (num, limit, goneTo, l10n) { + var current = 0; + var pages = Math.ceil(num / limit); + + // Create components + + // Previous button + var $left = $(''; + } + if (contentData.displayOptions.export && contentData.displayOptions.copy) { + html += '
    or
    '; + } + if (contentData.displayOptions.copy) { + html += ''; + } + + const dialog = new H5P.Dialog('reuse', H5P.t('reuseContent'), html, $element); + + // Selecting embed code when dialog is opened + H5P.jQuery(dialog).on('dialog-opened', function (e, $dialog) { + H5P.jQuery('More Info').click(function (e) { + e.stopPropagation(); + }).appendTo($dialog.find('h2')); + $dialog.find('.h5p-download-button').click(function () { + window.location.href = contentData.exportUrl; + instance.triggerXAPI('downloaded'); + dialog.close(); + }); + $dialog.find('.h5p-copy-button').click(function () { + const item = new H5P.ClipboardItem(library); + item.contentId = contentId; + H5P.setClipboard(item); + instance.triggerXAPI('copied'); + dialog.close(); + H5P.attachToastTo( + H5P.jQuery('.h5p-content:first')[0], + H5P.t('contentCopied'), + { + position: { + horizontal: 'centered', + vertical: 'centered', + noOverflowX: true + } + } + ); + }); + H5P.trigger(instance, 'resize'); + }).on('dialog-closed', function () { + H5P.trigger(instance, 'resize'); + }); + + dialog.open(); +}; + +/** + * Display a dialog containing the embed code. + * + * @param {H5P.jQuery} $element + * Element to insert dialog after. + * @param {string} embedCode + * The embed code. + * @param {string} resizeCode + * The advanced resize code + * @param {Object} size + * The content's size. + * @param {number} size.width + * @param {number} size.height + */ +H5P.openEmbedDialog = function ($element, embedCode, resizeCode, size, instance) { + 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 () { + 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')); + $content.hide(); + } + else { + $expander.addClass('h5p-open').text(H5P.t('hideAdvanced')); + $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); + } + }); + }).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 = '

    ' + label + '

    ' + html; + } + + // Add wrapper + html = '
    ' + html + '
    '; + } + + return html; + }; +}; + +/** + * A ordered list of copyright fields for media. + * + * @class + * @param {Object} copyright + * Copyright information fields. + * @param {Object} [labels] + * Translation of labels. + * @param {Array} [order] + * Order of the fields. + * @param {Object} [extraFields] + * Add extra copyright fields. + */ +H5P.MediaCopyright = function (copyright, labels, order, extraFields) { + var thumbnail; + var list = new H5P.DefinitionList(); + + /** + * Get translated label for field. + * + * @private + * @param {string} fieldName + * @returns {string} + */ + var getLabel = function (fieldName) { + if (labels === undefined || labels[fieldName] === undefined) { + return H5P.t(fieldName); + } + + return labels[fieldName]; + }; + + /** + * Get humanized value for the license field. + * + * @private + * @param {string} license + * @param {string} [version] + * @returns {string} + */ + var humanizeLicense = function (license, version) { + var copyrightLicense = H5P.copyrightLicenses[license]; + + // Build license string + var value = ''; + if (!(license === 'PD' && version)) { + // Add license label + value += (copyrightLicense.hasOwnProperty('label') ? copyrightLicense.label : copyrightLicense); + } + + // Check for version info + var versionInfo; + if (copyrightLicense.versions) { + if (copyrightLicense.versions.default && (!version || !copyrightLicense.versions[version])) { + version = copyrightLicense.versions.default; + } + if (version && copyrightLicense.versions[version]) { + versionInfo = copyrightLicense.versions[version]; + } + } + + if (versionInfo) { + // Add license version + if (value) { + value += ' '; + } + value += (versionInfo.hasOwnProperty('label') ? versionInfo.label : versionInfo); + } + + // Add link if specified + var link; + if (copyrightLicense.hasOwnProperty('link')) { + link = copyrightLicense.link.replace(':version', copyrightLicense.linkVersions ? copyrightLicense.linkVersions[version] : version); + } + else if (versionInfo && copyrightLicense.hasOwnProperty('link')) { + link = versionInfo.link; + } + if (link) { + value = '' + value + ''; + } + + // Generate parenthesis + var parenthesis = ''; + if (license !== 'PD' && license !== 'C') { + parenthesis += license; + } + if (version && version !== 'CC0 1.0') { + if (parenthesis && license !== 'GNU GPL') { + parenthesis += ' '; + } + parenthesis += version; + } + if (parenthesis) { + value += ' (' + parenthesis + ')'; + } + if (license === 'C') { + 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 = ['contentType', 'title', 'license', 'author', 'year', 'source', 'licenseExtras', 'changes']; + } + + for (var i = 0; i < order.length; i++) { + var fieldName = order[i]; + if (copyright[fieldName] !== undefined && copyright[fieldName] !== '') { + var humanValue = copyright[fieldName]; + if (fieldName === 'license') { + humanValue = humanizeLicense(copyright.license, copyright.version); + } + if (fieldName === 'source') { + humanValue = (humanValue) ? '' + humanValue + '' : undefined; + } + list.add(new H5P.Field(getLabel(fieldName), humanValue)); + } + } + } + + /** + * Set thumbnail. + * + * @param {H5P.Thumbnail} newThumbnail + */ + this.setThumbnail = function (newThumbnail) { + thumbnail = newThumbnail; + }; + + /** + * 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() === humanizeLicense('U')) { + return true; + } + } + return false; + }; + + /** + * 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 = ''; + } + + return html; + }; +}; + +/** + * A simple and elegant class for creating thumbnails of images. + * + * @class + * @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)); + } + + /** + * Print thumbnail. + * + * @returns {string} HTML. + */ + this.toString = function () { + return '' + H5P.t('thumbnail') + ''; + }; +}; + +/** + * Simple data structure class for storing a single field. + * + * @class + * @param {string} label + * @param {string} value + */ +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. + * + * @class + */ +H5P.DefinitionList = function () { + var fields = []; + + /** + * Add field to list. + * + * @param {H5P.Field} field + */ + this.add = function (field) { + fields.push(field); + }; + + /** + * Get Number of fields. + * + * @returns {number} + */ + this.size = function () { + return fields.length; + }; + + /** + * Get field at given index. + * + * @param {number} index + * @returns {H5P.Field} + */ + this.get = function (index) { + return fields[index]; + }; + + /** + * 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. + * + * @deprecated + * Will be removed march 2016. + * @class + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + */ +H5P.Coords = function (x, y, w, h) { + if ( !(this instanceof H5P.Coords) ) + return new H5P.Coords(x, y, w, h); + + /** @member {number} */ + this.x = 0; + /** @member {number} */ + this.y = 0; + /** @member {number} */ + this.w = 1; + /** @member {number} */ + 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 {Object} + * 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': parseInt(res[2]), + 'minorVersion': parseInt(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) { + if (H5PIntegration.urlLibraries !== undefined) { + // This is an override for those implementations that has a different libraries URL, e.g. Moodle + return H5PIntegration.urlLibraries + '/' + library; + } + else { + return H5PIntegration.url + '/libraries/' + library; + } +}; + +/** + * Recursivly clone the given object. + * + * @param {Object|Array} object + * Object to clone. + * @param {boolean} [recursive] + * @returns {Object|Array} + * A clone of object. + */ +H5P.cloneObject = function (object, recursive) { + // TODO: Consider if this needs to be in core. Doesn't $.extend do the same? + 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. + * + * @param {string} value + * @returns {string} + */ +H5P.trim = function (value) { + return value.replace(/^\s+|\s+$/g, ''); + + // 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? +}; + +/** + * 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. + * + * @param {Array} array + * Array to shuffle + * @returns {Array} + * The passed array is returned for chaining. + */ +H5P.shuffleArray = function (array) { + // TODO: Consider if this should be a part of core. I'm guessing very few libraries are going to use it. + 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; +}; + +/** + * Post finished results for user. + * + * @deprecated + * Do not use this function directly, trigger the finish event instead. + * Will be removed march 2016 + * @param {number} contentId + * Identifies the content + * @param {number} score + * Achieved score/points + * @param {number} maxScore + * The maximum score/points that can be achieved + * @param {number} [time] + * Reported time consumption/usage + */ +H5P.setFinished = function (contentId, score, maxScore, time) { + var validScore = typeof score === 'number' || score instanceof Number; + if (validScore && H5PIntegration.postUserStatistics === true) { + /** + * Return unix timestamp for the given JS Date. + * + * @private + * @param {Date} date + * @returns {Number} + */ + var toUnix = function (date) { + return Math.round(date.getTime() / 1000); + }; + + // Post the results + const data = { + contentId: contentId, + score: score, + maxScore: maxScore, + opened: toUnix(H5P.opened[contentId]), + finished: toUnix(new Date()), + time: time + }; + H5P.jQuery.post(H5PIntegration.ajax.setFinished, data) + .fail(function () { + H5P.offlineRequestQueue.add(H5PIntegration.ajax.setFinished, data); + }); + } +}; + +// 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 {Object} instance + * Instance of H5P content + * @param {string} eventType + * Type of event to trigger + * @param {*} data + * @param {Object} extras + */ +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 {Object} instance + * Instance of H5P content + * @param {string} eventType + * Type of event to listen for + * @param {H5P.EventCallback} 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); + } +}; + +/** + * Generate random 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); + }); +}; + +/** + * Create title + * + * @param {string} rawTitle + * @param {number} maxLength + * @returns {string} + */ +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) { + if (H5PIntegration.user === undefined) { + // Not logged in, no use in saving. + done('Not signed in.'); + return; + } + + 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.message); + return; + } + + if (response.data === false || response.data === undefined) { + done(); + return; + } + + done(undefined, response.data); + }; + } + + $.ajax(options); + } + + /** + * Get user data for given content. + * + * @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 + } + + H5PIntegration.contents = H5PIntegration.contents || {}; + var content = H5PIntegration.contents['cid-' + contentId] || {}; + var preloadedData = content.contentUserData; + if (preloadedData && preloadedData[subContentId] && preloadedData[subContentId][dataId] !== undefined) { + 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); + } + }); + } + }; + + /** + * Async error handling. + * + * @callback H5P.ErrorCallback + * @param {*} error + */ + + /** + * Set user data for given content. + * + * @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] + * Extra properties + * @param {string} [extras.subContentId] + * Identifies which data belongs to sub content. + * @param {boolean} [extras.preloaded=true] + * If the data should be loaded when content is loaded. + * @param {boolean} [extras.deleteOnChange=false] + * If the data should be invalidated when the content changes. + * @param {H5P.ErrorCallback} [extras.errorCallback] + * Callback with error as parameters. + * @param {boolean} [extras.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 === undefined) { + 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) { + if (options.errorCallback && error) { + options.errorCallback(error); + } + }, data, options.preloaded, options.deleteOnChange, options.async); + }; + + /** + * Delete user data for given content. + * + * @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); + }; + + /** + * Function for getting content for a certain ID + * + * @param {number} contentId + * @return {Object} + */ + H5P.getContentForInstance = function (contentId) { + var key = 'cid-' + contentId; + var exists = H5PIntegration && H5PIntegration.contents && + H5PIntegration.contents[key]; + + return exists ? H5PIntegration.contents[key] : undefined; + }; + + /** + * Prepares the content parameters for storing in the clipboard. + * + * @class + * @param {Object} parameters The parameters for the content to store + * @param {string} [genericProperty] If only part of the parameters are generic, which part + * @param {string} [specificKey] If the parameters are specific, what content type does it fit + * @returns {Object} Ready for the clipboard + */ + H5P.ClipboardItem = function (parameters, genericProperty, specificKey) { + var self = this; + + /** + * Set relative dimensions when params contains a file with a width and a height. + * Very useful to be compatible with wysiwyg editors. + * + * @private + */ + var setDimensionsFromFile = function () { + if (!self.generic) { + return; + } + var params = self.specific[self.generic]; + if (!params.params.file || !params.params.file.width || !params.params.file.height) { + return; + } + + self.width = 20; // % + self.height = (params.params.file.height / params.params.file.width) * self.width; + }; + + if (!genericProperty) { + genericProperty = 'action'; + parameters = { + action: parameters + }; + } + + self.specific = parameters; + + if (genericProperty && parameters[genericProperty]) { + self.generic = genericProperty; + } + if (specificKey) { + self.from = specificKey; + } + + if (window.H5PEditor && H5PEditor.contentId) { + self.contentId = H5PEditor.contentId; + } + + if (!self.specific.width && !self.specific.height) { + setDimensionsFromFile(); + } + }; + + /** + * Store item in the H5P Clipboard. + * + * @param {H5P.ClipboardItem|*} clipboardItem + */ + H5P.clipboardify = function (clipboardItem) { + if (!(clipboardItem instanceof H5P.ClipboardItem)) { + clipboardItem = new H5P.ClipboardItem(clipboardItem); + } + H5P.setClipboard(clipboardItem); + }; + + /** + * Retrieve parsed clipboard data. + * + * @return {Object} + */ + H5P.getClipboard = function () { + return parseClipboard(); + }; + + /** + * Set item in the H5P Clipboard. + * + * @param {H5P.ClipboardItem|object} clipboardItem - Data to be set. + */ + H5P.setClipboard = function (clipboardItem) { + localStorage.setItem('h5pClipboard', JSON.stringify(clipboardItem)); + + // Trigger an event so all 'Paste' buttons may be enabled. + H5P.externalDispatcher.trigger('datainclipboard', {reset: false}); + }; + + /** + * Get config for a library + * + * @param string machineName + * @return Object + */ + H5P.getLibraryConfig = function (machineName) { + var hasConfig = H5PIntegration.libraryConfig && H5PIntegration.libraryConfig[machineName]; + return hasConfig ? H5PIntegration.libraryConfig[machineName] : {}; + }; + + /** + * Get item from the H5P Clipboard. + * + * @private + * @return {Object} + */ + var parseClipboard = function () { + var clipboardData = localStorage.getItem('h5pClipboard'); + if (!clipboardData) { + return; + } + + // Try to parse clipboard dat + try { + clipboardData = JSON.parse(clipboardData); + } + catch (err) { + console.error('Unable to parse JSON from clipboard.', err); + return; + } + + // Update file URLs and reset content Ids + recursiveUpdate(clipboardData.specific, function (path) { + var isTmpFile = (path.substr(-4, 4) === '#tmp'); + if (!isTmpFile && clipboardData.contentId && !path.match(/^https?:\/\//i)) { + // Comes from existing content + + if (H5PEditor.contentId) { + // .. to existing content + return '../' + clipboardData.contentId + '/' + path; + } + else { + // .. to new content + return (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/' + path; + } + } + return path; // Will automatically be looked for in tmp folder + }); + + + if (clipboardData.generic) { + // Use reference instead of key + clipboardData.generic = clipboardData.specific[clipboardData.generic]; + } + + return clipboardData; + }; + + /** + * Update file URLs and reset content IDs. + * Useful when copying content. + * + * @private + * @param {object} params Reference + * @param {function} handler Modifies the path to work when pasted + */ + var recursiveUpdate = function (params, handler) { + for (var prop in params) { + if (params.hasOwnProperty(prop) && params[prop] instanceof Object) { + var obj = params[prop]; + if (obj.path !== undefined && obj.mime !== undefined) { + obj.path = handler(obj.path); + } + else { + if (obj.library !== undefined && obj.subContentId !== undefined) { + // Avoid multiple content with same ID + delete obj.subContentId; + } + recursiveUpdate(obj, handler); + } + } + } + }; + + // Init H5P when page is fully loadded + $(document).ready(function () { + + window.addEventListener('storage', function (event) { + // Pick up clipboard changes from other tabs + if (event.key === 'h5pClipboard') { + // Trigger an event so all 'Paste' buttons may be enabled. + H5P.externalDispatcher.trigger('datainclipboard', {reset: event.newValue === null}); + } + }); + + var ccVersions = { + 'default': '4.0', + '4.0': H5P.t('licenseCC40'), + '3.0': H5P.t('licenseCC30'), + '2.5': H5P.t('licenseCC25'), + '2.0': H5P.t('licenseCC20'), + '1.0': H5P.t('licenseCC10'), + }; + + /** + * Maps copyright license codes to their human readable counterpart. + * + * @type {Object} + */ + H5P.copyrightLicenses = { + 'U': H5P.t('licenseU'), + 'CC BY': { + label: H5P.t('licenseCCBY'), + link: 'http://creativecommons.org/licenses/by/:version', + versions: ccVersions + }, + 'CC BY-SA': { + label: H5P.t('licenseCCBYSA'), + link: 'http://creativecommons.org/licenses/by-sa/:version', + versions: ccVersions + }, + 'CC BY-ND': { + label: H5P.t('licenseCCBYND'), + link: 'http://creativecommons.org/licenses/by-nd/:version', + versions: ccVersions + }, + 'CC BY-NC': { + label: H5P.t('licenseCCBYNC'), + link: 'http://creativecommons.org/licenses/by-nc/:version', + versions: ccVersions + }, + 'CC BY-NC-SA': { + label: H5P.t('licenseCCBYNCSA'), + link: 'http://creativecommons.org/licenses/by-nc-sa/:version', + versions: ccVersions + }, + 'CC BY-NC-ND': { + label: H5P.t('licenseCCBYNCND'), + link: 'http://creativecommons.org/licenses/by-nc-nd/:version', + versions: ccVersions + }, + 'CC0 1.0': { + label: H5P.t('licenseCC010'), + link: 'https://creativecommons.org/publicdomain/zero/1.0/' + }, + 'GNU GPL': { + label: H5P.t('licenseGPL'), + link: 'http://www.gnu.org/licenses/gpl-:version-standalone.html', + linkVersions: { + 'v3': '3.0', + 'v2': '2.0', + 'v1': '1.0' + }, + versions: { + 'default': 'v3', + 'v3': H5P.t('licenseV3'), + 'v2': H5P.t('licenseV2'), + 'v1': H5P.t('licenseV1') + } + }, + 'PD': { + label: H5P.t('licensePD'), + versions: { + 'CC0 1.0': { + label: H5P.t('licenseCC010'), + link: 'https://creativecommons.org/publicdomain/zero/1.0/' + }, + 'CC PDM': { + label: H5P.t('licensePDM'), + link: 'https://creativecommons.org/publicdomain/mark/1.0/' + } + } + }, + 'ODC PDDL': 'Public Domain Dedication and Licence', + 'CC PDM': { + label: H5P.t('licensePDM'), + link: 'https://creativecommons.org/publicdomain/mark/1.0/' + }, + 'C': H5P.t('licenseC'), + }; + + /** + * Indicates if H5P is embedded on an external page using iframe. + * @member {boolean} H5P.externalEmbed + */ + + // Relay events to top window. This must be done before H5P.init + // since events may be fired on initialization. + if (H5P.isFramed && H5P.externalEmbed === false) { + H5P.externalDispatcher.on('*', function (event) { + window.parent.H5P.externalDispatcher.trigger.call(this, event); + }); + } + + /** + * Prevent H5P Core from initializing. Must be overriden before document ready. + * @member {boolean} H5P.preventInit + */ + 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) { + // When was the last state stored + var lastStoredOn = 0; + // Store the current state of the H5P when leaving the page. + var storeCurrentState = function () { + // Make sure at least 250 ms has passed since last save + var currentTime = new Date().getTime(); + if (currentTime - lastStoredOn > 250) { + lastStoredOn = currentTime; + 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}); + } + } + } + } + }; + // iPad does not support beforeunload, therefore using unload + H5P.$window.one('beforeunload unload', function () { + // Only want to do this once + H5P.$window.off('pagehide beforeunload unload'); + storeCurrentState(); + }); + // pagehide is used on iPad when tabs are switched + H5P.$window.on('pagehide', storeCurrentState); + } + }); + +})(H5P.jQuery); diff --git a/src/core/features/h5p/assets/js/jquery.js b/src/core/features/h5p/assets/js/jquery.js new file mode 100644 index 000000000..9583951e4 --- /dev/null +++ b/src/core/features/h5p/assets/js/jquery.js @@ -0,0 +1,13 @@ +/* The jQuery version has been modified to prevent warnings in the stores. This version has been patched to fix some security issues. */ +!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){var n=[],r=e.document,i=n.slice,o=n.concat,a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f={},d=function(e,t){return new d.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,h=/^-ms-/,g=/-([\da-z])/gi,m=function(e,t){return t.toUpperCase()};function v(e){var t=!!e&&"length"in e&&e.length,n=d.type(e);return"function"!==n&&!d.isWindow(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}d.fn=d.prototype={jquery:"123.4.5",constructor:d,selector:"",length:0,toArray:function(){return i.call(this)},get:function(e){return null!=e?e<0?this[e+this.length]:this[e]:i.call(this)},pushStack:function(e){var t=d.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e){return d.each(this,e)},map:function(e){return this.pushStack(d.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==d.type(e)||e.nodeType||d.isWindow(e))return!1;try{if(e.constructor&&!c.call(e,"constructor")&&!c.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(e){return!1}if(!f.ownFirst)for(t in e)return c.call(e,t);for(t in e);return void 0===t||c.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?u[l.call(e)]||"object":typeof e},globalEval:function(t){t&&d.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(h,"ms-").replace(g,m)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t){var n,r=0;if(v(e))for(n=e.length;r+~]|"+O+")"+O+"*"),X=new RegExp("="+O+"*([^\\]'\"]*?)"+O+"*\\]","g"),U=new RegExp(B),V=new RegExp("^"+R+"$"),Y={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+B),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),bool:new RegExp("^(?:"+M+")$","i"),needsContext:new RegExp("^"+O+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)","i")},J=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/[+~]/,ee=/'|\\/g,te=new RegExp("\\\\([\\da-f]{1,6}"+O+"?|("+O+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=function(){d()};try{q.apply(j=_.call(w.childNodes),w.childNodes),j[w.childNodes.length].nodeType}catch(e){q={apply:j.length?function(e,t){H.apply(e,_.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function ie(e,t,r,i){var o,s,l,c,f,h,v,y,T=t&&t.ownerDocument,C=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==C&&9!==C&&11!==C)return r;if(!i&&((t?t.ownerDocument||t:w)!==p&&d(t),t=t||p,g)){if(11!==C&&(h=K.exec(e)))if(o=h[1]){if(9===C){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(T&&(l=T.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(h[2])return q.apply(r,t.getElementsByTagName(e)),r;if((o=h[3])&&n.getElementsByClassName&&t.getElementsByClassName)return q.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!k[e+" "]&&(!m||!m.test(e))){if(1!==C)T=t,y=e;else if("object"!==t.nodeName.toLowerCase()){for((c=t.getAttribute("id"))?c=c.replace(ee,"\\$&"):t.setAttribute("id",c=b),s=(v=a(e)).length,f=V.test(c)?"#"+c:"[id='"+c+"']";s--;)v[s]=f+" "+ge(v[s]);y=v.join(","),T=Z.test(e)&&pe(t.parentNode)||t}if(y)try{return q.apply(r,T.querySelectorAll(y)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(I,"$1"),t,r,i)}function oe(){var e=[];return function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}}function ae(e){return e[b]=!0,e}function se(e){var t=p.createElement("div");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ue(e,t){for(var n=e.split("|"),i=n.length;i--;)r.attrHandle[n[i]]=t}function le(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||A)-(~e.sourceIndex||A);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function ce(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function fe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return ae(function(t){return t=+t,ae(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function pe(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=ie.support={},o=ie.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},d=ie.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==p&&9===a.nodeType&&a.documentElement?(h=(p=a).documentElement,g=!o(p),(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}},r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}}):(delete r.find.ID,r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],m=[],(n.qsa=Q.test(p.querySelectorAll))&&(se(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+O+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+O+"*(?:value|"+M+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]")}),se(function(e){var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+O+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Q.test(y=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),v.push("!=",B)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},S=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===p||e.ownerDocument===w&&x(w,e)?-1:t===p||t.ownerDocument===w&&x(w,t)?1:c?F(c,e)-F(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===p?-1:t===p?1:i?-1:o?1:c?F(c,e)-F(c,t):0;if(i===o)return le(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?le(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},p):p},ie.matches=function(e,t){return ie(e,null,null,t)},ie.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&d(e),t=t.replace(X,"='$1']"),n.matchesSelector&&g&&!k[t+" "]&&(!v||!v.test(t))&&(!m||!m.test(t)))try{var r=y.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return ie(t,p,null,[e]).length>0},ie.contains=function(e,t){return(e.ownerDocument||e)!==p&&d(e),x(e,t)},ie.attr=function(e,t){(e.ownerDocument||e)!==p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&D.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},ie.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},ie.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(S),f){for(;t=e[o++];)t===e[o]&&(i=r.push(o));for(;i--;)e.splice(r[i],1)}return c=null,e},i=ie.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=i(t);return n},(r=ie.selectors={cacheLength:50,createPseudo:ae,match:Y,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ie.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ie.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Y.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&U.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ie.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(W," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,d,p,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,x=!1;if(m){if(o){for(;g;){for(d=t;d=d[g];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&y){for(x=(p=(l=(c=(f=(d=m)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],d=p&&m.childNodes[p];d=++p&&d&&d[g]||(x=p=0)||h.pop();)if(1===d.nodeType&&++x&&d===t){c[e]=[T,p,x];break}}else if(y&&(x=p=(l=(c=(f=(d=t)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)for(;(d=++p&&d&&d[g]||(x=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++x||(y&&((c=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[T,x]),d!==t)););return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ie.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ae(function(e,n){for(var r,o=i(e,t),a=o.length;a--;)e[r=F(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ae(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[b]?ae(function(e,t,n,i){for(var o,a=r(e,null,i,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ae(function(e){return function(t){return ie(e,t).length>0}}),contains:ae(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:ae(function(e){return V.test(e||"")||ie.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return G.test(e.nodeName)},input:function(e){return J.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:de(function(){return[0]}),last:de(function(e,t){return[t-1]}),eq:de(function(e,t,n){return[n<0?n+t:n]}),even:de(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:de(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function ye(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1&&(o[l]=!(a[l]=f))}}else v=ye(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):q.apply(a,v)})}function be(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return F(t,e)>-1},s,!0),d=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&ve(d),u>1&&ge(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(I,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,m,v=0,y="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,N=C.length;for(c&&(l=a===p||a||c);y!==N&&null!=(f=C[y]);y++){if(i&&f){for(h=0,a||f.ownerDocument===p||(d(f),s=!g);m=e[h++];)if(m(f,a||p,s)){u.push(f);break}c&&(T=E)}n&&((f=!m&&f)&&v--,o&&x.push(f))}if(v+=y,n&&y!==v){for(h=0;m=t[h++];)m(x,b,a,s);if(o){if(v>0)for(;y--;)x[y]||b[y]||(b[y]=L.call(u));b=ye(b)}q.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&ie.uniqueSort(u)}return c&&(T=E,l=w),x};return n?ae(o):o}(o,i))).selector=e}return s},u=ie.select=function(e,t,i,o){var u,l,c,f,d,p="function"==typeof e&&e,h=!o&&a(e=p.selector||e);if(i=i||[],1===h.length){if((l=h[0]=h[0].slice(0)).length>2&&"ID"===(c=l[0]).type&&n.getById&&9===t.nodeType&&g&&r.relative[l[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return i;p&&(t=t.parentNode),e=e.slice(l.shift().value.length)}for(u=Y.needsContext.test(e)?0:l.length;u--&&(c=l[u],!r.relative[f=c.type]);)if((d=r.find[f])&&(o=d(c.matches[0].replace(te,ne),Z.test(l[0].type)&&pe(t.parentNode)||t))){if(l.splice(u,1),!(e=o.length&&ge(l)))return q.apply(i,o),i;break}}return(p||s(e,h))(o,t,!g,i,!t||Z.test(e)&&pe(t.parentNode)||t),i},n.sortStable=b.split("").sort(S).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),se(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ue("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ue("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||ue(M,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),ie}(e);d.find=y,d.expr=y.selectors,d.expr[":"]=d.expr.pseudos,d.uniqueSort=d.unique=y.uniqueSort,d.text=y.getText,d.isXMLDoc=y.isXML,d.contains=y.contains;var x=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&d(e).is(n))break;r.push(e)}return r},b=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},w=d.expr.match.needsContext,T=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,C=/^.[^:#\[\.,]*$/;function E(e,t,n){if(d.isFunction(t))return d.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return d.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(C.test(t))return d.filter(t,e,n);t=d.filter(t,e)}return d.grep(e,function(e){return d.inArray(e,t)>-1!==n})}d.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?d.find.matchesSelector(r,e)?[r]:[]:d.find.matches(e,d.grep(t,function(e){return 1===e.nodeType}))},d.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(d(e).filter(function(){for(t=0;t1?d.unique(n):n)).selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(E(this,e||[],!1))},not:function(e){return this.pushStack(E(this,e||[],!0))},is:function(e){return!!E(this,"string"==typeof e&&w.test(e)?d(e):e||[],!1).length}});var N,k=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/;(d.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||N,"string"==typeof e){if(!(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:k.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof d?t[0]:t,d.merge(this,d.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),T.test(i[1])&&d.isPlainObject(t))for(i in t)d.isFunction(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}if((o=r.getElementById(i[2]))&&o.parentNode){if(o.id!==i[2])return N.find(e);this.length=1,this[0]=o}return this.context=r,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):d.isFunction(e)?void 0!==n.ready?n.ready(e):e(d):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),d.makeArray(e,this))}).prototype=d.fn,N=d(r);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){do{e=e[t]}while(e&&1!==e.nodeType);return e}d.fn.extend({has:function(e){var t,n=d(e,this),r=n.length;return this.filter(function(){for(t=0;t-1:1===n.nodeType&&d.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?d.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?d.inArray(this[0],d(e)):d.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(d.uniqueSort(d.merge(this.get(),d(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),d.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x(e,"parentNode")},parentsUntil:function(e,t,n){return x(e,"parentNode",n)},next:function(e){return D(e,"nextSibling")},prev:function(e){return D(e,"previousSibling")},nextAll:function(e){return x(e,"nextSibling")},prevAll:function(e){return x(e,"previousSibling")},nextUntil:function(e,t,n){return x(e,"nextSibling",n)},prevUntil:function(e,t,n){return x(e,"previousSibling",n)},siblings:function(e){return b((e.parentNode||{}).firstChild,e)},children:function(e){return b(e.firstChild)},contents:function(e){return d.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:d.merge([],e.childNodes)}},function(e,t){d.fn[e]=function(n,r){var i=d.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=d.filter(r,i)),this.length>1&&(A[e]||(i=d.uniqueSort(i)),S.test(e)&&(i=i.reverse())),this.pushStack(i)}});var j,L,H=/\S+/g;function q(){r.addEventListener?(r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_)):(r.detachEvent("onreadystatechange",_),e.detachEvent("onload",_))}function _(){(r.addEventListener||"load"===e.event.type||"complete"===r.readyState)&&(q(),d.ready())}for(L in d.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return d.each(e.match(H)||[],function(e,n){t[n]=!0}),t}(e):d.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=e.once,r=t=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?d.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=!0,n||l.disable(),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},d.extend({Deferred:function(e){var t=[["resolve","done",d.Callbacks("once memory"),"resolved"],["reject","fail",d.Callbacks("once memory"),"rejected"],["notify","progress",d.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return d.Deferred(function(n){d.each(t,function(t,o){var a=d.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&d.isFunction(e.promise)?e.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?d.extend(e,r):r}},i={};return r.pipe=r.then,d.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,o=0,a=i.call(arguments),s=a.length,u=1!==s||e&&d.isFunction(e.promise)?s:0,l=1===u?e:d.Deferred(),c=function(e,n,r){return function(o){n[e]=this,r[e]=arguments.length>1?i.call(arguments):o,r===t?l.notifyWith(n,r):--u||l.resolveWith(n,r)}};if(s>1)for(t=new Array(s),n=new Array(s),r=new Array(s);o0||(j.resolveWith(r,[d]),d.fn.triggerHandler&&(d(r).triggerHandler("ready"),d(r).off("ready"))))}}),d.ready.promise=function(t){if(!j)if(j=d.Deferred(),"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll)e.setTimeout(d.ready);else if(r.addEventListener)r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_);else{r.attachEvent("onreadystatechange",_),e.attachEvent("onload",_);var n=!1;try{n=null==e.frameElement&&r.documentElement}catch(e){}n&&n.doScroll&&function t(){if(!d.isReady){try{n.doScroll("left")}catch(n){return e.setTimeout(t,50)}q(),d.ready()}}()}return j.promise(t)},d.ready.promise(),d(f))break;f.ownFirst="0"===L,f.inlineBlockNeedsLayout=!1,d(function(){var e,t,n,i;(n=r.getElementsByTagName("body")[0])&&n.style&&(t=r.createElement("div"),(i=r.createElement("div")).style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(i).appendChild(t),void 0!==t.style.zoom&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",f.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(i))}),function(){var e=r.createElement("div");f.deleteExpando=!0;try{delete e.test}catch(e){f.deleteExpando=!1}e=null}();var F,M=function(e){var t=d.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return(1===n||9===n)&&(!t||!0!==t&&e.getAttribute("classid")===t)},O=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,R=/([A-Z])/g;function P(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(R,"-$1").toLowerCase();if("string"==typeof(n=e.getAttribute(r))){try{n="true"===n||"false"!==n&&("null"===n?null:+n+""===n?+n:O.test(n)?d.parseJSON(n):n)}catch(e){}d.data(e,t,n)}else n=void 0}return n}function B(e){var t;for(t in e)if(("data"!==t||!d.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function W(e,t,r,i){if(M(e)){var o,a,s=d.expando,u=e.nodeType,l=u?d.cache:e,c=u?e[s]:e[s]&&s;if(c&&l[c]&&(i||l[c].data)||void 0!==r||"string"!=typeof t)return c||(c=u?e[s]=n.pop()||d.guid++:s),l[c]||(l[c]=u?{}:{toJSON:d.noop}),"object"!=typeof t&&"function"!=typeof t||(i?l[c]=d.extend(l[c],t):l[c].data=d.extend(l[c].data,t)),a=l[c],i||(a.data||(a.data={}),a=a.data),void 0!==r&&(a[d.camelCase(t)]=r),"string"==typeof t?null==(o=a[t])&&(o=a[d.camelCase(t)]):o=a,o}}function I(e,t,n){if(M(e)){var r,i,o=e.nodeType,a=o?d.cache:e,s=o?e[d.expando]:d.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){i=(t=d.isArray(t)?t.concat(d.map(t,d.camelCase)):t in r?[t]:(t=d.camelCase(t))in r?[t]:t.split(" ")).length;for(;i--;)delete r[t[i]];if(n?!B(r):!d.isEmptyObject(r))return}(n||(delete a[s].data,B(a[s])))&&(o?d.cleanData([e],!0):f.deleteExpando||a!=a.window?delete a[s]:a[s]=void 0)}}}d.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return!!(e=e.nodeType?d.cache[e[d.expando]]:e[d.expando])&&!B(e)},data:function(e,t,n){return W(e,t,n)},removeData:function(e,t){return I(e,t)},_data:function(e,t,n){return W(e,t,n,!0)},_removeData:function(e,t){return I(e,t,!0)}}),d.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=d.data(o),1===o.nodeType&&!d._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&0===(r=a[n].name).indexOf("data-")&&P(o,r=d.camelCase(r.slice(5)),i[r]);d._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){d.data(this,e)}):arguments.length>1?this.each(function(){d.data(this,e,t)}):o?P(o,e,d.data(o,e)):void 0},removeData:function(e){return this.each(function(){d.removeData(this,e)})}}),d.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=d._data(e,t),n&&(!r||d.isArray(n)?r=d._data(e,t,d.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=d.queue(e,t),r=n.length,i=n.shift(),o=d._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){d.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return d._data(e,n)||d._data(e,n,{empty:d.Callbacks("once memory").add(function(){d._removeData(e,t+"queue"),d._removeData(e,n)})})}}),d.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length
    a",f.leadingWhitespace=3===Y.firstChild.nodeType,f.tbody=!Y.getElementsByTagName("tbody").length,f.htmlSerialize=!!Y.getElementsByTagName("link").length,f.html5Clone="<:nav>"!==r.createElement("nav").cloneNode(!0).outerHTML,G.type="checkbox",G.checked=!0,J.appendChild(G),f.appendChecked=G.checked,Y.innerHTML="",f.noCloneChecked=!!Y.cloneNode(!0).lastChild.defaultValue,J.appendChild(Y),(G=r.createElement("input")).setAttribute("type","radio"),G.setAttribute("checked","checked"),G.setAttribute("name","t"),Y.appendChild(G),f.checkClone=Y.cloneNode(!0).cloneNode(!0).lastChild.checked,f.noCloneEvent=!!Y.addEventListener,Y[d.expando]=1,f.attributes=!Y.getAttribute(d.expando);var ie={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:f.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]};function oe(e,t){var n,r,i=0,o=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||d.nodeName(r,t)?o.push(r):d.merge(o,oe(r,t));return void 0===t||t&&d.nodeName(e,t)?d.merge([e],o):o}function ae(e,t){for(var n,r=0;null!=(n=e[r]);r++)d._data(n,"globalEval",!t||d._data(t[r],"globalEval"))}ie.optgroup=ie.option,ie.tbody=ie.tfoot=ie.colgroup=ie.caption=ie.thead,ie.th=ie.td;var se=/<|&#?\w+;/,ue=/
    t
    ",l.childNodes[0].style.borderCollapse="separate",(c=l.getElementsByTagName("td"))[0].style.cssText="margin:0;border:0;padding:0;display:none",(o=0===c[0].offsetHeight)&&(c[0].style.display="",c[1].style.display="none",o=0===c[0].offsetHeight)),d.removeChild(u)}l.style&&(l.style.cssText="float:left;opacity:.5",f.opacity="0.5"===l.style.opacity,f.cssFloat=!!l.style.cssFloat,l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",f.clearCloneStyle="content-box"===l.style.backgroundClip,(u=r.createElement("div")).style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",l.innerHTML="",u.appendChild(l),f.boxSizing=""===l.style.boxSizing||""===l.style.MozBoxSizing||""===l.style.WebkitBoxSizing,d.extend(f,{reliableHiddenOffsets:function(){return null==t&&c(),o},boxSizingReliable:function(){return null==t&&c(),i},pixelMarginRight:function(){return null==t&&c(),n},pixelPosition:function(){return null==t&&c(),t},reliableMarginRight:function(){return null==t&&c(),a},reliableMarginLeft:function(){return null==t&&c(),s}}))}();var $e,ze,Xe=/^(top|right|bottom|left)$/;function Ue(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}e.getComputedStyle?($e=function(t){var n=t.ownerDocument.defaultView;return n&&n.opener||(n=e),n.getComputedStyle(t)},ze=function(e,t,n){var r,i,o,a,s=e.style;return""!==(a=(n=n||$e(e))?n.getPropertyValue(t)||n[t]:void 0)&&void 0!==a||d.contains(e.ownerDocument,e)||(a=d.style(e,t)),n&&!f.pixelMarginRight()&&Be.test(a)&&Pe.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o),void 0===a?a:a+""}):Ie.currentStyle&&($e=function(e){return e.currentStyle},ze=function(e,t,n){var r,i,o,a,s=e.style;return null==(a=(n=n||$e(e))?n[t]:void 0)&&s&&s[t]&&(a=s[t]),Be.test(a)&&!Xe.test(t)&&(r=s.left,(o=(i=e.runtimeStyle)&&i.left)&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"});var Ve=/alpha\([^)]*\)/i,Ye=/opacity\s*=\s*([^)]*)/i,Je=/^(none|table(?!-c[ea]).+)/,Ge=new RegExp("^("+$+")(.*)$","i"),Qe={position:"absolute",visibility:"hidden",display:"block"},Ke={letterSpacing:"0",fontWeight:"400"},Ze=["Webkit","O","Moz","ms"],et=r.createElement("div").style;function tt(e){if(e in et)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=Ze.length;n--;)if((e=Ze[n]+t)in et)return e}function nt(e,t){for(var n,r,i,o=[],a=0,s=e.length;a=1||""===t)&&""===d.trim(o.replace(Ve,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=Ve.test(o)?o.replace(Ve,i):o+" "+i)}}),d.cssHooks.marginRight=Ue(f.reliableMarginRight,function(e,t){if(t)return We(e,{display:"inline-block"},ze,[e,"marginRight"])}),d.cssHooks.marginLeft=Ue(f.reliableMarginLeft,function(e,t){if(t)return(parseFloat(ze(e,"marginLeft"))||(d.contains(e.ownerDocument,e)?e.getBoundingClientRect().left-We(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}):0))+"px"}),d.each({margin:"",padding:"",border:"Width"},function(e,t){d.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+X[r]+t]=o[r]||o[r-2]||o[0];return i}},Pe.test(e)||(d.cssHooks[e+t].set=rt)}),d.fn.extend({css:function(e,t){return Q(this,function(e,t,n){var r,i,o={},a=0;if(d.isArray(t)){for(r=$e(e),i=t.length;a1)},show:function(){return nt(this,!0)},hide:function(){return nt(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){U(this)?d(this).show():d(this).hide()})}}),d.Tween=at,at.prototype={constructor:at,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||d.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(d.cssNumber[n]?"":"px")},cur:function(){var e=at.propHooks[this.prop];return e&&e.get?e.get(this):at.propHooks._default.get(this)},run:function(e){var t,n=at.propHooks[this.prop];return this.options.duration?this.pos=t=d.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):at.propHooks._default.set(this),this}},at.prototype.init.prototype=at.prototype,at.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=d.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){d.fx.step[e.prop]?d.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[d.cssProps[e.prop]]&&!d.cssHooks[e.prop]?e.elem[e.prop]=e.now:d.style(e.elem,e.prop,e.now+e.unit)}}},at.propHooks.scrollTop=at.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},d.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},d.fx=at.prototype.init,d.fx.step={};var st,ut,lt=/^(?:toggle|show|hide)$/,ct=/queueHooks$/;function ft(){return e.setTimeout(function(){st=void 0}),st=d.now()}function dt(e,t){var n,r={height:e},i=0;for(t=t?1:0;i<4;i+=2-t)r["margin"+(n=X[i])]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function pt(e,t,n){for(var r,i=(ht.tweeners[t]||[]).concat(ht.tweeners["*"]),o=0,a=i.length;o
    a",e=n.getElementsByTagName("a")[0],t.setAttribute("type","checkbox"),n.appendChild(t),(e=n.getElementsByTagName("a")[0]).style.cssText="top:1px",f.getSetAttribute="t"!==n.className,f.style=/top/.test(e.getAttribute("style")),f.hrefNormalized="/a"===e.getAttribute("href"),f.checkOn=!!t.value,f.optSelected=o.selected,f.enctype=!!r.createElement("form").enctype,i.disabled=!0,f.optDisabled=!o.disabled,(t=r.createElement("input")).setAttribute("value",""),f.input=""===t.getAttribute("value"),t.value="t",t.setAttribute("type","radio"),f.radioValue="t"===t.value}();var gt=/\r/g,mt=/[\x20\t\r\n\f]+/g;d.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=d.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,d(this).val()):e)?i="":"number"==typeof i?i+="":d.isArray(i)&&(i=d.map(i,function(e){return null==e?"":e+""})),(t=d.valHooks[this.type]||d.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))})):i?(t=d.valHooks[i.type]||d.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(gt,""):null==n?"":n:void 0}}),d.extend({valHooks:{option:{get:function(e){var t=d.find.attr(e,"value");return null!=t?t:d.trim(d.text(e)).replace(mt," ")}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||i<0,a=o?null:[],s=o?i+1:r.length,u=i<0?s:o?i:0;u-1)try{r.selected=n=!0}catch(e){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),d.each(["radio","checkbox"],function(){d.valHooks[this]={set:function(e,t){if(d.isArray(t))return e.checked=d.inArray(d(e).val(),t)>-1}},f.checkOn||(d.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var vt,yt,xt=d.expr.attrHandle,bt=/^(?:checked|selected)$/i,wt=f.getSetAttribute,Tt=f.input;d.fn.extend({attr:function(e,t){return Q(this,d.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){d.removeAttr(this,e)})}}),d.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===e.getAttribute?d.prop(e,t,n):(1===o&&d.isXMLDoc(e)||(t=t.toLowerCase(),i=d.attrHooks[t]||(d.expr.match.bool.test(t)?yt:vt)),void 0!==n?null===n?void d.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=d.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!f.radioValue&&"radio"===t&&d.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(H);if(o&&1===e.nodeType)for(;n=o[i++];)r=d.propFix[n]||n,d.expr.match.bool.test(n)?Tt&&wt||!bt.test(n)?e[r]=!1:e[d.camelCase("default-"+n)]=e[r]=!1:d.attr(e,n,""),e.removeAttribute(wt?n:r)}}),yt={set:function(e,t,n){return!1===t?d.removeAttr(e,n):Tt&&wt||!bt.test(n)?e.setAttribute(!wt&&d.propFix[n]||n,n):e[d.camelCase("default-"+n)]=e[n]=!0,n}},d.each(d.expr.match.bool.source.match(/\w+/g),function(e,t){var n=xt[t]||d.find.attr;Tt&&wt||!bt.test(t)?xt[t]=function(e,t,r){var i,o;return r||(o=xt[t],xt[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,xt[t]=o),i}:xt[t]=function(e,t,n){if(!n)return e[d.camelCase("default-"+t)]?t.toLowerCase():null}}),Tt&&wt||(d.attrHooks.value={set:function(e,t,n){if(!d.nodeName(e,"input"))return vt&&vt.set(e,t,n);e.defaultValue=t}}),wt||(vt={set:function(e,t,n){var r=e.getAttributeNode(n);if(r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n))return t}},xt.id=xt.name=xt.coords=function(e,t,n){var r;if(!n)return(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},d.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);if(n&&n.specified)return n.value},set:vt.set},d.attrHooks.contenteditable={set:function(e,t,n){vt.set(e,""!==t&&t,n)}},d.each(["width","height"],function(e,t){d.attrHooks[t]={set:function(e,n){if(""===n)return e.setAttribute(t,"auto"),n}}})),f.style||(d.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var Ct=/^(?:input|select|textarea|button|object)$/i,Et=/^(?:a|area)$/i;d.fn.extend({prop:function(e,t){return Q(this,d.prop,e,t,arguments.length>1)},removeProp:function(e){return e=d.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(e){}})}}),d.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&d.isXMLDoc(e)||(t=d.propFix[t]||t,i=d.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=d.find.attr(e,"tabindex");return t?parseInt(t,10):Ct.test(e.nodeName)||Et.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),f.hrefNormalized||d.each(["href","src"],function(e,t){d.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),f.optSelected||(d.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),d.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){d.propFix[this.toLowerCase()]=this}),f.enctype||(d.propFix.enctype="encoding");var Nt=/[\t\r\n\f]/g;function kt(e){return d.attr(e,"class")||""}d.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).addClass(e.call(this,t,kt(this)))});if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(d.isFunction(e))return this.each(function(t){d(this).removeClass(e.call(this,t,kt(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof e&&e)for(t=e.match(H)||[];n=this[u++];)if(i=kt(n),r=1===n.nodeType&&(" "+i+" ").replace(Nt," ")){for(a=0;o=t[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");i!==(s=d.trim(r))&&d.attr(n,"class",s)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):d.isFunction(e)?this.each(function(n){d(this).toggleClass(e.call(this,n,kt(this),t),t)}):this.each(function(){var t,r,i,o;if("string"===n)for(r=0,i=d(this),o=e.match(H)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else void 0!==e&&"boolean"!==n||((t=kt(this))&&d._data(this,"__className__",t),d.attr(this,"class",t||!1===e?"":d._data(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+kt(n)+" ").replace(Nt," ").indexOf(t)>-1)return!0;return!1}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){d.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),d.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}});var St=e.location,At=d.now(),Dt=/\?/,jt=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;d.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=d.trim(t+"");return i&&!d.trim(i.replace(jt,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():d.error("Invalid JSON: "+t)},d.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{e.DOMParser?n=(new e.DOMParser).parseFromString(t,"text/xml"):((n=new e.ActiveXObject("Microsoft.XMLDOM")).async="false",n.loadXML(t))}catch(e){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||d.error("Invalid XML: "+t),n};var Lt=/#.*$/,Ht=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,_t=/^(?:GET|HEAD)$/,Ft=/^\/\//,Mt=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Ot={},Rt={},Pt="*/".concat("*"),Bt=St.href,Wt=Mt.exec(Bt.toLowerCase())||[];function It(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(H)||[];if(d.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function $t(e,t,n,r){var i={},o=e===Rt;function a(s){var u;return i[s]=!0,d.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=d.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&d.extend(!0,e,n),e}function Xt(e){return e.style&&e.style.display||d.css(e,"display")}d.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Bt,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Wt[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Pt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,d.ajaxSettings),t):zt(d.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Rt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f=d.ajaxSetup({},n),p=f.context||f,h=f.context&&(p.nodeType||p.jquery)?d(p):d.event,g=d.Deferred(),m=d.Callbacks("once memory"),v=f.statusCode||{},y={},x={},b=0,w="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=qt.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=x[n]=x[n]||e,y[e]=t),this},overrideMimeType:function(e){return b||(f.mimeType=e),this},statusCode:function(e){var t;if(e)if(b<2)for(t in e)v[t]=[v[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||w;return l&&l.abort(t),C(0,t),this}};if(g.promise(T).complete=m.add,T.success=T.done,T.error=T.fail,f.url=((t||f.url||Bt)+"").replace(Lt,"").replace(Ft,Wt[1]+"//"),f.type=n.method||n.type||f.method||f.type,f.dataTypes=d.trim(f.dataType||"*").toLowerCase().match(H)||[""],null==f.crossDomain&&(r=Mt.exec(f.url.toLowerCase()),f.crossDomain=!(!r||r[1]===Wt[1]&&r[2]===Wt[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(Wt[3]||("http:"===Wt[1]?"80":"443")))),f.data&&f.processData&&"string"!=typeof f.data&&(f.data=d.param(f.data,f.traditional)),$t(Ot,f,n,T),2===b)return T;for(i in(u=d.event&&f.global)&&0==d.active++&&d.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!_t.test(f.type),o=f.url,f.hasContent||(f.data&&(o=f.url+=(Dt.test(o)?"&":"?")+f.data,delete f.data),!1===f.cache&&(f.url=Ht.test(o)?o.replace(Ht,"$1_="+At++):o+(Dt.test(o)?"&":"?")+"_="+At++)),f.ifModified&&(d.lastModified[o]&&T.setRequestHeader("If-Modified-Since",d.lastModified[o]),d.etag[o]&&T.setRequestHeader("If-None-Match",d.etag[o])),(f.data&&f.hasContent&&!1!==f.contentType||n.contentType)&&T.setRequestHeader("Content-Type",f.contentType),T.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Pt+"; q=0.01":""):f.accepts["*"]),f.headers)T.setRequestHeader(i,f.headers[i]);if(f.beforeSend&&(!1===f.beforeSend.call(p,T,f)||2===b))return T.abort();for(i in w="abort",{success:1,error:1,complete:1})T[i](f[i]);if(l=$t(Rt,f,n,T)){if(T.readyState=1,u&&h.trigger("ajaxSend",[T,f]),2===b)return T;f.async&&f.timeout>0&&(s=e.setTimeout(function(){T.abort("timeout")},f.timeout));try{b=1,l.send(y,C)}catch(e){if(!(b<2))throw e;C(-1,e)}}else C(-1,"No Transport");function C(t,n,r,i){var c,y,x,w,C,E=n;2!==b&&(b=2,s&&e.clearTimeout(s),l=void 0,a=i||"",T.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(w=function(e,t,n){for(var r,i,o,a,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){u.unshift(a);break}if(u[0]in n)o=u[0];else{for(a in n){if(!u[0]||e.converters[a+" "+u[0]]){o=a;break}r||(r=a)}o=o||r}if(o)return o!==u[0]&&u.unshift(o),n[o]}(f,T,r)),w=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(f,w,T,c),c?(f.ifModified&&((C=T.getResponseHeader("Last-Modified"))&&(d.lastModified[o]=C),(C=T.getResponseHeader("etag"))&&(d.etag[o]=C)),204===t||"HEAD"===f.type?E="nocontent":304===t?E="notmodified":(E=w.state,y=w.data,c=!(x=w.error))):(x=E,!t&&E||(E="error",t<0&&(t=0))),T.status=t,T.statusText=(n||E)+"",c?g.resolveWith(p,[y,E,T]):g.rejectWith(p,[T,E,x]),T.statusCode(v),v=void 0,u&&h.trigger(c?"ajaxSuccess":"ajaxError",[T,f,c?y:x]),m.fireWith(p,[T,E]),u&&(h.trigger("ajaxComplete",[T,f]),--d.active||d.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return d.get(e,t,n,"json")},getScript:function(e,t){return d.get(e,void 0,t,"script")}}),d.each(["get","post"],function(e,t){d[t]=function(e,n,r,i){return d.isFunction(n)&&(i=i||r,r=n,n=void 0),d.ajax(d.extend({url:e,type:t,dataType:i,data:n,success:r},d.isPlainObject(e)&&e))}}),d._evalUrl=function(e){return d.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},d.fn.extend({wrapAll:function(e){if(d.isFunction(e))return this.each(function(t){d(this).wrapAll(e.call(this,t))});if(this[0]){var t=d(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return d.isFunction(e)?this.each(function(t){d(this).wrapInner(e.call(this,t))}):this.each(function(){var t=d(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=d.isFunction(e);return this.each(function(n){d(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()}}),d.expr.filters.hidden=function(e){return f.reliableHiddenOffsets()?e.offsetWidth<=0&&e.offsetHeight<=0&&!e.getClientRects().length:function(e){if(!d.contains(e.ownerDocument||r,e))return!0;for(;e&&1===e.nodeType;){if("none"===Xt(e)||"hidden"===e.type)return!0;e=e.parentNode}return!1}(e)},d.expr.filters.visible=function(e){return!d.expr.filters.hidden(e)};var Ut=/%20/g,Vt=/\[\]$/,Yt=/\r?\n/g,Jt=/^(?:submit|button|image|reset|file)$/i,Gt=/^(?:input|select|textarea|keygen)/i;function Qt(e,t,n,r){var i;if(d.isArray(t))d.each(t,function(t,i){n||Vt.test(e)?r(e,i):Qt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==d.type(t))r(e,t);else for(i in t)Qt(e+"["+i+"]",t[i],n,r)}d.param=function(e,t){var n,r=[],i=function(e,t){t=d.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=d.ajaxSettings&&d.ajaxSettings.traditional),d.isArray(e)||e.jquery&&!d.isPlainObject(e))d.each(e,function(){i(this.name,this.value)});else for(n in e)Qt(n,e[n],t,i);return r.join("&").replace(Ut,"+")},d.fn.extend({serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=d.prop(this,"elements");return e?d.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!d(this).is(":disabled")&&Gt.test(this.nodeName)&&!Jt.test(e)&&(this.checked||!K.test(e))}).map(function(e,t){var n=d(this).val();return null==n?null:d.isArray(n)?d.map(n,function(e){return{name:t.name,value:e.replace(Yt,"\r\n")}}):{name:t.name,value:n.replace(Yt,"\r\n")}}).get()}}),d.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return this.isLocal?nn():r.documentMode>8?tn():/^(get|post|head|put|delete|options)$/i.test(this.type)&&tn()||nn()}:tn;var Kt=0,Zt={},en=d.ajaxSettings.xhr();function tn(){try{return new e.XMLHttpRequest}catch(e){}}function nn(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}e.attachEvent&&e.attachEvent("onunload",function(){for(var e in Zt)Zt[e](void 0,!0)}),f.cors=!!en&&"withCredentials"in en,(en=f.ajax=!!en)&&d.ajaxTransport(function(t){var n;if(!t.crossDomain||f.cors)return{send:function(r,i){var o,a=t.xhr(),s=++Kt;if(a.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)a[o]=t.xhrFields[o];for(o in t.mimeType&&a.overrideMimeType&&a.overrideMimeType(t.mimeType),t.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest"),r)void 0!==r[o]&&a.setRequestHeader(o,r[o]+"");a.send(t.hasContent&&t.data||null),n=function(e,r){var o,u,l;if(n&&(r||4===a.readyState))if(delete Zt[s],n=void 0,a.onreadystatechange=d.noop,r)4!==a.readyState&&a.abort();else{l={},o=a.status,"string"==typeof a.responseText&&(l.text=a.responseText);try{u=a.statusText}catch(e){u=""}o||!t.isLocal||t.crossDomain?1223===o&&(o=204):o=l.text?200:404}l&&i(o,u,l,a.getAllResponseHeaders())},t.async?4===a.readyState?e.setTimeout(n):a.onreadystatechange=Zt[s]=n:n()},abort:function(){n&&n(void 0,!0)}}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return d.globalEval(e),e}}}),d.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),d.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=r.head||d("head")[0]||r.documentElement;return{send:function(i,o){(t=r.createElement("script")).async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||o(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var rn=[],on=/(=)\?(?=&|$)|\?\?/;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=rn.pop()||d.expando+"_"+At++;return this[e]=!0,e}}),d.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=!1!==t.jsonp&&(on.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&on.test(t.data)&&"data");if(s||"jsonp"===t.dataTypes[0])return i=t.jsonpCallback=d.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(on,"$1"+i):!1!==t.jsonp&&(t.url+=(Dt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||d.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){void 0===o?d(e).removeProp(i):e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,rn.push(i)),a&&d.isFunction(o)&&o(a[0]),a=o=void 0}),"script"}),d.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||r;var i=T.exec(e),o=!n&&[];return i?[t.createElement(i[1])]:(i=ce([e],t,o),o&&o.length&&d(o).remove(),d.merge([],i.childNodes))};var an=d.fn.load;function sn(e){return d.isWindow(e)?e:9===e.nodeType&&(e.defaultView||e.parentWindow)}d.fn.load=function(e,t,n){if("string"!=typeof e&&an)return an.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=d.trim(e.slice(s,e.length)),e=e.slice(0,s)),d.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),a.length>0&&d.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?d("
    ").append(d.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},d.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){d.fn[t]=function(e){return this.on(t,e)}}),d.expr.filters.animated=function(e){return d.grep(d.timers,function(t){return e===t.elem}).length},d.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=d.css(e,"position"),c=d(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=d.css(e,"top"),u=d.css(e,"left"),("absolute"===l||"fixed"===l)&&d.inArray("auto",[o,u])>-1?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),d.isFunction(t)&&(t=t.call(e,n,d.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},d.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){d.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;return o?(t=o.documentElement,d.contains(t,i)?(void 0!==i.getBoundingClientRect&&(r=i.getBoundingClientRect()),n=sn(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r):void 0},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===d.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),d.nodeName(e[0],"html")||(n=e.offset()),n.top+=d.css(e[0],"borderTopWidth",!0),n.left+=d.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-d.css(r,"marginTop",!0),left:t.left-n.left-d.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&!d.nodeName(e,"html")&&"static"===d.css(e,"position");)e=e.offsetParent;return e||Ie})}}),d.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);d.fn[e]=function(r){return Q(this,function(e,r,i){var o=sn(e);if(void 0===i)return o?t in o?o[t]:o.document.documentElement[r]:e[r];o?o.scrollTo(n?d(o).scrollLeft():i,n?i:d(o).scrollTop()):e[r]=i},e,r,arguments.length,null)}}),d.each(["top","left"],function(e,t){d.cssHooks[t]=Ue(f.pixelPosition,function(e,n){if(n)return n=ze(e,t),Be.test(n)?d(e).position()[t]+"px":n})}),d.each({Height:"height",Width:"width"},function(e,t){d.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){d.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(!0===r||!0===i?"margin":"border");return Q(this,function(t,n,r){var i;return d.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?d.css(t,n,a):d.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),d.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}}),d.fn.size=function(){return this.length},d.fn.andSelf=d.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return d});var un=e.jQuery,ln=e.$;return d.noConflict=function(t){return e.$===d&&(e.$=ln),t&&e.jQuery===d&&(e.jQuery=un),d},t||(e.jQuery=e.$=d),d}); + +// Snap this specific version of jQuery into H5P. jQuery.noConflict will +// revert the globals to what they were before this file was loaded. +var H5P = window.H5P = window.H5P || {}; + +H5P.jQuery = jQuery.noConflict(true); +H5P.jQuery.ajaxPrefilter(function (s) { + if (s.crossDomain) { + s.contents.script = false; + } +}); diff --git a/src/core/features/h5p/assets/js/request-queue.js b/src/core/features/h5p/assets/js/request-queue.js new file mode 100644 index 000000000..5b367e147 --- /dev/null +++ b/src/core/features/h5p/assets/js/request-queue.js @@ -0,0 +1,436 @@ +/** + * Queue requests and handle them at your convenience + * + * @type {RequestQueue} + */ +H5P.RequestQueue = (function ($, EventDispatcher) { + /** + * A queue for requests, will be automatically processed when regaining connection + * + * @param {boolean} [options.showToast] Show toast when losing or regaining connection + * @constructor + */ + const RequestQueue = function (options) { + EventDispatcher.call(this); + this.processingQueue = false; + options = options || {}; + + this.showToast = options.showToast; + this.itemName = 'requestQueue'; + }; + + /** + * Add request to queue. Only supports posts currently. + * + * @param {string} url + * @param {Object} data + * @returns {boolean} + */ + RequestQueue.prototype.add = function (url, data) { + if (!window.localStorage) { + return false; + } + + let storedStatements = this.getStoredRequests(); + if (!storedStatements) { + storedStatements = []; + } + + storedStatements.push({ + url: url, + data: data, + }); + + window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements)); + + this.trigger('requestQueued', { + storedStatements: storedStatements, + processingQueue: this.processingQueue, + }); + return true; + }; + + /** + * Get stored requests + * + * @returns {boolean|Array} Stored requests + */ + RequestQueue.prototype.getStoredRequests = function () { + if (!window.localStorage) { + return false; + } + + const item = window.localStorage.getItem(this.itemName); + if (!item) { + return []; + } + + return JSON.parse(item); + }; + + /** + * Clear stored requests + * + * @returns {boolean} True if the storage was successfully cleared + */ + RequestQueue.prototype.clearQueue = function () { + if (!window.localStorage) { + return false; + } + + window.localStorage.removeItem(this.itemName); + return true; + }; + + /** + * Start processing of requests queue + * + * @return {boolean} Returns false if it was not possible to resume processing queue + */ + RequestQueue.prototype.resumeQueue = function () { + // Not supported + if (!H5PIntegration || !window.navigator || !window.localStorage) { + return false; + } + + // Already processing + if (this.processingQueue) { + return false; + } + + // Attempt to send queued requests + const queue = this.getStoredRequests(); + const queueLength = queue.length; + + // Clear storage, failed requests will be re-added + this.clearQueue(); + + // No items left in queue + if (!queueLength) { + this.trigger('emptiedQueue', queue); + return true; + } + + // Make sure requests are not changed while they're being handled + this.processingQueue = true; + + // Process queue in original order + this.processQueue(queue); + return true + }; + + /** + * Process first item in the request queue + * + * @param {Array} queue Request queue + */ + RequestQueue.prototype.processQueue = function (queue) { + if (!queue.length) { + return; + } + + this.trigger('processingQueue'); + + // Make sure the requests are processed in a FIFO order + const request = queue.shift(); + + const self = this; + $.post(request.url, request.data) + .fail(self.onQueuedRequestFail.bind(self, request)) + .always(self.onQueuedRequestProcessed.bind(self, queue)) + }; + + /** + * Request fail handler + * + * @param {Object} request + */ + RequestQueue.prototype.onQueuedRequestFail = function (request) { + // Queue the failed request again if we're offline + if (!window.navigator.onLine) { + this.add(request.url, request.data); + } + }; + + /** + * An item in the queue was processed + * + * @param {Array} queue Queue that was processed + */ + RequestQueue.prototype.onQueuedRequestProcessed = function (queue) { + if (queue.length) { + this.processQueue(queue); + return; + } + + // Finished processing this queue + this.processingQueue = false; + + // Run empty queue callback with next request queue + const requestQueue = this.getStoredRequests(); + this.trigger('queueEmptied', requestQueue); + }; + + /** + * Display toast message on the first content of current page + * + * @param {string} msg Message to display + * @param {boolean} [forceShow] Force override showing the toast + * @param {Object} [configOverride] Override toast message config + */ + RequestQueue.prototype.displayToastMessage = function (msg, forceShow, configOverride) { + if (!this.showToast && !forceShow) { + return; + } + + const config = H5P.jQuery.extend(true, {}, { + position: { + horizontal : 'centered', + vertical: 'centered', + noOverflowX: true, + } + }, configOverride); + + H5P.attachToastTo(H5P.jQuery('.h5p-content:first')[0], msg, config); + }; + + return RequestQueue; +})(H5P.jQuery, H5P.EventDispatcher); + +/** + * Request queue for retrying failing requests, will automatically retry them when you come online + * + * @type {offlineRequestQueue} + */ +H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) { + + /** + * Constructor + * + * @param {Object} [options] Options for offline request queue + * @param {Object} [options.instance] The H5P instance which UI components are placed within + */ + const offlineRequestQueue = function (options) { + const requestQueue = new RequestQueue(); + + // We could handle requests from previous pages here, but instead we throw them away + requestQueue.clearQueue(); + + let startTime = null; + const retryIntervals = [10, 20, 40, 60, 120, 300, 600]; + let intervalIndex = -1; + let currentInterval = null; + let isAttached = false; + let isShowing = false; + let isLoading = false; + const instance = options.instance; + + const offlineDialog = new Dialog({ + headerText: H5P.t('offlineDialogHeader'), + dialogText: H5P.t('offlineDialogBody'), + confirmText: H5P.t('offlineDialogRetryButtonLabel'), + hideCancel: true, + hideExit: true, + classes: ['offline'], + instance: instance, + skipRestoreFocus: true, + }); + + const dialog = offlineDialog.getElement(); + + // Add retry text to body + const countDownText = document.createElement('div'); + countDownText.classList.add('count-down'); + countDownText.innerHTML = H5P.t('offlineDialogRetryMessage') + .replace(':num', '0'); + + dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText); + const countDownNum = countDownText.querySelector('.count-down-num'); + + // Create throbber + const throbberWrapper = document.createElement('div'); + throbberWrapper.classList.add('throbber-wrapper'); + const throbber = document.createElement('div'); + throbber.classList.add('sending-requests-throbber'); + throbberWrapper.appendChild(throbber); + + requestQueue.on('requestQueued', function (e) { + // Already processing queue, wait until queue has finished processing before showing dialog + if (e.data && e.data.processingQueue) { + return; + } + + if (!isAttached) { + const rootContent = document.body.querySelector('.h5p-content'); + if (!rootContent) { + return; + } + offlineDialog.appendTo(rootContent); + rootContent.appendChild(throbberWrapper); + isAttached = true; + } + + startCountDown(); + }.bind(this)); + + requestQueue.on('queueEmptied', function (e) { + if (e.data && e.data.length) { + // New requests were added while processing queue or requests failed again. Re-queue requests. + startCountDown(true); + return; + } + + // Successfully emptied queue + clearInterval(currentInterval); + toggleThrobber(false); + intervalIndex = -1; + if (isShowing) { + offlineDialog.hide(); + isShowing = false; + } + + requestQueue.displayToastMessage( + H5P.t('offlineSuccessfulSubmit'), + true, + { + position: { + vertical: 'top', + offsetVertical: '100', + } + } + ); + + }.bind(this)); + + offlineDialog.on('confirmed', function () { + // Show dialog on next render in case it is being hidden by the 'confirm' button + isShowing = false; + setTimeout(function () { + retryRequests(); + }, 100); + }.bind(this)); + + // Initialize listener for when requests are added to queue + window.addEventListener('online', function () { + retryRequests(); + }.bind(this)); + + // Listen for queued requests outside the iframe + window.addEventListener('message', function (event) { + const isValidQueueEvent = window.parent === event.source + && event.data.context === 'h5p' + && event.data.action === 'queueRequest'; + + if (!isValidQueueEvent) { + return; + } + + this.add(event.data.url, event.data.data); + }.bind(this)); + + /** + * Toggle throbber visibility + * + * @param {boolean} [forceShow] Will force throbber visibility if set + */ + const toggleThrobber = function (forceShow) { + isLoading = !isLoading; + if (forceShow !== undefined) { + isLoading = forceShow; + } + + if (isLoading && isShowing) { + offlineDialog.hide(); + isShowing = false; + } + + if (isLoading) { + throbberWrapper.classList.add('show'); + } + else { + throbberWrapper.classList.remove('show'); + } + }; + /** + * Retries the failed requests + */ + const retryRequests = function () { + clearInterval(currentInterval); + toggleThrobber(true); + requestQueue.resumeQueue(); + }; + + /** + * Increments retry interval + */ + const incrementRetryInterval = function () { + intervalIndex += 1; + if (intervalIndex >= retryIntervals.length) { + intervalIndex = retryIntervals.length - 1; + } + }; + + /** + * Starts counting down to retrying queued requests. + * + * @param forceDelayedShow + */ + const startCountDown = function (forceDelayedShow) { + // Already showing, wait for retry + if (isShowing) { + return; + } + + toggleThrobber(false); + if (!isShowing) { + if (forceDelayedShow) { + // Must force delayed show since dialog may be hiding, and confirmation dialog does not + // support this. + setTimeout(function () { + offlineDialog.show(0); + }, 100); + } + else { + offlineDialog.show(0); + } + } + isShowing = true; + startTime = new Date().getTime(); + incrementRetryInterval(); + clearInterval(currentInterval); + currentInterval = setInterval(updateCountDown, 100); + }; + + /** + * Updates the count down timer. Retries requests when time expires. + */ + const updateCountDown = function () { + const time = new Date().getTime(); + const timeElapsed = Math.floor((time - startTime) / 1000); + const timeLeft = retryIntervals[intervalIndex] - timeElapsed; + countDownNum.textContent = timeLeft.toString(); + + // Retry interval reached, retry requests + if (timeLeft <= 0) { + retryRequests(); + } + }; + + /** + * Add request to offline request queue. Only supports posts for now. + * + * @param {string} url The request url + * @param {Object} data The request data + */ + this.add = function (url, data) { + // Only queue request if it failed because we are offline + if (window.navigator.onLine) { + return false; + } + + requestQueue.add(url, data); + }; + }; + + return offlineRequestQueue; +})(H5P.RequestQueue, H5P.ConfirmationDialog); diff --git a/src/core/features/h5p/assets/js/settings/h5p-disable-hub.js b/src/core/features/h5p/assets/js/settings/h5p-disable-hub.js new file mode 100644 index 000000000..406e8b2d1 --- /dev/null +++ b/src/core/features/h5p/assets/js/settings/h5p-disable-hub.js @@ -0,0 +1,68 @@ +/* global H5PDisableHubData */ + +/** + * Global data for disable hub functionality + * + * @typedef {object} H5PDisableHubData Data passed in from the backend + * + * @property {string} selector Selector for the disable hub check-button + * @property {string} overlaySelector Selector for the element that the confirmation dialog will mask + * @property {Array} errors Errors found with the current server setup + * + * @property {string} header Header of the confirmation dialog + * @property {string} confirmationDialogMsg Body of the confirmation dialog + * @property {string} cancelLabel Cancel label of the confirmation dialog + * @property {string} confirmLabel Confirm button label of the confirmation dialog + * + */ +/** + * Utility that makes it possible to force the user to confirm that he really + * wants to use the H5P hub without proper server settings. + */ +(function ($) { + + $(document).on('ready', function () { + + // No data found + if (!H5PDisableHubData) { + return; + } + + // No errors found, no need for confirmation dialog + if (!H5PDisableHubData.errors || !H5PDisableHubData.errors.length) { + return; + } + + H5PDisableHubData.selector = H5PDisableHubData.selector || + '.h5p-settings-disable-hub-checkbox'; + H5PDisableHubData.overlaySelector = H5PDisableHubData.overlaySelector || + '.h5p-settings-container'; + + var dialogHtml = '
    ' + + '

    ' + H5PDisableHubData.errors.join('

    ') + '

    ' + + '

    ' + H5PDisableHubData.confirmationDialogMsg + '

    '; + + // Create confirmation dialog, make sure to include translations + var confirmationDialog = new H5P.ConfirmationDialog({ + headerText: H5PDisableHubData.header, + dialogText: dialogHtml, + cancelText: H5PDisableHubData.cancelLabel, + confirmText: H5PDisableHubData.confirmLabel + }).appendTo($(H5PDisableHubData.overlaySelector).get(0)); + + confirmationDialog.on('confirmed', function () { + enableButton.get(0).checked = true; + }); + + confirmationDialog.on('canceled', function () { + enableButton.get(0).checked = false; + }); + + var enableButton = $(H5PDisableHubData.selector); + enableButton.change(function () { + if ($(this).is(':checked')) { + confirmationDialog.show(enableButton.offset().top); + } + }); + }); +})(H5P.jQuery); diff --git a/src/core/features/h5p/assets/moodle/js/embed.js b/src/core/features/h5p/assets/moodle/js/embed.js new file mode 100644 index 000000000..904eded89 --- /dev/null +++ b/src/core/features/h5p/assets/moodle/js/embed.js @@ -0,0 +1,203 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/* global H5PEmbedCommunicator:true */ +/** + * When embedded the communicator helps talk to the parent page. + * This is a copy of the H5P.communicator, which we need to communicate in this context + * + * @type {H5PEmbedCommunicator} + * @module core_h5p + * @copyright 2019 Joubel AS + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +H5PEmbedCommunicator = (function() { + /** + * @class + * @private + */ + 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. + * + * @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. + * + * @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, '*'); + }; + + /** + * Send a xAPI statement to LMS. + * + * @param {string} component + * @param {Object} statements + */ + self.post = function(component, statements) { + window.parent.postMessage({ + environment: 'moodleapp', + context: 'h5p', + action: 'xapi_post_statement', + component: component, + statements: statements, + }, '*'); + }; + } + + return (window.postMessage && window.addEventListener ? new Communicator() : undefined); +})(); + +document.onreadystatechange = function() { + // Wait for instances to be initialize. + if (document.readyState !== 'complete') { + return; + } + + // Check for H5P iFrame. + var iFrame = document.querySelector('.h5p-iframe'); + if (!iFrame || !iFrame.contentWindow) { + return; + } + var H5P = iFrame.contentWindow.H5P; + + // Check for H5P instances. + if (!H5P || !H5P.instances || !H5P.instances[0]) { + return; + } + + var resizeDelay; + var instance = H5P.instances[0]; + var parentIsFriendly = false; + + // Handle that the resizer is loaded after the iframe. + H5PEmbedCommunicator.on('ready', function() { + H5PEmbedCommunicator.send('hello'); + }); + + // Handle hello message from our parent window. + H5PEmbedCommunicator.on('hello', function() { + // Initial setup/handshake is done. + parentIsFriendly = true; + + // Hide scrollbars for correct size. + iFrame.contentDocument.body.style.overflow = 'hidden'; + + document.body.classList.add('h5p-resizing'); + + // 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. + H5PEmbedCommunicator.on('resizePrepared', function() { + H5PEmbedCommunicator.send('resize', { + scrollHeight: iFrame.contentDocument.body.scrollHeight + }); + }); + + H5PEmbedCommunicator.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) { + H5PEmbedCommunicator.send('prepareResize', + { + scrollHeight: iFrame.contentDocument.body.scrollHeight, + clientHeight: iFrame.contentDocument.body.clientHeight + } + ); + } else { + H5PEmbedCommunicator.send('hello'); + } + }, 0); + }); + + // Get emitted xAPI data. + H5P.externalDispatcher.on('xAPI', function(event) { + var moodlecomponent = H5P.getMoodleComponent(); + if (moodlecomponent == undefined) { + return; + } + // Skip malformed events. + var hasStatement = event && event.data && event.data.statement; + if (!hasStatement) { + return; + } + + var statement = event.data.statement; + var validVerb = statement.verb && statement.verb.id; + if (!validVerb) { + return; + } + + var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' + || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed'; + + var isChild = statement.context && statement.context.contextActivities && + statement.context.contextActivities.parent && + statement.context.contextActivities.parent[0] && + statement.context.contextActivities.parent[0].id; + + if (isCompleted && !isChild) { + var statements = H5P.getXAPIStatements(this.contentId, statement); + H5PEmbedCommunicator.post(moodlecomponent, statements); + } + }); + + // Trigger initial resize for instance. + H5P.trigger(instance, 'resize'); +}; diff --git a/src/core/features/h5p/assets/moodle/js/h5p_overrides.js b/src/core/features/h5p/assets/moodle/js/h5p_overrides.js new file mode 100644 index 000000000..c9350e204 --- /dev/null +++ b/src/core/features/h5p/assets/moodle/js/h5p_overrides.js @@ -0,0 +1,55 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +H5P.findInstanceFromId = function (contentId) { + if (!contentId) { + return H5P.instances[0]; + } + if (H5P.instances !== undefined) { + for (var i = 0; i < H5P.instances.length; i++) { + if (H5P.instances[i].contentId === contentId) { + return H5P.instances[i]; + } + } + } + return undefined; +}; +H5P.getXAPIStatements = function (contentId, statement) { + var statements = []; + var instance = H5P.findInstanceFromId(contentId); + if (!instance){ + return statements; + } + if (instance.getXAPIData == undefined) { + var xAPIData = { + statement: statement + }; + } else { + var xAPIData = instance.getXAPIData(); + } + if (xAPIData.statement != undefined) { + statements.push(xAPIData.statement); + } + if (xAPIData.children != undefined) { + statements = statements.concat(xAPIData.children.map(a => a.statement)); + } + return statements; +}; +H5P.getMoodleComponent = function () { + if (H5PIntegration.moodleComponent) { + return H5PIntegration.moodleComponent; + } + return undefined; +}; \ No newline at end of file diff --git a/src/core/features/h5p/assets/moodle/js/params.js b/src/core/features/h5p/assets/moodle/js/params.js new file mode 100644 index 000000000..87722aacd --- /dev/null +++ b/src/core/features/h5p/assets/moodle/js/params.js @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Handle params included in the URL and put them in the H5PIntegration object if it exists. + */ + +if (window.H5PIntegration && window.H5PIntegration.contents && location.search) { + var contentData = window.H5PIntegration.contents[Object.keys(window.H5PIntegration.contents)[0]]; + + var search = location.search.replace(/^\?/, ''); + var split = search.split('&'); + + split.forEach(function(param) { + var nameAndValue = param.split('='); + + if (nameAndValue[0] == 'displayOptions' && contentData) { + try { + contentData.displayOptions = contentData.displayOptions || {}; + + var displayOptions = JSON.parse(decodeURIComponent(nameAndValue[1])); + + if (displayOptions && typeof displayOptions == 'object') { + Object.assign(contentData.displayOptions, displayOptions); + } + } catch (error) { + console.error('Error parsing display options', decodeURIComponent(nameAndValue[1])); + } + } else if (nameAndValue[0] == 'component') { + window.H5PIntegration.moodleComponent = nameAndValue[1]; + } else if (nameAndValue[0] == 'trackingUrl' && contentData) { + contentData.url = nameAndValue[1]; + } + }); +} diff --git a/src/core/features/h5p/assets/styles/h5p-admin.css b/src/core/features/h5p/assets/styles/h5p-admin.css new file mode 100644 index 000000000..68306126f --- /dev/null +++ b/src/core/features/h5p/assets/styles/h5p-admin.css @@ -0,0 +1,358 @@ +/* Administration interface styling */ + +.h5p-content { + border: 1px solid #DDD; + border-radius: 3px; + padding: 10px; +} + +.h5p-admin-table, +.h5p-admin-table > tbody { + border: none; + width: 100%; +} + +.h5p-admin-table tr:nth-child(odd), +.h5p-data-view tr:nth-child(odd) { + background-color: #F9F9F9; +} +.h5p-admin-table tbody tr:hover { + background-color: #EEE; +} +.h5p-admin-table.empty { + padding: 1em; + background-color: #EEE; + font-size: 1.2em; + font-weight: bold; +} + +.h5p-admin-table.libraries th:last-child, +.h5p-admin-table.libraries td:last-child { + text-align: right; +} + +.h5p-admin-buttons-wrapper { + white-space: nowrap; +} + +.h5p-admin-table.libraries button { + font-size: 2em; + cursor: pointer; + border: 1px solid #AAA; + border-radius: .2em; + background-color: #e0e0e0; + text-shadow: 0 0 0.5em #fff; + padding: 0; + line-height: 1em; + width: 1.125em; + height: 1.05em; + text-indent: -0.125em; + margin: 0.125em 0.125em 0 0.125em; +} +.h5p-admin-upgrade-library:before { + font-family: 'H5P'; + content: "\e888"; +} +.h5p-admin-view-library:before { + font-family: 'H5P'; + content: "\e889"; +} +.h5p-admin-delete-library:before { + font-family: 'H5P'; + content: "\e890"; +} + +.h5p-admin-table.libraries button:hover { + background-color: #d0d0d0; +} +.h5p-admin-table.libraries button:disabled:hover { + background-color: #e0e0e0; + cursor: default; +} + +.h5p-admin-upgrade-library { + color: #339900; +} +.h5p-admin-view-library { + color: #0066cc; +} +.h5p-admin-delete-library { + color: #990000; +} +.h5p-admin-delete-library:disabled, +.h5p-admin-upgrade-library:disabled { + cursor: default; + color: #c0c0c0; +} + +.h5p-library-info { + padding: 1em 1em; + margin: 1em 0; + + width: 350px; + + border: 1px solid #DDD; + border-radius: 3px; +} + +/* Labeled field (label + value) */ +.h5p-labeled-field { + border-bottom: 1px solid #ccc; +} +.h5p-labeled-field:last-child { + border-bottom: none; +} + +.h5p-labeled-field .h5p-label { + display: inline-block; + min-width: 150px; + font-size: 1.2em; + font-weight: bold; + padding: 0.2em; +} + +.h5p-labeled-field .h5p-value { + display: inline-block; + padding: 0.2em; +} + +/* Search element */ +.h5p-content-search { + display: inline-block; + position: relative; + + width: 100%; + padding: 5px 0; + margin-top: 10px; + + border: 1px solid #CCC; + border-radius: 3px; + box-shadow: 2px 2px 5px #888888; +} +.h5p-content-search:before { + font-family: 'H5P'; + vertical-align: bottom; + content: "\e88a"; + font-size: 2em; + line-height: 1.25em; +} +.h5p-content-search input { + font-size: 120%; + line-height: 120%; +} +.h5p-admin-search-results { + margin-left: 10px; + color: #888; +} + +.h5p-admin-pager-size-selector { + position: absolute; + right: 10px; + top: .75em; + display: inline-block; +} +.h5p-admin-pager-size-selector > span { + padding: 5px; + margin-left: 10px; + cursor: pointer; + border: 1px solid #CCC; + border-radius: 3px; +} +.h5p-admin-pager-size-selector > span.selected { + background-color: #edf5fa; +} +.h5p-admin-pager-size-selector > span:hover { + background-color: #555; + color: #FFF; +} + +/* Generic "javascript"-action button */ +button.h5p-admin { + border: 1px solid #AAA; + border-radius: 5px; + padding: 3px 10px; + background-color: #EEE; + cursor: pointer; + display: inline-block; + text-align: center; + color: #222; +} +button.h5p-admin:hover { + background-color: #555; + color: #FFF; +} +button.h5p-admin.disabled, +button.h5p-admin.disabled:hover { + cursor: auto; + color: #CCC; + background-color: #FFF; +} + +/* Pager element */ +.h5p-content-pager { + display: inline-block; + border: 1px solid #CCC; + border-radius: 3px; + box-shadow: 2px 2px 5px #888888; + width: 100%; + text-align: center; + padding: 3px 0; +} +.h5p-content-pager > button { + min-width: 80px; + font-size: 130%; + line-height: 130%; + border: none; + background: none; + font-family: 'H5P'; + font-size: 1.4em; +} +.h5p-content-pager > button:focus { + outline: 0; +} +.h5p-content-pager > button:last-child { + margin-left: 10px; +} +.h5p-content-pager > .pager-info { + cursor: pointer; + padding: 5px; + border-radius: 3px; +} +.h5p-content-pager > .pager-info:hover { + background-color: #555; + color: #FFF; +} +.h5p-content-pager > .pager-info, +.h5p-content-pager > .h5p-pager-goto { + margin: 0 10px; + line-height: 130%; + display: inline-block; +} + +.h5p-admin-header { + margin-top: 1.5em; +} +#h5p-library-upload-form.h5p-admin-upload-libraries-form, +#h5p-content-type-cache-update-form.h5p-admin-upload-libraries-form { + position: relative; + margin: 0; + +} +.h5p-admin-upload-libraries-form .form-submit { + position: absolute; + top: 0; + right: 0; +} +.h5p-spinner { + padding: 0 0.5em; + font-size: 1.5em; + font-weight: bold; +} +#h5p-admin-container .h5p-admin-center { + text-align: center; +} +.h5p-pagination { + text-align: center; +} +.h5p-pagination > span, .h5p-pagination > input { + margin: 0 1em; +} +.h5p-data-view input[type="text"] { + margin-bottom: 0.5em; + margin-right: 0.5em; + float: left; +} +.h5p-data-view input[type="text"]::-ms-clear { + display: none; +} + +.h5p-data-view .h5p-others-contents-toggler-wrapper { + float: right; + line-height: 2; + margin-right: 0.5em; +} + +.h5p-data-view .h5p-others-contents-toggler-label { + font-size: 14px; +} + +.h5p-data-view .h5p-others-contents-toggler { + margin-right: 0.5em; +} + +.h5p-data-view th[role="button"] { + cursor: pointer; +} +.h5p-data-view th[role="button"].h5p-sort:after, +.h5p-data-view th[role="button"]:hover:after, +.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after { + content: "\25BE"; + position: relative; + left: 0.5em; + top: -1px; +} +.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:after, +.h5p-data-view th[role="button"].h5p-sort:hover:after { + content: "\25B4"; + top: -2px; +} +.h5p-data-view th[role="button"]:hover:after, +.h5p-data-view th[role="button"].h5p-sort.h5p-reverse:hover:after, +.h5p-data-view th[role="button"].h5p-sort:hover:after { + color: #999; +} +.h5p-data-view .h5p-facet { + cursor: pointer; + color: #0073aa; + outline: none; +} +.h5p-data-view .h5p-facet:hover, +.h5p-data-view .h5p-facet:active { + color: #00a0d2; +} +.h5p-data-view .h5p-facet:focus { + color: #124964; + box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8); +} +.h5p-data-view .h5p-facet-wrapper { + line-height: 23px; +} +.h5p-data-view .h5p-facet-tag { + margin: 2px 0 0 0.5em; + font-size: 12px; + background: #e8e8e8; + border: 1px solid #cbcbcc; + border-radius: 5px; + color: #5d5d5d; + padding: 0 24px 0 10px; + display: inline-block; + position: relative; +} +.h5p-data-view .h5p-facet-tag > span { + position: absolute; + right: 0; + top: auto; + bottom: auto; + font-size: 18px; + color: #a2a2a2; + outline: none; + width: 21px; + text-indent: 4px; + letter-spacing: 10px; + overflow: hidden; + cursor: pointer; +} +.h5p-data-view .h5p-facet-tag > span:before { + content: "×"; + font-weight: bold; +} +.h5p-data-view .h5p-facet-tag > span:hover, +.h5p-data-view .h5p-facet-tag > span:focus { + color: #a20000; +} +.h5p-data-view .h5p-facet-tag > span:active { + color: #d20000; +} +.content-upgrade-log { + color: red; +} diff --git a/src/core/features/h5p/assets/styles/h5p-confirmation-dialog.css b/src/core/features/h5p/assets/styles/h5p-confirmation-dialog.css new file mode 100644 index 000000000..e849c23e6 --- /dev/null +++ b/src/core/features/h5p/assets/styles/h5p-confirmation-dialog.css @@ -0,0 +1,183 @@ +.h5p-confirmation-dialog-background { + position: fixed; + height: 100%; + width: 100%; + left: 0; + top: 0; + + background: rgba(44, 44, 44, 0.9); + opacity: 1; + visibility: visible; + -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0s; + transition: opacity 0.1s linear 0s, visibility 0s linear 0s; + + z-index: 201; +} + +.h5p-confirmation-dialog-background.hidden { + display: none; +} + +.h5p-confirmation-dialog-background.hiding { + opacity: 0; + visibility: hidden; + -webkit-transition: opacity 0.1s, linear 0s, visibility 0s linear 0.1s; + transition: opacity 0.1s linear 0s, visibility 0s linear 0.1s; +} + +.h5p-confirmation-dialog-popup:focus { + outline: none; +} + +.h5p-confirmation-dialog-popup { + position: absolute; + display: flex; + flex-direction: column; + justify-content: center; + + box-sizing: border-box; + max-width: 35em; + min-width: 25em; + + top: 2em; + left: 50%; + -webkit-transform: translate(-50%, 0%); + -ms-transform: translate(-50%, 0%); + transform: translate(-50%, 0%); + + color: #555; + box-shadow: 0 0 6px 6px rgba(10,10,10,0.3); + + -webkit-transition: transform 0.1s ease-in; + transition: transform 0.1s ease-in; +} + +.h5p-confirmation-dialog-popup.hidden { + -webkit-transform: translate(-50%, 50%); + -ms-transform: translate(-50%, 50%); + transform: translate(-50%, 50%); +} + +.h5p-confirmation-dialog-header { + padding: 1.5em; + background: #fff; + color: #356593; +} + +.h5p-confirmation-dialog-header-text { + font-size: 1.25em; +} + +.h5p-confirmation-dialog-body { + background: #fafbfc; + border-top: solid 1px #dde0e9; + padding: 1.25em 1.5em; +} + +.h5p-confirmation-dialog-text { + margin-bottom: 1.5em; +} + +.h5p-confirmation-dialog-buttons { + float: right; +} + +button.h5p-confirmation-dialog-exit:visited, +button.h5p-confirmation-dialog-exit:link, +button.h5p-confirmation-dialog-exit { + position: absolute; + background: none; + border: none; + font-size: 2.5em; + top: -0.9em; + right: -1.15em; + color: #fff; + cursor: pointer; + text-decoration: none; +} + +button.h5p-confirmation-dialog-exit:focus, +button.h5p-confirmation-dialog-exit:hover { + color: #E4ECF5; +} + +.h5p-confirmation-dialog-exit:before { + font-family: "H5P"; + content: "\e890"; +} + +.h5p-core-button.h5p-confirmation-dialog-confirm-button { + padding-left: 0.75em; + margin-bottom: 0; +} + +.h5p-core-button.h5p-confirmation-dialog-confirm-button:before { + content: "\e601"; + margin-top: -6px; + display: inline-block; +} + +.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-buttons { + float: none; + text-align: center; +} + +.h5p-confirmation-dialog-popup.offline .count-down { + font-family: Arial; + margin-top: 0.15em; + color: #000; +} + +.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-confirm-button:before { + content: "\e90b"; + font-weight: normal; + vertical-align: text-bottom; +} + +.throbber-wrapper { + display: none; + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + z-index: 1; + background: rgba(44, 44, 44, 0.9); +} + +.throbber-wrapper.show { + display: block; +} + +.throbber-wrapper .throbber-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.throbber-wrapper .sending-requests-throbber{ + position: absolute; + top: 7em; + left: 50%; + transform: translateX(-50%); +} + +.throbber-wrapper .sending-requests-throbber:before { + display: block; + font-family: 'H5P'; + content: "\e90b"; + color: white; + font-size: 10em; + animation: request-throbber 1.5s infinite linear; +} + +@keyframes request-throbber { + from { + transform: rotate(0); + } + + to { + transform: rotate(359deg); + } +} diff --git a/src/core/features/h5p/assets/styles/h5p-core-button.css b/src/core/features/h5p/assets/styles/h5p-core-button.css new file mode 100644 index 000000000..e6511dc38 --- /dev/null +++ b/src/core/features/h5p/assets/styles/h5p-core-button.css @@ -0,0 +1,60 @@ +button.h5p-core-button:visited, +button.h5p-core-button:link, +button.h5p-core-button { + font-family: "Open Sans", sans-serif; + font-weight: 600; + font-size: 1em; + line-height: 1.2; + padding: 0.5em 1.25em; + border-radius: 2em; + + background: #2579c6; + color: #fff; + + cursor: pointer; + border: none; + box-shadow: none; + outline: none; + + display: inline-block; + text-align: center; + text-shadow: none; + vertical-align: baseline; + text-decoration: none; + + -webkit-transition: initial; + transition: initial; +} +button.h5p-core-button:focus { + background: #1f67a8; +} +button.h5p-core-button:hover { + background: rgba(31, 103, 168, 0.83); +} +button.h5p-core-button:active { + background: #104888; +} +button.h5p-core-button:before { + font-family: 'H5P'; + padding-right: 0.15em; + font-size: 1.5em; + vertical-align: middle; + line-height: 0.7; +} +button.h5p-core-cancel-button:visited, +button.h5p-core-cancel-button:link, +button.h5p-core-cancel-button { + border: none; + background: none; + color: #a00; + margin-right: 1em; + font-size: 1em; + text-decoration: none; + cursor: pointer; +} +button.h5p-core-cancel-button:hover, +button.h5p-core-cancel-button:focus { + background: none; + border: none; + color: #e40000; +} diff --git a/src/core/features/h5p/assets/styles/h5p.css b/src/core/features/h5p/assets/styles/h5p.css new file mode 100644 index 000000000..1f89e4ead --- /dev/null +++ b/src/core/features/h5p/assets/styles/h5p.css @@ -0,0 +1,566 @@ +/* General CSS for H5P. Licensed under the MIT License.*/ + +/* Custom H5P font to use for icons. */ +@font-face { + font-family: 'h5p'; + src: url('../fonts/h5p-core-23.eot?mz1lkp'); + src: url('../fonts/h5p-core-23.eot?mz1lkp#iefix') format('embedded-opentype'), + url('../fonts/h5p-core-23.ttf?mz1lkp') format('truetype'), + url('../fonts/h5p-core-23.woff?mz1lkp') format('woff'), + url('../fonts/h5p-core-23.svg?mz1lkp#h5p') format('svg'); + font-weight: normal; + font-style: normal; +} + +html.h5p-iframe, html.h5p-iframe > body { + font-family: Sans-Serif; /* Use the browser's default sans-serif font. (Since Heletica doesn't look nice on Windows, and Arial on OS X.) */ + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} +.h5p-semi-fullscreen, .h5p-fullscreen, html.h5p-iframe .h5p-container { + overflow: hidden; +} +.h5p-content { + position: relative; + background: #fefefe; + border: 1px solid #EEE; + border-bottom: none; + box-sizing: border-box; + -moz-box-sizing: border-box; +} +.h5p-noselect +{ + -khtml-user-select: none; + -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} +html.h5p-iframe .h5p-content { + font-size: 16px; + line-height: 1.5em; + width: 100%; + height: auto; +} +html.h5p-iframe .h5p-fullscreen .h5p-content, +html.h5p-iframe .h5p-semi-fullscreen .h5p-content { + height: 100%; +} +.h5p-content.h5p-no-frame, +.h5p-fullscreen .h5p-content, +.h5p-semi-fullscreen .h5p-content { + border: 0; +} +.h5p-container { + position: relative; + z-index: 1; +} +.h5p-iframe-wrapper.h5p-fullscreen { + background-color: #000; +} +body.h5p-semi-fullscreen { + position: fixed; + width: 100%; + height: 100%; +} +.h5p-container.h5p-semi-fullscreen { + position: fixed; + top: 0; + left: 0; + z-index: 101; + width: 100%; + height: 100%; + background-color: #FFF; +} + +.h5p-content-controls { + margin: 0; + position: absolute; + right: 0; + top: 0; + z-index: 3; +} +.h5p-fullscreen .h5p-content-controls { + display: none; +} + +.h5p-content-controls > a:link, .h5p-content-controls > a:visited, a.h5p-disable-fullscreen:link, a.h5p-disable-fullscreen:visited { + color: #e5eef6; +} + +.h5p-enable-fullscreen:before { + font-family: 'H5P'; + content: "\e88c"; +} +.h5p-disable-fullscreen:before { + font-family: 'H5P'; + content: "\e891"; +} +.h5p-enable-fullscreen, .h5p-disable-fullscreen { + cursor: pointer; + color: #EEE; + background: rgb(0,0,0); + background: rgba(0,0,0,0.3); + line-height: 0.975em; + font-size: 2em; + width: 1.125em; + height: 1em; + text-indent: 0.04em; +} +.h5p-disable-fullscreen { + line-height: 0.925em; + width: 1.1em; + height: 0.9em; +} + +.h5p-enable-fullscreen:focus, +.h5p-disable-fullscreen:focus { + outline-style: solid; + outline-width: 1px; + outline-offset: 0.25em; +} + +.h5p-enable-fullscreen:hover, .h5p-disable-fullscreen:hover { + background: rgba(0,0,0,0.5); +} +.h5p-semi-fullscreen .h5p-enable-fullscreen { + display: none; +} + +div.h5p-fullscreen { + width: 100%; + height: 100%; +} +.h5p-iframe-wrapper { + width: auto; + height: auto; +} + +.h5p-fullscreen .h5p-iframe-wrapper, +.h5p-semi-fullscreen .h5p-iframe-wrapper { + width: 100%; + height: 100%; +} + +.h5p-iframe-wrapper.h5p-semi-fullscreen { + width: auto; + height: auto; + background: black; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100001; +} +.h5p-iframe-wrapper.h5p-semi-fullscreen .buttons { + position: absolute; + top: 0; + right: 0; + z-index: 20; +} +.h5p-iframe-wrapper iframe.h5p-iframe { + /* Hack for IOS landscape / portrait */ + width: 10px; + min-width: 100%; + *width: 100%; + /* End of hack */ + height: 100%; + z-index: 10; + overflow: hidden; + border: 0; + display: block; +} + +.h5p-content ul.h5p-actions { + box-sizing: border-box; + -moz-box-sizing: border-box; + overflow: hidden; + list-style: none; + padding: 0px 10px; + margin: 0; + height: 25px; + font-size: 12px; + background: #FAFAFA; + border-top: 1px solid #EEE; + border-bottom: 1px solid #EEE; + clear: both; + font-family: Sans-Serif; +} +.h5p-fullscreen .h5p-actions, .h5p-semi-fullscreen .h5p-actions { + display: none; +} +.h5p-actions > .h5p-button { + float: left; + cursor: pointer; + margin: 0 0.5em 0 0; + background: none; + padding: 0 0.75em 0 0.25em; + vertical-align: top; + color: #999; + text-decoration: none; + outline: none; + line-height: 23px; +} +.h5p-actions > .h5p-button:hover { + color: #666; +} +.h5p-actions > .h5p-button:active, +.h5p-actions > .h5p-button:focus, +.h5p-actions .h5p-link:active, +.h5p-actions .h5p-link:focus { + color: #666; +} +.h5p-actions > .h5p-button:focus, +.h5p-actions .h5p-link:focus { + outline-style: solid; + outline-width: thin; + outline-offset: -2px; + outline-color: #9ecaed; +} +.h5p-actions > .h5p-button:before { + font-family: 'H5P'; + font-size: 20px; + line-height: 20px; + vertical-align: top; + padding-right: 0; +} +.h5p-actions > .h5p-button.h5p-export:before { + content: "\e90b"; +} +.h5p-actions > .h5p-button.h5p-copyrights:before { + content: "\e88f"; +} +.h5p-actions > .h5p-button.h5p-embed:before { + content: "\e892"; +} +.h5p-actions .h5p-link { + float: right; + margin-right: 0; + font-size: 2.0em; + line-height: 23px; + overflow: hidden; + color: #999; + text-decoration: none; + outline: none; +} +.h5p-actions .h5p-link:before { + font-family: 'H5P'; + content: "\e88e"; + vertical-align: bottom; +} +.h5p-actions > li { + margin: 0; + list-style: none; +} +.h5p-popup-dialog { + position: absolute; + top: 0; + left: 0; + width: 100%; + min-height: 100%; + z-index: 100; + padding: 2em; + box-sizing: border-box; + -moz-box-sizing: border-box; + opacity: 0; + -webkit-transition: opacity 0.2s; + -moz-transition: opacity 0.2s; + -o-transition: opacity 0.2s; + transition: opacity 0.2s; + background:#000; + background:rgba(0,0,0,0.75); +} +.h5p-popup-dialog.h5p-open { + opacity: 1; +} +.h5p-popup-dialog .h5p-inner { + box-sizing: border-box; + -moz-box-sizing: border-box; + background: #fff; + height: 100%; + max-height: 100%; + position: relative; +} +.h5p-popup-dialog .h5p-inner > h2 { + position: absolute; + box-sizing: border-box; + -moz-box-sizing: border-box; + width: 100%; + margin: 0; + background: #eee; + display: block; + color: #656565; + font-size: 1.25em; + padding: 0.325em 0.5em 0.25em; + line-height: 1.25em; + border-bottom: 1px solid #ccc; + z-index: 2; +} +.h5p-popup-dialog .h5p-inner > h2 > a { + font-size: 12px; + margin-left: 1em; +} +.h5p-embed-dialog .h5p-inner, +.h5p-reuse-dialog .h5p-inner, +.h5p-content-user-data-reset-dialog .h5p-inner { + min-width: 316px; + max-width: 400px; + left: 50%; + top: 50%; + transform: translateX(-50%); +} +.h5p-embed-dialog .h5p-embed-code-container, +.h5p-embed-size { + resize: none; + outline: none; + width: 100%; + padding: 0.375em 0.5em 0.25em; + margin: 0; + overflow: hidden; + border: 1px solid #ccc; + box-shadow: 0 1px 2px 0 #d0d0d0 inset; + font-size: 0.875em; + letter-spacing: 0.065em; + font-family: sans-serif; + white-space: pre; + line-height: 1.5em; + height: 2.0714em; + background: #f5f5f5; + box-sizing: border-box; + -moz-box-sizing: border-box; +} +.h5p-embed-dialog .h5p-embed-code-container:focus { + height: 5em; +} +.h5p-embed-size { + width: 3.5em; + text-align: right; + margin: 0.5em 0; + line-height: 2em; +} +.h5p-popup-dialog .h5p-scroll-content { + border-top: 2.25em solid transparent; + padding: 1em; + box-sizing: border-box; + -moz-box-sizing: border-box; + color: #555555; + z-index: 1; +} +.h5p-popup-dialog.h5p-open .h5p-scroll-content { + overflow: auto; + overflow-x: hidden; + overflow-y: auto; + height: 100%; +} +.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar { + width: 8px; +} +.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-track { + background: #e0e0e0; +} +.h5p-popup-dialog .h5p-scroll-content::-webkit-scrollbar-thumb { + box-shadow: 0 0 10px #000 inset; + border-radius: 4px; +} +.h5p-popup-dialog .h5p-close { + cursor: pointer; +} +.h5p-popup-dialog .h5p-close:after { + font-family: 'H5P'; + content: "\e894"; + font-size: 2em; + position: absolute; + right: 0; + top: 0; + width: 1.125em; + height: 1.125em; + line-height: 1.125em; + color: #656565; + cursor: pointer; + text-indent: -0.065em; + z-index: 3 +} +.h5p-popup-dialog .h5p-close:hover:after, +.h5p-popup-dialog .h5p-close:focus:after { + color: #454545; +} +.h5p-popup-dialog .h5p-close:active:after { + color: #252525; +} +.h5p-poopup-dialog h2 { + margin: 0.25em 0 0.5em; +} +.h5p-popup-dialog h3 { + margin: 0.75em 0 0.25em; +} +.h5p-popup-dialog dl { + margin: 0.25em 0 0.75em; +} +.h5p-popup-dialog dt { + float: left; + margin: 0 0.75em 0 0; +} +.h5p-popup-dialog dt:after { + content: ':'; +} +.h5p-popup-dialog dd { + margin: 0; +} +.h5p-expander { + cursor: pointer; + font-size: 1.125em; + outline: none; + margin: 0.5em 0 0; + display: inline-block; +} +.h5p-expander:before { + content: "+"; + width: 1em; + display: inline-block; + font-weight: bold; +} +.h5p-expander.h5p-open:before { + content: "-"; + text-indent: 0.125em; +} +.h5p-expander:hover, +.h5p-expander:focus { + color: #303030; +} +.h5p-expander:active { + color: #202020; +} +.h5p-expander-content { + display: none; +} +.h5p-expander-content p { + margin: 0.5em 0; +} +.h5p-content-copyrights { + border-left: 0.25em solid #d0d0d0; + margin-left: 0.25em; + padding-left: 0.25em; +} +.h5p-throbber { + background: url('../images/throbber.gif?ver=1.2.1') 10px center no-repeat; + padding-left: 38px; + min-height: 30px; + line-height: 30px; +} +.h5p-dialog-ok-button { + cursor: default; + float: right; + outline: none; + border: 2px solid #ccc; + padding: 0.25em 0.75em 0.125em; + background: #eee; +} +.h5p-dialog-ok-button:hover, +.h5p-dialog-ok-button:focus { + background: #fafafa; +} +.h5p-dialog-ok-button:active { + background: #eeffee; +} +.h5p-big-button { + line-height: 1.25; + display: block; + position: relative; + cursor: pointer; + width: 100%; + padding: 1em 1em 1em 3.75em; + text-align: left; + border: 1px solid #dedede; + background: linear-gradient(#ffffff, #f1f1f2); + border-radius: 0.25em; +} +.h5p-big-button:before { + font-family: 'h5p'; + content: "\e893"; + line-height: 1; + font-size: 3em; + color: #2747f7; + position: absolute; + left: 0.125em; + top: 0.125em; +} +.h5p-copy-button:before { + content: "\e905"; +} +.h5p-big-button:hover { + border: 1px solid #2747f7; + background: #eff1fe; +} +.h5p-big-button:active { + border: 1px solid #dedede; + background: #dfe4fe; +} +.h5p-button-title { + color: #2747f7; + font-size: 15px; + font-weight: bold; + margin-bottom: 0.5em; +} +.h5p-button-description { + color: #757575; +} +.h5p-horizontal-line-text { + border-top: 1px solid #dadada; + line-height: 1; + color: #474747; + text-align: center; + position: relative; + margin: 1.25em 0; +} +.h5p-horizontal-line-text > span { + background: white; + padding: 0.5em; + position: absolute; + top: -1em; + left: 50%; + transform: translateX(-50%); +} +.h5p-toast { + font-size: 0.75em; + background-color: rgba(0, 0, 0, 0.9); + color: #fff; + z-index: 110; + position: absolute; + padding: 0 0.5em; + line-height: 2; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + top: 0; + opacity: 1; + visibility: visible; + transition: opacity 1s; +} +.h5p-toast-disabled { + opacity: 0; + visibility: hidden; +} + + +/* This is loaded as part of Core and not Editor since this needs to be outside the editor iframe */ +.h5peditor-semi-fullscreen { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 101; +} +iframe.h5peditor-semi-fullscreen { + background: #fff; + z-index: 100001; +} + +.h5p-content.using-mouse *:not(textarea):focus { + outline: none !important; +} diff --git a/src/core/features/h5p/lang.json b/src/core/features/h5p/lang.json new file mode 100644 index 000000000..19018057f --- /dev/null +++ b/src/core/features/h5p/lang.json @@ -0,0 +1,93 @@ +{ + "additionallicenseinfo": "Any additional information about the licence", + "author": "Author", + "authorcomments": "Author comments", + "authorcommentsdescription": "Comments for the editor of the content. (This text will not be published as a part of the copyright info.)", + "authorname": "Author's name", + "authorrole": "Author's role", + "by": "by", + "cancellabel": "Cancel", + "ccattribution": "Attribution (CC BY)", + "ccattributionnc": "Attribution-NonCommercial (CC BY-NC)", + "ccattributionncnd": "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)", + "ccattributionncsa": "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)", + "ccattributionnd": "Attribution-NoDerivs (CC BY-ND)", + "ccattributionsa": "Attribution-ShareAlike (CC BY-SA)", + "ccpdd": "Public Domain Dedication (CC0)", + "changedby": "Changed by", + "changedescription": "Description of change", + "changelog": "Changelog", + "changeplaceholder": "Photo cropped, text changed, etc.", + "close": "Close", + "confirmdialogbody": "Please confirm that you wish to proceed. This action cannot be undone.", + "confirmdialogheader": "Confirm action", + "confirmlabel": "Confirm", + "connectionLost": "Connection lost. Results will be stored and sent when the connection is reestablished.", + "connectionReestablished": "Connection reestablished.", + "contentCopied": "Content is copied to the clipboard", + "contentchanged": "This content has changed since you last used it.", + "contenttype": "Content type", + "copyright": "Rights of use", + "copyrightinfo": "Copyright information", + "copyrightstring": "Copyright", + "copyrighttitle": "View copyright information for this content.", + "creativecommons": "Creative Commons", + "date": "Date", + "disablefullscreen": "Disable fullscreen", + "download": "Download", + "downloadtitle": "Download this content as a H5P file.", + "editor": "Editor", + "embed": "Embed", + "embedtitle": "View the embed code for this content.", + "errorgetemail": "Error obtaining the user email. Please check your connection and try again.", + "fullscreen": "Fullscreen", + "gpl": "General Public License v3", + "h5ptitle": "Visit h5p.org to check out more content.", + "hideadvanced": "Hide advanced", + "license": "Licence", + "licenseCC010": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", + "licenseCC010U": "CC0 1.0 Universal", + "licenseCC10": "1.0 Generic", + "licenseCC20": "2.0 Generic", + "licenseCC25": "2.5 Generic", + "licenseCC30": "3.0 Unported", + "licenseCC40": "4.0 International", + "licenseGPL": "General Public License", + "licenseV1": "Version 1", + "licenseV2": "Version 2", + "licenseV3": "Version 3", + "licensee": "Licensee", + "licenseextras": "Licence extras", + "licenseversion": "Licence version", + "nocopyright": "No copyright information available for this content.", + "offlineDialogBody": "We were unable to send information about your completion of this task. Please check your internet connection.", + "offlineDialogHeader": "Your connection to the server was lost", + "offlineDialogRetryButtonLabel": "Retry now", + "offlineDialogRetryMessage": "Retrying in :num....", + "offlineSuccessfulSubmit": "Successfully submitted results.", + "offlinedisabled": "The site doesn't allow downloading H5P packages.", + "originator": "Originator", + "pd": "Public Domain", + "pddl": "Public Domain Dedication and Licence", + "pdm": "Public Domain Mark (PDM)", + "play": "Play H5P", + "resizescript": "Include this script on your website if you want dynamic sizing of the embedded content:", + "resubmitScores": "Attempting to submit stored results.", + "reuse": "Reuse", + "reuseContent": "Reuse content", + "reuseDescription": "Reuse this content.", + "showadvanced": "Show advanced", + "showless": "Show less", + "showmore": "Show more", + "size": "Size", + "source": "Source", + "startingover": "You'll be starting over.", + "sublevel": "Sublevel", + "thumbnail": "Thumbnail", + "title": "Title", + "undisclosed": "Undisclosed", + "year": "Year", + "years": "Year(s)", + "yearsfrom": "Years (from)", + "yearsto": "Years (to)" +} \ No newline at end of file