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 }; } }