', {
+ 'class': 'h5p-others-contents-toggler-wrapper'
+ }).append($label)
+ .appendTo(this.$container);
+ };
+
+ return H5PDataView;
+})(H5P.jQuery);
diff --git a/src/core/features/h5p/assets/js/h5p-display-options.js b/src/core/features/h5p/assets/js/h5p-display-options.js
new file mode 100644
index 000000000..216515274
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-display-options.js
@@ -0,0 +1,54 @@
+/**
+ * Utility that makes it possible to hide fields when a checkbox is unchecked
+ */
+(function ($) {
+ function setupHiding() {
+ var $toggler = $(this);
+
+ // Getting the field which should be hidden:
+ var $subject = $($toggler.data('h5p-visibility-subject-selector'));
+
+ var toggle = function () {
+ $subject.toggle($toggler.is(':checked'));
+ };
+
+ $toggler.change(toggle);
+ toggle();
+ }
+
+ function setupRevealing() {
+ var $button = $(this);
+
+ // Getting the field which should have the value:
+ var $input = $('#' + $button.data('control'));
+
+ if (!$input.data('value')) {
+ $button.remove();
+ return;
+ }
+
+ // Setup button action
+ var revealed = false;
+ var text = $button.html();
+ $button.click(function () {
+ if (revealed) {
+ $input.val('');
+ $button.html(text);
+ revealed = false;
+ }
+ else {
+ $input.val($input.data('value'));
+ $button.html($button.data('hide'));
+ revealed = true;
+ }
+ });
+ }
+
+ $(document).ready(function () {
+ // Get the checkboxes making other fields being hidden:
+ $('.h5p-visibility-toggler').each(setupHiding);
+
+ // Get the buttons making other fields have hidden values:
+ $('.h5p-reveal-value').each(setupRevealing);
+ });
+})(H5P.jQuery);
diff --git a/src/core/features/h5p/assets/js/h5p-embed.js b/src/core/features/h5p/assets/js/h5p-embed.js
new file mode 100644
index 000000000..c14145670
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-embed.js
@@ -0,0 +1,75 @@
+/*jshint multistr: true */
+
+/**
+ * Converts old script tag embed to iframe
+ */
+var H5POldEmbed = H5POldEmbed || (function () {
+ var head = document.getElementsByTagName('head')[0];
+ var resizer = false;
+
+ /**
+ * Loads the resizing script
+ */
+ var loadResizer = function (url) {
+ var data, callback = 'H5POldEmbed';
+ resizer = true;
+
+ // Callback for when content data is loaded.
+ window[callback] = function (content) {
+ // Add resizing script to head
+ var resizer = document.createElement('script');
+ resizer.src = content;
+ head.appendChild(resizer);
+
+ // Clean up
+ head.removeChild(data);
+ delete window[callback];
+ };
+
+ // Create data script
+ data = document.createElement('script');
+ data.src = url + (url.indexOf('?') === -1 ? '?' : '&') + 'callback=' + callback;
+ head.appendChild(data);
+ };
+
+ /**
+ * Replaced script tag with iframe
+ */
+ var addIframe = function (script) {
+ // Add iframe
+ var iframe = document.createElement('iframe');
+ iframe.src = script.getAttribute('data-h5p');
+ iframe.frameBorder = false;
+ iframe.allowFullscreen = true;
+ var parent = script.parentNode;
+ parent.insertBefore(iframe, script);
+ parent.removeChild(script);
+ };
+
+ /**
+ * Go throught all script tags with the data-h5p attribute and load content.
+ */
+ function H5POldEmbed() {
+ var scripts = document.getElementsByTagName('script');
+ var h5ps = []; // Use seperate array since scripts grow in size.
+ for (var i = 0; i < scripts.length; i++) {
+ var script = scripts[i];
+ if (script.src.indexOf('/h5p-resizer.js') !== -1) {
+ resizer = true;
+ }
+ else if (script.hasAttribute('data-h5p')) {
+ h5ps.push(script);
+ }
+ }
+ for (i = 0; i < h5ps.length; i++) {
+ if (!resizer) {
+ loadResizer(h5ps[i].getAttribute('data-h5p'));
+ }
+ addIframe(h5ps[i]);
+ }
+ }
+
+ return H5POldEmbed;
+})();
+
+new H5POldEmbed();
diff --git a/src/core/features/h5p/assets/js/h5p-event-dispatcher.js b/src/core/features/h5p/assets/js/h5p-event-dispatcher.js
new file mode 100644
index 000000000..2027cf7ac
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-event-dispatcher.js
@@ -0,0 +1,258 @@
+var H5P = window.H5P = window.H5P || {};
+
+/**
+ * The Event class for the EventDispatcher.
+ *
+ * @class
+ * @param {string} type
+ * @param {*} data
+ * @param {Object} [extras]
+ * @param {boolean} [extras.bubbles]
+ * @param {boolean} [extras.external]
+ */
+H5P.Event = function (type, data, extras) {
+ this.type = type;
+ this.data = data;
+ var bubbles = false;
+
+ // Is this an external event?
+ var external = false;
+
+ // Is this event scheduled to be sent externally?
+ var scheduledForExternal = false;
+
+ if (extras === undefined) {
+ extras = {};
+ }
+ if (extras.bubbles === true) {
+ bubbles = true;
+ }
+ if (extras.external === true) {
+ external = true;
+ }
+
+ /**
+ * Prevent this event from bubbling up to parent
+ */
+ this.preventBubbling = function () {
+ bubbles = false;
+ };
+
+ /**
+ * Get bubbling status
+ *
+ * @returns {boolean}
+ * true if bubbling false otherwise
+ */
+ this.getBubbles = function () {
+ return bubbles;
+ };
+
+ /**
+ * Try to schedule an event for externalDispatcher
+ *
+ * @returns {boolean}
+ * true if external and not already scheduled, otherwise false
+ */
+ this.scheduleForExternal = function () {
+ if (external && !scheduledForExternal) {
+ scheduledForExternal = true;
+ return true;
+ }
+ return false;
+ };
+};
+
+/**
+ * Callback type for event listeners.
+ *
+ * @callback H5P.EventCallback
+ * @param {H5P.Event} event
+ */
+
+H5P.EventDispatcher = (function () {
+
+ /**
+ * The base of the event system.
+ * Inherit this class if you want your H5P to dispatch events.
+ *
+ * @class
+ * @memberof H5P
+ */
+ function EventDispatcher() {
+ var self = this;
+
+ /**
+ * Keep track of listeners for each event.
+ *
+ * @private
+ * @type {Object}
+ */
+ var triggers = {};
+
+ /**
+ * Add new event listener.
+ *
+ * @throws {TypeError}
+ * listener must be a function
+ * @param {string} type
+ * Event type
+ * @param {H5P.EventCallback} listener
+ * Event listener
+ * @param {Object} [thisArg]
+ * Optionally specify the this value when calling listener.
+ */
+ this.on = function (type, listener, thisArg) {
+ if (typeof listener !== 'function') {
+ throw TypeError('listener must be a function');
+ }
+
+ // Trigger event before adding to avoid recursion
+ self.trigger('newListener', {'type': type, 'listener': listener});
+
+ var trigger = {'listener': listener, 'thisArg': thisArg};
+ if (!triggers[type]) {
+ // First
+ triggers[type] = [trigger];
+ }
+ else {
+ // Append
+ triggers[type].push(trigger);
+ }
+ };
+
+ /**
+ * Add new event listener that will be fired only once.
+ *
+ * @throws {TypeError}
+ * listener must be a function
+ * @param {string} type
+ * Event type
+ * @param {H5P.EventCallback} listener
+ * Event listener
+ * @param {Object} thisArg
+ * Optionally specify the this value when calling listener.
+ */
+ this.once = function (type, listener, thisArg) {
+ if (!(listener instanceof Function)) {
+ throw TypeError('listener must be a function');
+ }
+
+ var once = function (event) {
+ self.off(event.type, once);
+ listener.call(this, event);
+ };
+
+ self.on(type, once, thisArg);
+ };
+
+ /**
+ * Remove event listener.
+ * If no listener is specified, all listeners will be removed.
+ *
+ * @throws {TypeError}
+ * listener must be a function
+ * @param {string} type
+ * Event type
+ * @param {H5P.EventCallback} listener
+ * Event listener
+ */
+ this.off = function (type, listener) {
+ if (listener !== undefined && !(listener instanceof Function)) {
+ throw TypeError('listener must be a function');
+ }
+
+ if (triggers[type] === undefined) {
+ return;
+ }
+
+ if (listener === undefined) {
+ // Remove all listeners
+ delete triggers[type];
+ self.trigger('removeListener', type);
+ return;
+ }
+
+ // Find specific listener
+ for (var i = 0; i < triggers[type].length; i++) {
+ if (triggers[type][i].listener === listener) {
+ triggers[type].splice(i, 1);
+ self.trigger('removeListener', type, {'listener': listener});
+ break;
+ }
+ }
+
+ // Clean up empty arrays
+ if (!triggers[type].length) {
+ delete triggers[type];
+ }
+ };
+
+ /**
+ * Try to call all event listeners for the given event type.
+ *
+ * @private
+ * @param {string} Event type
+ */
+ var call = function (type, event) {
+ if (triggers[type] === undefined) {
+ return;
+ }
+
+ // Clone array (prevents triggers from being modified during the event)
+ var handlers = triggers[type].slice();
+
+ // Call all listeners
+ for (var i = 0; i < handlers.length; i++) {
+ var trigger = handlers[i];
+ var thisArg = (trigger.thisArg ? trigger.thisArg : this);
+ trigger.listener.call(thisArg, event);
+ }
+ };
+
+ /**
+ * Dispatch event.
+ *
+ * @param {string|H5P.Event} event
+ * Event object or event type as string
+ * @param {*} [eventData]
+ * Custom event data(used when event type as string is used as first
+ * argument).
+ * @param {Object} [extras]
+ * @param {boolean} [extras.bubbles]
+ * @param {boolean} [extras.external]
+ */
+ this.trigger = function (event, eventData, extras) {
+ if (event === undefined) {
+ return;
+ }
+ if (event instanceof String || typeof event === 'string') {
+ event = new H5P.Event(event, eventData, extras);
+ }
+ else if (eventData !== undefined) {
+ event.data = eventData;
+ }
+
+ // Check to see if this event should go externally after all triggering and bubbling is done
+ var scheduledForExternal = event.scheduleForExternal();
+
+ // Call all listeners
+ call.call(this, event.type, event);
+
+ // Call all * listeners
+ call.call(this, '*', event);
+
+ // Bubble
+ if (event.getBubbles() && self.parent instanceof H5P.EventDispatcher &&
+ (self.parent.trigger instanceof Function || typeof self.parent.trigger === 'function')) {
+ self.parent.trigger(event);
+ }
+
+ if (scheduledForExternal) {
+ H5P.externalDispatcher.trigger.call(this, event);
+ }
+ };
+ }
+
+ return EventDispatcher;
+})();
diff --git a/src/core/features/h5p/assets/js/h5p-library-details.js b/src/core/features/h5p/assets/js/h5p-library-details.js
new file mode 100644
index 000000000..b5ee01232
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-library-details.js
@@ -0,0 +1,297 @@
+/* global H5PAdminIntegration H5PUtils */
+var H5PLibraryDetails = H5PLibraryDetails || {};
+
+(function ($) {
+
+ H5PLibraryDetails.PAGER_SIZE = 20;
+ /**
+ * Initializing
+ */
+ H5PLibraryDetails.init = function () {
+ H5PLibraryDetails.$adminContainer = H5P.jQuery(H5PAdminIntegration.containerSelector);
+ H5PLibraryDetails.library = H5PAdminIntegration.libraryInfo;
+
+ // currentContent holds the current list if data (relevant for filtering)
+ H5PLibraryDetails.currentContent = H5PLibraryDetails.library.content;
+
+ // The current page index (for pager)
+ H5PLibraryDetails.currentPage = 0;
+
+ // The current filter
+ H5PLibraryDetails.currentFilter = '';
+
+ // We cache the filtered results, so we don't have to do unneccessary searches
+ H5PLibraryDetails.filterCache = [];
+
+ // Append library info
+ H5PLibraryDetails.$adminContainer.append(H5PLibraryDetails.createLibraryInfo());
+
+ // Append node list
+ H5PLibraryDetails.$adminContainer.append(H5PLibraryDetails.createContentElement());
+ };
+
+ /**
+ * Create the library details view
+ */
+ H5PLibraryDetails.createLibraryInfo = function () {
+ var $libraryInfo = $('
');
+
+ $.each(H5PLibraryDetails.library.info, function (title, value) {
+ $libraryInfo.append(H5PUtils.createLabeledField(title, value));
+ });
+
+ return $libraryInfo;
+ };
+
+ /**
+ * Create the content list with searching and paging
+ */
+ H5PLibraryDetails.createContentElement = function () {
+ if (H5PLibraryDetails.library.notCached !== undefined) {
+ return H5PUtils.getRebuildCache(H5PLibraryDetails.library.notCached);
+ }
+
+ if (H5PLibraryDetails.currentContent === undefined) {
+ H5PLibraryDetails.$content = $('
');
+ H5PLibraryDetails.createSearchElement();
+ H5PLibraryDetails.createPageSizeSelector();
+ H5PLibraryDetails.createContentTable();
+ H5PLibraryDetails.createPagerElement();
+ return H5PLibraryDetails.$content;
+ }
+ };
+
+ /**
+ * Creates the content list
+ */
+ H5PLibraryDetails.createContentTable = function () {
+ // Remove it if it exists:
+ if (H5PLibraryDetails.$contentTable) {
+ H5PLibraryDetails.$contentTable.remove();
+ }
+
+ H5PLibraryDetails.$contentTable = H5PUtils.createTable();
+
+ var i = (H5PLibraryDetails.currentPage*H5PLibraryDetails.PAGER_SIZE);
+ var lastIndex = (i+H5PLibraryDetails.PAGER_SIZE);
+
+ if (lastIndex > H5PLibraryDetails.currentContent.length) {
+ lastIndex = H5PLibraryDetails.currentContent.length;
+ }
+ for (; i
' + content.title + '']));
+ }
+
+ // Appends it to the browser DOM
+ H5PLibraryDetails.$contentTable.insertAfter(H5PLibraryDetails.$search);
+ };
+
+ /**
+ * Creates the pager element on the bottom of the list
+ */
+ H5PLibraryDetails.createPagerElement = function () {
+ H5PLibraryDetails.$previous = $('');
+ 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('');
+
+ // 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 = $('', 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 = $('');
+ 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++) {
+ $('', {
+ 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 = $(' | ');
+ var $tr = $('
').appendTo($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 = $(' | ');
+ var $tr = $('
').appendTo($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 = $('', {
+ html: '<',
+ 'class': 'button',
+ title: l10n.previousPage
+ }).click(function () {
+ goTo(current - 1);
+ });
+
+ // Current page text
+ var $text = $('').click(function () {
+ $input.width($text.width()).show().val(current + 1).focus();
+ $text.hide();
+ });
+
+ // Jump to page input
+ var $input = $('', {
+ type: 'number',
+ min : 1,
+ max: pages,
+ on: {
+ 'blur': function () {
+ gotInput();
+ },
+ 'keyup': function (event) {
+ if (event.keyCode === 13) {
+ gotInput();
+ return false;
+ }
+ }
+ }
+ }).hide();
+
+ // Next button
+ var $right = $('', {
+ html: '>',
+ 'class': 'button',
+ title: l10n.nextPage
+ }).click(function () {
+ goTo(current + 1);
+ });
+
+ /**
+ * Check what page the user has typed in and jump to it.
+ *
+ * @private
+ */
+ var gotInput = function () {
+ var page = parseInt($input.hide().val());
+ if (!isNaN(page)) {
+ goTo(page - 1);
+ }
+ $text.show();
+ };
+
+ /**
+ * Update UI elements.
+ *
+ * @private
+ */
+ var updateUI = function () {
+ var next = current + 1;
+
+ // Disable or enable buttons
+ $left.attr('disabled', current === 0);
+ $right.attr('disabled', next === pages);
+
+ // Update counter
+ $text.html(l10n.currentPage.replace('$current', next).replace('$total', pages));
+ };
+
+ /**
+ * Try to go to the requested page.
+ *
+ * @private
+ * @param {Number} page
+ */
+ var goTo = function (page) {
+ if (page === current || page < 0 || page >= pages) {
+ return; // Invalid page number
+ }
+ current = page;
+
+ updateUI();
+
+ // Fire callback
+ goneTo(page * limit);
+ };
+
+ /**
+ * Update number of items and limit.
+ *
+ * @public
+ * @param {Number} newNum Total number of items to pagiate.
+ * @param {Number} newLimit Number of items to dispaly per page.
+ */
+ this.update = function (newNum, newLimit) {
+ if (newNum !== num || newLimit !== limit) {
+ // Update num and limit
+ num = newNum;
+ limit = newLimit;
+ pages = Math.ceil(num / limit);
+ $input.attr('max', pages);
+
+ if (current >= pages) {
+ // Content is gone, move to last page.
+ goTo(pages - 1);
+ return;
+ }
+
+ updateUI();
+ }
+ };
+
+ /**
+ * Append the pagination widget to the given container.
+ *
+ * @public
+ * @param {jQuery} $container
+ */
+ this.appendTo = function ($container) {
+ $left.add($text).add($input).add($right).appendTo($container);
+ };
+
+ // Update UI
+ updateUI();
+ };
+
+})(H5P.jQuery);
diff --git a/src/core/features/h5p/assets/js/h5p-version.js b/src/core/features/h5p/assets/js/h5p-version.js
new file mode 100644
index 000000000..8457341d6
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-version.js
@@ -0,0 +1,40 @@
+H5P.Version = (function () {
+ /**
+ * Make it easy to keep track of version details.
+ *
+ * @class
+ * @namespace H5P
+ * @param {String} version
+ */
+ function Version(version) {
+
+ if (typeof version === 'string') {
+ // Name version string (used by content upgrade)
+ var versionSplit = version.split('.', 3);
+ this.major =+ versionSplit[0];
+ this.minor =+ versionSplit[1];
+ }
+ else {
+ // Library objects (used by editor)
+ if (version.localMajorVersion !== undefined) {
+ this.major =+ version.localMajorVersion;
+ this.minor =+ version.localMinorVersion;
+ }
+ else {
+ this.major =+ version.majorVersion;
+ this.minor =+ version.minorVersion;
+ }
+ }
+
+ /**
+ * Public. Custom string for this object.
+ *
+ * @returns {String}
+ */
+ this.toString = function () {
+ return version;
+ };
+ }
+
+ return Version;
+})();
diff --git a/src/core/features/h5p/assets/js/h5p-x-api-event.js b/src/core/features/h5p/assets/js/h5p-x-api-event.js
new file mode 100644
index 000000000..a26b58b70
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-x-api-event.js
@@ -0,0 +1,331 @@
+var H5P = window.H5P = window.H5P || {};
+
+/**
+ * Used for xAPI events.
+ *
+ * @class
+ * @extends H5P.Event
+ */
+H5P.XAPIEvent = function () {
+ H5P.Event.call(this, 'xAPI', {'statement': {}}, {bubbles: true, external: true});
+};
+
+H5P.XAPIEvent.prototype = Object.create(H5P.Event.prototype);
+H5P.XAPIEvent.prototype.constructor = H5P.XAPIEvent;
+
+/**
+ * Set scored result statements.
+ *
+ * @param {number} score
+ * @param {number} maxScore
+ * @param {object} instance
+ * @param {boolean} completion
+ * @param {boolean} success
+ */
+H5P.XAPIEvent.prototype.setScoredResult = function (score, maxScore, instance, completion, success) {
+ this.data.statement.result = {};
+
+ if (typeof score !== 'undefined') {
+ if (typeof maxScore === 'undefined') {
+ this.data.statement.result.score = {'raw': score};
+ }
+ else {
+ this.data.statement.result.score = {
+ 'min': 0,
+ 'max': maxScore,
+ 'raw': score
+ };
+ if (maxScore > 0) {
+ this.data.statement.result.score.scaled = Math.round(score / maxScore * 10000) / 10000;
+ }
+ }
+ }
+
+ if (typeof completion === 'undefined') {
+ this.data.statement.result.completion = (this.getVerb() === 'completed' || this.getVerb() === 'answered');
+ }
+ else {
+ this.data.statement.result.completion = completion;
+ }
+
+ if (typeof success !== 'undefined') {
+ this.data.statement.result.success = success;
+ }
+
+ if (instance && instance.activityStartTime) {
+ var duration = Math.round((Date.now() - instance.activityStartTime ) / 10) / 100;
+ // xAPI spec allows a precision of 0.01 seconds
+
+ this.data.statement.result.duration = 'PT' + duration + 'S';
+ }
+};
+
+/**
+ * Set a verb.
+ *
+ * @param {string} verb
+ * Verb in short form, one of the verbs defined at
+ * {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary}
+ *
+ */
+H5P.XAPIEvent.prototype.setVerb = function (verb) {
+ if (H5P.jQuery.inArray(verb, H5P.XAPIEvent.allowedXAPIVerbs) !== -1) {
+ this.data.statement.verb = {
+ 'id': 'http://adlnet.gov/expapi/verbs/' + verb,
+ 'display': {
+ 'en-US': verb
+ }
+ };
+ }
+ else if (verb.id !== undefined) {
+ this.data.statement.verb = verb;
+ }
+};
+
+/**
+ * Get the statements verb id.
+ *
+ * @param {boolean} full
+ * if true the full verb id prefixed by http://adlnet.gov/expapi/verbs/
+ * will be returned
+ * @returns {string}
+ * Verb or null if no verb with an id has been defined
+ */
+H5P.XAPIEvent.prototype.getVerb = function (full) {
+ var statement = this.data.statement;
+ if ('verb' in statement) {
+ if (full === true) {
+ return statement.verb;
+ }
+ return statement.verb.id.slice(31);
+ }
+ else {
+ return null;
+ }
+};
+
+/**
+ * Set the object part of the statement.
+ *
+ * The id is found automatically (the url to the content)
+ *
+ * @param {Object} instance
+ * The H5P instance
+ */
+H5P.XAPIEvent.prototype.setObject = function (instance) {
+ if (instance.contentId) {
+ this.data.statement.object = {
+ 'id': this.getContentXAPIId(instance),
+ 'objectType': 'Activity',
+ 'definition': {
+ 'extensions': {
+ 'http://h5p.org/x-api/h5p-local-content-id': instance.contentId
+ }
+ }
+ };
+ if (instance.subContentId) {
+ this.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'] = instance.subContentId;
+ // Don't set titles on main content, title should come from publishing platform
+ if (typeof instance.getTitle === 'function') {
+ this.data.statement.object.definition.name = {
+ "en-US": instance.getTitle()
+ };
+ }
+ }
+ else {
+ var content = H5P.getContentForInstance(instance.contentId);
+ if (content && content.metadata && content.metadata.title) {
+ this.data.statement.object.definition.name = {
+ "en-US": H5P.createTitle(content.metadata.title)
+ };
+ }
+ }
+ }
+ else {
+ // Content types view always expect to have a contentId when they are displayed.
+ // This is not the case if they are displayed in the editor as part of a preview.
+ // The fix is to set an empty object with definition for the xAPI event, so all
+ // the content types that rely on this does not have to handle it. This means
+ // that content types that are being previewed will send xAPI completed events,
+ // but since there are no scripts that catch these events in the editor,
+ // this is not a problem.
+ this.data.statement.object = {
+ definition: {}
+ };
+ }
+};
+
+/**
+ * Set the context part of the statement.
+ *
+ * @param {Object} instance
+ * The H5P instance
+ */
+H5P.XAPIEvent.prototype.setContext = function (instance) {
+ if (instance.parent && (instance.parent.contentId || instance.parent.subContentId)) {
+ this.data.statement.context = {
+ "contextActivities": {
+ "parent": [
+ {
+ "id": this.getContentXAPIId(instance.parent),
+ "objectType": "Activity"
+ }
+ ]
+ }
+ };
+ }
+ if (instance.libraryInfo) {
+ if (this.data.statement.context === undefined) {
+ this.data.statement.context = {"contextActivities":{}};
+ }
+ this.data.statement.context.contextActivities.category = [
+ {
+ "id": "http://h5p.org/libraries/" + instance.libraryInfo.versionedNameNoSpaces,
+ "objectType": "Activity"
+ }
+ ];
+ }
+};
+
+/**
+ * Set the actor. Email and name will be added automatically.
+ */
+H5P.XAPIEvent.prototype.setActor = function () {
+ if (H5PIntegration.user !== undefined) {
+ this.data.statement.actor = {
+ 'name': H5PIntegration.user.name,
+ 'mbox': 'mailto:' + H5PIntegration.user.mail,
+ 'objectType': 'Agent'
+ };
+ }
+ else {
+ var uuid;
+ try {
+ if (localStorage.H5PUserUUID) {
+ uuid = localStorage.H5PUserUUID;
+ }
+ else {
+ uuid = H5P.createUUID();
+ localStorage.H5PUserUUID = uuid;
+ }
+ }
+ catch (err) {
+ // LocalStorage and Cookies are probably disabled. Do not track the user.
+ uuid = 'not-trackable-' + H5P.createUUID();
+ }
+ this.data.statement.actor = {
+ 'account': {
+ 'name': uuid,
+ 'homePage': H5PIntegration.siteUrl
+ },
+ 'objectType': 'Agent'
+ };
+ }
+};
+
+/**
+ * Get the max value of the result - score part of the statement
+ *
+ * @returns {number}
+ * The max score, or null if not defined
+ */
+H5P.XAPIEvent.prototype.getMaxScore = function () {
+ return this.getVerifiedStatementValue(['result', 'score', 'max']);
+};
+
+/**
+ * Get the raw value of the result - score part of the statement
+ *
+ * @returns {number}
+ * The score, or null if not defined
+ */
+H5P.XAPIEvent.prototype.getScore = function () {
+ return this.getVerifiedStatementValue(['result', 'score', 'raw']);
+};
+
+/**
+ * Get content xAPI ID.
+ *
+ * @param {Object} instance
+ * The H5P instance
+ */
+H5P.XAPIEvent.prototype.getContentXAPIId = function (instance) {
+ var xAPIId;
+ if (instance.contentId && H5PIntegration && H5PIntegration.contents && H5PIntegration.contents['cid-' + instance.contentId]) {
+ xAPIId = H5PIntegration.contents['cid-' + instance.contentId].url;
+ if (instance.subContentId) {
+ xAPIId += '?subContentId=' + instance.subContentId;
+ }
+ }
+ return xAPIId;
+};
+
+/**
+ * Check if this event is sent from a child (i.e not from grandchild)
+ *
+ * @return {Boolean}
+ */
+H5P.XAPIEvent.prototype.isFromChild = function () {
+ var parentId = this.getVerifiedStatementValue(['context', 'contextActivities', 'parent', 0, 'id']);
+ return !parentId || parentId.indexOf('subContentId') === -1;
+};
+
+/**
+ * Figure out if a property exists in the statement and return it
+ *
+ * @param {string[]} keys
+ * List describing the property we're looking for. For instance
+ * ['result', 'score', 'raw'] for result.score.raw
+ * @returns {*}
+ * The value of the property if it is set, null otherwise.
+ */
+H5P.XAPIEvent.prototype.getVerifiedStatementValue = function (keys) {
+ var val = this.data.statement;
+ for (var i = 0; i < keys.length; i++) {
+ if (val[keys[i]] === undefined) {
+ return null;
+ }
+ val = val[keys[i]];
+ }
+ return val;
+};
+
+/**
+ * List of verbs defined at {@link http://adlnet.gov/expapi/verbs/|ADL xAPI Vocabulary}
+ *
+ * @type Array
+ */
+H5P.XAPIEvent.allowedXAPIVerbs = [
+ 'answered',
+ 'asked',
+ 'attempted',
+ 'attended',
+ 'commented',
+ 'completed',
+ 'exited',
+ 'experienced',
+ 'failed',
+ 'imported',
+ 'initialized',
+ 'interacted',
+ 'launched',
+ 'mastered',
+ 'passed',
+ 'preferred',
+ 'progressed',
+ 'registered',
+ 'responded',
+ 'resumed',
+ 'scored',
+ 'shared',
+ 'suspended',
+ 'terminated',
+ 'voided',
+
+ // Custom verbs used for action toolbar below content
+ 'downloaded',
+ 'copied',
+ 'accessed-reuse',
+ 'accessed-embed',
+ 'accessed-copyright'
+];
diff --git a/src/core/features/h5p/assets/js/h5p-x-api.js b/src/core/features/h5p/assets/js/h5p-x-api.js
new file mode 100644
index 000000000..66971cdef
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p-x-api.js
@@ -0,0 +1,119 @@
+var H5P = window.H5P = window.H5P || {};
+
+/**
+ * The external event dispatcher. Others, outside of H5P may register and
+ * listen for H5P Events here.
+ *
+ * @type {H5P.EventDispatcher}
+ */
+H5P.externalDispatcher = new H5P.EventDispatcher();
+
+// EventDispatcher extensions
+
+/**
+ * Helper function for triggering xAPI added to the EventDispatcher.
+ *
+ * @param {string} verb
+ * The short id of the verb we want to trigger
+ * @param {Oject} [extra]
+ * Extra properties for the xAPI statement
+ */
+H5P.EventDispatcher.prototype.triggerXAPI = function (verb, extra) {
+ this.trigger(this.createXAPIEventTemplate(verb, extra));
+};
+
+/**
+ * Helper function to create event templates added to the EventDispatcher.
+ *
+ * Will in the future be used to add representations of the questions to the
+ * statements.
+ *
+ * @param {string} verb
+ * Verb id in short form
+ * @param {Object} [extra]
+ * Extra values to be added to the statement
+ * @returns {H5P.XAPIEvent}
+ * Instance
+ */
+H5P.EventDispatcher.prototype.createXAPIEventTemplate = function (verb, extra) {
+ var event = new H5P.XAPIEvent();
+
+ event.setActor();
+ event.setVerb(verb);
+ if (extra !== undefined) {
+ for (var i in extra) {
+ event.data.statement[i] = extra[i];
+ }
+ }
+ if (!('object' in event.data.statement)) {
+ event.setObject(this);
+ }
+ if (!('context' in event.data.statement)) {
+ event.setContext(this);
+ }
+ return event;
+};
+
+/**
+ * Helper function to create xAPI completed events
+ *
+ * DEPRECATED - USE triggerXAPIScored instead
+ *
+ * @deprecated
+ * since 1.5, use triggerXAPIScored instead.
+ * @param {number} score
+ * Will be set as the 'raw' value of the score object
+ * @param {number} maxScore
+ * will be set as the "max" value of the score object
+ * @param {boolean} success
+ * will be set as the "success" value of the result object
+ */
+H5P.EventDispatcher.prototype.triggerXAPICompleted = function (score, maxScore, success) {
+ this.triggerXAPIScored(score, maxScore, 'completed', true, success);
+};
+
+/**
+ * Helper function to create scored xAPI events
+ *
+ * @param {number} score
+ * Will be set as the 'raw' value of the score object
+ * @param {number} maxScore
+ * Will be set as the "max" value of the score object
+ * @param {string} verb
+ * Short form of adl verb
+ * @param {boolean} completion
+ * Is this a statement from a completed activity?
+ * @param {boolean} success
+ * Is this a statement from an activity that was done successfully?
+ */
+H5P.EventDispatcher.prototype.triggerXAPIScored = function (score, maxScore, verb, completion, success) {
+ var event = this.createXAPIEventTemplate(verb);
+ event.setScoredResult(score, maxScore, this, completion, success);
+ this.trigger(event);
+};
+
+H5P.EventDispatcher.prototype.setActivityStarted = function () {
+ if (this.activityStartTime === undefined) {
+ // Don't trigger xAPI events in the editor
+ if (this.contentId !== undefined &&
+ H5PIntegration.contents !== undefined &&
+ H5PIntegration.contents['cid-' + this.contentId] !== undefined) {
+ this.triggerXAPI('attempted');
+ }
+ this.activityStartTime = Date.now();
+ }
+};
+
+/**
+ * Internal H5P function listening for xAPI completed events and stores scores
+ *
+ * @param {H5P.XAPIEvent} event
+ */
+H5P.xAPICompletedListener = function (event) {
+ if ((event.getVerb() === 'completed' || event.getVerb() === 'answered') && !event.getVerifiedStatementValue(['context', 'contextActivities', 'parent'])) {
+ var score = event.getScore();
+ var maxScore = event.getMaxScore();
+ var contentId = event.getVerifiedStatementValue(['object', 'definition', 'extensions', 'http://h5p.org/x-api/h5p-local-content-id']);
+ H5P.setFinished(contentId, score, maxScore);
+ }
+};
diff --git a/src/core/features/h5p/assets/js/h5p.js b/src/core/features/h5p/assets/js/h5p.js
new file mode 100644
index 000000000..5d2a8f623
--- /dev/null
+++ b/src/core/features/h5p/assets/js/h5p.js
@@ -0,0 +1,2847 @@
+/*jshint multistr: true */
+// TODO: Should we split up the generic parts needed by the editor(and others), and the parts needed to "run" H5Ps?
+
+/** @namespace */
+var H5P = window.H5P = window.H5P || {};
+
+/**
+ * Tells us if we're inside of an iframe.
+ * @member {boolean}
+ */
+H5P.isFramed = (window.self !== window.parent);
+
+/**
+ * jQuery instance of current window.
+ * @member {H5P.jQuery}
+ */
+H5P.$window = H5P.jQuery(window);
+
+/**
+ * List over H5P instances on the current page.
+ * @member {Array}
+ */
+H5P.instances = [];
+
+// Detect if we support fullscreen, and what prefix to use.
+if (document.documentElement.requestFullScreen) {
+ /**
+ * Browser prefix to use when entering fullscreen mode.
+ * undefined means no fullscreen support.
+ * @member {string}
+ */
+ H5P.fullScreenBrowserPrefix = '';
+}
+else if (document.documentElement.webkitRequestFullScreen) {
+ H5P.safariBrowser = navigator.userAgent.match(/version\/([.\d]+)/i);
+ H5P.safariBrowser = (H5P.safariBrowser === null ? 0 : parseInt(H5P.safariBrowser[1]));
+
+ // Do not allow fullscreen for safari < 7.
+ if (H5P.safariBrowser === 0 || H5P.safariBrowser > 6) {
+ H5P.fullScreenBrowserPrefix = 'webkit';
+ }
+}
+else if (document.documentElement.mozRequestFullScreen) {
+ H5P.fullScreenBrowserPrefix = 'moz';
+}
+else if (document.documentElement.msRequestFullscreen) {
+ H5P.fullScreenBrowserPrefix = 'ms';
+}
+
+/**
+ * Keep track of when the H5Ps where started.
+ *
+ * @type {Object[]}
+ */
+H5P.opened = {};
+
+/**
+ * Initialize H5P content.
+ * Scans for ".h5p-content" in the document and initializes H5P instances where found.
+ *
+ * @param {Object} target DOM Element
+ */
+H5P.init = function (target) {
+ // Useful jQuery object.
+ if (H5P.$body === undefined) {
+ H5P.$body = H5P.jQuery(document.body);
+ }
+
+ // Determine if we can use full screen
+ if (H5P.fullscreenSupported === undefined) {
+ /**
+ * Use this variable to check if fullscreen is supported. Fullscreen can be
+ * restricted when embedding since not all browsers support the native
+ * fullscreen, and the semi-fullscreen solution doesn't work when embedded.
+ * @type {boolean}
+ */
+ H5P.fullscreenSupported = !H5PIntegration.fullscreenDisabled && !H5P.fullscreenDisabled && (!(H5P.isFramed && H5P.externalEmbed !== false) || !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled));
+ // -We should consider document.msFullscreenEnabled when they get their
+ // -element sizing corrected. Ref. https://connect.microsoft.com/IE/feedback/details/838286/ie-11-incorrectly-reports-dom-element-sizes-in-fullscreen-mode-when-fullscreened-element-is-within-an-iframe
+ // Update: Seems to be no need as they've moved on to Webkit
+ }
+
+ // Deprecated variable, kept to maintain backwards compatability
+ if (H5P.canHasFullScreen === undefined) {
+ /**
+ * @deprecated since version 1.11
+ * @type {boolean}
+ */
+ H5P.canHasFullScreen = H5P.fullscreenSupported;
+ }
+
+ // H5Ps added in normal DIV.
+ H5P.jQuery('.h5p-content:not(.h5p-initialized)', target).each(function () {
+ var $element = H5P.jQuery(this).addClass('h5p-initialized');
+ var $container = H5P.jQuery('').appendTo($element);
+ var contentId = $element.data('content-id');
+ var contentData = H5PIntegration.contents['cid-' + contentId];
+ if (contentData === undefined) {
+ return H5P.error('No data for content id ' + contentId + '. Perhaps the library is gone?');
+ }
+ var library = {
+ library: contentData.library,
+ params: JSON.parse(contentData.jsonContent),
+ metadata: contentData.metadata
+ };
+
+ H5P.getUserData(contentId, 'state', function (err, previousState) {
+ if (previousState) {
+ library.userDatas = {
+ state: previousState
+ };
+ }
+ else if (previousState === null && H5PIntegration.saveFreq) {
+ // Content has been reset. Display dialog.
+ delete contentData.contentUserData;
+ var dialog = new H5P.Dialog('content-user-data-reset', 'Data Reset', ' ' + H5P.t('contentChanged') + ' ' + H5P.t('startingOver') + ' OK ', $container);
+ H5P.jQuery(dialog).on('dialog-opened', function (event, $dialog) {
+
+ var closeDialog = function (event) {
+ if (event.type === 'click' || event.which === 32) {
+ dialog.close();
+ H5P.deleteUserData(contentId, 'state', 0);
+ }
+ };
+
+ $dialog.find('.h5p-dialog-ok-button').click(closeDialog).keypress(closeDialog);
+ H5P.trigger(instance, 'resize');
+ }).on('dialog-closed', function () {
+ H5P.trigger(instance, 'resize');
+ });
+ dialog.open();
+ }
+ // If previousState is false we don't have a previous state
+ });
+
+ // Create new instance.
+ var instance = H5P.newRunnable(library, contentId, $container, true, {standalone: true});
+
+ H5P.offlineRequestQueue = new H5P.OfflineRequestQueue({instance: instance});
+
+ // Check if we should add and display a fullscreen button for this H5P.
+ if (contentData.fullScreen == 1 && H5P.fullscreenSupported) {
+ H5P.jQuery(
+ '')
+ .prependTo($container)
+ .children()
+ .click(function () {
+ H5P.fullScreen($container, instance);
+ })
+ .keydown(function (e) {
+ if (e.which === 32 || e.which === 13) {
+ H5P.fullScreen($container, instance);
+ return false;
+ }
+ })
+ ;
+ }
+
+ /**
+ * Create action bar
+ */
+ var displayOptions = contentData.displayOptions;
+ var displayFrame = false;
+ if (displayOptions.frame) {
+ // Special handling of copyrights
+ if (displayOptions.copyright) {
+ var copyrights = H5P.getCopyrights(instance, library.params, contentId, library.metadata);
+ if (!copyrights) {
+ displayOptions.copyright = false;
+ }
+ }
+
+ // Create action bar
+ var actionBar = new H5P.ActionBar(displayOptions);
+ var $actions = actionBar.getDOMElement();
+
+ actionBar.on('reuse', function () {
+ H5P.openReuseDialog($actions, contentData, library, instance, contentId);
+ instance.triggerXAPI('accessed-reuse');
+ });
+ actionBar.on('copyrights', function () {
+ var dialog = new H5P.Dialog('copyrights', H5P.t('copyrightInformation'), copyrights, $container);
+ dialog.open(true);
+ instance.triggerXAPI('accessed-copyright');
+ });
+ actionBar.on('embed', function () {
+ H5P.openEmbedDialog($actions, contentData.embedCode, contentData.resizeCode, {
+ width: $element.width(),
+ height: $element.height()
+ }, instance);
+ instance.triggerXAPI('accessed-embed');
+ });
+
+ if (actionBar.hasActions()) {
+ displayFrame = true;
+ $actions.insertAfter($container);
+ }
+ }
+
+ $element.addClass(displayFrame ? 'h5p-frame' : 'h5p-no-frame');
+
+ // Keep track of when we started
+ H5P.opened[contentId] = new Date();
+
+ // Handle events when the user finishes the content. Useful for logging exercise results.
+ H5P.on(instance, 'finish', function (event) {
+ if (event.data !== undefined) {
+ H5P.setFinished(contentId, event.data.score, event.data.maxScore, event.data.time);
+ }
+ });
+
+ // Listen for xAPI events.
+ H5P.on(instance, 'xAPI', H5P.xAPICompletedListener);
+
+ // Auto save current state if supported
+ if (H5PIntegration.saveFreq !== false && (
+ instance.getCurrentState instanceof Function ||
+ typeof instance.getCurrentState === 'function')) {
+
+ var saveTimer, save = function () {
+ var state = instance.getCurrentState();
+ if (state !== undefined) {
+ H5P.setUserData(contentId, 'state', state, {deleteOnChange: true});
+ }
+ if (H5PIntegration.saveFreq) {
+ // Continue autosave
+ saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
+ }
+ };
+
+ if (H5PIntegration.saveFreq) {
+ // Start autosave
+ saveTimer = setTimeout(save, H5PIntegration.saveFreq * 1000);
+ }
+
+ // xAPI events will schedule a save in three seconds.
+ H5P.on(instance, 'xAPI', function (event) {
+ var verb = event.getVerb();
+ if (verb === 'completed' || verb === 'progressed') {
+ clearTimeout(saveTimer);
+ saveTimer = setTimeout(save, 3000);
+ }
+ });
+ }
+
+ if (H5P.isFramed) {
+ var resizeDelay;
+ if (H5P.externalEmbed === false) {
+ // Internal embed
+ // Make it possible to resize the iframe when the content changes size. This way we get no scrollbars.
+ var iframe = window.frameElement;
+ var resizeIframe = function () {
+ if (window.parent.H5P.isFullscreen) {
+ return; // Skip if full screen.
+ }
+
+ // Retain parent size to avoid jumping/scrolling
+ var parentHeight = iframe.parentElement.style.height;
+ iframe.parentElement.style.height = iframe.parentElement.clientHeight + 'px';
+
+ // Note: Force layout reflow
+ // This fixes a flickering bug for embedded content on iPads
+ // @see https://github.com/h5p/h5p-moodle-plugin/issues/237
+ iframe.getBoundingClientRect();
+
+ // Reset iframe height, in case content has shrinked.
+ iframe.style.height = '1px';
+
+ // Resize iframe so all content is visible.
+ iframe.style.height = (iframe.contentDocument.body.scrollHeight) + 'px';
+
+ // Free parent
+ iframe.parentElement.style.height = parentHeight;
+ };
+
+ H5P.on(instance, 'resize', function () {
+ // Use a delay to make sure iframe is resized to the correct size.
+ clearTimeout(resizeDelay);
+ resizeDelay = setTimeout(function () {
+ resizeIframe();
+ }, 1);
+ });
+ }
+ else if (H5P.communicator) {
+ // External embed
+ var parentIsFriendly = false;
+
+ // Handle that the resizer is loaded after the iframe
+ H5P.communicator.on('ready', function () {
+ H5P.communicator.send('hello');
+ });
+
+ // Handle hello message from our parent window
+ H5P.communicator.on('hello', function () {
+ // Initial setup/handshake is done
+ parentIsFriendly = true;
+
+ // Make iframe responsive
+ document.body.style.height = 'auto';
+
+ // Hide scrollbars for correct size
+ document.body.style.overflow = 'hidden';
+
+ // Content need to be resized to fit the new iframe size
+ H5P.trigger(instance, 'resize');
+ });
+
+ // When resize has been prepared tell parent window to resize
+ H5P.communicator.on('resizePrepared', function () {
+ H5P.communicator.send('resize', {
+ scrollHeight: document.body.scrollHeight
+ });
+ });
+
+ H5P.communicator.on('resize', function () {
+ H5P.trigger(instance, 'resize');
+ });
+
+ H5P.on(instance, 'resize', function () {
+ if (H5P.isFullscreen) {
+ return; // Skip iframe resize
+ }
+
+ // Use a delay to make sure iframe is resized to the correct size.
+ clearTimeout(resizeDelay);
+ resizeDelay = setTimeout(function () {
+ // Only resize if the iframe can be resized
+ if (parentIsFriendly) {
+ H5P.communicator.send('prepareResize', {
+ scrollHeight: document.body.scrollHeight,
+ clientHeight: document.body.clientHeight
+ });
+ }
+ else {
+ H5P.communicator.send('hello');
+ }
+ }, 0);
+ });
+ }
+ }
+
+ if (!H5P.isFramed || H5P.externalEmbed === false) {
+ // Resize everything when window is resized.
+ H5P.jQuery(window.parent).resize(function () {
+ if (window.parent.H5P.isFullscreen) {
+ // Use timeout to avoid bug in certain browsers when exiting fullscreen. Some browser will trigger resize before the fullscreenchange event.
+ H5P.trigger(instance, 'resize');
+ }
+ else {
+ H5P.trigger(instance, 'resize');
+ }
+ });
+ }
+
+ H5P.instances.push(instance);
+
+ // Resize content.
+ H5P.trigger(instance, 'resize');
+
+ // Logic for hiding focus effects when using mouse
+ $element.addClass('using-mouse');
+ $element.on('mousedown keydown keyup', function (event) {
+ $element.toggleClass('using-mouse', event.type === 'mousedown');
+ });
+
+ if (H5P.externalDispatcher) {
+ H5P.externalDispatcher.trigger('initialized');
+ }
+ });
+
+ // Insert H5Ps that should be in iframes.
+ H5P.jQuery('iframe.h5p-iframe:not(.h5p-initialized)', target).each(function () {
+ var contentId = H5P.jQuery(this).addClass('h5p-initialized').data('content-id');
+ this.contentDocument.open();
+ this.contentDocument.write('' + H5P.getHeadTags(contentId) + '');
+ this.contentDocument.close();
+ });
+};
+
+/**
+ * Loop through assets for iframe content and create a set of tags for head.
+ *
+ * @private
+ * @param {number} contentId
+ * @returns {string} HTML
+ */
+H5P.getHeadTags = function (contentId) {
+ var createStyleTags = function (styles) {
+ var tags = '';
+ for (var i = 0; i < styles.length; i++) {
+ tags += '';
+ }
+ return tags;
+ };
+
+ var createScriptTags = function (scripts) {
+ var tags = '';
+ for (var i = 0; i < scripts.length; i++) {
+ tags += '';
+ }
+ return tags;
+ };
+
+ return '' +
+ createStyleTags(H5PIntegration.core.styles) +
+ createStyleTags(H5PIntegration.contents['cid-' + contentId].styles) +
+ createScriptTags(H5PIntegration.core.scripts) +
+ createScriptTags(H5PIntegration.contents['cid-' + contentId].scripts) +
+ '';
+};
+
+/**
+ * When embedded the communicator helps talk to the parent page.
+ *
+ * @type {Communicator}
+ */
+H5P.communicator = (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, '*');
+ };
+ }
+
+ return (window.postMessage && window.addEventListener ? new Communicator() : undefined);
+})();
+
+/**
+ * Enter semi fullscreen for the given H5P instance
+ *
+ * @param {H5P.jQuery} $element Content container.
+ * @param {Object} instance
+ * @param {function} exitCallback Callback function called when user exits fullscreen.
+ * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
+ */
+H5P.semiFullScreen = function ($element, instance, exitCallback, body) {
+ H5P.fullScreen($element, instance, exitCallback, body, true);
+};
+
+/**
+ * Enter fullscreen for the given H5P instance.
+ *
+ * @param {H5P.jQuery} $element Content container.
+ * @param {Object} instance
+ * @param {function} exitCallback Callback function called when user exits fullscreen.
+ * @param {H5P.jQuery} $body For internal use. Gives the body of the iframe.
+ * @param {Boolean} forceSemiFullScreen
+ */
+H5P.fullScreen = function ($element, instance, exitCallback, body, forceSemiFullScreen) {
+ if (H5P.exitFullScreen !== undefined) {
+ return; // Cannot enter new fullscreen until previous is over
+ }
+
+ if (H5P.isFramed && H5P.externalEmbed === false) {
+ // Trigger resize on wrapper in parent window.
+ window.parent.H5P.fullScreen($element, instance, exitCallback, H5P.$body.get(), forceSemiFullScreen);
+ H5P.isFullscreen = true;
+ H5P.exitFullScreen = function () {
+ window.parent.H5P.exitFullScreen();
+ };
+ H5P.on(instance, 'exitFullScreen', function () {
+ H5P.isFullscreen = false;
+ H5P.exitFullScreen = undefined;
+ });
+ return;
+ }
+
+ var $container = $element;
+ var $classes, $iframe, $body;
+ if (body === undefined) {
+ $body = H5P.$body;
+ }
+ else {
+ // We're called from an iframe.
+ $body = H5P.jQuery(body);
+ $classes = $body.add($element.get());
+ var iframeSelector = '#h5p-iframe-' + $element.parent().data('content-id');
+ $iframe = H5P.jQuery(iframeSelector);
+ $element = $iframe.parent(); // Put iframe wrapper in fullscreen, not container.
+ }
+
+ $classes = $element.add(H5P.$body).add($classes);
+
+ /**
+ * Prepare for resize by setting the correct styles.
+ *
+ * @private
+ * @param {string} classes CSS
+ */
+ var before = function (classes) {
+ $classes.addClass(classes);
+
+ if ($iframe !== undefined) {
+ // Set iframe to its default size(100%).
+ $iframe.css('height', '');
+ }
+ };
+
+ /**
+ * Gets called when fullscreen mode has been entered.
+ * Resizes and sets focus on content.
+ *
+ * @private
+ */
+ var entered = function () {
+ // Do not rely on window resize events.
+ H5P.trigger(instance, 'resize');
+ H5P.trigger(instance, 'focus');
+ H5P.trigger(instance, 'enterFullScreen');
+ };
+
+ /**
+ * Gets called when fullscreen mode has been exited.
+ * Resizes and sets focus on content.
+ *
+ * @private
+ * @param {string} classes CSS
+ */
+ var done = function (classes) {
+ H5P.isFullscreen = false;
+ $classes.removeClass(classes);
+
+ // Do not rely on window resize events.
+ H5P.trigger(instance, 'resize');
+ H5P.trigger(instance, 'focus');
+
+ H5P.exitFullScreen = undefined;
+ if (exitCallback !== undefined) {
+ exitCallback();
+ }
+
+ H5P.trigger(instance, 'exitFullScreen');
+ };
+
+ H5P.isFullscreen = true;
+ if (H5P.fullScreenBrowserPrefix === undefined || forceSemiFullScreen === true) {
+ // Create semi fullscreen.
+
+ if (H5P.isFramed) {
+ return; // TODO: Should we support semi-fullscreen for IE9 & 10 ?
+ }
+
+ before('h5p-semi-fullscreen');
+ var $disable = H5P.jQuery('').appendTo($container.find('.h5p-content-controls'));
+ var keyup, disableSemiFullscreen = H5P.exitFullScreen = function () {
+ if (prevViewportContent) {
+ // Use content from the previous viewport tag
+ h5pViewport.content = prevViewportContent;
+ }
+ else {
+ // Remove viewport tag
+ head.removeChild(h5pViewport);
+ }
+ $disable.remove();
+ $body.unbind('keyup', keyup);
+ done('h5p-semi-fullscreen');
+ };
+ keyup = function (event) {
+ if (event.keyCode === 27) {
+ disableSemiFullscreen();
+ }
+ };
+ $disable.click(disableSemiFullscreen);
+ $body.keyup(keyup);
+
+ // Disable zoom
+ var prevViewportContent, h5pViewport;
+ var metaTags = document.getElementsByTagName('meta');
+ for (var i = 0; i < metaTags.length; i++) {
+ if (metaTags[i].name === 'viewport') {
+ // Use the existing viewport tag
+ h5pViewport = metaTags[i];
+ prevViewportContent = h5pViewport.content;
+ break;
+ }
+ }
+ if (!prevViewportContent) {
+ // Create a new viewport tag
+ h5pViewport = document.createElement('meta');
+ h5pViewport.name = 'viewport';
+ }
+ h5pViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
+ if (!prevViewportContent) {
+ // Insert the new viewport tag
+ var head = document.getElementsByTagName('head')[0];
+ head.appendChild(h5pViewport);
+ }
+
+ entered();
+ }
+ else {
+ // Create real fullscreen.
+
+ before('h5p-fullscreen');
+ var first, eventName = (H5P.fullScreenBrowserPrefix === 'ms' ? 'MSFullscreenChange' : H5P.fullScreenBrowserPrefix + 'fullscreenchange');
+ document.addEventListener(eventName, function () {
+ if (first === undefined) {
+ // We are entering fullscreen mode
+ first = false;
+ entered();
+ return;
+ }
+
+ // We are exiting fullscreen
+ done('h5p-fullscreen');
+ document.removeEventListener(eventName, arguments.callee, false);
+ });
+
+ if (H5P.fullScreenBrowserPrefix === '') {
+ $element[0].requestFullScreen();
+ }
+ else {
+ var method = (H5P.fullScreenBrowserPrefix === 'ms' ? 'msRequestFullscreen' : H5P.fullScreenBrowserPrefix + 'RequestFullScreen');
+ var params = (H5P.fullScreenBrowserPrefix === 'webkit' && H5P.safariBrowser === 0 ? Element.ALLOW_KEYBOARD_INPUT : undefined);
+ $element[0][method](params);
+ }
+
+ // Allows everone to exit
+ H5P.exitFullScreen = function () {
+ if (H5P.fullScreenBrowserPrefix === '') {
+ document.exitFullscreen();
+ }
+ else if (H5P.fullScreenBrowserPrefix === 'moz') {
+ document.mozCancelFullScreen();
+ }
+ else {
+ document[H5P.fullScreenBrowserPrefix + 'ExitFullscreen']();
+ }
+ };
+ }
+};
+
+(function () {
+ /**
+ * Helper for adding a query parameter to an existing path that may already
+ * contain one or a hash.
+ *
+ * @param {string} path
+ * @param {string} parameter
+ * @return {string}
+ */
+ H5P.addQueryParameter = function (path, parameter) {
+ let newPath, secondSplit;
+ const firstSplit = path.split('?');
+ if (firstSplit[1]) {
+ // There is already an existing query
+ secondSplit = firstSplit[1].split('#');
+ newPath = firstSplit[0] + '?' + secondSplit[0] + '&';
+ }
+ else {
+ // No existing query, just need to take care of the hash
+ secondSplit = firstSplit[0].split('#');
+ newPath = secondSplit[0] + '?';
+ }
+ newPath += parameter;
+ if (secondSplit[1]) {
+ // Add back the hash
+ newPath += '#' + secondSplit[1];
+ }
+ return newPath;
+ };
+
+ /**
+ * Helper for setting the crossOrigin attribute + the complete correct source.
+ * Note: This will start loading the resource.
+ *
+ * @param {Element} element DOM element, typically img, video or audio
+ * @param {Object} source File object from parameters/json_content (created by H5PEditor)
+ * @param {number} contentId Needed to determine the complete correct file path
+ */
+ H5P.setSource = function (element, source, contentId) {
+ let path = source.path;
+
+ const crossOrigin = H5P.getCrossOrigin(source);
+ if (crossOrigin) {
+ element.crossOrigin = crossOrigin;
+
+ if (H5PIntegration.crossoriginCacheBuster) {
+ // Some sites may want to add a cache buster in case the same resource
+ // is used elsewhere without the crossOrigin attribute
+ path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
+ }
+ }
+ else {
+ // In case this element has been used before.
+ element.removeAttribute('crossorigin');
+ }
+
+ element.src = H5P.getPath(path, contentId);
+ };
+
+ /**
+ * Check if the given path has a protocol.
+ *
+ * @private
+ * @param {string} path
+ * @return {string}
+ */
+ var hasProtocol = function (path) {
+ return path.match(/^[a-z0-9]+:\/\//i);
+ };
+
+ /**
+ * Get the crossOrigin policy to use for img, video and audio tags on the current site.
+ *
+ * @param {Object|string} source File object from parameters/json_content - Can also be URL(deprecated usage)
+ * @returns {string|null} crossOrigin attribute value required by the source
+ */
+ H5P.getCrossOrigin = function (source) {
+ if (typeof source !== 'object') {
+ // Deprecated usage.
+ return H5PIntegration.crossorigin && H5PIntegration.crossoriginRegex && source.match(H5PIntegration.crossoriginRegex) ? H5PIntegration.crossorigin : null;
+ }
+
+ if (H5PIntegration.crossorigin && !hasProtocol(source.path)) {
+ // This is a local file, use the local crossOrigin policy.
+ return H5PIntegration.crossorigin;
+ // Note: We cannot use this for all external sources since we do not know
+ // each server's individual policy. We could add support for a list of
+ // external sources and their policy later on.
+ }
+ };
+
+ /**
+ * Find the path to the content files based on the id of the content.
+ * Also identifies and returns absolute paths.
+ *
+ * @param {string} path
+ * Relative to content folder or absolute.
+ * @param {number} contentId
+ * ID of the content requesting the path.
+ * @returns {string}
+ * Complete URL to path.
+ */
+ H5P.getPath = function (path, contentId) {
+ if (hasProtocol(path)) {
+ return path;
+ }
+
+ var prefix;
+ var isTmpFile = (path.substr(-4,4) === '#tmp');
+ if (contentId !== undefined && !isTmpFile) {
+ // Check for custom override URL
+ if (H5PIntegration.contents !== undefined &&
+ H5PIntegration.contents['cid-' + contentId]) {
+ prefix = H5PIntegration.contents['cid-' + contentId].contentUrl;
+ }
+ if (!prefix) {
+ prefix = H5PIntegration.url + '/content/' + contentId;
+ }
+ }
+ else if (window.H5PEditor !== undefined) {
+ prefix = H5PEditor.filesPath;
+ }
+ else {
+ return;
+ }
+
+ if (!hasProtocol(prefix)) {
+ // Use absolute urls
+ prefix = window.location.protocol + "//" + window.location.host + prefix;
+ }
+
+ return prefix + '/' + path;
+ };
+})();
+
+/**
+ * THIS FUNCTION IS DEPRECATED, USE getPath INSTEAD
+ * Will be remove march 2016.
+ *
+ * Find the path to the content files folder based on the id of the content
+ *
+ * @deprecated
+ * Will be removed march 2016.
+ * @param contentId
+ * Id of the content requesting the path
+ * @returns {string}
+ * URL
+ */
+H5P.getContentPath = function (contentId) {
+ return H5PIntegration.url + '/content/' + contentId;
+};
+
+/**
+ * Get library class constructor from H5P by classname.
+ * Note that this class will only work for resolve "H5P.NameWithoutDot".
+ * Also check out {@link H5P.newRunnable}
+ *
+ * Used from libraries to construct instances of other libraries' objects by name.
+ *
+ * @param {string} name Name of library
+ * @returns {Object} Class constructor
+ */
+H5P.classFromName = function (name) {
+ var arr = name.split(".");
+ return this[arr[arr.length - 1]];
+};
+
+/**
+ * A safe way of creating a new instance of a runnable H5P.
+ *
+ * @param {Object} library
+ * Library/action object form params.
+ * @param {number} contentId
+ * Identifies the content.
+ * @param {H5P.jQuery} [$attachTo]
+ * Element to attach the instance to.
+ * @param {boolean} [skipResize]
+ * Skip triggering of the resize event after attaching.
+ * @param {Object} [extras]
+ * Extra parameters for the H5P content constructor
+ * @returns {Object}
+ * Instance.
+ */
+H5P.newRunnable = function (library, contentId, $attachTo, skipResize, extras) {
+ var nameSplit, versionSplit, machineName;
+ try {
+ nameSplit = library.library.split(' ', 2);
+ machineName = nameSplit[0];
+ versionSplit = nameSplit[1].split('.', 2);
+ }
+ catch (err) {
+ return H5P.error('Invalid library string: ' + library.library);
+ }
+
+ if ((library.params instanceof Object) !== true || (library.params instanceof Array) === true) {
+ H5P.error('Invalid library params for: ' + library.library);
+ return H5P.error(library.params);
+ }
+
+ // Find constructor function
+ var constructor;
+ try {
+ nameSplit = nameSplit[0].split('.');
+ constructor = window;
+ for (var i = 0; i < nameSplit.length; i++) {
+ constructor = constructor[nameSplit[i]];
+ }
+ if (typeof constructor !== 'function') {
+ throw null;
+ }
+ }
+ catch (err) {
+ return H5P.error('Unable to find constructor for: ' + library.library);
+ }
+
+ if (extras === undefined) {
+ extras = {};
+ }
+ if (library.subContentId) {
+ extras.subContentId = library.subContentId;
+ }
+
+ if (library.userDatas && library.userDatas.state && H5PIntegration.saveFreq) {
+ extras.previousState = library.userDatas.state;
+ }
+
+ if (library.metadata) {
+ extras.metadata = library.metadata;
+ }
+
+ // Makes all H5P libraries extend H5P.ContentType:
+ var standalone = extras.standalone || false;
+ // This order makes it possible for an H5P library to override H5P.ContentType functions!
+ constructor.prototype = H5P.jQuery.extend({}, H5P.ContentType(standalone).prototype, constructor.prototype);
+
+ var instance;
+ // Some old library versions have their own custom third parameter.
+ // Make sure we don't send them the extras.
+ // (they will interpret it as something else)
+ if (H5P.jQuery.inArray(library.library, ['H5P.CoursePresentation 1.0', 'H5P.CoursePresentation 1.1', 'H5P.CoursePresentation 1.2', 'H5P.CoursePresentation 1.3']) > -1) {
+ instance = new constructor(library.params, contentId);
+ }
+ else {
+ instance = new constructor(library.params, contentId, extras);
+ }
+
+ if (instance.$ === undefined) {
+ instance.$ = H5P.jQuery(instance);
+ }
+
+ if (instance.contentId === undefined) {
+ instance.contentId = contentId;
+ }
+ if (instance.subContentId === undefined && library.subContentId) {
+ instance.subContentId = library.subContentId;
+ }
+ if (instance.parent === undefined && extras && extras.parent) {
+ instance.parent = extras.parent;
+ }
+ if (instance.libraryInfo === undefined) {
+ instance.libraryInfo = {
+ versionedName: library.library,
+ versionedNameNoSpaces: machineName + '-' + versionSplit[0] + '.' + versionSplit[1],
+ machineName: machineName,
+ majorVersion: versionSplit[0],
+ minorVersion: versionSplit[1]
+ };
+ }
+
+ if ($attachTo !== undefined) {
+ $attachTo.toggleClass('h5p-standalone', standalone);
+ instance.attach($attachTo);
+ H5P.trigger(instance, 'domChanged', {
+ '$target': $attachTo,
+ 'library': machineName,
+ 'key': 'newLibrary'
+ }, {'bubbles': true, 'external': true});
+
+ if (skipResize === undefined || !skipResize) {
+ // Resize content.
+ H5P.trigger(instance, 'resize');
+ }
+ }
+ return instance;
+};
+
+/**
+ * Used to print useful error messages. (to JavaScript error console)
+ *
+ * @param {*} err Error to print.
+ */
+H5P.error = function (err) {
+ if (window.console !== undefined && console.error !== undefined) {
+ console.error(err.stack ? err.stack : err);
+ }
+};
+
+/**
+ * Translate text strings.
+ *
+ * @param {string} key
+ * Translation identifier, may only contain a-zA-Z0-9. No spaces or special chars.
+ * @param {Object} [vars]
+ * Data for placeholders.
+ * @param {string} [ns]
+ * Translation namespace. Defaults to H5P.
+ * @returns {string}
+ * Translated text
+ */
+H5P.t = function (key, vars, ns) {
+ if (ns === undefined) {
+ ns = 'H5P';
+ }
+
+ if (H5PIntegration.l10n[ns] === undefined) {
+ return '[Missing translation namespace "' + ns + '"]';
+ }
+
+ if (H5PIntegration.l10n[ns][key] === undefined) {
+ return '[Missing translation "' + key + '" in "' + ns + '"]';
+ }
+
+ var translation = H5PIntegration.l10n[ns][key];
+
+ if (vars !== undefined) {
+ // Replace placeholder with variables.
+ for (var placeholder in vars) {
+ translation = translation.replace(placeholder, vars[placeholder]);
+ }
+ }
+
+ return translation;
+};
+
+/**
+ * Creates a new popup dialog over the H5P content.
+ *
+ * @class
+ * @param {string} name
+ * Used for html class.
+ * @param {string} title
+ * Used for header.
+ * @param {string} content
+ * Displayed inside the dialog.
+ * @param {H5P.jQuery} $element
+ * Which DOM element the dialog should be inserted after.
+ */
+H5P.Dialog = function (name, title, content, $element) {
+ /** @alias H5P.Dialog# */
+ var self = this;
+ var $dialog = H5P.jQuery('')
+ .insertAfter($element)
+ .click(function (e) {
+ if (e && e.originalEvent && e.originalEvent.preventClosing) {
+ return;
+ }
+
+ self.close();
+ })
+ .children('.h5p-inner')
+ .click(function (e) {
+ e.originalEvent.preventClosing = true;
+ })
+ .find('.h5p-close')
+ .click(function () {
+ self.close();
+ })
+ .end()
+ .find('a')
+ .click(function (e) {
+ e.stopPropagation();
+ })
+ .end()
+ .end();
+
+ /**
+ * Opens the dialog.
+ */
+ self.open = function (scrollbar) {
+ if (scrollbar) {
+ $dialog.css('height', '100%');
+ }
+ setTimeout(function () {
+ $dialog.addClass('h5p-open'); // Fade in
+ // Triggering an event, in case something has to be done after dialog has been opened.
+ H5P.jQuery(self).trigger('dialog-opened', [$dialog]);
+ }, 1);
+ };
+
+ /**
+ * Closes the dialog.
+ */
+ self.close = function () {
+ $dialog.removeClass('h5p-open'); // Fade out
+ setTimeout(function () {
+ $dialog.remove();
+ H5P.jQuery(self).trigger('dialog-closed', [$dialog]);
+ }, 200);
+ };
+};
+
+/**
+ * Gather copyright information for the given content.
+ *
+ * @param {Object} instance
+ * H5P instance to get copyright information for.
+ * @param {Object} parameters
+ * Parameters of the content instance.
+ * @param {number} contentId
+ * Identifies the H5P content
+ * @param {Object} metadata
+ * Metadata of the content instance.
+ * @returns {string} Copyright information.
+ */
+H5P.getCopyrights = function (instance, parameters, contentId, metadata) {
+ var copyrights;
+
+ if (instance.getCopyrights !== undefined) {
+ try {
+ // Use the instance's own copyright generator
+ copyrights = instance.getCopyrights();
+ }
+ catch (err) {
+ // Failed, prevent crashing page.
+ }
+ }
+
+ if (copyrights === undefined) {
+ // Create a generic flat copyright list
+ copyrights = new H5P.ContentCopyrights();
+ H5P.findCopyrights(copyrights, parameters, contentId);
+ }
+
+ var metadataCopyrights = H5P.buildMetadataCopyrights(metadata, instance.libraryInfo.machineName);
+ if (metadataCopyrights !== undefined) {
+ copyrights.addMediaInFront(metadataCopyrights);
+ }
+
+ if (copyrights !== undefined) {
+ // Convert to string
+ copyrights = copyrights.toString();
+ }
+ return copyrights;
+};
+
+/**
+ * Gather a flat list of copyright information from the given parameters.
+ *
+ * @param {H5P.ContentCopyrights} info
+ * Used to collect all information in.
+ * @param {(Object|Array)} parameters
+ * To search for file objects in.
+ * @param {number} contentId
+ * Used to insert thumbnails for images.
+ * @param {Object} extras - Extras.
+ * @param {object} extras.metadata - Metadata
+ * @param {object} extras.machineName - Library name of some kind.
+ * Metadata of the content instance.
+ */
+H5P.findCopyrights = function (info, parameters, contentId, extras) {
+ // If extras are
+ if (extras) {
+ extras.params = parameters;
+ buildFromMetadata(extras, extras.machineName, contentId);
+ }
+
+ var lastContentTypeName;
+ // Cycle through parameters
+ for (var field in parameters) {
+ if (!parameters.hasOwnProperty(field)) {
+ continue; // Do not check
+ }
+
+ /**
+ * @deprecated This hack should be removed after 2017-11-01
+ * The code that was using this was removed by HFP-574
+ * This note was seen on 2018-04-04, and consultation with
+ * higher authorities lead to keeping the code for now ;-)
+ */
+ if (field === 'overrideSettings') {
+ console.warn("The semantics field 'overrideSettings' is DEPRECATED and should not be used.");
+ console.warn(parameters);
+ continue;
+ }
+
+ var value = parameters[field];
+
+ if (value && value.library && typeof value.library === 'string') {
+ lastContentTypeName = value.library.split(' ')[0];
+ }
+ else if (value && value.library && typeof value.library === 'object') {
+ lastContentTypeName = (value.library.library && typeof value.library.library === 'string') ? value.library.library.split(' ')[0] : lastContentTypeName;
+ }
+
+ if (value instanceof Array) {
+ // Cycle through array
+ H5P.findCopyrights(info, value, contentId);
+ }
+ else if (value instanceof Object) {
+ buildFromMetadata(value, lastContentTypeName, contentId);
+
+ // Check if object is a file with copyrights (old core)
+ if (value.copyright === undefined ||
+ value.copyright.license === undefined ||
+ value.path === undefined ||
+ value.mime === undefined) {
+
+ // Nope, cycle throught object
+ H5P.findCopyrights(info, value, contentId);
+ }
+ else {
+ // Found file, add copyrights
+ var copyrights = new H5P.MediaCopyright(value.copyright);
+ if (value.width !== undefined && value.height !== undefined) {
+ copyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(value.path, contentId), value.width, value.height));
+ }
+ info.addMedia(copyrights);
+ }
+ }
+ }
+
+ function buildFromMetadata(data, name, contentId) {
+ if (data.metadata) {
+ const metadataCopyrights = H5P.buildMetadataCopyrights(data.metadata, name);
+ if (metadataCopyrights !== undefined) {
+ if (data.params && data.params.contentName === 'Image' && data.params.file) {
+ const path = data.params.file.path;
+ const width = data.params.file.width;
+ const height = data.params.file.height;
+ metadataCopyrights.setThumbnail(new H5P.Thumbnail(H5P.getPath(path, contentId), width, height));
+ }
+ info.addMedia(metadataCopyrights);
+ }
+ }
+ }
+};
+
+H5P.buildMetadataCopyrights = function (metadata) {
+ if (metadata && metadata.license !== undefined && metadata.license !== 'U') {
+ var dataset = {
+ contentType: metadata.contentType,
+ title: metadata.title,
+ author: (metadata.authors && metadata.authors.length > 0) ? metadata.authors.map(function (author) {
+ return (author.role) ? author.name + ' (' + author.role + ')' : author.name;
+ }).join(', ') : undefined,
+ source: metadata.source,
+ year: (metadata.yearFrom) ? (metadata.yearFrom + ((metadata.yearTo) ? '-' + metadata.yearTo: '')) : undefined,
+ license: metadata.license,
+ version: metadata.licenseVersion,
+ licenseExtras: metadata.licenseExtras,
+ changes: (metadata.changes && metadata.changes.length > 0) ? metadata.changes.map(function (change) {
+ return change.log + (change.author ? ', ' + change.author : '') + (change.date ? ', ' + change.date : '');
+ }).join(' / ') : undefined
+ };
+
+ return new H5P.MediaCopyright(dataset);
+ }
+};
+
+/**
+ * Display a dialog containing the download button and copy button.
+ *
+ * @param {H5P.jQuery} $element
+ * @param {Object} contentData
+ * @param {Object} library
+ * @param {Object} instance
+ * @param {number} contentId
+ */
+H5P.openReuseDialog = function ($element, contentData, library, instance, contentId) {
+ let html = '';
+ if (contentData.displayOptions.export) {
+ html += '';
+ }
+ 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 = '' + 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 ' ';
+ };
+};
+
+/**
+ * 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.lengtha",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=/"!==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=/ |