import Toposort from 'toposort-class';
import H5P from 'imports-loader?H5PIntegration=>window.H5PIntegration!H5P';
H5PIntegration = window.H5PIntegration;
interface Options {
id?: string
frameCss: string
frameJs: string
preventH5PInit: boolean
}
interface Library {
machineName: string
minorVersion: string
majorVersion: string
dependencies: LibraryRef[]
preloadedCss: string[]
preloadedJs: string[]
}
interface LibraryRef {
machineName: string
minorVersion: string
majorVersion: string
}
interface H5Pjson {
mainLibrary: string
preloadedDependencies: LibraryRef[]
}
export default class H5PStandalone {
id: string
librariesPath: string
path: string
h5p: H5Pjson
mainLibrary: Library
pathIncludesVersion: boolean
mainLibraryPath: string
constructor(el: HTMLElement, pathToContent = 'workspace', options: Options = {}, displayOptions = {}, librariesPath: string) {
this.id = options.id || Math.random().toString(36).substr(2, 9);
this.path = pathToContent;
if (!librariesPath) {
this.librariesPath = this.path;
} else {
this.librariesPath = librariesPath;
}
console.log(this.librariesPath);
this.initElement(el);
this.initH5P(options.frameCss, options.frameJs, displayOptions, options.preventH5PInit);
return this;
}
initElement(el: HTMLElement) {
if (!(el instanceof HTMLElement)) {
throw new Error('createH5P must be passed an element');
}
el.innerHTML = `
`;
}
async initH5P(frameCss = './styles/h5p.css', frameJs = './frame.bundle.js', displayOptions, preventH5PInit: boolean) {
this.h5p = (await this.getJSON(`${this.path}/h5p.json`));
const content = await this.getJSON(`${this.path}/content/content.json`);
H5PIntegration.pathIncludesVersion = this.pathIncludesVersion = await this.checkIfPathIncludesVersion();
this.mainLibrary = await this.findMainLibrary();
const dependencies = await this.findAllDependencies();
const { styles, scripts } = this.sortDependencies(dependencies);
H5PIntegration.url = this.path;
H5PIntegration.contents = H5PIntegration.contents ? H5PIntegration.contents : {};
H5PIntegration.core = {
styles: [frameCss],
scripts: [frameJs]
};
H5PIntegration.contents[`cid-${this.id}`] = {
library: `${this.mainLibrary.machineName} ${this.mainLibrary.majorVersion}.${this.mainLibrary.minorVersion}`,
jsonContent: JSON.stringify(content),
styles: styles,
scripts: scripts,
displayOptions: displayOptions
};
// if (!preventH5PInit) {
H5P.init();
// }
}
async getJSON(url: string): Promise {
const res = await fetch(url);
return res.json();
}
/**
* Check if the library folder include the version or not
* This was changed at some point in H5P and we need to be backwards compatible
*
* @return {boolean}
*/
async checkIfPathIncludesVersion(): Promise {
let dependency = this.h5p.preloadedDependencies[0];
let machinePath = dependency.machineName + "-" + dependency.majorVersion + "." + dependency.minorVersion;
let pathIncludesVersion: boolean;
try {
await this.getJSON(`${this.librariesPath}/${machinePath}/library.json`);
pathIncludesVersion = true;
} catch (e) {
pathIncludesVersion = false;
}
return pathIncludesVersion;
}
/**
* return the path to a library
* @param {object} library
* @return {string}
*/
libraryPath(library: LibraryRef): string {
return library.machineName + (this.pathIncludesVersion ? "-" + library.majorVersion + "." + library.minorVersion : '');
}
/**
* FInd the main library for this H5P
* @return {Promise}
*/
async findMainLibrary(): Promise {
const mainLibraryInfo = this.h5p.preloadedDependencies.find(dep => dep.machineName === this.h5p.mainLibrary);
this.mainLibraryPath = this.h5p.mainLibrary + (this.pathIncludesVersion ? "-" + mainLibraryInfo.majorVersion + "." + mainLibraryInfo.minorVersion : '');
return this.getJSON(`${this.librariesPath}/${this.mainLibraryPath}/library.json`);
}
/**
* find all the libraries used in this H5P
* @return {Promise}
*/
async findAllDependencies() {
const directDependencyNames = this.h5p.preloadedDependencies.map(dependency => this.libraryPath(dependency));
return this.loadDependencies(directDependencyNames, []);
}
/**
* searches through all supplied libraries for dependencies, this is recursive and repeats until all deep dependencies have been found
* @param {string[]} toFind list of libraries to find the dependencies of
* @param {string[]} alreadyFound the dependencies that have already been found
*/
async loadDependencies(toFind: string[], alreadyFound: string[]) {
// console.log(`loading dependency level: ${dependencyDepth}`);
// dependencyDepth++;
let dependencies = alreadyFound;
let findNext = [];
let newDependencies = await Promise.all(toFind.map((libraryName) => this.findLibraryDependencies(libraryName)));
// loop over newly found libraries
newDependencies.forEach((library: any) => {
// push into found list
dependencies.push(library);
// check if any dependencies haven't been found yet
library.dependencies.forEach((dependency) => {
if (!dependencies.find((foundLibrary) => foundLibrary.libraryPath === dependency) && !newDependencies.find((foundLibrary) => foundLibrary.libraryPath === dependency)) {
findNext.push(dependency);
}
});
});
if (findNext.length > 0) {
return this.loadDependencies(findNext, dependencies);
}
return dependencies;
}
/**
* Loads a dependencies library.json and finds the libraries it dependson as well ass the JS and CSS it needs
* @param {string} libraryName
*/
async findLibraryDependencies(libraryName: string): Promise {
const library = await this.getJSON(`${this.librariesPath}/${libraryName}/library.json`);
const libraryPath = this.libraryPath(library);
let dependencies = [];
if (library.preloadedDependencies) {
dependencies = library.preloadedDependencies.map(dependency => this.libraryPath(dependency));
}
return { libraryPath, dependencies, preloadedCss: library.preloadedCss, preloadedJs: library.preloadedJs };
}
/**
* Resolves the library dependency tree and sorts the JS and CSS files into order
* @param {object[]} dependencies
* @return {object}
*/
sortDependencies(dependencies) {
const dependencySorter = new Toposort();
let CSSDependencies = {};
let JSDependencies = {};
dependencies.forEach(dependency => {
dependencySorter.add(dependency.libraryPath, dependency.dependencies);
if (dependency.preloadedCss) {
CSSDependencies[dependency.libraryPath] = CSSDependencies[dependency.libraryPath] ? CSSDependencies[dependency.libraryPath] : [];
dependency.preloadedCss.forEach(style => {
CSSDependencies[dependency.libraryPath].push(`${this.librariesPath}/${dependency.libraryPath}/${style.path}`);
});
}
if (dependency.preloadedJs) {
JSDependencies[dependency.libraryPath] = JSDependencies[dependency.libraryPath] ? JSDependencies[dependency.libraryPath] : [];
dependency.preloadedJs.forEach(script => {
JSDependencies[dependency.libraryPath].push(`${this.librariesPath}/${dependency.libraryPath}/${script.path}`);
});
}
});
let styles = [];
let scripts = [];
dependencySorter.sort().reverse().forEach(function (dependencyName) {
Array.prototype.push.apply(styles, CSSDependencies[dependencyName]);
Array.prototype.push.apply(scripts, JSDependencies[dependencyName]);
});
if (this.mainLibrary.preloadedCss) {
Array.prototype.push.apply(styles, this.mainLibrary.preloadedCss.map(style => `${this.librariesPath}/${this.mainLibraryPath}/${style.path}`));
}
if (this.mainLibrary.preloadedJs) {
Array.prototype.push.apply(scripts, this.mainLibrary.preloadedJs.map(script => `${this.librariesPath}/${this.mainLibraryPath}/${script.path}`));
}
return { styles, scripts };
}
}