Current Path : C:/xampp/htdocs/moodle/mod/hvp/editor/scripts/ |
Current File : C:/xampp/htdocs/moodle/mod/hvp/editor/scripts/h5peditor.js |
/* global ns */ /** * This file contains helper functions for the editor. */ // Grab common resources set in parent window, but avoid sharing back resources set in iframe) window.ns = window.H5PEditor = H5P.jQuery.extend(false, {}, window.parent.H5PEditor); ns.$ = H5P.jQuery; // Load needed resources from parent. H5PIntegration = H5P.jQuery.extend(false, {}, window.parent.H5PIntegration); H5PIntegration.loadedJs = []; H5PIntegration.loadedCss = []; /** * Constants used within editor * * @type {{otherLibraries: string}} */ ns.constants = { otherLibraries: 'Other Libraries', }; /** * Keep track of our widgets. */ ns.widgets = {}; /** * Caches library data (semantics, js and css) */ ns.libraryCache = {}; /** * Keeps track of callbacks to run once a library gets loaded. */ ns.loadedCallbacks = []; /** * Keep track of which libraries have been loaded in the browser, i.e CSS is * added and JS have been run * * @type {Object} */ ns.libraryLoaded = {}; /** * Indiciates if the user is using Internet Explorer. */ ns.isIE = navigator.userAgent.match(/; MSIE \d+.\d+;/) !== null; /** * Keep track of renderable common fields. * * @type {Object} */ ns.renderableCommonFields = {}; (() => { const loading = {}; // Map of callbacks for each src being loaded /** * Help load JavaScripts, prevents double loading. * * @param {string} src * @param {Function} done Callback */ ns.loadJs = (src, done) => { if (H5P.jsLoaded(src)) { // Already loaded done(); return; } if (loading[src] !== undefined) { // Loading in progress... loading[src].push(done); return; } loading[src] = [done]; // Load using script tag var script = document.createElement('script'); script.type = 'text/javascript'; script.charset = 'UTF-8'; script.async = false; script.onload = function () { H5PIntegration.loadedJs.push(src); loading[src].forEach(cb => cb()); delete loading[src]; }; script.onerror = function (err) { loading[src].forEach(cb => cb(err)); delete loading[src]; }; script.src = src; document.head.appendChild(script); }; })(); /** * Helper function invoked when a library is requested. Will add CSS and eval JS * if not already done. * * @private * @param {string} libraryName On the form "machineName majorVersion.minorVersion" * @param {Function} callback */ ns.libraryRequested = function (libraryName, callback) { var libraryData = ns.libraryCache[libraryName]; if (!ns.libraryLoaded[libraryName]) { // Add CSS. if (libraryData.css !== undefined) { libraryData.css.forEach(function (path) { if (!H5P.cssLoaded(path)) { H5PIntegration.loadedCss.push(path); if (path) { ns.$('head').append('<link ' + 'rel="stylesheet" ' + 'href="' + path + '" ' + 'type="text/css" ' + '/>'); } } }); } // Add JS var loadingJs = false; if (libraryData.javascript !== undefined && libraryData.javascript.length) { libraryData.javascript.forEach(function (path) { if (!H5P.jsLoaded(path)) { loadingJs = true; ns.loadJs(path, function (err) { if (err) { console.error('Error while loading script', err); return; } var isFinishedLoading = libraryData.javascript.reduce(function (hasLoaded, jsPath) { return hasLoaded && H5P.jsLoaded(jsPath); }, true); if (isFinishedLoading) { ns.libraryLoaded[libraryName] = true; // Need to set translations after all scripts have been loaded if (libraryData.translations) { for (var machineName in libraryData.translations) { H5PEditor.language[machineName] = libraryData.translations[machineName]; } } callback(ns.libraryCache[libraryName].semantics); } }); } }); } if (!loadingJs) { // Don't have to wait for any scripts, run callback ns.libraryLoaded[libraryName] = true; callback(ns.libraryCache[libraryName].semantics); } } else { // Already loaded, run callback callback(ns.libraryCache[libraryName].semantics); } }; /** * Loads the given library, inserts any css and js and * then runs the callback with the samantics as an argument. * * @param {string} libraryName * On the form machineName majorVersion.minorVersion * @param {function} callback * @returns {undefined} */ ns.loadLibrary = function (libraryName, callback) { switch (ns.libraryCache[libraryName]) { default: // Get semantics from cache. ns.libraryRequested(libraryName, callback); break; case 0: // Add to queue. if (ns.loadedCallbacks[libraryName] === undefined) { ns.loadedCallbacks[libraryName] = []; } ns.loadedCallbacks[libraryName].push(callback); break; case undefined: // Load semantics. ns.libraryCache[libraryName] = 0; // Indicates that others should queue. ns.loadedCallbacks[libraryName] = []; // Other callbacks to run once loaded. var library = ns.libraryFromString(libraryName); var url = ns.getAjaxUrl('libraries', library); // Add content language to URL if (ns.contentLanguage !== undefined) { url += (url.indexOf('?') === -1 ? '?' : '&') + 'language=' + ns.contentLanguage; } // Add common fields default lanuage to URL const defaultLanguage = ns.defaultLanguage; // Avoid changes after sending AJAX if (defaultLanguage !== undefined) { url += (url.indexOf('?') === -1 ? '?' : '&') + 'default-language=' + defaultLanguage; } // Fire away! ns.$.ajax({ url: url, success: function (libraryData) { libraryData.translation = { // Used to cache all the translations en: libraryData.semantics }; let languageSemantics = []; if (libraryData.language !== null) { languageSemantics = JSON.parse(libraryData.language).semantics; delete libraryData.language; // Avoid caching a lot of unused data } var semantics = ns.$.extend(true, [], libraryData.semantics, languageSemantics); if (libraryData.defaultLanguage !== null) { libraryData.translation[defaultLanguage] = JSON.parse(libraryData.defaultLanguage).semantics; delete libraryData.defaultLanguage; // Avoid caching a lot of unused data ns.updateCommonFieldsDefault(semantics, libraryData.translation[defaultLanguage]); } libraryData.semantics = semantics; ns.libraryCache[libraryName] = libraryData; ns.libraryRequested(libraryName, function (semantics) { callback(semantics); // Run queue. if (ns.loadedCallbacks[libraryName]) { for (var i = 0; i < ns.loadedCallbacks[libraryName].length; i++) { ns.loadedCallbacks[libraryName][i](semantics); } } }); }, error: function (jqXHR, textStatus, errorThrown) { if (window['console'] !== undefined) { console.warn('Ajax request failed'); console.warn(jqXHR); console.warn(textStatus); console.warn(errorThrown); } }, dataType: 'json' }); } }; /** * Update common fields default values for the given semantics. * Works by reference. * * @param {Array} semantics * @param {Array} translation * @param {boolean} [parentIsCommon] Used to indicated that one of the ancestors is a common field */ ns.updateCommonFieldsDefault = function (semantics, translation, parentIsCommon) { for (let i = 0; i < semantics.length; i++) { const isCommon = (semantics[i].common === true || parentIsCommon); if (isCommon && semantics[i].default !== undefined && translation[i] !== undefined && translation[i].default !== undefined) { // Update value semantics[i].default = translation[i].default; } if (semantics[i].fields !== undefined && semantics[i].fields.length && translation[i].fields !== undefined && translation[i].fields.length) { // Look into sub fields ns.updateCommonFieldsDefault(semantics[i].fields, translation[i].fields, isCommon); } if (semantics[i].field !== undefined && translation[i].field !== undefined ) { // Look into sub field ns.updateCommonFieldsDefault([semantics[i].field], [translation[i].field], isCommon); } } }; /** * Reset loaded libraries - i.e removes CSS added previously. * @method * @return {[type]} */ ns.resetLoadedLibraries = function () { ns.$('head style.h5p-editor-style').remove(); H5PIntegration.loadedCss = []; H5PIntegration.loadedJs = []; ns.loadedCallbacks = []; ns.libraryLoaded = {}; ns.libraryCache = {}; }; /** * Render common fields of content type with given machine name * * @param {string} machineName Machine name of content type with common fields * @param {Array} [libraries] Library data for machine name */ ns.renderCommonField = function (machineName, libraries) { var commonFields = ns.renderableCommonFields[machineName].fields; var renderableCommonFields = []; var ancestor; commonFields.forEach(function (field) { if (!field.rendered) { var commonField = ns.addCommonField( field.field, field.parent, field.params, field.ancestor, true ); if (commonField.setValues.length === 1) { renderableCommonFields.push({ field: field, instance: commonField.instance }); field.instance = commonField.instance; } } field.rendered = true; }); // Render common fields if found if (renderableCommonFields.length) { var libraryName = machineName === ns.constants.otherLibraries ? machineName : (machineName.length ? machineName.split(' ')[0] : ''); if (libraries.length && libraries[0].title) { libraryName = libraries[0].title; } // Create a library wrapper var hasLibraryWrapper = !!ns.renderableCommonFields[machineName].wrapper; var commonFieldsLibraryWrapper = ns.renderableCommonFields[machineName].wrapper; if (!hasLibraryWrapper) { commonFieldsLibraryWrapper = document.createElement('fieldset'); var libraryWrapperClass = libraryName.replace(/\s+/g, '-').toLowerCase(); commonFieldsLibraryWrapper.classList.add('common-fields-library-wrapper'); commonFieldsLibraryWrapper.classList.add('common-fields-' + libraryWrapperClass); var libraryTitle = document.createElement('legend'); libraryTitle.classList.add('common-field-legend'); libraryTitle.textContent = libraryName; libraryTitle.tabIndex = '0'; libraryTitle.setAttribute('role', 'button'); libraryTitle.addEventListener('click', function () { commonFieldsLibraryWrapper.classList.toggle('expanded'); }); libraryTitle.addEventListener('keypress', function (e) { if (e.which === 32) { commonFieldsLibraryWrapper.classList.toggle('expanded'); } }); commonFieldsLibraryWrapper.appendChild(libraryTitle); ns.renderableCommonFields[machineName].wrapper = commonFieldsLibraryWrapper; } renderableCommonFields.forEach(function (commonField) { commonField.instance.appendTo(ns.$(commonFieldsLibraryWrapper)); // Gather under a common ancestor if (commonField.field && commonField.field.ancestor) { ancestor = commonField.field.ancestor; // Ensure that params are updated after common field instance is // appended since this ensures that defaults are set for common fields const field = commonField.field; const library = field.parent.currentLibrary; const fieldName = field.field.name; const ancestorField = ancestor.commonFields[library][fieldName]; ancestorField.params = field.params[fieldName]; } }); if (!hasLibraryWrapper && ancestor) { ancestor.$common[0].appendChild(commonFieldsLibraryWrapper); } } }; /** * Recursively traverse parents to find the library our field belongs to * * @param parent * @returns {*} */ ns.getParentLibrary = function (parent) { if (!parent) { return null; } if (parent.currentLibrary) { return parent.currentLibrary; } return ns.getParentLibrary(parent.parent); }; /** * Recursive processing of the semantics chunks. * * @param {array} semanticsChunk * @param {object} params * @param {jQuery} $wrapper * @param {mixed} parent * @param {string} [machineName] Machine name of library that is being processed * @returns {undefined} */ ns.processSemanticsChunk = function (semanticsChunk, params, $wrapper, parent, machineName) { var ancestor; parent.children = []; if (parent.passReadies === undefined) { throw 'Widget tried to run processSemanticsChunk without handling ready callbacks. [field:' + parent.field.type + ':' + parent.field.name + ']'; } if (!parent.passReadies) { // If the parent can't pass ready callbacks we need to take care of them. parent.readies = []; } for (var i = 0; i < semanticsChunk.length; i++) { var field = semanticsChunk[i]; // Check generic field properties. if (field.name === undefined) { throw ns.t('core', 'missingProperty', {':index': i, ':property': 'name'}); } if (field.type === undefined) { throw ns.t('core', 'missingProperty', {':index': i, ':property': 'type'}); } // Set default value. if (params[field.name] === undefined && field['default'] !== undefined) { params[field.name] = field['default']; } var widget = ns.getWidgetName(field); // TODO: Remove later, this is here for debugging purposes. if (ns.widgets[widget] === undefined) { $wrapper.append('<div>[field:' + field.type + ':' + widget + ':' + field.name + ']</div>'); continue; } // Add common fields to bottom of form. if (field.common !== undefined && field.common) { if (ancestor === undefined) { ancestor = ns.findAncestor(parent); } var parentLibrary = ns.getParentLibrary(parent); var library = machineName ? machineName : (field.library ? field.library : (parentLibrary ? parentLibrary : ns.constants.otherLibraries)); ns.renderableCommonFields[library] = ns.renderableCommonFields[library] || {}; ns.renderableCommonFields[library].fields = ns.renderableCommonFields[library].fields || []; // Add renderable if it doesn't exist ns.renderableCommonFields[library].fields.push({ field: field, parent: parent, params: params, ancestor: ancestor, rendered: false }); continue; } var fieldInstance = new ns.widgets[widget](parent, field, params[field.name], function (field, value) { if (value === undefined) { delete params[field.name]; } else { params[field.name] = value; } }); fieldInstance.appendTo($wrapper); parent.children.push(fieldInstance); } // Render all gathered common field if (ns.renderableCommonFields) { for (var commonFieldMachineName in ns.renderableCommonFields) { if (commonFieldMachineName === ns.constants.otherLibraries) { // No need to grab library info ns.renderCommonField(commonFieldMachineName); } else { // Get title for common fields group H5PEditor.LibraryListCache.getLibraries( [commonFieldMachineName], ns.renderCommonField.bind(this, commonFieldMachineName) ); } } } if (!parent.passReadies) { // Run ready callbacks. for (i = 0; i < parent.readies.length; i++) { parent.readies[i](); } delete parent.readies; } }; /** * Attach ancestor of parent's common fields to a new wrapper * * @param {Object} parent Parent content type instance that common fields should be attached to * @param {HTMLElement} wrapper New wrapper of common fields */ ns.setCommonFieldsWrapper = function (parent, wrapper) { var ancestor = ns.findAncestor(parent); // Hide the ancestor whose children will be reattached elsewhere wrapper.appendChild(ancestor.$common[0]); }; /** * Add a field to the common container. * * @param {object} field * @param {object} parent * @param {object} params * @param {object} ancestor * @param {boolean} [skipAppendTo] Skips appending the common field if set * @returns {undefined} */ ns.addCommonField = function (field, parent, params, ancestor, skipAppendTo) { var commonField; // Group all fields based on library name + version if (ancestor.commonFields[parent.currentLibrary] === undefined) { ancestor.commonFields[parent.currentLibrary] = {}; } // Field name will have to be unique for library if (ancestor.commonFields[parent.currentLibrary][field.name] === undefined) { var widget = ns.getWidgetName(field); ancestor.commonFields[parent.currentLibrary][field.name] = { instance: new ns.widgets[widget](parent, field, params[field.name], function (field, value) { for (var i = 0; i < commonField.setValues.length; i++) { commonField.setValues[i](field, value); } }), setValues: [], parents: [] }; } commonField = ancestor.commonFields[parent.currentLibrary][field.name]; commonField.parents.push(ns.findLibraryAncestor(parent)); commonField.setValues.push(function (field, value) { if (value === undefined) { delete params[field.name]; } else { params[field.name] = value; } }); if (commonField.setValues.length === 1) { ancestor.$common.parent().removeClass('hidden'); if (!skipAppendTo) { commonField.instance.appendTo(ancestor.$common); } commonField.params = params[field.name]; } else { params[field.name] = commonField.params; } parent.children.push(commonField.instance); return commonField; }; /** * Find the nearest library ancestor. Used when adding commonfields. * * @param {object} parent * @returns {ns.findLibraryAncestor.parent|@exp;ns@call;findLibraryAncestor} */ ns.findLibraryAncestor = function (parent) { if (parent.parent === undefined || parent.field.type === 'library') { return parent; } return ns.findLibraryAncestor(parent.parent); }; /** * getParentZebra * * Alternate the background color of fields * * @param parent * @returns {string} to determine background color of callee */ ns.getParentZebra = function (parent) { if (parent.zebra) { return parent.zebra; } else { return ns.getParentZebra(parent.parent); } }; /** * Find the nearest ancestor which handles commonFields. * * @param {type} parent * @returns {@exp;ns@call;findAncestor|ns.findAncestor.parent} */ ns.findAncestor = function (parent) { if (parent.commonFields === undefined) { return ns.findAncestor(parent.parent); } return parent; }; /** * Call remove on the given children. * * @param {Array} children * @returns {unresolved} */ ns.removeChildren = function (children) { if (children === undefined) { return; } for (var i = 0; i < children.length; i++) { // Common fields will be removed by library. var isCommonField = (children[i].field === undefined || children[i].field.common === undefined || !children[i].field.common); var hasRemove = (children[i].remove instanceof Function || typeof children[i].remove === 'function'); if (isCommonField && hasRemove) { children[i].remove(); } } }; /** * Find field from path. * * @param {String} path * @param {Object} parent * @returns {@exp;ns.Form@call;findField|Boolean} */ ns.findField = function (path, parent) { if (typeof path === 'string') { path = path.split('/'); } if (path[0] === '..') { path.splice(0, 1); return ns.findField(path, parent.parent); } if (parent.children) { for (var i = 0; i < parent.children.length; i++) { if (parent.children[i].field.name === path[0]) { path.splice(0, 1); if (path.length) { return ns.findField(path, parent.children[i]); } else { return parent.children[i]; } } } } return false; }; /** * Find a semantics field in the semantics structure by name of the field * Will return the first found by depth first search if there are identically named fields * * @param {string} fieldName Name of the field we wish to find * @param {Object|Array} semanticsStructure Semantics we wish to find the field within * @returns {null|Object} Returns the field if found, otherwise null. */ ns.findSemanticsField = function (fieldName, semanticsStructure) { if (Array.isArray(semanticsStructure)) { for (let i = 0; i < semanticsStructure.length; i++) { var semanticsField = ns.findSemanticsField(fieldName, semanticsStructure[i]); if (semanticsField !== null) { // Return immediately if field is found return semanticsField; } } return null; } else if (semanticsStructure.name === fieldName) { return semanticsStructure; } else if (semanticsStructure.field) { // Process field return ns.findSemanticsField(fieldName, semanticsStructure.field); } else if (semanticsStructure.fields) { // Process fields return ns.findSemanticsField(fieldName, semanticsStructure.fields); } else { // No matching semantics found within known properties and list structures return null; } }; /** * Follow a field and get all changes to its params. * * @param {Object} parent The parent object of the field. * @param {String} path Relative to parent object. * @param {Function} callback Gets called for params changes. * @returns {undefined} */ ns.followField = function (parent, path, callback) { if (path === undefined) { return; } // Find field when tree is ready. parent.ready(function () { var def; if (path instanceof Object) { // We have an object with default values def = H5P.cloneObject(path); if (path.field === undefined) { callback(path, null); return; // Exit if we have no field to follow. } path = def.field; delete def.field; } var field = ns.findField(path, parent); if (!field) { throw ns.t('core', 'unknownFieldPath', {':path': path}); } if (field.changes === undefined) { throw ns.t('core', 'noFollow', {':path': path}); } var params = (field.params === undefined ? def : field.params); callback(params, field.changes.length + 1); field.changes.push(function () { var params = (field.params === undefined ? def : field.params); callback(params); }); }); }; /** * Create HTML wrapper for error messages. * * @param {String} message * @returns {String} */ ns.createError = function (message) { return '<p>' + message + '</p>'; }; /** * Turn a numbered importance into a string. * * @param {string} importance * @returns {String} */ ns.createImportance = function (importance) { return importance ? 'importance-' + importance : ''; }; /** * Create HTML wrapper for field items. * Makes sure the different elements are placed in an consistent order. * * @param {string} type * @param {string} [label] * @param {string} [description] * @param {string} [content] * @deprecated since version 1.12 (Jan. 2017, will be removed Jan. 2018). Use createFieldMarkup instead. * @see createFieldMarkup * @returns {string} HTML */ ns.createItem = function (type, label, description, content) { return '<div class="field ' + type + '">' + (label ? label : '') + (description ? '<div class="h5peditor-field-description">' + description + '</div>' : '') + (content ? content : '') + '<div class="h5p-errors"></div>' + '</div>'; }; /** * An object describing the semantics of a field * @typedef {Object} SemanticField * @property {string} name * @property {string} type * @property {string} label * @property {string} [importance] * @property {string} [description] * @property {string} [widget] * @property {boolean} [optional] */ /** * Create HTML wrapper for a field item. * Replacement for createItem() * * @since 1.12 * @param {SemanticField} field * @param {string} content * @param {string} [inputId] * @return {string} */ ns.createFieldMarkup = function (field, content, inputId) { content = content || ''; var markup = this.createLabel(field, '', inputId) + this.createDescription(field.description, inputId) + content; return this.wrapFieldMarkup(field, markup); }; /** * Create HTML wrapper for a boolean field item. * * @param {SemanticField} field * @param {string} content * @param {string} [inputId] * * @return {string} */ ns.createBooleanFieldMarkup = function (field, content, inputId) { var markup = '<label class="h5peditor-label">' + content + (field.label || field.name || '') + '</label>' + this.createDescription(field.description, inputId); return this.wrapFieldMarkup(field, markup); }; /** * Wraps a field with some metadata classes, and adds error field * * @param {SemanticField} field * @param {string} markup * * @private * @return {string} */ ns.wrapFieldMarkup = function (field, markup) { // removes undefined and joins var wrapperClasses = this.joinNonEmptyStrings(['field', 'field-name-' + field.name, field.type, ns.createImportance(field.importance), field.widget]); // wrap and return return '<div class="' + wrapperClasses + '">' + markup + '<div class="h5p-errors"></div>' + '</div>'; }; /** * Joins an array of strings if they are defined and non empty * * @param {string[]} arr * @param {string} [separator] Default is space * @return {string} */ ns.joinNonEmptyStrings = function (arr, separator) { separator = separator || ' '; return arr.filter(function (str) { return str !== undefined && str.length > 0; }).join(separator); }; /** * Create HTML for select options. * * @param {String} value * @param {String} text * @param {Boolean} selected * @returns {String} */ ns.createOption = function (value, text, selected) { return '<option value="' + value + '"' + (selected !== undefined && selected ? ' selected="selected"' : '') + '>' + text + '</option>'; }; /** * Create HTML for text input. * * @param {String} value * @param {number} maxLength * @param {String} placeholder * @param {number} [id] * @param {number} [describedby] * @returns {String} */ ns.createText = function (value, maxLength, placeholder, id, describedby) { var html = '<input class="h5peditor-text" type="text"'; if (id !== undefined) { html += ' id="' + id + '"'; } if (describedby !== undefined) { html += ' aria-describedby="' + describedby + '"'; } if (value !== undefined) { html += ' value="' + value + '"'; } if (placeholder !== undefined) { html += ' placeholder="' + placeholder + '"'; } html += ' maxlength="' + (maxLength === undefined ? 255 : maxLength) + '"/>'; return html; }; ns.getNextFieldId = (function (counter) { /** * Generates a consistent and unique field ID for the given field. * * @param {Object} field * @return {number} */ return function (field) { return 'field-' + field.name.toLowerCase() + '-' + (counter++); }; })(-1); /** * Helps generates a consistent description ID across fields. * * @param {string} id * @return {string} */ ns.getDescriptionId = function (id) { return id + '-description'; }; /** * Create a label to wrap content in. * * @param {SemanticField} field * @param {String} [content] * @param {String} [inputId] * @returns {String} */ ns.createLabel = function (field, content, inputId) { // New items can be added next to the label within the flex-wrapper var html = '<label class="h5peditor-label-wrapper"'; if (inputId !== undefined) { html += ' for="' + inputId + '"'; } html+= '>' // Temporary fix for the old version of CoursePresentation's custom editor if (field.widget === 'coursepresentation' && field.name === 'presentation') { field.label = 0; } if (field.label !== 0) { html += '<span class="h5peditor-label' + (field.optional ? '' : ' h5peditor-required') + '">' + (field.label === undefined ? field.name : field.label) + '</span>'; } return html + (content || '') + '</label>'; }; /** * Create a description * @param {String} description * @param {number} [inputId] Used to reference description from input * @returns {string} */ ns.createDescription = function (description, inputId) { var html = ''; if (description !== undefined) { html += '<div class="h5peditor-field-description"'; if (inputId !== undefined) { html += ' id="' + ns.getDescriptionId(inputId) + '"'; } html += '>' + description + '</div>'; } return html; }; /** * Create an important description * @param {Object} importantDescription * @returns {String} */ ns.createImportantDescription = function (importantDescription) { var html = ''; if (importantDescription !== undefined) { html += '<div class="h5peditor-field-important-description">' + '<div class="important-description-tail">' + '</div>' + '<div class="important-description-close" role="button" tabindex="0" aria-label="' + ns.t('core', 'hideImportantInstructions') + '">' + '<span>' + ns.t('core', 'hide') + '</span>' + '</div>' + '<span class="h5p-info-icon">' + '</span>' + '<span class="important-description-title">' + ns.t('core', 'importantInstructions') + '</span>'; if (importantDescription.description !== undefined) { html += '<div class="important-description-content">' + importantDescription.description + '</div>'; } if (importantDescription.example !== undefined) { html += '<div class="important-description-example">' + '<div class="important-description-example-title">' + '<span>' + ns.t('core', 'example') + ':</span>' + '</div>' + '<div class="important-description-example-text">' + '<span>' + importantDescription.example + '</span>' + '</div>' + '</div>'; } html += '</div>' + '<span class="important-description-show" role="button" tabindex="0">' + ns.t('core', 'showImportantInstructions') + '</span><span class="important-description-clear-right"></span>'; } return html; }; /** * Bind events to important description * @param {Object} widget * @param {String} fieldName * @param {Object} parent */ ns.bindImportantDescriptionEvents = function (widget, fieldName, parent) { var context; if (!widget.field.important) { return; } // Generate a context string for using as referance in ex. localStorage. var librarySelector = ns.findLibraryAncestor(parent); if (librarySelector.currentLibrary !== undefined) { var lib = librarySelector.currentLibrary.split(' ')[0]; context = (lib + '-' + fieldName).replace(/\.|_/g,'-') + '-important-description-open'; } // Set first occurance to visible ns.storage.get(context, function (value) { if (value === undefined || value === true) { widget.$item.addClass('important-description-visible'); } }); widget.$item.addClass('has-important-description'); // Bind events to toggle button and update aria-pressed widget.$item.find('.important-description-show') .click(function () { widget.$item.addClass('important-description-visible'); ns.storage.set(context, true); }) .keydown(function (event) { if (event.which == 13 || event.which == 32) { ns.$(this).trigger('click'); event.preventDefault(); } }); // Bind events to close button and update aria-pressed of toggle button widget.$item.find('.important-description-close') .click(function () { widget.$item.removeClass('important-description-visible'); ns.storage.set(context, false); }) .keydown(function (event) { if (event.which == 13 || event.which == 32) { ns.$(this).trigger('click'); event.preventDefault(); } }); }; /** * Generate markup for the copy and paste buttons. * * @returns {string} HTML */ ns.createCopyPasteButtons = function () { return '<div class="h5peditor-copypaste-wrap">' + '<button class="h5peditor-copy-button disabled" title="' + H5PEditor.t('core', 'copyToClipboard') + '" disabled>' + ns.t('core', 'copyButton') + '</button>' + '<button class="h5peditor-paste-button disabled" title="' + H5PEditor.t('core', 'pasteFromClipboard') + '" disabled>' + ns.t('core', 'pasteButton') + '</button>' + '</div><div class="h5peditor-clearfix"></div>'; }; /** * Confirm replace if there is content selected * * @param {string} library Current selected library * @param {number} top Offset * @param {function} next Next callback */ ns.confirmReplace = function (library, top, next) { if (library) { // Confirm changing library var confirmReplace = new H5P.ConfirmationDialog({ headerText: H5PEditor.t('core', 'pasteContent'), dialogText: H5PEditor.t('core', 'confirmPasteContent'), confirmText: H5PEditor.t('core', 'confirmPasteButtonText') }).appendTo(document.body); confirmReplace.on('confirmed', next); confirmReplace.show(top); } else { // No need to confirm next(); } }; /** * Check if any errors has been set. * * @param {jQuery} $errors * @param {jQuery} $input * @param {String} value * @returns {mixed} */ ns.checkErrors = function ($errors, $input, value) { if ($errors.children().length) { $input.keyup(function (event) { if (event.keyCode === 9) { // TAB return; } $errors.html(''); $input.removeClass('error'); $input.unbind('keyup'); }); return false; } return value; }; /** * @param {object} library * with machineName, majorVersion and minorVersion params * @returns {string} * Concatinated version of the library */ ns.libraryToString = function (library) { return library.name + ' ' + library.majorVersion + '.' + library.minorVersion; }; /** * TODO: Remove from here, and use from H5P instead(move this to the h5p.js...) * * @param {string} library * library in the format machineName majorVersion.minorVersion * @returns * library as an object with machineName, majorVersion and minorVersion properties * return false if the library parameter is invalid */ ns.libraryFromString = function (library) { var regExp = /(.+)\s(\d+)\.(\d+)$/g; var res = regExp.exec(library); if (res !== null) { return { 'machineName': res[1], 'majorVersion': res[2], 'minorVersion': res[3] }; } else { H5P.error('Invalid überName'); return false; } }; /** * Helper function for detecting field widget. * * @param {Object} field * @returns {String} Widget name */ ns.getWidgetName = function (field) { return (field.widget === undefined ? field.type : field.widget); }; /** * Mimics how php's htmlspecialchars works (the way we uses it) */ ns.htmlspecialchars = function (string) { return string.toString().replace(/</g, '<').replace(/>/g, '>').replace(/'/g, ''').replace(/"/g, '"'); }; /** * Makes it easier to add consistent buttons across the editor widget. * * @param {string} id Typical CSS class format * @param {string} title Human readable format * @param {function} handler Action handler when triggered * @param {boolean} [displayTitle=false] Show button with text * @return {H5P.jQuery} */ ns.createButton = function (id, title, handler, displayTitle) { var options = { class: 'h5peditor-button ' + (displayTitle ? 'h5peditor-button-textual ' : '') + id, role: 'button', tabIndex: 0, 'aria-disabled': 'false', on: { click: function () { handler.call(this); }, keydown: function (event) { switch (event.which) { case 13: // Enter case 32: // Space handler.call(this); event.preventDefault(); } } } }; // Determine if we're a icon only button or have a textual label options[displayTitle ? 'html' : 'aria-label'] = title; return ns.$('<div/>', options); }; /** * Check if the current library is entitled for the metadata button. True by default. * * It will probably be okay to remove this check at some point in time when * the majority of content types and plugins have been updated to a version * that supports the metadata system. * * @param {string} library - Current library. * @return {boolean} True, if form should have the metadata button. */ ns.enableMetadata = function (library) { if (!library || typeof library !== 'string') { return false; } library = H5P.libraryFromString(library); if (!library) { return false; } // This list holds all old libraries (/older versions implicitly) that need an update for metadata const blackList = [ // Should never have metadata because it does not make sense 'H5P.IVHotspot 1.2', 'H5P.Link 1.3', 'H5P.TwitterUserFeed 1.0', 'H5P.GoToQuestion 1.3', 'H5P.Nil 1.0', // Copyright information moved to metadata 'H5P.Audio 1.2', 'H5P.Video 1.4', 'H5P.Image 1.0', // Title moved to metadata 'H5P.DocumentExportPage 1.3', 'H5P.ExportableTextArea 1.2', 'H5P.GoalsAssessmentPage 1.3', 'H5P.GoalsPage 1.4', 'H5P.StandardPage 1.3', 'H5P.DragQuestion 1.12', 'H5P.ImageHotspotQuestion 1.7', // Custom editor changed 'H5P.CoursePresentation 1.19', 'H5P.InteractiveVideo 1.19' ]; let block = blackList.filter(function (item) { // + ' ' makes sure to avoid partial matches return item.indexOf(library.machineName + ' ') !== -1; }); if (block.length === 0) { return true; } block = H5P.libraryFromString(block[0]); if (library.majorVersion > block.majorVersion || library.majorVersion === block.majorVersion && library.minorVersion > block.minorVersion) { return true; } return false; }; // Backwards compatibilty ns.attachToastTo = H5P.attachToastTo; /** * Check if clipboard can be pasted. * * @param {Object} clipboard Clipboard data. * @param {Object} libs Libraries to compare against. * @return {boolean} True, if content can be pasted. */ ns.canPaste = function (clipboard, libs) { return (this.canPastePlus(clipboard, libs)).canPaste; }; /** * Check if clipboard can be pasted and give reason if not. * * @param {Object} clipboard Clipboard data. * @param {Object} libs Libraries to compare against. * @return {Object} Results. {canPaste: boolean, reason: string, description: string}. */ ns.canPastePlus = function (clipboard, libs) { // Clipboard is empty if (!clipboard || !clipboard.generic) { return { canPaste: false, reason: 'pasteNoContent', description: ns.t('core', 'pasteNoContent') }; } // No libraries to compare to if (libs === undefined) { return { canPaste: false, reason: 'pasteError', description: ns.t('core', 'pasteError') }; } // Translate Hub format to common library format if (libs.libraries !== undefined) { libs = libs.libraries; libs.forEach(function (lib) { lib.name = lib.machineName; lib.majorVersion = lib.localMajorVersion; lib.minorVersion = lib.localMinorVersion; }); } // Check if clipboard library type is available const machineNameClip = clipboard.generic.library.split(' ')[0]; let candidates = libs.filter(function (library) { return library.name === machineNameClip; }); if (candidates.length === 0) { return { canPaste: false, reason: 'pasteContentNotSupported', description: ns.t('core', 'pasteContentNotSupported') }; } // Check if clipboard library version is available const versionClip = clipboard.generic.library.split(' ')[1]; for (let i = 0; i < candidates.length; i++) { if (candidates[i].majorVersion + '.' + candidates[i].minorVersion === versionClip) { if (candidates[i].restricted !== true) { return { canPaste: true }; } else { return { canPaste: false, reason: 'pasteContentRestricted', description: ns.t('core', 'pasteContentRestricted') }; } } } // Sort remaining candidates by version number candidates = candidates .map(function (candidate) { return '' + candidate.majorVersion + '.' + candidate.minorVersion; }) .map(function (candidate) { return candidate.replace(/\d+/g, function (d) { return +d + 1000; }); }) .sort() .map(function (candidate) { return candidate.replace(/\d+/g, function (d) { return +d - 1000; }); }); // Clipboard library is newer than latest available local library const candidateMax = candidates.slice(-1)[0]; if (+candidateMax.split('.')[0] < +versionClip.split('.')[0] || (+candidateMax.split('.')[0] === +versionClip.split('.')[0] && +candidateMax.split('.')[1] < +versionClip.split('.')[1])) { return { canPaste: false, reason: 'pasteTooNew', description: ns.t('core', 'pasteTooNew', { ':clip': versionClip, ':local': candidateMax }) }; } // Clipboard library is older than latest available local library const candidateMin = candidates.slice(0, 1)[0]; if (+candidateMin.split('.')[0] > +versionClip.split('.')[0] || (+candidateMin.split('.')[0] === +versionClip.split('.')[0] && +candidateMin.split('.')[1] > +versionClip.split('.')[1])) { return { canPaste: false, reason: 'pasteTooOld', description: ns.t('core', 'pasteTooOld', { ':clip': versionClip, ':local': candidateMin }) }; } return { canPaste: false, reason: 'pasteError', description: ns.t('core', 'pasteError') }; }; // Factory for creating storage instance ns.storage = (function () { var instance = { get: function (key, next) { var value; // Get value from browser storage if (window.localStorage !== undefined) { value = !!window.localStorage.getItem(key); } // Try to get a better value from user data storage try { H5P.getUserData(0, key, function (err, result) { if (!err) { value = result; } next(value); }); } catch (err) { next(value); } }, set: function (key, value) { // Store in browser if (window.localStorage !== undefined) { window.localStorage.setItem(key, value); } // Try to store in user data storage try { H5P.setUserData(0, key, value); } catch (err) { /*Intentionally left empty*/ } } }; return instance; })(); /** * Small helper class for library data. * * @class * @param {string} nameVersionString */ ns.ContentType = function ContentType(nameVersionString) { const libraryNameSplit = nameVersionString.split(' '); const libraryVersionSplit = libraryNameSplit[1].split('.'); this.machineName = libraryNameSplit[0]; this.majorVersion = libraryVersionSplit[0]; this.minorVersion = libraryVersionSplit[1]; }; /** * Look for the best possible upgrade for the given library * * @param {ns.ContentType} library * @param {Array} libraries Where to look */ ns.ContentType.getPossibleUpgrade = function (library, libraries) { let possibleUpgrade; for (let i = 0; i < libraries.length; i++) { const candiate = libraries[i]; if (candiate.installed !== false && ns.ContentType.hasSameName(candiate, library) && ns.ContentType.isHigherVersion(candiate, library)) { // Check if the upgrade is better than the previous upgrade we found if (!possibleUpgrade || ns.ContentType.isHigherVersion(candiate, possibleUpgrade)) { possibleUpgrade = candiate; } } } return possibleUpgrade; }; /** * Check if candiate is a higher version than original. * * @param {Object} candiate Library object * @param {Object} original Library object * @returns {boolean} */ ns.ContentType.isHigherVersion = function (candiate, original) { return (ns.ContentType.getMajorVersion(candiate) > ns.ContentType.getMajorVersion(original) || (ns.ContentType.getMajorVersion(candiate) == ns.ContentType.getMajorVersion(original) && ns.ContentType.getMinorVersion(candiate) > ns.ContentType.getMinorVersion(original))); }; /** * Check if candiate has same name as original. * * @param {Object} candiate Library object * @param {Object} original Library object * @returns {boolean} */ ns.ContentType.hasSameName = function (candiate, original) { return (ns.ContentType.getName(candiate) === ns.ContentType.getName(original)); }; /** * Check if candiate has same name as original. * * @param {Object} candiate Library object * @param {Object} original Library object * @returns {string} */ ns.ContentType.getNameVersionString = function (library) { return ns.ContentType.getName(library) + ' ' + ns.ContentType.getMajorVersion(library) + '.' + ns.ContentType.getMinorVersion(library); }; /** * Get the major version from a library object. * * @param {Object} library * @returns {number} */ ns.ContentType.getMajorVersion = function (library) { return parseInt((library.localMajorVersion !== undefined ? library.localMajorVersion : library.majorVersion)); }; /** * Get the minor version from a library object. * * @param {Object} library * @returns {number} */ ns.ContentType.getMinorVersion = function (library) { return parseInt((library.localMinorVersion !== undefined ? library.localMinorVersion : library.minorVersion)); }; /** * Get the name from a library object. * * @param {Object} library * @returns {string} */ ns.ContentType.getName = function (library) { return (library.machineName !== undefined ? library.machineName : library.name); }; ns.upgradeContent = (function () { /** * A wrapper for loading library data for the content upgrade scripts. * * @param {string} name Library name * @param {H5P.Version} version * @param {Function} next Callback */ const loadLibrary = function (name, version, next) { const library = name + ' ' + version.major + '.' + version.minor; ns.loadLibrary(library, function () { next(null, ns.libraryCache[library]); }); }; return function contentUpgrade(fromLibrary, toLibrary, parameters, done) { ns.loadJs(H5PIntegration.libraryUrl + '/h5p-version.js' + H5PIntegration.pluginCacheBuster, function (err) { ns.loadJs(H5PIntegration.libraryUrl + '/h5p-content-upgrade-process.js' + H5PIntegration.pluginCacheBuster, function (err) { // TODO: Avoid stringify the parameters new H5P.ContentUpgradeProcess(ns.ContentType.getName(fromLibrary), new H5P.Version(fromLibrary), new H5P.Version(toLibrary), JSON.stringify(parameters), 1, function (name, version, next) { loadLibrary(name, version, function (err, library) { if (library.upgradesScript) { ns.loadJs(library.upgradesScript, function (err) { if (err) { err = 'Error loading upgrades ' + name + ' ' + version; } next(err, library); }); } else { next(null, library); } }); }, function (err, result) { if (err) { let header = 'Failed'; let message = 'Could not upgrade content'; switch (err.type) { case 'errorTooHighVersion': message += ': ' + ns.t('core', 'errorTooHighVersion', {'%used': err.used, '%supported': err.supported}); break; case 'errorNotSupported': message += ': ' + ns.t('core', 'errorNotSupported', {'%used': err.used}); break; case 'errorParamsBroken': message += ': ' + ns.t('core', 'errorParamsBroken'); break; case 'libraryMissing': message += ': ' + ns.t('core', 'libraryMissing', {'%lib': err.library}); break; case 'scriptMissing': message += ': ' + ns.t('core', 'scriptMissing', {'%lib': err.library}); break; } var confirmErrorDialog = new H5P.ConfirmationDialog({ headerText: header, dialogText: message, confirmText: 'Continue' }).appendTo(document.body); confirmErrorDialog.show(); } done(err, result); }); }); }); }; })(); // List of language code mappings used by the editor ns.supportedLanguages = { 'aa': 'Afar', 'ab': 'Abkhazian (аҧсуа бызшәа)', 'ae': 'Avestan', 'af': 'Afrikaans', 'ak': 'Akan', 'am': 'Amharic (አማርኛ)', 'ar': 'Arabic (العربية)', 'as': 'Assamese', 'ast': 'Asturian', 'av': 'Avar', 'ay': 'Aymara', 'az': 'Azerbaijani (azərbaycan)', 'ba': 'Bashkir', 'be': 'Belarusian (Беларуская)', 'bg': 'Bulgarian (Български)', 'bh': 'Bihari', 'bi': 'Bislama', 'bm': 'Bambara (Bamanankan)', 'bn': 'Bengali', 'bo': 'Tibetan', 'br': 'Breton', 'bs': 'Bosnian (Bosanski)', 'ca': 'Catalan (Català)', 'ce': 'Chechen', 'ch': 'Chamorro', 'co': 'Corsican', 'cr': 'Cree', 'cs': 'Czech (Čeština)', 'cu': 'Old Slavonic', 'cv': 'Chuvash', 'cy': 'Welsh (Cymraeg)', 'da': 'Danish (Dansk)', 'de': 'German (Deutsch)', 'dv': 'Maldivian', 'dz': 'Bhutani', 'ee': 'Ewe (Ɛʋɛ)', 'el': 'Greek (Ελληνικά)', 'en': 'English', 'en-gb': 'English, British', 'eo': 'Esperanto', 'es': 'Spanish (Español)', 'es-mx': 'Spanish, Mexican', 'et': 'Estonian (Eesti)', 'eu': 'Basque (Euskera)', 'fa': 'Persian (فارسی)', 'ff': 'Fulah (Fulfulde)', 'fi': 'Finnish (Suomi)', 'fil': 'Filipino', 'fj': 'Fiji', 'fo': 'Faeroese', 'fr': 'French (Français)', 'fy': 'Frisian (Frysk)', 'ga': 'Irish (Gaeilge)', 'gd': 'Scots Gaelic', 'gl': 'Galician (Galego)', 'gn': 'Guarani', 'gsw-berne': 'Swiss German', 'gu': 'Gujarati', 'gv': 'Manx', 'ha': 'Hausa', 'he': 'Hebrew (עברית)', 'hi': 'Hindi (हिन्दी)', 'ho': 'Hiri Motu', 'hr': 'Croatian (Hrvatski)', 'hsb': 'Upper Sorbian (hornjoserbšćina)', 'ht': 'Haitian Creole', 'hu': 'Hungarian (Magyar)', 'hy': 'Armenian (Հայերեն)', 'hz': 'Herero', 'ia': 'Interlingua', 'id': 'Indonesian (Bahasa Indonesia)', 'ie': 'Interlingue', 'ig': 'Igbo', 'ik': 'Inupiak', 'is': 'Icelandic (Íslenska)', 'it': 'Italian (Italiano)', 'iu': 'Inuktitut', 'ja': 'Japanese (日本語)', 'jv': 'Javanese', 'ka': 'Georgian', 'kg': 'Kongo', 'ki': 'Kikuyu', 'kj': 'Kwanyama', 'kk': 'Kazakh (Қазақ)', 'kl': 'Greenlandic', 'km': 'Cambodian', 'kn': 'Kannada (ಕನ್ನಡ)', 'ko': 'Korean (한국어)', 'kr': 'Kanuri', 'ks': 'Kashmiri', 'ku': 'Kurdish (Kurdî)', 'kv': 'Komi', 'kw': 'Cornish', 'ky': 'Kyrgyz (Кыргызча)', 'la': 'Latin (Latina)', 'lb': 'Luxembourgish', 'lg': 'Luganda', 'ln': 'Lingala', 'lo': 'Laothian', 'lt': 'Lithuanian (Lietuvių)', 'lv': 'Latvian (Latviešu)', 'mg': 'Malagasy', 'mh': 'Marshallese', 'mi': 'Māori', 'mk': 'Macedonian (Македонски)', 'ml': 'Malayalam (മലയാളം)', 'mn': 'Mongolian', 'mo': 'Moldavian', 'mr': 'Marathi', 'ms': 'Malay (Bahasa Melayu)', 'mt': 'Maltese (Malti)', 'my': 'Burmese', 'na': 'Nauru', 'nd': 'North Ndebele', 'ne': 'Nepali', 'ng': 'Ndonga', 'nl': 'Dutch (Nederlands)', 'nb': 'Norwegian Bokmål (Bokmål)', 'nn': 'Norwegian Nynorsk (Nynorsk)', 'nr': 'South Ndebele', 'nv': 'Navajo', 'ny': 'Chichewa', 'oc': 'Occitan', 'om': 'Oromo', 'or': 'Oriya', 'os': 'Ossetian', 'pa': 'Punjabi', 'pi': 'Pali', 'pl': 'Polish (Polski)', 'ps': 'Pashto (پښتو)', 'pt': 'Portuguese, International', 'pt-pt': 'Portuguese, Portugal (Português)', 'pt-br': 'Portuguese, Brazil (Português)', 'qu': 'Quechua', 'rm': 'Rhaeto-Romance', 'rn': 'Kirundi', 'ro': 'Romanian (Română)', 'ru': 'Russian (Русский)', 'rw': 'Kinyarwanda', 'sa': 'Sanskrit', 'sc': 'Sardinian', 'sco': 'Scots', 'sd': 'Sindhi', 'se': 'Northern Sami', 'sg': 'Sango', 'sh': 'Serbo-Croatian', 'si': 'Sinhala (සිංහල)', 'sk': 'Slovak (Slovenčina)', 'sl': 'Slovenian (Slovenščina)', 'sm': 'Samoan', 'sma': 'Sámi (Southern)', 'sme': 'Sámi (Northern)', 'smj': 'Sámi (Lule)', 'sn': 'Shona', 'so': 'Somali', 'sq': 'Albanian (Shqip)', 'sr': 'Serbian (Српски)', 'ss': 'Siswati', 'st': 'Sesotho', 'su': 'Sudanese', 'sv': 'Swedish (Svenska)', 'sw': 'Swahili (Kiswahili)', 'ta': 'Tamil (தமிழ்)', 'te': 'Telugu (తెలుగు)', 'tg': 'Tajik', 'th': 'Thai (ภาษาไทย)', 'ti': 'Tigrinya', 'tk': 'Turkmen', 'tl': 'Tagalog', 'tn': 'Setswana', 'to': 'Tonga', 'tr': 'Turkish (Türkçe)', 'ts': 'Tsonga', 'tt': 'Tatar (Tatarça)', 'tw': 'Twi', 'ty': 'Tahitian', 'ug': 'Uyghur', 'uk': 'Ukrainian (Українська)', 'ur': 'Urdu (اردو)', 'uz': "Uzbek (o'zbek)", 've': 'Venda', 'vi': 'Vietnamese (Tiếng Việt)', 'wo': 'Wolof', 'xh': 'Xhosa (isiXhosa)', 'xx-lolspeak': 'Lolspeak)', 'yi': 'Yiddish', 'yo': 'Yoruba (Yorùbá)', 'za': 'Zhuang', 'zh': 'Chinese', 'zh-hans': 'Chinese, Simplified (简体中文)', 'zh-hant': 'Chinese, Traditional (繁體中文)', 'zh-tw': 'Chinese, Taiwan, Traditional', 'zu': 'Zulu (isiZulu)' };